From db99933ca2e53f74b45cf7fbc7b8bcec5caa7d70 Mon Sep 17 00:00:00 2001 From: Arthit Suriyawongkul Date: Wed, 2 Sep 2026 04:20:41 +0000 Subject: [PATCH 01/35] Add PEP 751 pylock.toml resolved-dependency support pylock.toml is build-backend-agnostic, so it's checked unconditionally in read_pyproject() rather than gated behind [tool.poetry] detection like poetry.lock. Reuses ProjectMetadata.locked_dependencies and the existing additive dependsOn/completeness wiring poetry.lock already built. When both poetry.lock and pylock.toml are present, pylock.toml (the PEP 751 standard) takes priority, with a WARNING naming the override. Signed-off-by: Arthit Suriyawongkul --- CHANGELOG.md | 3 + src/pitloom/extract/_pylock.py | 138 +++++++++++ src/pitloom/extract/_pyproject.py | 6 +- tests/extract/test_pylock.py | 215 ++++++++++++++++++ working-docs/design/lock-files.md | 16 +- working-docs/design/roadmap.md | 40 ++-- .../implementation/pep751-pylock-support.md | 139 +++++++++++ 7 files changed, 535 insertions(+), 22 deletions(-) create mode 100644 src/pitloom/extract/_pylock.py create mode 100644 tests/extract/test_pylock.py create mode 100644 working-docs/implementation/pep751-pylock-support.md diff --git a/CHANGELOG.md b/CHANGELOG.md index dae9d99c..2969aab3 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -41,6 +41,9 @@ and this project adheres to - Add PEP 639 `[project.license-files]` support: each declared license file gets a `software_File` element at the real wheel's `.dist-info/licenses/` path and a `hasDeclaredLicense` relationship ([#207]) +- Add PEP 751 `pylock.toml` resolved-dependency parsing for `loom + project`/`loom generate`, taking priority over `poetry.lock` when both + are present ### Fixed diff --git a/src/pitloom/extract/_pylock.py b/src/pitloom/extract/_pylock.py new file mode 100644 index 00000000..519cd923 --- /dev/null +++ b/src/pitloom/extract/_pylock.py @@ -0,0 +1,138 @@ +# SPDX-FileContributor: Arthit Suriyawongkul +# SPDX-FileCopyrightText: 2026-present Arthit Suriyawongkul +# SPDX-FileType: SOURCE +# SPDX-License-Identifier: Apache-2.0 + +"""Extractor for resolved dependencies from a PEP 751 ``pylock.toml``. + +See also: :mod:`pitloom.extract._poetry_lock` (the ``poetry.lock`` +extractor this module mirrors in shape -- same source-stage-only scoping, +same ``name==version`` output, same "no silent deviations" warning +policy) and :func:`pitloom.extract._pyproject._apply_pylock_dependencies` +(the call site that overlays this module's output onto +``ProjectMetadata.locked_dependencies``, taking priority over any +``poetry.lock``-resolved set already present). + +``pylock.toml`` (PEP 751) is the build-backend-agnostic Python +interoperability standard for recording a fully resolved dependency set -- +produced by tools like ``uv``, ``pdm``, and ``poetry`` (via ``export``), +consumed only by installers, never by a PEP 517 build backend. That makes +it a **source-stage-only** artifact, the same class as ``poetry.lock``: +appropriate for ``loom project``/``loom generate`` (a static file sitting +next to ``pyproject.toml``), never for ``loom wheel``/``embed-wheel`` (the +real wheel's own metadata is ground truth and never consults a lock) or +``loom env`` (live introspection of what's actually installed is strictly +more authoritative than a lock that may be stale relative to it). +""" + +from __future__ import annotations + +import logging +from pathlib import Path +from typing import Any + +from pitloom.extract._toml_io import TOMLDecodeError, load_toml_file + +log = logging.getLogger(__name__) + +__all__ = ["extract_pylock_dependencies"] + +_NON_REGISTRY_SOURCE_KEYS = ("vcs", "directory", "archive") + + +def extract_pylock_dependencies(project_dir: Path) -> list[str]: + """Read ``pylock.toml`` next to ``pyproject.toml`` and return its + resolved packages as exact-pin PEP 508 strings. + + Returns an empty list when no ``pylock.toml`` is present, or when it + can't be parsed -- this is optional enrichment, never a requirement. + + Unlike ``poetry.lock``, PEP 751 has no ``groups``-style per-package + membership to filter on: a ``pylock.toml`` is already the flattened, + fully resolved package set for whichever extras/dependency-groups the + tool that generated it was asked to include, so every ``[[packages]]`` + entry is taken as-is. + """ + lock_path = project_dir / "pylock.toml" + try: + data = load_toml_file(lock_path) + except FileNotFoundError: + return [] + except (OSError, TOMLDecodeError) as exc: + log.warning("Failed to parse %s: %s", lock_path, exc) + return [] + + if not isinstance(data.get("lock-version"), str): + log.warning( + "%s: missing or non-string top-level 'lock-version' key -- " + "ignoring pylock.toml", + lock_path, + ) + return [] + + packages = data.get("packages", []) + if not isinstance(packages, list): + log.warning( + "%s: top-level 'packages' key is %s, expected a list -- " + "ignoring pylock.toml", + lock_path, + type(packages).__name__, + ) + return [] + + dependencies: list[str] = [] + for pkg in packages: + dep = _pinned_dep_for_package(pkg) + if dep is not None: + dependencies.append(dep) + return dependencies + + +def _pinned_dep_for_package(pkg: Any) -> str | None: + """Return ``name==version`` for one ``[[packages]]`` table entry, or + ``None`` when it's malformed or sourced from a location that + ``name==version`` can't represent. + + A package pinned via ``vcs``, ``directory``, or ``archive`` (PEP 751's + non-registry source tables) has no meaningful PyPI version pin, so + including it here would misrepresent it as an ordinary published + release (wrong PURL, bogus PyPI enrichment lookup) -- mirrors + ``poetry.lock``'s equivalent ``directory``/``file``/``git``/``url`` + skip in :func:`pitloom.extract._poetry_lock._pinned_dep_for_package`. + A registry-resolved package sourced via ``sdist``/``wheels`` (or with + no source table at all) is always included when it has a version. + """ + if not isinstance(pkg, dict): + log.warning( + "Skipping malformed pylock.toml [[packages]] entry: expected a " + "table, got %s", + type(pkg).__name__, + ) + return None + name = pkg.get("name") + if not isinstance(name, str) or not name: + log.warning( + "Skipping malformed pylock.toml [[packages]] entry: missing or " + "non-string 'name' (name=%r)", + name, + ) + return None + non_registry_source = next( + (key for key in _NON_REGISTRY_SOURCE_KEYS if key in pkg), None + ) + if non_registry_source is not None: + log.warning( + "Skipping pylock.toml entry %r: %s-sourced dependencies cannot " + "be represented as a PEP 508 specifier", + name, + non_registry_source, + ) + return None + version = pkg.get("version") + if not isinstance(version, str) or not version: + log.warning( + "Skipping pylock.toml entry %r: missing or non-string 'version'", + name, + ) + return None + return f"{name}=={version}" diff --git a/src/pitloom/extract/_pyproject.py b/src/pitloom/extract/_pyproject.py index 0d2332fd..2b07270d 100644 --- a/src/pitloom/extract/_pyproject.py +++ b/src/pitloom/extract/_pyproject.py @@ -55,10 +55,8 @@ def _read_pyproject_fallback( prov["name"] = "Source: pyproject.toml | Field: project.name" if license_prov: prov["license"] = license_prov - return ( - ProjectMetadata(name=name, license_name=license_name, provenance=prov), - pitloom_config, - ) + metadata = ProjectMetadata(name=name, license_name=license_name, provenance=prov) + return metadata, pitloom_config def _is_license_classifier_conflict(exc: ConfigurationError) -> bool: diff --git a/tests/extract/test_pylock.py b/tests/extract/test_pylock.py new file mode 100644 index 00000000..f7fcaa82 --- /dev/null +++ b/tests/extract/test_pylock.py @@ -0,0 +1,215 @@ +# SPDX-FileContributor: Arthit Suriyawongkul +# SPDX-FileCopyrightText: 2026-present Arthit Suriyawongkul +# SPDX-FileType: SOURCE +# SPDX-License-Identifier: Apache-2.0 + +"""Tests for PEP 751 ``pylock.toml`` dependency parsing +(:mod:`pitloom.extract._pylock`) and its overlay onto +``ProjectMetadata.locked_dependencies`` via ``read_pyproject()``. + +See also: test_poetry_lock.py for the sibling ``poetry.lock`` extractor +this module's tests mirror in shape. +""" + +import logging +import tempfile +from pathlib import Path + +import pytest + +from pitloom.extract._pylock import _pinned_dep_for_package, extract_pylock_dependencies +from pitloom.extract._pyproject import read_pyproject + +_LOCK_VERSION = 'lock-version = "1.0"\ncreated-by = "test"\n' + + +def _write_lock(tmp_dir: Path, packages: str = "") -> None: + (tmp_dir / "pylock.toml").write_text(_LOCK_VERSION + packages, encoding="utf-8") + + +def test_no_lock_file_returns_empty_list() -> None: + with tempfile.TemporaryDirectory() as tmp: + assert not extract_pylock_dependencies(Path(tmp)) + + +def test_malformed_toml_returns_empty_list_and_warns( + caplog: pytest.LogCaptureFixture, +) -> None: + with tempfile.TemporaryDirectory() as tmp: + tmp_path = Path(tmp) + (tmp_path / "pylock.toml").write_text( + "this is not [ valid toml", encoding="utf-8" + ) + + with caplog.at_level(logging.WARNING): + result = extract_pylock_dependencies(tmp_path) + + assert not result + assert "Failed to parse" in caplog.text + + +def test_missing_lock_version_returns_empty_list_and_warns( + caplog: pytest.LogCaptureFixture, +) -> None: + with tempfile.TemporaryDirectory() as tmp: + tmp_path = Path(tmp) + (tmp_path / "pylock.toml").write_text( + '[[packages]]\nname = "requests"\nversion = "2.31.0"\n', + encoding="utf-8", + ) + + with caplog.at_level(logging.WARNING): + result = extract_pylock_dependencies(tmp_path) + + assert not result + assert "lock-version" in caplog.text + + +def test_package_included() -> None: + with tempfile.TemporaryDirectory() as tmp: + tmp_path = Path(tmp) + _write_lock(tmp_path, '[[packages]]\nname = "requests"\nversion = "2.31.0"\n') + + assert extract_pylock_dependencies(tmp_path) == ["requests==2.31.0"] + + +def test_packages_key_not_a_list_returns_empty_list_and_warns( + caplog: pytest.LogCaptureFixture, +) -> None: + with tempfile.TemporaryDirectory() as tmp: + tmp_path = Path(tmp) + _write_lock(tmp_path, 'packages = "not-a-list"\n') + + with caplog.at_level(logging.WARNING): + result = extract_pylock_dependencies(tmp_path) + + assert not result + assert "expected a list" in caplog.text + + +def test_pinned_dep_for_package_non_dict_entry_returns_none() -> None: + assert _pinned_dep_for_package("not-a-dict") is None + assert _pinned_dep_for_package(["still", "not", "a", "dict"]) is None + + +def test_malformed_package_entry_skipped_and_warns( + caplog: pytest.LogCaptureFixture, +) -> None: + with tempfile.TemporaryDirectory() as tmp: + tmp_path = Path(tmp) + _write_lock( + tmp_path, + '[[packages]]\nversion = "1.0.0"\n\n' + '[[packages]]\nname = "complete-pkg"\nversion = "2.0.0"\n', + ) + + with caplog.at_level(logging.WARNING): + result = extract_pylock_dependencies(tmp_path) + + assert result == ["complete-pkg==2.0.0"] + assert "malformed" in caplog.text.lower() + + +def test_missing_version_skipped_and_warns(caplog: pytest.LogCaptureFixture) -> None: + with tempfile.TemporaryDirectory() as tmp: + tmp_path = Path(tmp) + _write_lock(tmp_path, '[[packages]]\nname = "no-version"\n') + + with caplog.at_level(logging.WARNING): + result = extract_pylock_dependencies(tmp_path) + + assert not result + assert "missing" in caplog.text.lower() + + +@pytest.mark.parametrize("source_key", ["vcs", "directory", "archive"]) +def test_non_registry_sourced_package_excluded( + source_key: str, caplog: pytest.LogCaptureFixture +) -> None: + """A package pinned via `vcs`/`directory`/`archive` has no meaningful + PyPI version pin -- excluded the same way poetry.lock's equivalent + non-registry sources are excluded.""" + with tempfile.TemporaryDirectory() as tmp: + tmp_path = Path(tmp) + _write_lock( + tmp_path, + '[[packages]]\nname = "local-dep"\nversion = "0.1.0"\n' + f'[packages.{source_key}]\nurl = "https://example.com"\n', + ) + + with caplog.at_level(logging.WARNING): + result = extract_pylock_dependencies(tmp_path) + + assert not result + assert "local-dep" in caplog.text + + +def test_sdist_sourced_package_included() -> None: + with tempfile.TemporaryDirectory() as tmp: + tmp_path = Path(tmp) + _write_lock( + tmp_path, + '[[packages]]\nname = "requests"\nversion = "2.31.0"\n' + '[packages.sdist]\nurl = "https://example.com/requests-2.31.0.tar.gz"\n', + ) + + assert extract_pylock_dependencies(tmp_path) == ["requests==2.31.0"] + + +def test_read_pyproject_populates_locked_dependencies() -> None: + """Integration: `read_pyproject()` overlays `pylock.toml` parsing onto + `ProjectMetadata.locked_dependencies` with its own provenance entry, + for a plain PEP 621 project (no `[tool.poetry]` involved).""" + with tempfile.TemporaryDirectory() as tmp: + tmp_path = Path(tmp) + (tmp_path / "pyproject.toml").write_text( + '[project]\nname = "pkg"\nversion = "1.0.0"\n', encoding="utf-8" + ) + _write_lock(tmp_path, '[[packages]]\nname = "requests"\nversion = "2.31.0"\n') + + metadata, _config = read_pyproject(tmp_path / "pyproject.toml") + + assert metadata.locked_dependencies == ["requests==2.31.0"] + assert metadata.provenance["locked_dependencies"] == ( + "Source: pylock.toml | Method: resolved_lockfile" + ) + + +def test_read_pyproject_no_lock_file_leaves_locked_dependencies_empty() -> None: + with tempfile.TemporaryDirectory() as tmp: + tmp_path = Path(tmp) + (tmp_path / "pyproject.toml").write_text( + '[project]\nname = "pkg"\nversion = "1.0.0"\n', encoding="utf-8" + ) + + metadata, _config = read_pyproject(tmp_path / "pyproject.toml") + + assert metadata.locked_dependencies == [] + assert "locked_dependencies" not in metadata.provenance + + +def test_read_pyproject_pylock_takes_priority_over_poetry_lock( + caplog: pytest.LogCaptureFixture, +) -> None: + """Regression: when both a `poetry.lock` and a `pylock.toml` are + present, PEP 751's `pylock.toml` wins -- and the override is never + silent.""" + with tempfile.TemporaryDirectory() as tmp: + tmp_path = Path(tmp) + (tmp_path / "pyproject.toml").write_text( + '[tool.poetry]\nname = "pkg"\nversion = "1.0.0"\n', encoding="utf-8" + ) + (tmp_path / "poetry.lock").write_text( + '[[package]]\nname = "requests"\nversion = "2.31.0"\ngroups = ["main"]\n', + encoding="utf-8", + ) + _write_lock(tmp_path, '[[packages]]\nname = "httpx"\nversion = "0.27.0"\n') + + with caplog.at_level(logging.WARNING): + metadata, _config = read_pyproject(tmp_path / "pyproject.toml") + + assert metadata.locked_dependencies == ["httpx==0.27.0"] + assert metadata.provenance["locked_dependencies"] == ( + "Source: pylock.toml | Method: resolved_lockfile" + ) + assert "pylock.toml (PEP 751) takes priority" in caplog.text diff --git a/working-docs/design/lock-files.md b/working-docs/design/lock-files.md index 4545f3f5..2144c4b4 100644 --- a/working-docs/design/lock-files.md +++ b/working-docs/design/lock-files.md @@ -1,6 +1,6 @@ --- Created: 2026-08-31 -Last-Modified: 2026-08-31 +Last-Modified: 2026-09-02 SPDX-FileCopyrightText: 2026-present Arthit Suriyawongkul SPDX-FileType: DOCUMENTATION SPDX-License-Identifier: CC0-1.0 @@ -18,6 +18,18 @@ design (source-stage-only scoping, direct/transitive dedup, additive [sbom-lifecycle-stages.md](sbom-lifecycle-stages.md)'s source/build/deployed staging model, which this document's priority table doesn't use -- worth reconciling if the two priority framings diverge as more formats land. + +[pep751-pylock-support.md](../implementation/pep751-pylock-support.md) -- +`pylock.toml` (PEP 751, Phase 1's headline item) support shipped +(2026-09-02), reusing `poetry.lock`'s established shape +(`ProjectMetadata.locked_dependencies`, additive `dependsOn` edges, +`completeness` tagging, source-stage-only scoping) rather than this +document's illustrative Pydantic/CycloneDX sketch. It also settles the +"which lock file wins" question this document's intro previously left +open for the two-lock-files case: `pylock.toml` overrides an +already-applied `poetry.lock`-resolved set, since it's the +build-backend-agnostic interoperability standard. + See `working-docs/design/roadmap.md`'s "Remaining lock formats as a resolved-dependency source" item for the up-to-date status of every other format below. @@ -52,7 +64,7 @@ simply by asking users to run `[tool] export --format pylock`. | Phase | Target Format | Why It Matters for AI/ML & Python | | --- | --- | --- | -| **1: The Universal Core** | `pylock.toml` (PEP 751) | The official Python interoperability standard. Universal fallback. | +| **1: The Universal Core** | `pylock.toml` (PEP 751) | **Done (2026-09-02)** -- see the "See also" note above. The official Python interoperability standard. Universal fallback. | | | `pyproject.toml` | Standard project metadata (PEP 621) to define the root SBOM component. | | | `uv.lock` | The dominant lock file for modern, high-performance ML inference stacks (vLLM, FastAPI). | | | `requirements.txt` | Ubiquitous in ML research Dockerfiles, PyTorch deployments, and Hugging Face spaces. | diff --git a/working-docs/design/roadmap.md b/working-docs/design/roadmap.md index fb0131ab..b71a7cdf 100644 --- a/working-docs/design/roadmap.md +++ b/working-docs/design/roadmap.md @@ -155,24 +155,32 @@ table in [non-hatchling-file-discovery.md](non-hatchling-file-discovery.md)); `main`-group resolved transitive dependencies, additive to the direct constraints, source-stage-only. See [poetry-support.md](../implementation/poetry-support.md). +- [x] **`pylock.toml` (PEP 751)** -- done (2026-09-04): `loom project`/ + `loom generate` reads a sibling `pylock.toml`, when present, for its + resolved `[[packages]]` set, reusing `ProjectMetadata.locked_dependencies` + and the same additive `dependsOn`/`RelationshipCompleteness.complete` + wiring as `poetry.lock`. Build-backend-agnostic, so it's checked + unconditionally rather than gated behind `[tool.poetry]` detection. + See [pep751-pylock-support.md](../implementation/pep751-pylock-support.md) + and [lock-file-cascade.md](../implementation/lock-file-cascade.md) for + the shared priority mechanism across all lock formats. - [ ] **Remaining lock formats as a resolved-dependency source** - (`Pipfile.lock`, `uv.lock`, pinned `requirements.txt`) -- `loom - project` still records only the declared version specifier from - `pyproject.toml [project] dependencies` + (`uv.lock`, `pixi.lock`, `conda-lock.yml`, `pdm.lock`, `Pipfile.lock`, + pinned `requirements.txt`) -- `loom project` still records only the + declared version specifier from `pyproject.toml [project] dependencies` (`normalize_dependency_specifier`, `src/pitloom/extract/_pyproject.py:220`, - e.g. `requests>=2.0`) for every non-Poetry project, never a concrete - resolved version. Parsing one when present would let a Source SBOM - carry the actual pinned version a build will use, not just the - declared range -- closer to what CISA's Source SBOM guidance expects. - The `poetry.lock` case above establishes the pattern (additive - transitive-only edges, `completeness` tagging, source-stage-only - scoping); needs a source-priority decision analogous to - `metadata-sources.md`'s existing tiering (which lock file wins if more - than one is present) and a provenance `method` tag per lock format. - See [lock-files.md](./lock-files.md) for the broader multi-format - extraction-priority roadmap (PEP 751 `pylock.toml`, `uv.lock`, - `pixi.lock`, `conda-lock.yml`, `pdm.lock`, `Pipfile.lock`) this item - now defers to. + e.g. `requests>=2.0`) for a project with none of `poetry.lock`/ + `pylock.toml` present, never a concrete resolved version. Parsing one + when present would let a Source SBOM carry the actual pinned version a + build will use, not just the declared range -- closer to what CISA's + Source SBOM guidance expects. The `poetry.lock`/`pylock.toml` cases + above establish the pattern (additive transitive-only edges, + `completeness` tagging, source-stage-only scoping, `pylock.toml` + overriding `poetry.lock` when both are present); each further format + added needs its own slot in that same priority order and a provenance + `method` tag. See [lock-files.md](./lock-files.md) for the broader + multi-format extraction-priority roadmap (`uv.lock`, `pixi.lock`, + `conda-lock.yml`, `pdm.lock`, `Pipfile.lock`) this item now defers to. ### PEP 770 / embed-wheel diff --git a/working-docs/implementation/pep751-pylock-support.md b/working-docs/implementation/pep751-pylock-support.md new file mode 100644 index 00000000..37bda85f --- /dev/null +++ b/working-docs/implementation/pep751-pylock-support.md @@ -0,0 +1,139 @@ +--- +Created: 2026-09-02 +Last-Modified: 2026-09-02 +SPDX-FileCopyrightText: 2026-present Arthit Suriyawongkul +SPDX-FileType: DOCUMENTATION +SPDX-License-Identifier: CC0-1.0 +--- + +# PEP 751 (`pylock.toml`) support -- implementation notes + +See also: [poetry-support.md](poetry-support.md)'s "`poetry.lock` +transitive dependencies" section -- this feature reuses that shape +almost unchanged; [lock-files.md](../design/lock-files.md) for the +broader multi-format lock-file roadmap this closes Phase 1's headline +item of; [sbom-lifecycle-stages.md](sbom-lifecycle-stages.md) for the +source/build/deployed staging model that makes this source-stage-only. + +## Motivation + +[PEP 751] standardizes `pylock.toml` as a build-backend-agnostic, +fully resolved dependency snapshot -- produced by `uv export --format +pylock.toml`, `pdm lock --format pylock`, `poetry export +--format=pylock.toml`, and similar, consumed only by installers. Per +[lock-files.md](../design/lock-files.md), it's the "universal core" a +Python SBOM generator should support first, since it works for any +project regardless of build backend, unlike the already-shipped +`poetry.lock` support which only applies to Poetry projects. + +[PEP 751]: https://peps.python.org/pep-0751/ + +## Source files + +| File | Role | +| :--- | :--- | +| `src/pitloom/extract/_pylock.py` | `pylock.toml` resolved-dependency extraction (source-stage only) | +| `src/pitloom/extract/_pyproject.py` | Wires `pylock.toml` reading into `read_pyproject()`, unconditionally | +| `tests/extract/test_pylock.py` | `pylock.toml` parsing unit and integration tests | + +No changes were needed in `src/pitloom/assemble/spdx3/deps.py` or +`document.py` -- both already operate on the generic +`ProjectMetadata.locked_dependencies` field the `poetry.lock` work +introduced, with no knowledge of which lock format populated it. The +existing `tests/assemble/test_deps_locked_dependencies.py` suite +already covers that layer generically and needed no changes either. + +## Extraction function + +### `extract_pylock_dependencies(project_dir)` + +Reads `pylock.toml` next to `pyproject.toml` and returns its resolved +`[[packages]]` entries as exact-pin `name==version` PEP 508 strings. +Returns an empty list when no `pylock.toml` is present or it can't be +parsed -- optional enrichment, never a requirement. + +Unlike `poetry.lock`, PEP 751 has no `groups`-style per-package +membership tag to filter on: a `pylock.toml` is already the flattened, +fully resolved package set for whichever extras/dependency-groups the +tool that generated it was asked to include (`dependency-groups`/ +`default-groups`/`extras` are file-level generation inputs, not a +per-package "which group requested me" marker). So every `[[packages]]` +entry is taken as-is, with no group-based filtering. + +A malformed lock (missing/non-string top-level `lock-version`, a +`packages` key that isn't a list, or an individual `[[packages]]` entry +missing/non-string `name`/`version`) is skipped with a `WARNING:`, not +silently dropped, per this repo's "no silent deviations" rule -- +mirrors `poetry.lock`'s equivalent malformed-entry handling. + +## Non-registry sources + +A package pinned via PEP 751's `vcs`, `directory`, or `archive` source +tables has no meaningful PyPI version pin, so including it as +`name==version` would misrepresent it as an ordinary published release +(wrong PURL, bogus PyPI enrichment lookup downstream). These are +skipped with a `WARNING:` naming the package and source kind -- +mirrors `poetry.lock`'s equivalent `directory`/`file`/`git`/`url` skip +in `_poetry_lock.py`. A package sourced via `sdist`/`wheels` (or with no +source table at all) is included whenever it has a version. + +## Wiring into `read_pyproject()` + +`_apply_pylock_dependencies()` is called unconditionally at the end of +every `read_pyproject()` code path (both the `[project]`-primary path +and the `[tool.poetry]`/no-`[project]` fallback path), overlaying +`pylock.toml`'s resolved dependencies onto `ProjectMetadata` in place +when a `pylock.toml` is present. + +This differs from `poetry.lock`'s wiring in one deliberate way: +`poetry.lock` reading is gated behind `[tool.poetry]` detection inside +`_try_read_poetry()`, since `poetry.lock` only makes sense for a Poetry +project. `pylock.toml` is build-backend-agnostic -- a plain PEP 621 +project with no Poetry involvement at all can have one -- so it's +checked unconditionally in `read_pyproject()` itself, independent of +which metadata-extraction branch ran. + +No `include_locked_dependencies`-style build-stage guard was needed +here: unlike `poetry.lock` (whose gap-fill helper `_try_read_poetry()` +is also called directly by the Hatchling build hook's +`_poetry_fallback_metadata()`, which must pass +`include_locked_dependencies=False` to avoid leaking a source-stage +artifact into a build-stage SBOM), `read_pyproject()` itself is never +called from the Hatchling build hook -- only from +`pitloom.extract.project.read_project()`, the CLI/library source-stage +path. So the unconditional call is already scoped correctly without +needing an extra parameter. + +## Priority when both `poetry.lock` and `pylock.toml` are present + +This is [lock-files.md](../design/lock-files.md)'s previously-open +"which lock file wins" question for the two-lock-files case: +`pylock.toml` -- the newer, build-backend-agnostic interoperability +standard -- always overrides an already-applied `poetry.lock`-resolved +set. `_apply_pylock_dependencies()` logs a `WARNING:` naming the +override whenever both are present, per this repo's "no silent +deviations" rule; it never merges the two sets. + +## Known limitations + +- **No dependency graph, same as `poetry.lock`.** PEP 751 packages can + declare a `dependencies` list identifying their own resolved pins + (useful for reconstructing a full transitive graph), but this + extractor -- matching `poetry.lock`'s existing flat-list shape -- + only returns the flattened package list, not that graph. Every + locked package not already a direct dependency gets one additive + `dependsOn` edge straight from the main package + (`_locked_transitive_only_dependencies()` in `document.py`), the same + as `poetry.lock`. +- **No marker evaluation.** A `pylock.toml` entry may carry a `marker` + (environment marker) restricting when it applies (e.g. a + platform-specific package). This extractor doesn't evaluate markers + against any particular environment -- every `[[packages]]` entry is + included regardless, the same simplification `poetry.lock` parsing + already makes for direct dependency constraints. +- **`pylock..toml` named locks are not discovered.** PEP 751 + allows a `pylock..toml` naming convention (e.g. + `pylock.dev.toml`) for multiple named locks in one project; only the + bare `pylock.toml` is read. Extending discovery to named locks would + need its own priority/selection rule and is left for a future change + if a real project motivates it. From 60e8138ca9bda87934d61c097399cab67151d461 Mon Sep 17 00:00:00 2001 From: Arthit Suriyawongkul Date: Fri, 4 Sep 2026 16:24:00 +0700 Subject: [PATCH 02/35] Fix provenence Signed-off-by: Arthit Suriyawongkul --- pyproject.toml | 3 + src/pitloom/assemble/_model_generator.py | 3 + src/pitloom/assemble/spdx3/document.py | 1 + src/pitloom/cli/commands/embed_wheel.py | 13 +- src/pitloom/cli/options.py | 8 +- src/pitloom/core/models.py | 18 + src/pitloom/embed.py | 5 +- src/pitloom/extract/_locked_dependencies.py | 96 + src/pitloom/extract/_pylock.py | 8 +- src/pitloom/extract/project.py | 44 +- .../assemble/test_deps_locked_dependencies.py | 55 + .../test_model_generator_doc_identity.py | 48 +- tests/extract/test_locked_dependencies.py | 130 ++ tests/extract/test_poetry_lock.py | 59 + tests/extract/test_project.py | 57 + tests/extract/test_pylock.py | 66 +- tests/fixtures/real-world-locks/README.md | 107 + .../poetry/cleo-2.1.0/poetry.lock | 1359 +++++++++++++ .../poetry/cleo-2.1.0/pyproject.toml | 149 ++ .../poetry/pastel-0.2.1/poetry.lock | 572 ++++++ .../poetry/pastel-0.2.1/pyproject.toml | 27 + .../poetry/pendulum-3.2.0/poetry.lock | 1366 +++++++++++++ .../poetry/pendulum-3.2.0/pyproject.toml | 227 +++ .../poetry/tomlkit-0.15.1/poetry.lock | 1205 +++++++++++ .../poetry/tomlkit-0.15.1/pyproject.toml | 68 + .../pylock/pipenv-2026.8.0/pylock.toml | 1774 +++++++++++++++++ .../pylock/pipenv-2026.8.0/pyproject.toml | 338 ++++ .../pylock/snowflake-cli-3.26.0/LICENSE | 201 ++ .../pylock/snowflake-cli-3.26.0/pylock.toml | 839 ++++++++ .../snowflake-cli-3.26.0/pyproject.toml | 249 +++ tests/fixtures/real-world-projects/README.md | 24 +- .../implementation/lock-file-cascade.md | 154 ++ .../implementation/pep751-pylock-support.md | 69 +- 33 files changed, 9256 insertions(+), 86 deletions(-) create mode 100644 src/pitloom/extract/_locked_dependencies.py create mode 100644 tests/extract/test_locked_dependencies.py create mode 100644 tests/fixtures/real-world-locks/README.md create mode 100644 tests/fixtures/real-world-locks/poetry/cleo-2.1.0/poetry.lock create mode 100644 tests/fixtures/real-world-locks/poetry/cleo-2.1.0/pyproject.toml create mode 100644 tests/fixtures/real-world-locks/poetry/pastel-0.2.1/poetry.lock create mode 100644 tests/fixtures/real-world-locks/poetry/pastel-0.2.1/pyproject.toml create mode 100644 tests/fixtures/real-world-locks/poetry/pendulum-3.2.0/poetry.lock create mode 100644 tests/fixtures/real-world-locks/poetry/pendulum-3.2.0/pyproject.toml create mode 100644 tests/fixtures/real-world-locks/poetry/tomlkit-0.15.1/poetry.lock create mode 100644 tests/fixtures/real-world-locks/poetry/tomlkit-0.15.1/pyproject.toml create mode 100644 tests/fixtures/real-world-locks/pylock/pipenv-2026.8.0/pylock.toml create mode 100644 tests/fixtures/real-world-locks/pylock/pipenv-2026.8.0/pyproject.toml create mode 100644 tests/fixtures/real-world-locks/pylock/snowflake-cli-3.26.0/LICENSE create mode 100644 tests/fixtures/real-world-locks/pylock/snowflake-cli-3.26.0/pylock.toml create mode 100644 tests/fixtures/real-world-locks/pylock/snowflake-cli-3.26.0/pyproject.toml create mode 100644 working-docs/implementation/lock-file-cascade.md diff --git a/pyproject.toml b/pyproject.toml index 04a0d687..8b44c3ff 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -194,10 +194,13 @@ exclude = [ "tests/fixtures/croissant/*.json", "tests/fixtures/fragments/*.json", "tests/fixtures/huggingface-hub/*.txt", + "tests/fixtures/real-world-projects/", + "tests/fixtures/real-world-locks/", ] [tool.hatch.build.targets.wheel] packages = ["src/pitloom"] +exclude = ["tests/fixtures/"] [tool.hatch.version] path = "src/pitloom/__about__.py" diff --git a/src/pitloom/assemble/_model_generator.py b/src/pitloom/assemble/_model_generator.py index 44f5a719..121f188d 100644 --- a/src/pitloom/assemble/_model_generator.py +++ b/src/pitloom/assemble/_model_generator.py @@ -77,6 +77,9 @@ def _project_doc_identity(project_dir: Path) -> tuple[str, str]: dependencies=project_metadata.dependencies, merkle_root=merkle_root, locked_dependencies=project_metadata.locked_dependencies, + locked_dependencies_provenance=project_metadata.provenance.get( + "locked_dependencies" + ), ) return project_metadata.name, doc_uuid diff --git a/src/pitloom/assemble/spdx3/document.py b/src/pitloom/assemble/spdx3/document.py index 76cd3d7d..02387720 100644 --- a/src/pitloom/assemble/spdx3/document.py +++ b/src/pitloom/assemble/spdx3/document.py @@ -223,6 +223,7 @@ def build( dependencies=metadata.dependencies, merkle_root=merkle_root, locked_dependencies=metadata.locked_dependencies, + locked_dependencies_provenance=metadata.provenance.get("locked_dependencies"), ) _clear_doc_counters(doc_uuid) diff --git a/src/pitloom/cli/commands/embed_wheel.py b/src/pitloom/cli/commands/embed_wheel.py index 226ae539..cb8ca74b 100644 --- a/src/pitloom/cli/commands/embed_wheel.py +++ b/src/pitloom/cli/commands/embed_wheel.py @@ -155,10 +155,17 @@ def _resolve_project_dir_and_config( to cwd with no project file there is not an error (a standalone-wheel embed with no project directory is a legitimate use), so that case silently returns the default config with `project_dir=None`. + + Only ``[tool.pitloom]`` config is used here; ``read_project()``'s + lock/pin cascade is skipped (``include_locked_dependencies=False``) + -- ``embed-wheel`` is build-stage, and a source-stage lock file's + resolved dependencies must never leak into a wheel-embedded SBOM. """ if project_dir is None: try: - _, pitloom_config, _ = read_project(Path.cwd()) + _, pitloom_config, _ = read_project( + Path.cwd(), include_locked_dependencies=False + ) return Path.cwd(), pitloom_config except FileNotFoundError: return None, PitloomConfig() @@ -168,7 +175,9 @@ def _resolve_project_dir_and_config( print(f"ERROR: project directory not found: {proj_path}", file=sys.stderr) return None try: - _, pitloom_config, _ = read_project(proj_path) + _, pitloom_config, _ = read_project( + proj_path, include_locked_dependencies=False + ) except FileNotFoundError as exc: # read_project()'s own message already names the specific reason # (no config file at all, vs. a config file present but resolving diff --git a/src/pitloom/cli/options.py b/src/pitloom/cli/options.py index d6ffa24a..66b3c008 100644 --- a/src/pitloom/cli/options.py +++ b/src/pitloom/cli/options.py @@ -220,7 +220,13 @@ def _resolve_common_options( lookup_dir = lookup_dir.parent try: - _, pitloom_config, _ = read_project(lookup_dir) + # Only [tool.pitloom] config is used here, shared across every + # subcommand (including build-stage ones like embed-wheel) -- + # skip the lock/pin cascade so it never runs for a caller that + # would discard the result anyway. + _, pitloom_config, _ = read_project( + lookup_dir, include_locked_dependencies=False + ) except FileNotFoundError: pitloom_config = PitloomConfig() else: diff --git a/src/pitloom/core/models.py b/src/pitloom/core/models.py index 2a91f316..bc5cba85 100644 --- a/src/pitloom/core/models.py +++ b/src/pitloom/core/models.py @@ -108,6 +108,7 @@ def compute_doc_uuid( dependencies: list[str], merkle_root: str | None = None, locked_dependencies: list[str] | None = None, + locked_dependencies_provenance: str | None = None, ) -> str: """Compute a deterministic UUIDv5 for the SPDX document. @@ -118,12 +119,29 @@ def compute_doc_uuid( describing different dependency content. Omitted or empty leaves the seed byte-identical to a document with no locked dependencies at all, so every non-Poetry (and lock-less Poetry) document is unaffected. + + *locked_dependencies_provenance* (the resolved + ``ProjectMetadata.provenance["locked_dependencies"]`` string, e.g. + ``"Source: pylock.toml | Method: resolved_lockfile"``) is folded in + too, alongside *locked_dependencies* itself: as more lock/pin formats + land in ``pitloom.extract._locked_dependencies``'s cascade, two + different formats can plausibly resolve to the identical dependency + set for a small + project -- e.g. a ``poetry.lock``-only run and a ``pylock.toml``-only + run of the same project landing on the same pins. Seeding on + dependency content alone would collide those two documents' UUIDs + despite their generated ``provenance["locked_dependencies"]`` fields + (and any override note) differing -- a real content difference the + seed is supposed to guard against. Omitted or empty leaves the seed + unaffected, same as *locked_dependencies*. """ normalized_deps = sorted(_normalize_dep(dep) for dep in dependencies) seed = "\x00".join([name, version, "\x00".join(normalized_deps)]) if locked_dependencies: normalized_locked = sorted(_normalize_dep(dep) for dep in locked_dependencies) seed += "\x00" + "\x00".join(normalized_locked) + if locked_dependencies_provenance: + seed += "\x00" + locked_dependencies_provenance if merkle_root is not None: seed += "\x00" + merkle_root return str(uuid5(PITLOOM_NS, seed)) diff --git a/src/pitloom/embed.py b/src/pitloom/embed.py index 55e58718..dfdcce83 100644 --- a/src/pitloom/embed.py +++ b/src/pitloom/embed.py @@ -362,7 +362,10 @@ def _generate_embed_sbom_json( proj_root = Path(project_dir).resolve() if pitloom_config is None: - _, cfg, _ = read_project(proj_root) + # Only [tool.pitloom] config is used here -- skip the lock/pin + # cascade (embed-wheel is build-stage; a source-stage lock file's + # resolved dependencies must never leak into an embedded SBOM). + _, cfg, _ = read_project(proj_root, include_locked_dependencies=False) else: cfg = pitloom_config diff --git a/src/pitloom/extract/_locked_dependencies.py b/src/pitloom/extract/_locked_dependencies.py new file mode 100644 index 00000000..21d0a4b0 --- /dev/null +++ b/src/pitloom/extract/_locked_dependencies.py @@ -0,0 +1,96 @@ +# SPDX-FileContributor: Arthit Suriyawongkul +# SPDX-FileCopyrightText: 2026-present Arthit Suriyawongkul +# SPDX-FileType: SOURCE +# SPDX-License-Identifier: Apache-2.0 + +"""Priority cascade choosing which lock/pin format supplies +:attr:`~pitloom.core.project.ProjectMetadata.locked_dependencies`. + +Called once from :func:`pitloom.extract.project.read_project`, after +metadata resolution succeeds regardless of which source won +(``pyproject.toml``, a ``pyproject.toml``-with-no-usable-``[project]`` +fallback to ``setup.cfg``/``setup.py``, or ``setup.cfg``/``setup.py`` +alone) -- not from :func:`pitloom.extract._pyproject.read_pyproject`. +Several of the formats this module cascades over (``Pipfile.lock``, +pinned ``requirements.txt``) predate PEP 621 almost entirely and pair +with a bare ``setup.py`` in real projects, never a ``pyproject.toml``, +so a cascade wired only inside ``read_pyproject()`` would never see them. + +``poetry.lock`` is *not* one of the sources listed here: it stays gated +inside :func:`pitloom.extract._pyproject._try_read_poetry`'s +``include_locked_dependencies`` build-stage flag, since it only ever +makes sense alongside a ``[tool.poetry]`` table, which requires +``pyproject.toml`` to exist regardless. This cascade runs *after* that +poetry.lock resolution, so a higher-priority format here can still +override an already-set poetry.lock result -- see +:data:`_LOCK_SOURCES`'s ordering. +""" + +from __future__ import annotations + +import logging +from collections.abc import Callable +from pathlib import Path + +from pitloom.assemble.spdx3._provenance_encoders import parse_provenance_value +from pitloom.core.project import ProjectMetadata +from pitloom.extract._pylock import extract_pylock_dependencies + +log = logging.getLogger(__name__) + +__all__ = ["apply_locked_dependencies"] + +_LockExtractor = Callable[[Path], list[str]] + +#: Priority-ordered (highest first) lock/pin sources this cascade +#: chooses among. Each entry is ``(source filename, extractor function, +#: provenance Method tag)``. The extractor always takes a project +#: directory and returns exact-pin PEP 508 strings, or an empty list +#: when the source is absent/unusable. See +#: ``working-docs/design/roadmap.md``'s "Remaining lock formats" item +#: for why this order was chosen (build-backend-agnostic and universal +#: beats tool-specific; a real resolver lock beats a merely-pinned file). +_LOCK_SOURCES: list[tuple[str, _LockExtractor, str]] = [ + ("pylock.toml", extract_pylock_dependencies, "resolved_lockfile"), +] + + +def apply_locked_dependencies(metadata: ProjectMetadata, project_dir: Path) -> None: + """Overlay the highest-priority available lock/pin source's resolved + dependencies onto *metadata*, in place. + + Tries each entry of :data:`_LOCK_SOURCES` in priority order; the + first one that yields a non-empty result wins and every lower + priority source is left unconsidered. If *metadata* already carries + a ``locked_dependencies`` result (from an already-applied + ``poetry.lock``, or nothing at all), a winning source here replaces + it and a ``WARNING:`` names the override -- and, per this repo's "no + silent deviations" principle, the fact that a source was superseded + is also recorded in the resulting ``provenance["locked_dependencies"]`` + string itself (as a trailing ``| Note: supersedes ``), not only + logged, so a reader of the generated SBOM can see it too. + """ + for source_name, extractor, method in _LOCK_SOURCES: + dependencies = extractor(project_dir) + if not dependencies: + continue + + provenance = f"Source: {source_name} | Method: {method}" + previous = metadata.provenance.get("locked_dependencies") + if previous is not None: + superseded = parse_provenance_value(previous).get( + "source", "unknown source" + ) + log.warning( + "%s: both %s and %s resolved-dependency data are present -- " + "%s takes priority", + project_dir, + superseded, + source_name, + source_name, + ) + provenance += f" | Note: supersedes {superseded}" + + metadata.locked_dependencies = dependencies + metadata.provenance["locked_dependencies"] = provenance + return diff --git a/src/pitloom/extract/_pylock.py b/src/pitloom/extract/_pylock.py index 519cd923..d200cc29 100644 --- a/src/pitloom/extract/_pylock.py +++ b/src/pitloom/extract/_pylock.py @@ -8,10 +8,10 @@ See also: :mod:`pitloom.extract._poetry_lock` (the ``poetry.lock`` extractor this module mirrors in shape -- same source-stage-only scoping, same ``name==version`` output, same "no silent deviations" warning -policy) and :func:`pitloom.extract._pyproject._apply_pylock_dependencies` -(the call site that overlays this module's output onto -``ProjectMetadata.locked_dependencies``, taking priority over any -``poetry.lock``-resolved set already present). +policy) and :mod:`pitloom.extract._locked_dependencies` (the cascade +module that calls this extractor and overlays its output onto +``ProjectMetadata.locked_dependencies``, in priority order against every +other lock format). ``pylock.toml`` (PEP 751) is the build-backend-agnostic Python interoperability standard for recording a fully resolved dependency set -- diff --git a/src/pitloom/extract/project.py b/src/pitloom/extract/project.py index 641ec2cc..0806f33d 100644 --- a/src/pitloom/extract/project.py +++ b/src/pitloom/extract/project.py @@ -18,6 +18,7 @@ from pitloom.core.config import PitloomConfig from pitloom.core.project import ProjectMetadata +from pitloom.extract._locked_dependencies import apply_locked_dependencies from pitloom.extract._pyproject import read_pyproject from pitloom.extract._sdist import read_sdist from pitloom.extract._setuptools import read_setuptools @@ -37,6 +38,8 @@ def _is_sdist_archive(path: Path) -> bool: def read_project( project_path: Path, + *, + include_locked_dependencies: bool = True, ) -> tuple[ProjectMetadata, PitloomConfig, Path | None]: """Resolve project metadata and Pitloom config from *project_path*. @@ -45,8 +48,28 @@ def read_project( Otherwise, treats *project_path* as a directory and tries ``pyproject.toml`` first, then ``setup.cfg``/``setup.py``. + For every directory-based resolution (not the sdist-archive case), + also overlays a sibling lock/pin file's resolved dependencies onto + the result via a single, shared call to + :func:`pitloom.extract._locked_dependencies.apply_locked_dependencies` + -- applied uniformly regardless of which metadata source won, since + some lock formats (``Pipfile.lock``, pinned ``requirements.txt``) + pair with a bare ``setup.py`` in real projects, never ``pyproject.toml``. + + ``include_locked_dependencies``, mirroring + :func:`pitloom.extract._pyproject._try_read_poetry`'s + ``poetry.lock``-specific flag of the same name, lets a build-stage or + config-only caller (e.g. ``embed-wheel``, or a shared CLI helper that + only wants ``[tool.pitloom]`` settings and discards the metadata) + explicitly opt out -- source-stage lock/pin data must never leak into + a build-stage SBOM, and skipping the cascade here also skips its file + I/O for a caller that would discard the result anyway. + Args: project_path: Project root directory or sdist archive path. + include_locked_dependencies: Whether to overlay a sibling lock/pin + file's resolved dependencies (default ``True``). Pass + ``False`` from any build-stage or metadata-discarding caller. Returns: A 3-tuple of: @@ -73,6 +96,7 @@ def read_project( setup_cfg = project_path / "setup.cfg" setup_py = project_path / "setup.py" + config_path: Path | None pyproject_path = project_path / "pyproject.toml" if pyproject_path.exists(): metadata, pitloom_config = read_pyproject(pyproject_path) @@ -103,17 +127,19 @@ def read_project( if pitloom_config == PitloomConfig(): pitloom_config = setuptools_pitloom_config config_path = setup_cfg if setup_cfg.exists() else setup_py - return metadata, pitloom_config, config_path - return metadata, pitloom_config, pyproject_path - - if setup_cfg.exists() or setup_py.exists(): + else: + config_path = pyproject_path + elif setup_cfg.exists() or setup_py.exists(): metadata, pitloom_config = read_setuptools(project_path) config_path = setup_cfg if setup_cfg.exists() else setup_py - return metadata, pitloom_config, config_path - - raise FileNotFoundError( - f"No pyproject.toml, setup.cfg, or setup.py found in {project_path}" - ) + else: + raise FileNotFoundError( + f"No pyproject.toml, setup.cfg, or setup.py found in {project_path}" + ) + + if include_locked_dependencies: + apply_locked_dependencies(metadata, project_path) + return metadata, pitloom_config, config_path __all__ = ["read_project"] diff --git a/tests/assemble/test_deps_locked_dependencies.py b/tests/assemble/test_deps_locked_dependencies.py index 38dc8e98..ed7a0d69 100644 --- a/tests/assemble/test_deps_locked_dependencies.py +++ b/tests/assemble/test_deps_locked_dependencies.py @@ -188,6 +188,61 @@ def test_locked_dependencies_change_doc_uuid() -> None: assert len({base, with_lock_a, with_lock_b}) == 3 +def test_locked_dependencies_same_content_different_provenance_changes_doc_uuid() -> ( + None +): + """Two documents with an *identical* resolved dependency set but from + different lock sources (e.g. a ``poetry.lock``-only run and a + ``pylock.toml``-only run of the same project happening to resolve to + the same pins) must not collide on the same doc UUID either -- their + ``provenance["locked_dependencies"]`` strings (and any override note) + differ, which is a real content difference in the generated document + that seeding on dependency content alone would miss.""" + same_content = ["idna==3.7"] + from_poetry = compute_doc_uuid( + "pkg", + "1.0.0", + ["requests>=2.0"], + locked_dependencies=same_content, + locked_dependencies_provenance=( + "Source: poetry.lock | Method: resolved_lockfile" + ), + ) + from_pylock = compute_doc_uuid( + "pkg", + "1.0.0", + ["requests>=2.0"], + locked_dependencies=same_content, + locked_dependencies_provenance=( + "Source: pylock.toml | Method: resolved_lockfile" + ), + ) + unattributed = compute_doc_uuid( + "pkg", "1.0.0", ["requests>=2.0"], locked_dependencies=same_content + ) + + assert len({from_poetry, from_pylock, unattributed}) == 3 + + +def test_locked_dependencies_provenance_omitted_matches_empty_string() -> None: + """Omitting ``locked_dependencies_provenance`` (every pre-existing + call site) must produce the same UUID as every caller that predates + this parameter -- purely additive, no behavior change for callers + that don't know about it.""" + omitted = compute_doc_uuid( + "pkg", "1.0.0", ["requests>=2.0"], locked_dependencies=["idna==3.7"] + ) + explicit_none = compute_doc_uuid( + "pkg", + "1.0.0", + ["requests>=2.0"], + locked_dependencies=["idna==3.7"], + locked_dependencies_provenance=None, + ) + + assert omitted == explicit_none + + def test_locked_dependencies_omitted_matches_empty_list() -> None: """Omitting ``locked_dependencies`` entirely (every pre-existing call site) must produce the same UUID as passing an empty list -- the new diff --git a/tests/assemble/test_model_generator_doc_identity.py b/tests/assemble/test_model_generator_doc_identity.py index ab57a5b4..d315e90c 100644 --- a/tests/assemble/test_model_generator_doc_identity.py +++ b/tests/assemble/test_model_generator_doc_identity.py @@ -17,13 +17,39 @@ from pathlib import Path +from spdx_python_model.bindings import v3_0_1 as spdx3 + from pitloom.assemble._model_generator import _project_doc_identity -from pitloom.core.models import compute_doc_uuid, get_wheel_files +from pitloom.assemble.spdx3.document import build +from pitloom.core.creation import CreationMetadata +from pitloom.core.document import DocumentModel +from pitloom.core.models import get_wheel_files from pitloom.extract.project import read_project FIXTURES = Path(__file__).parent.parent / "fixtures" / "projects" POETRY_FIXTURE = FIXTURES / "sampleproject-poetry" +#: A UUID's canonical string form is always exactly 36 characters +#: (8-4-4-4-12 hex, hyphen-separated) -- long enough that it can't +#: collide with a project name containing hyphens, so slicing the tail +#: off an SpdxDocument's spdxId (``https://spdx.org/spdxdocs/-``) +#: recovers the real doc_uuid `build()` used, regardless of ``. +_UUID_LENGTH = 36 + + +def _real_build_doc_uuid(doc: DocumentModel) -> str: + """Build *doc* for real via :func:`~pitloom.assemble.spdx3.document.build` + and recover the ``doc_uuid`` it actually used, by reading it back off + the emitted ``SpdxDocument`` element's ``spdxId`` -- not a second, + independently-maintained ``compute_doc_uuid()`` call that could drift + from ``build()``'s own formula without either test noticing.""" + exporter = build(doc, offline=True) + spdx_doc = next( + o for o in exporter.object_set.objects if isinstance(o, spdx3.SpdxDocument) + ) + assert spdx_doc.spdxId is not None + return spdx_doc.spdxId[-_UUID_LENGTH:] + def test_project_doc_identity_matches_build_doc_uuid_with_locked_dependencies() -> None: """For a Poetry project with a ``poetry.lock`` (non-empty @@ -32,19 +58,21 @@ def test_project_doc_identity_matches_build_doc_uuid_with_locked_dependencies() the same project -- both derive from the same :class:`~pitloom.core.project.ProjectMetadata` and ``merkle_root``. Regression test: ``_project_doc_identity`` used to omit - ``locked_dependencies`` from its ``compute_doc_uuid`` call, diverging - from ``build()``'s doc_uuid for any project with a lock file.""" + ``locked_dependencies`` (and later, ``locked_dependencies``' + provenance) from its ``compute_doc_uuid`` call, diverging from + ``build()``'s doc_uuid for any project with a lock file.""" project_metadata, _config, _config_path = read_project(POETRY_FIXTURE) assert project_metadata.locked_dependencies # guard: fixture must exercise this - merkle_root, _project_files = get_wheel_files(POETRY_FIXTURE) - expected_doc_uuid = compute_doc_uuid( - name=project_metadata.name, - version=project_metadata.version or "unknown", - dependencies=project_metadata.dependencies, - merkle_root=merkle_root, - locked_dependencies=project_metadata.locked_dependencies, + merkle_root, project_files = get_wheel_files(POETRY_FIXTURE) + project_metadata.files = project_files + doc = DocumentModel( + project=project_metadata, + creation_metadata=CreationMetadata( + creation_datetime="2026-01-01T00:00:00+00:00" + ), ) + expected_doc_uuid = _real_build_doc_uuid(doc) _doc_name, doc_uuid = _project_doc_identity(POETRY_FIXTURE) diff --git a/tests/extract/test_locked_dependencies.py b/tests/extract/test_locked_dependencies.py new file mode 100644 index 00000000..3a9808ea --- /dev/null +++ b/tests/extract/test_locked_dependencies.py @@ -0,0 +1,130 @@ +# SPDX-FileContributor: Arthit Suriyawongkul +# SPDX-FileCopyrightText: 2026-present Arthit Suriyawongkul +# SPDX-FileType: SOURCE +# SPDX-License-Identifier: Apache-2.0 + +"""Tests for the lock/pin priority cascade +(:mod:`pitloom.extract._locked_dependencies`) as a mechanism: priority +ordering, the override provenance note, and -- most importantly -- that +``read_project()`` applies it uniformly regardless of which metadata +source (``pyproject.toml`` or bare ``setup.py``/``setup.cfg``) resolved +the project's name/version. + +Per-format parsing correctness lives in each format's own +``test_.py`` (e.g. test_pylock.py, test_poetry_lock.py); this +file only exercises the cascade and wiring, using ``pylock.toml`` and +``poetry.lock`` as the two currently-registered/available sources. +""" + +import logging +import tempfile +from pathlib import Path + +import pytest + +from pitloom.core.project import ProjectMetadata +from pitloom.extract._locked_dependencies import apply_locked_dependencies +from pitloom.extract.project import read_project + + +def _write_pylock(tmp_dir: Path, name: str, version: str) -> None: + (tmp_dir / "pylock.toml").write_text( + f'lock-version = "1.0"\ncreated-by = "test"\n' + f'[[packages]]\nname = "{name}"\nversion = "{version}"\n', + encoding="utf-8", + ) + + +def test_apply_locked_dependencies_sets_provenance_when_no_prior_source() -> None: + with tempfile.TemporaryDirectory() as tmp: + tmp_path = Path(tmp) + _write_pylock(tmp_path, "requests", "2.31.0") + metadata = ProjectMetadata(name="pkg") + + apply_locked_dependencies(metadata, tmp_path) + + assert metadata.locked_dependencies == ["requests==2.31.0"] + assert metadata.provenance["locked_dependencies"] == ( + "Source: pylock.toml | Method: resolved_lockfile" + ) + + +def test_apply_locked_dependencies_no_source_present_leaves_metadata_untouched() -> ( + None +): + with tempfile.TemporaryDirectory() as tmp: + tmp_path = Path(tmp) + metadata = ProjectMetadata(name="pkg") + + apply_locked_dependencies(metadata, tmp_path) + + assert metadata.locked_dependencies == [] + assert "locked_dependencies" not in metadata.provenance + + +def test_apply_locked_dependencies_overrides_prior_source_with_note( + caplog: pytest.LogCaptureFixture, +) -> None: + """A source already recorded in `metadata.provenance` (e.g. by + `poetry.lock` via `_try_read_poetry()`, which runs before this + cascade in `read_pyproject()`) is overridden by a higher-priority + cascade entry -- logged, and recorded in the provenance string + itself, not only logged.""" + with tempfile.TemporaryDirectory() as tmp: + tmp_path = Path(tmp) + _write_pylock(tmp_path, "httpx", "0.27.0") + metadata = ProjectMetadata( + name="pkg", + locked_dependencies=["requests==2.31.0"], + provenance={ + "locked_dependencies": "Source: poetry.lock | Method: resolved_lockfile" + }, + ) + + with caplog.at_level(logging.WARNING): + apply_locked_dependencies(metadata, tmp_path) + + assert metadata.locked_dependencies == ["httpx==0.27.0"] + assert metadata.provenance["locked_dependencies"] == ( + "Source: pylock.toml | Method: resolved_lockfile " + "| Note: supersedes poetry.lock" + ) + assert "poetry.lock and pylock.toml" in caplog.text + assert "pylock.toml takes priority" in caplog.text + + +def test_read_project_applies_cascade_for_setup_py_only_project() -> None: + """Regression: a project with no `pyproject.toml` at all -- just a + bare `setup.py`, the realistic pairing for `Pipfile.lock`/pinned + `requirements.txt` in real projects -- still gets + `locked_dependencies` populated via `read_project()`'s cascade. This + is exactly the gap the cascade's `read_project()`-level call site + (rather than being wired only inside `read_pyproject()`) exists to + close.""" + with tempfile.TemporaryDirectory() as tmp: + tmp_path = Path(tmp) + (tmp_path / "setup.py").write_text( + "from setuptools import setup\nsetup(name='pkg', version='1.0')\n", + encoding="utf-8", + ) + _write_pylock(tmp_path, "requests", "2.31.0") + + metadata, _config, _path = read_project(tmp_path) + + assert metadata.name == "pkg" + assert metadata.locked_dependencies == ["requests==2.31.0"] + assert metadata.provenance["locked_dependencies"] == ( + "Source: pylock.toml | Method: resolved_lockfile" + ) + + +def test_read_project_no_metadata_source_never_reaches_cascade() -> None: + """A directory with neither `pyproject.toml` nor `setup.cfg`/ + `setup.py` still raises before the cascade runs -- a lock file alone + is optional enrichment, never a metadata source of record.""" + with tempfile.TemporaryDirectory() as tmp: + tmp_path = Path(tmp) + _write_pylock(tmp_path, "requests", "2.31.0") + + with pytest.raises(FileNotFoundError): + read_project(tmp_path) diff --git a/tests/extract/test_poetry_lock.py b/tests/extract/test_poetry_lock.py index aad9de0d..37e2b6f7 100644 --- a/tests/extract/test_poetry_lock.py +++ b/tests/extract/test_poetry_lock.py @@ -25,6 +25,9 @@ FIXTURES = Path(__file__).parent.parent / "fixtures" / "projects" POETRY_FIXTURE = FIXTURES / "sampleproject-poetry" +REAL_WORLD_LOCKS = ( + Path(__file__).parent.parent / "fixtures" / "real-world-locks" / "poetry" +) def _write_lock(tmp_dir: Path, content: str) -> None: @@ -264,3 +267,59 @@ def test_read_pyproject_no_lock_file_leaves_locked_dependencies_empty() -> None: assert metadata.locked_dependencies == [] assert "locked_dependencies" not in metadata.provenance + + +def test_real_world_pendulum_hybrid_project_and_tool_poetry_tables() -> None: + """`pendulum` declares both `[project]` (PEP 621) and `[tool.poetry]` + -- confirms the hybrid shape resolves name/version via `[project]` + while `poetry.lock` reading still runs.""" + metadata, _config = read_pyproject( + REAL_WORLD_LOCKS / "pendulum-3.2.0" / "pyproject.toml" + ) + + assert metadata.name == "pendulum" + assert metadata.locked_dependencies + assert metadata.provenance["locked_dependencies"] == ( + "Source: poetry.lock | Method: resolved_lockfile" + ) + + +def test_real_world_cleo_tool_poetry_only() -> None: + """`cleo` has `[tool.poetry]` only, no `[project]` table at all.""" + metadata, _config = read_pyproject( + REAL_WORLD_LOCKS / "cleo-2.1.0" / "pyproject.toml" + ) + + assert metadata.name == "cleo" + assert metadata.locked_dependencies + assert metadata.provenance["locked_dependencies"] == ( + "Source: poetry.lock | Method: resolved_lockfile" + ) + + +def test_real_world_pastel_tool_poetry_only() -> None: + metadata, _config = read_pyproject( + REAL_WORLD_LOCKS / "pastel-0.2.1" / "pyproject.toml" + ) + + assert metadata.name == "pastel" + assert metadata.locked_dependencies + assert metadata.provenance["locked_dependencies"] == ( + "Source: poetry.lock | Method: resolved_lockfile" + ) + + +def test_real_world_tomlkit_has_no_main_group_dependencies() -> None: + """`tomlkit` is a standalone TOML library with no runtime + dependencies -- every entry in its `poetry.lock` belongs to the + `dev`/docs/test groups, none to `main`. A real, valid "empty + resolved set" case: `read_pyproject()` still succeeds, but leaves + `locked_dependencies` empty and sets no provenance for it, same as + the no-lock-file case.""" + metadata, _config = read_pyproject( + REAL_WORLD_LOCKS / "tomlkit-0.15.1" / "pyproject.toml" + ) + + assert metadata.name == "tomlkit" + assert metadata.locked_dependencies == [] + assert "locked_dependencies" not in metadata.provenance diff --git a/tests/extract/test_project.py b/tests/extract/test_project.py index 9f6bc130..7af1d354 100644 --- a/tests/extract/test_project.py +++ b/tests/extract/test_project.py @@ -96,6 +96,38 @@ def test_read_project_fallback_preserves_pyproject_pitloom_config( assert pitloom_config.sbom_basename == "custom-name" +def test_read_project_fallback_still_applies_lock_cascade(tmp_path: Path) -> None: + """Regression: the pyproject.toml-with-no-usable-metadata -> + setup.cfg/setup.py fallback branch (previous two tests) must still + get `apply_locked_dependencies()`'s cascade applied to the + setuptools-resolved metadata, not skip it or apply it to a stale + pre-fallback object -- this is the one of `read_project()`'s three + directory-based resolution paths that had no dedicated coverage for + the lock cascade.""" + pyproject_path = tmp_path / "pyproject.toml" + pyproject_path.write_text( + '[build-system]\nrequires = ["setuptools"]\n' + 'build-backend = "custom_pep517_wrapper"\n', + encoding="utf-8", + ) + (tmp_path / "setup.cfg").write_text( + "[metadata]\nname = real-pkg\nversion = 1.2.3\n", encoding="utf-8" + ) + (tmp_path / "pylock.toml").write_text( + 'lock-version = "1.0"\ncreated-by = "test"\n' + '[[packages]]\nname = "requests"\nversion = "2.31.0"\n', + encoding="utf-8", + ) + + metadata, _pitloom_config, _config_path = read_project(tmp_path) + + assert metadata.name == "real-pkg" + assert metadata.locked_dependencies == ["requests==2.31.0"] + assert metadata.provenance["locked_dependencies"] == ( + "Source: pylock.toml | Method: resolved_lockfile" + ) + + def test_read_project_build_system_only_pyproject_no_setuptools_fallback( tmp_path: Path, ) -> None: @@ -166,3 +198,28 @@ def test_read_project_malformed_pitloom_config_raises(tmp_path: Path) -> None: with pytest.raises(ValueError): read_project(tmp_path) + + +def test_read_project_include_locked_dependencies_false_skips_cascade( + tmp_path: Path, +) -> None: + """`include_locked_dependencies=False` (used by build-stage/config-only + callers like `embed-wheel` and the shared CLI options helper) must + skip the lock/pin cascade entirely, not just discard its result -- + a sibling `pylock.toml` is present but must never reach + `locked_dependencies`.""" + (tmp_path / "pyproject.toml").write_text( + '[project]\nname = "pkg"\nversion = "1.0.0"\n', encoding="utf-8" + ) + (tmp_path / "pylock.toml").write_text( + 'lock-version = "1.0"\ncreated-by = "test"\n' + '[[packages]]\nname = "requests"\nversion = "2.31.0"\n', + encoding="utf-8", + ) + + metadata, _pitloom_config, _config_path = read_project( + tmp_path, include_locked_dependencies=False + ) + + assert metadata.locked_dependencies == [] + assert "locked_dependencies" not in metadata.provenance diff --git a/tests/extract/test_pylock.py b/tests/extract/test_pylock.py index f7fcaa82..6edb943e 100644 --- a/tests/extract/test_pylock.py +++ b/tests/extract/test_pylock.py @@ -5,10 +5,13 @@ """Tests for PEP 751 ``pylock.toml`` dependency parsing (:mod:`pitloom.extract._pylock`) and its overlay onto -``ProjectMetadata.locked_dependencies`` via ``read_pyproject()``. +``ProjectMetadata.locked_dependencies`` via ``read_project()``'s lock +cascade (:mod:`pitloom.extract._locked_dependencies`). See also: test_poetry_lock.py for the sibling ``poetry.lock`` extractor -this module's tests mirror in shape. +this module's tests mirror in shape; test_locked_dependencies.py for the +cascade mechanism's own tests (priority ordering, the ``setup.py``-only +wiring, the override provenance note). """ import logging @@ -18,10 +21,14 @@ import pytest from pitloom.extract._pylock import _pinned_dep_for_package, extract_pylock_dependencies -from pitloom.extract._pyproject import read_pyproject +from pitloom.extract.project import read_project _LOCK_VERSION = 'lock-version = "1.0"\ncreated-by = "test"\n' +REAL_WORLD_LOCKS = ( + Path(__file__).parent.parent / "fixtures" / "real-world-locks" / "pylock" +) + def _write_lock(tmp_dir: Path, packages: str = "") -> None: (tmp_dir / "pylock.toml").write_text(_LOCK_VERSION + packages, encoding="utf-8") @@ -156,10 +163,11 @@ def test_sdist_sourced_package_included() -> None: assert extract_pylock_dependencies(tmp_path) == ["requests==2.31.0"] -def test_read_pyproject_populates_locked_dependencies() -> None: - """Integration: `read_pyproject()` overlays `pylock.toml` parsing onto - `ProjectMetadata.locked_dependencies` with its own provenance entry, - for a plain PEP 621 project (no `[tool.poetry]` involved).""" +def test_read_project_populates_locked_dependencies() -> None: + """Integration: `read_project()`'s lock cascade overlays `pylock.toml` + parsing onto `ProjectMetadata.locked_dependencies` with its own + provenance entry, for a plain PEP 621 project (no `[tool.poetry]` + involved).""" with tempfile.TemporaryDirectory() as tmp: tmp_path = Path(tmp) (tmp_path / "pyproject.toml").write_text( @@ -167,7 +175,7 @@ def test_read_pyproject_populates_locked_dependencies() -> None: ) _write_lock(tmp_path, '[[packages]]\nname = "requests"\nversion = "2.31.0"\n') - metadata, _config = read_pyproject(tmp_path / "pyproject.toml") + metadata, _config, _path = read_project(tmp_path) assert metadata.locked_dependencies == ["requests==2.31.0"] assert metadata.provenance["locked_dependencies"] == ( @@ -175,25 +183,25 @@ def test_read_pyproject_populates_locked_dependencies() -> None: ) -def test_read_pyproject_no_lock_file_leaves_locked_dependencies_empty() -> None: +def test_read_project_no_lock_file_leaves_locked_dependencies_empty() -> None: with tempfile.TemporaryDirectory() as tmp: tmp_path = Path(tmp) (tmp_path / "pyproject.toml").write_text( '[project]\nname = "pkg"\nversion = "1.0.0"\n', encoding="utf-8" ) - metadata, _config = read_pyproject(tmp_path / "pyproject.toml") + metadata, _config, _path = read_project(tmp_path) assert metadata.locked_dependencies == [] assert "locked_dependencies" not in metadata.provenance -def test_read_pyproject_pylock_takes_priority_over_poetry_lock( +def test_read_project_pylock_takes_priority_over_poetry_lock( caplog: pytest.LogCaptureFixture, ) -> None: """Regression: when both a `poetry.lock` and a `pylock.toml` are present, PEP 751's `pylock.toml` wins -- and the override is never - silent.""" + silent: it's both logged and recorded in the provenance string.""" with tempfile.TemporaryDirectory() as tmp: tmp_path = Path(tmp) (tmp_path / "pyproject.toml").write_text( @@ -206,10 +214,38 @@ def test_read_pyproject_pylock_takes_priority_over_poetry_lock( _write_lock(tmp_path, '[[packages]]\nname = "httpx"\nversion = "0.27.0"\n') with caplog.at_level(logging.WARNING): - metadata, _config = read_pyproject(tmp_path / "pyproject.toml") + metadata, _config, _path = read_project(tmp_path) assert metadata.locked_dependencies == ["httpx==0.27.0"] assert metadata.provenance["locked_dependencies"] == ( - "Source: pylock.toml | Method: resolved_lockfile" + "Source: pylock.toml | Method: resolved_lockfile " + "| Note: supersedes poetry.lock" ) - assert "pylock.toml (PEP 751) takes priority" in caplog.text + assert "poetry.lock and pylock.toml" in caplog.text + assert "pylock.toml takes priority" in caplog.text + + +def test_real_world_snowflake_cli() -> None: + """`snowflakedb/snowflake-cli` -- a real, committed `pylock.toml` at + the GitHub tag matching its PyPI release (not in the sdist itself; + see tests/fixtures/real-world-locks/README.md).""" + metadata, _config, _path = read_project(REAL_WORLD_LOCKS / "snowflake-cli-3.26.0") + + assert metadata.name == "snowflake-cli" + assert metadata.locked_dependencies + assert metadata.provenance["locked_dependencies"] == ( + "Source: pylock.toml | Method: resolved_lockfile" + ) + + +def test_real_world_pipenv() -> None: + """`pypa/pipenv` -- PEP 751's own reference implementation + (`pipenv/utils/pylock.py`), and a real, committed `pylock.toml` at + the GitHub tag matching its PyPI release.""" + metadata, _config, _path = read_project(REAL_WORLD_LOCKS / "pipenv-2026.8.0") + + assert metadata.name == "pipenv" + assert metadata.locked_dependencies + assert metadata.provenance["locked_dependencies"] == ( + "Source: pylock.toml | Method: resolved_lockfile" + ) diff --git a/tests/fixtures/real-world-locks/README.md b/tests/fixtures/real-world-locks/README.md new file mode 100644 index 00000000..b9e9e5bb --- /dev/null +++ b/tests/fixtures/real-world-locks/README.md @@ -0,0 +1,107 @@ +--- +SPDX-FileCopyrightText: 2026-present Arthit Suriyawongkul +SPDX-FileType: DOCUMENTATION +SPDX-License-Identifier: CC0-1.0 +--- + +# Real-world lock-file fixtures + +See also: [../real-world-projects/README.md](../real-world-projects/README.md) +(the sibling fixture set for build-backend wheel-file discovery -- a +different concern, vendoring a full sdist archive per project; this +directory only ever needs two small text files); +[../projects/README.md](../projects/README.md) (small, synthetic, +single-behaviour lock fixtures, e.g. malformed/edge-case content written +inline in each format's own test file, not vendored here); +[`../../../working-docs/implementation/lock-file-cascade.md`](../../../working-docs/implementation/lock-file-cascade.md) +for the priority-cascade mechanism these fixtures exercise; +[`../../extract/test_locked_dependencies.py`](../../extract/test_locked_dependencies.py) +for the cascade's own mechanism tests. + +## What's here + +Each `/-/` directory holds: + +- The project's real metadata file -- `pyproject.toml` for every format + except `Pipfile.lock` (which pairs with a real `setup.py` instead -- + see the table's "Metadata source" column and the note below). +- The project's real, unmodified lock/pin file for that format + (`pylock.toml`, `poetry.lock`, `uv.lock`, `pdm.lock`, `Pipfile.lock`, + or `requirements.txt`), committed as plain text. +- Occasionally a third file the metadata file itself references (e.g. + `snowflake-cli`'s `LICENSE`, required because its `pyproject.toml` + declares `license = { file = "LICENSE" }`). + +No sdist archive, no `.git` history, no source code -- these fixtures +exist only to exercise `pitloom.extract.project.read_project()`'s lock +cascade (`pitloom.extract._locked_dependencies.apply_locked_dependencies`), +which only ever reads a project's metadata file and a sibling lock file +by name. Each pair is a few KB to a couple hundred KB of plain text. + +**Important, checked and confirmed, not assumed:** almost none of these +lock files ship inside the project's own PyPI sdist (lock files are +dev-time artifacts, routinely excluded from `MANIFEST`/sdist packaging) +-- only `flask`'s `uv.lock` was found there. Every other lock file here +was fetched from the project's GitHub repository at the release tag +matching the chosen PyPI version instead +(`raw.githubusercontent.com////`), paired with +that same tag's metadata file. This is a deliberate difference from +`real-world-projects/`'s "vendor the sdist verbatim, don't reach into +git" method -- the sdist doesn't contain what these fixtures need, and +vendoring a whole sdist archive just to reach two small files inside it +would be unjustified bloat. + +This directory (like the rest of `tests/fixtures/`) is excluded from +Pitloom's own published sdist and wheel +(`pyproject.toml`'s `[tool.hatch.build.targets.sdist]`/`[tool.hatch.build.targets.wheel]`), +to avoid redistributing vendored third-party source in a release +artifact. + +## Notable cases + +- **`Pipfile.lock`'s metadata source is `setup.py`, not `pyproject.toml`.** + `Pipfile.lock` (Pipenv) predates PEP 621 almost entirely -- every real + project checked that ships one uses a bare `setup.py`. This is exactly + the case `read_project()`'s cascade wiring (rather than being wired + only inside `read_pyproject()`) exists to cover -- see + `lock-file-cascade.md`. Both `requests-html`'s and `responder`'s + `setup.py` declare `name`/`version` via module-level constants + (`NAME = 'requests-html'`, `setup(name=NAME, ...)`), which + `_setuptools_py.py`'s AST-literal extractor can't resolve (a known, + separate, pre-existing gap -- see the `pyyaml` entry in + `real-world-projects/README.md`) -- so `metadata.name` won't resolve + for either. Tests against these two fixtures assert on + `locked_dependencies`/`provenance`, not `metadata.name`. +- **`pipenv`'s `pylock.toml` fixture reuses a version already vendored + elsewhere.** `pypa/pipenv` `2026.8.0` is the same release already + vendored as a full sdist in + `../real-world-projects/setuptools/pipenv-2026.8.0/` for the + unrelated wheel-file-discovery fixture set. Deliberate reuse of the + same upstream release for a different, much smaller purpose here -- + not a duplicate. +- **`tomlkit` resolves to zero locked dependencies.** `tomlkit` is a + standalone TOML library with no runtime dependencies at all -- every + entry in its `poetry.lock` belongs to the `dev`/docs/test groups, none + to `main`. A real, valid "empty resolved set" case, not a broken + fixture. +- **`pendulum` and `cleo`/`tomlkit`/`pastel` diversify `[tool.poetry]` + detection.** `pendulum`'s `pyproject.toml` has both `[project]` and + `[tool.poetry]` (hybrid); the other three have `[tool.poetry]` only, + no `[project]` table at all -- both of `read_pyproject()`'s + Poetry-detection shapes get real coverage. + +## Fixtures + +| Format | Project | Version | License | Metadata source | Lock file source | +| :--- | :--- | :--- | :--- | :--- | :--- | +| `pylock.toml` (PEP 751) | [snowflakedb/snowflake-cli](https://github.com/snowflakedb/snowflake-cli) | 3.26.0 | Apache-2.0 | GitHub tag `v3.26.0` | GitHub tag `v3.26.0` | +| `pylock.toml` (PEP 751) | [pypa/pipenv](https://github.com/pypa/pipenv) | 2026.8.0 | MIT | GitHub tag `v2026.8.0` | GitHub tag `v2026.8.0` | +| `poetry.lock` | [sdispater/pendulum](https://github.com/sdispater/pendulum) | 3.2.0 | MIT | PyPI sdist (hybrid `[project]` + `[tool.poetry]`) | GitHub tag `3.2.0` | +| `poetry.lock` | [python-poetry/cleo](https://github.com/python-poetry/cleo) | 2.1.0 | MIT | PyPI sdist (`[tool.poetry]` only) | GitHub tag `2.1.0` | +| `poetry.lock` | [python-poetry/tomlkit](https://github.com/python-poetry/tomlkit) | 0.15.1 | MIT | PyPI sdist (`[tool.poetry]` only) | GitHub tag `0.15.1` | +| `poetry.lock` | [sdispater/pastel](https://github.com/sdispater/pastel) | 0.2.1 | MIT | PyPI sdist (`[tool.poetry]` only) | GitHub tag `0.2.1` | + +`uv.lock`, `pdm.lock`, `Pipfile.lock`, and pinned `requirements.txt` +fixtures land in their own follow-up changes, alongside each format's +own extractor -- see `working-docs/design/roadmap.md`'s "Remaining lock +formats as a resolved-dependency source" item. diff --git a/tests/fixtures/real-world-locks/poetry/cleo-2.1.0/poetry.lock b/tests/fixtures/real-world-locks/poetry/cleo-2.1.0/poetry.lock new file mode 100644 index 00000000..a23d89e1 --- /dev/null +++ b/tests/fixtures/real-world-locks/poetry/cleo-2.1.0/poetry.lock @@ -0,0 +1,1359 @@ +# This file is automatically @generated by Poetry 1.6.1 and should not be changed by hand. + +[[package]] +name = "alabaster" +version = "0.7.13" +description = "A configurable sidebar-enabled Sphinx theme" +optional = false +python-versions = ">=3.6" +files = [ + {file = "alabaster-0.7.13-py3-none-any.whl", hash = "sha256:1ee19aca801bbabb5ba3f5f258e4422dfa86f82f3e9cefb0859b283cdd7f62a3"}, + {file = "alabaster-0.7.13.tar.gz", hash = "sha256:a27a4a084d5e690e16e01e03ad2b2e552c61a65469419b907243193de1a84ae2"}, +] + +[[package]] +name = "babel" +version = "2.13.1" +description = "Internationalization utilities" +optional = false +python-versions = ">=3.7" +files = [ + {file = "Babel-2.13.1-py3-none-any.whl", hash = "sha256:7077a4984b02b6727ac10f1f7294484f737443d7e2e66c5e4380e41a3ae0b4ed"}, + {file = "Babel-2.13.1.tar.gz", hash = "sha256:33e0952d7dd6374af8dbf6768cc4ddf3ccfefc244f9986d4074704f2fbd18900"}, +] + +[package.dependencies] +pytz = {version = ">=2015.7", markers = "python_version < \"3.9\""} +setuptools = {version = "*", markers = "python_version >= \"3.12\""} + +[package.extras] +dev = ["freezegun (>=1.0,<2.0)", "pytest (>=6.0)", "pytest-cov"] + +[[package]] +name = "certifi" +version = "2023.7.22" +description = "Python package for providing Mozilla's CA Bundle." +optional = false +python-versions = ">=3.6" +files = [ + {file = "certifi-2023.7.22-py3-none-any.whl", hash = "sha256:92d6037539857d8206b8f6ae472e8b77db8058fec5937a1ef3f54304089edbb9"}, + {file = "certifi-2023.7.22.tar.gz", hash = "sha256:539cc1d13202e33ca466e88b2807e29f4c13049d6d87031a3c110744495cb082"}, +] + +[[package]] +name = "cfgv" +version = "3.3.1" +description = "Validate configuration and produce human readable error messages." +optional = false +python-versions = ">=3.6.1" +files = [ + {file = "cfgv-3.3.1-py2.py3-none-any.whl", hash = "sha256:c6a0883f3917a037485059700b9e75da2464e6c27051014ad85ba6aaa5884426"}, + {file = "cfgv-3.3.1.tar.gz", hash = "sha256:f5a830efb9ce7a445376bb66ec94c638a9787422f96264c98edc6bdeed8ab736"}, +] + +[[package]] +name = "cfgv" +version = "3.4.0" +description = "Validate configuration and produce human readable error messages." +optional = false +python-versions = ">=3.8" +files = [ + {file = "cfgv-3.4.0-py2.py3-none-any.whl", hash = "sha256:b7265b1f29fd3316bfcd2b330d63d024f2bfd8bcb8b0272f8e19a504856c48f9"}, + {file = "cfgv-3.4.0.tar.gz", hash = "sha256:e52591d4c5f5dead8e0f673fb16db7949d2cfb3f7da4582893288f0ded8fe560"}, +] + +[[package]] +name = "charset-normalizer" +version = "3.3.1" +description = "The Real First Universal Charset Detector. Open, modern and actively maintained alternative to Chardet." +optional = false +python-versions = ">=3.7.0" +files = [ + {file = "charset-normalizer-3.3.1.tar.gz", hash = "sha256:d9137a876020661972ca6eec0766d81aef8a5627df628b664b234b73396e727e"}, + {file = "charset_normalizer-3.3.1-cp310-cp310-macosx_10_9_universal2.whl", hash = "sha256:8aee051c89e13565c6bd366813c386939f8e928af93c29fda4af86d25b73d8f8"}, + {file = "charset_normalizer-3.3.1-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:352a88c3df0d1fa886562384b86f9a9e27563d4704ee0e9d56ec6fcd270ea690"}, + {file = "charset_normalizer-3.3.1-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:223b4d54561c01048f657fa6ce41461d5ad8ff128b9678cfe8b2ecd951e3f8a2"}, + {file = "charset_normalizer-3.3.1-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:4f861d94c2a450b974b86093c6c027888627b8082f1299dfd5a4bae8e2292821"}, + {file = "charset_normalizer-3.3.1-cp310-cp310-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:1171ef1fc5ab4693c5d151ae0fdad7f7349920eabbaca6271f95969fa0756c2d"}, + {file = "charset_normalizer-3.3.1-cp310-cp310-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:28f512b9a33235545fbbdac6a330a510b63be278a50071a336afc1b78781b147"}, + {file = "charset_normalizer-3.3.1-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:c0e842112fe3f1a4ffcf64b06dc4c61a88441c2f02f373367f7b4c1aa9be2ad5"}, + {file = "charset_normalizer-3.3.1-cp310-cp310-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:3f9bc2ce123637a60ebe819f9fccc614da1bcc05798bbbaf2dd4ec91f3e08846"}, + {file = "charset_normalizer-3.3.1-cp310-cp310-musllinux_1_1_aarch64.whl", hash = "sha256:f194cce575e59ffe442c10a360182a986535fd90b57f7debfaa5c845c409ecc3"}, + {file = "charset_normalizer-3.3.1-cp310-cp310-musllinux_1_1_i686.whl", hash = "sha256:9a74041ba0bfa9bc9b9bb2cd3238a6ab3b7618e759b41bd15b5f6ad958d17605"}, + {file = "charset_normalizer-3.3.1-cp310-cp310-musllinux_1_1_ppc64le.whl", hash = "sha256:b578cbe580e3b41ad17b1c428f382c814b32a6ce90f2d8e39e2e635d49e498d1"}, + {file = "charset_normalizer-3.3.1-cp310-cp310-musllinux_1_1_s390x.whl", hash = "sha256:6db3cfb9b4fcecb4390db154e75b49578c87a3b9979b40cdf90d7e4b945656e1"}, + {file = "charset_normalizer-3.3.1-cp310-cp310-musllinux_1_1_x86_64.whl", hash = "sha256:debb633f3f7856f95ad957d9b9c781f8e2c6303ef21724ec94bea2ce2fcbd056"}, + {file = "charset_normalizer-3.3.1-cp310-cp310-win32.whl", hash = "sha256:87071618d3d8ec8b186d53cb6e66955ef2a0e4fa63ccd3709c0c90ac5a43520f"}, + {file = "charset_normalizer-3.3.1-cp310-cp310-win_amd64.whl", hash = "sha256:e372d7dfd154009142631de2d316adad3cc1c36c32a38b16a4751ba78da2a397"}, + {file = "charset_normalizer-3.3.1-cp311-cp311-macosx_10_9_universal2.whl", hash = "sha256:ae4070f741f8d809075ef697877fd350ecf0b7c5837ed68738607ee0a2c572cf"}, + {file = "charset_normalizer-3.3.1-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:58e875eb7016fd014c0eea46c6fa92b87b62c0cb31b9feae25cbbe62c919f54d"}, + {file = "charset_normalizer-3.3.1-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:dbd95e300367aa0827496fe75a1766d198d34385a58f97683fe6e07f89ca3e3c"}, + {file = "charset_normalizer-3.3.1-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:de0b4caa1c8a21394e8ce971997614a17648f94e1cd0640fbd6b4d14cab13a72"}, + {file = "charset_normalizer-3.3.1-cp311-cp311-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:985c7965f62f6f32bf432e2681173db41336a9c2611693247069288bcb0c7f8b"}, + {file = "charset_normalizer-3.3.1-cp311-cp311-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:a15c1fe6d26e83fd2e5972425a772cca158eae58b05d4a25a4e474c221053e2d"}, + {file = "charset_normalizer-3.3.1-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:ae55d592b02c4349525b6ed8f74c692509e5adffa842e582c0f861751701a673"}, + {file = "charset_normalizer-3.3.1-cp311-cp311-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:be4d9c2770044a59715eb57c1144dedea7c5d5ae80c68fb9959515037cde2008"}, + {file = "charset_normalizer-3.3.1-cp311-cp311-musllinux_1_1_aarch64.whl", hash = "sha256:851cf693fb3aaef71031237cd68699dded198657ec1e76a76eb8be58c03a5d1f"}, + {file = "charset_normalizer-3.3.1-cp311-cp311-musllinux_1_1_i686.whl", hash = "sha256:31bbaba7218904d2eabecf4feec0d07469284e952a27400f23b6628439439fa7"}, + {file = "charset_normalizer-3.3.1-cp311-cp311-musllinux_1_1_ppc64le.whl", hash = "sha256:871d045d6ccc181fd863a3cd66ee8e395523ebfbc57f85f91f035f50cee8e3d4"}, + {file = "charset_normalizer-3.3.1-cp311-cp311-musllinux_1_1_s390x.whl", hash = "sha256:501adc5eb6cd5f40a6f77fbd90e5ab915c8fd6e8c614af2db5561e16c600d6f3"}, + {file = "charset_normalizer-3.3.1-cp311-cp311-musllinux_1_1_x86_64.whl", hash = "sha256:f5fb672c396d826ca16a022ac04c9dce74e00a1c344f6ad1a0fdc1ba1f332213"}, + {file = "charset_normalizer-3.3.1-cp311-cp311-win32.whl", hash = "sha256:bb06098d019766ca16fc915ecaa455c1f1cd594204e7f840cd6258237b5079a8"}, + {file = "charset_normalizer-3.3.1-cp311-cp311-win_amd64.whl", hash = "sha256:8af5a8917b8af42295e86b64903156b4f110a30dca5f3b5aedea123fbd638bff"}, + {file = "charset_normalizer-3.3.1-cp312-cp312-macosx_10_9_universal2.whl", hash = "sha256:7ae8e5142dcc7a49168f4055255dbcced01dc1714a90a21f87448dc8d90617d1"}, + {file = "charset_normalizer-3.3.1-cp312-cp312-macosx_10_9_x86_64.whl", hash = "sha256:5b70bab78accbc672f50e878a5b73ca692f45f5b5e25c8066d748c09405e6a55"}, + {file = "charset_normalizer-3.3.1-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:5ceca5876032362ae73b83347be8b5dbd2d1faf3358deb38c9c88776779b2e2f"}, + {file = "charset_normalizer-3.3.1-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:34d95638ff3613849f473afc33f65c401a89f3b9528d0d213c7037c398a51296"}, + {file = "charset_normalizer-3.3.1-cp312-cp312-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:9edbe6a5bf8b56a4a84533ba2b2f489d0046e755c29616ef8830f9e7d9cf5728"}, + {file = "charset_normalizer-3.3.1-cp312-cp312-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:f6a02a3c7950cafaadcd46a226ad9e12fc9744652cc69f9e5534f98b47f3bbcf"}, + {file = "charset_normalizer-3.3.1-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:10b8dd31e10f32410751b3430996f9807fc4d1587ca69772e2aa940a82ab571a"}, + {file = "charset_normalizer-3.3.1-cp312-cp312-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:edc0202099ea1d82844316604e17d2b175044f9bcb6b398aab781eba957224bd"}, + {file = "charset_normalizer-3.3.1-cp312-cp312-musllinux_1_1_aarch64.whl", hash = "sha256:b891a2f68e09c5ef989007fac11476ed33c5c9994449a4e2c3386529d703dc8b"}, + {file = "charset_normalizer-3.3.1-cp312-cp312-musllinux_1_1_i686.whl", hash = "sha256:71ef3b9be10070360f289aea4838c784f8b851be3ba58cf796262b57775c2f14"}, + {file = "charset_normalizer-3.3.1-cp312-cp312-musllinux_1_1_ppc64le.whl", hash = "sha256:55602981b2dbf8184c098bc10287e8c245e351cd4fdcad050bd7199d5a8bf514"}, + {file = "charset_normalizer-3.3.1-cp312-cp312-musllinux_1_1_s390x.whl", hash = "sha256:46fb9970aa5eeca547d7aa0de5d4b124a288b42eaefac677bde805013c95725c"}, + {file = "charset_normalizer-3.3.1-cp312-cp312-musllinux_1_1_x86_64.whl", hash = "sha256:520b7a142d2524f999447b3a0cf95115df81c4f33003c51a6ab637cbda9d0bf4"}, + {file = "charset_normalizer-3.3.1-cp312-cp312-win32.whl", hash = "sha256:8ec8ef42c6cd5856a7613dcd1eaf21e5573b2185263d87d27c8edcae33b62a61"}, + {file = "charset_normalizer-3.3.1-cp312-cp312-win_amd64.whl", hash = "sha256:baec8148d6b8bd5cee1ae138ba658c71f5b03e0d69d5907703e3e1df96db5e41"}, + {file = "charset_normalizer-3.3.1-cp37-cp37m-macosx_10_9_x86_64.whl", hash = "sha256:63a6f59e2d01310f754c270e4a257426fe5a591dc487f1983b3bbe793cf6bac6"}, + {file = "charset_normalizer-3.3.1-cp37-cp37m-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:1d6bfc32a68bc0933819cfdfe45f9abc3cae3877e1d90aac7259d57e6e0f85b1"}, + {file = "charset_normalizer-3.3.1-cp37-cp37m-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:4f3100d86dcd03c03f7e9c3fdb23d92e32abbca07e7c13ebd7ddfbcb06f5991f"}, + {file = "charset_normalizer-3.3.1-cp37-cp37m-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:39b70a6f88eebe239fa775190796d55a33cfb6d36b9ffdd37843f7c4c1b5dc67"}, + {file = "charset_normalizer-3.3.1-cp37-cp37m-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:4e12f8ee80aa35e746230a2af83e81bd6b52daa92a8afaef4fea4a2ce9b9f4fa"}, + {file = "charset_normalizer-3.3.1-cp37-cp37m-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:7b6cefa579e1237ce198619b76eaa148b71894fb0d6bcf9024460f9bf30fd228"}, + {file = "charset_normalizer-3.3.1-cp37-cp37m-musllinux_1_1_aarch64.whl", hash = "sha256:61f1e3fb621f5420523abb71f5771a204b33c21d31e7d9d86881b2cffe92c47c"}, + {file = "charset_normalizer-3.3.1-cp37-cp37m-musllinux_1_1_i686.whl", hash = "sha256:4f6e2a839f83a6a76854d12dbebde50e4b1afa63e27761549d006fa53e9aa80e"}, + {file = "charset_normalizer-3.3.1-cp37-cp37m-musllinux_1_1_ppc64le.whl", hash = "sha256:1ec937546cad86d0dce5396748bf392bb7b62a9eeb8c66efac60e947697f0e58"}, + {file = "charset_normalizer-3.3.1-cp37-cp37m-musllinux_1_1_s390x.whl", hash = "sha256:82ca51ff0fc5b641a2d4e1cc8c5ff108699b7a56d7f3ad6f6da9dbb6f0145b48"}, + {file = "charset_normalizer-3.3.1-cp37-cp37m-musllinux_1_1_x86_64.whl", hash = "sha256:633968254f8d421e70f91c6ebe71ed0ab140220469cf87a9857e21c16687c034"}, + {file = "charset_normalizer-3.3.1-cp37-cp37m-win32.whl", hash = "sha256:c0c72d34e7de5604df0fde3644cc079feee5e55464967d10b24b1de268deceb9"}, + {file = "charset_normalizer-3.3.1-cp37-cp37m-win_amd64.whl", hash = "sha256:63accd11149c0f9a99e3bc095bbdb5a464862d77a7e309ad5938fbc8721235ae"}, + {file = "charset_normalizer-3.3.1-cp38-cp38-macosx_10_9_universal2.whl", hash = "sha256:5a3580a4fdc4ac05f9e53c57f965e3594b2f99796231380adb2baaab96e22761"}, + {file = "charset_normalizer-3.3.1-cp38-cp38-macosx_10_9_x86_64.whl", hash = "sha256:2465aa50c9299d615d757c1c888bc6fef384b7c4aec81c05a0172b4400f98557"}, + {file = "charset_normalizer-3.3.1-cp38-cp38-macosx_11_0_arm64.whl", hash = "sha256:cb7cd68814308aade9d0c93c5bd2ade9f9441666f8ba5aa9c2d4b389cb5e2a45"}, + {file = "charset_normalizer-3.3.1-cp38-cp38-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:91e43805ccafa0a91831f9cd5443aa34528c0c3f2cc48c4cb3d9a7721053874b"}, + {file = "charset_normalizer-3.3.1-cp38-cp38-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:854cc74367180beb327ab9d00f964f6d91da06450b0855cbbb09187bcdb02de5"}, + {file = "charset_normalizer-3.3.1-cp38-cp38-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:c15070ebf11b8b7fd1bfff7217e9324963c82dbdf6182ff7050519e350e7ad9f"}, + {file = "charset_normalizer-3.3.1-cp38-cp38-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:2c4c99f98fc3a1835af8179dcc9013f93594d0670e2fa80c83aa36346ee763d2"}, + {file = "charset_normalizer-3.3.1-cp38-cp38-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:3fb765362688821404ad6cf86772fc54993ec11577cd5a92ac44b4c2ba52155b"}, + {file = "charset_normalizer-3.3.1-cp38-cp38-musllinux_1_1_aarch64.whl", hash = "sha256:dced27917823df984fe0c80a5c4ad75cf58df0fbfae890bc08004cd3888922a2"}, + {file = "charset_normalizer-3.3.1-cp38-cp38-musllinux_1_1_i686.whl", hash = "sha256:a66bcdf19c1a523e41b8e9d53d0cedbfbac2e93c649a2e9502cb26c014d0980c"}, + {file = "charset_normalizer-3.3.1-cp38-cp38-musllinux_1_1_ppc64le.whl", hash = "sha256:ecd26be9f112c4f96718290c10f4caea6cc798459a3a76636b817a0ed7874e42"}, + {file = "charset_normalizer-3.3.1-cp38-cp38-musllinux_1_1_s390x.whl", hash = "sha256:3f70fd716855cd3b855316b226a1ac8bdb3caf4f7ea96edcccc6f484217c9597"}, + {file = "charset_normalizer-3.3.1-cp38-cp38-musllinux_1_1_x86_64.whl", hash = "sha256:17a866d61259c7de1bdadef418a37755050ddb4b922df8b356503234fff7932c"}, + {file = "charset_normalizer-3.3.1-cp38-cp38-win32.whl", hash = "sha256:548eefad783ed787b38cb6f9a574bd8664468cc76d1538215d510a3cd41406cb"}, + {file = "charset_normalizer-3.3.1-cp38-cp38-win_amd64.whl", hash = "sha256:45f053a0ece92c734d874861ffe6e3cc92150e32136dd59ab1fb070575189c97"}, + {file = "charset_normalizer-3.3.1-cp39-cp39-macosx_10_9_universal2.whl", hash = "sha256:bc791ec3fd0c4309a753f95bb6c749ef0d8ea3aea91f07ee1cf06b7b02118f2f"}, + {file = "charset_normalizer-3.3.1-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:0c8c61fb505c7dad1d251c284e712d4e0372cef3b067f7ddf82a7fa82e1e9a93"}, + {file = "charset_normalizer-3.3.1-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:2c092be3885a1b7899cd85ce24acedc1034199d6fca1483fa2c3a35c86e43041"}, + {file = "charset_normalizer-3.3.1-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:c2000c54c395d9e5e44c99dc7c20a64dc371f777faf8bae4919ad3e99ce5253e"}, + {file = "charset_normalizer-3.3.1-cp39-cp39-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:4cb50a0335382aac15c31b61d8531bc9bb657cfd848b1d7158009472189f3d62"}, + {file = "charset_normalizer-3.3.1-cp39-cp39-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:c30187840d36d0ba2893bc3271a36a517a717f9fd383a98e2697ee890a37c273"}, + {file = "charset_normalizer-3.3.1-cp39-cp39-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:fe81b35c33772e56f4b6cf62cf4aedc1762ef7162a31e6ac7fe5e40d0149eb67"}, + {file = "charset_normalizer-3.3.1-cp39-cp39-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:d0bf89afcbcf4d1bb2652f6580e5e55a840fdf87384f6063c4a4f0c95e378656"}, + {file = "charset_normalizer-3.3.1-cp39-cp39-musllinux_1_1_aarch64.whl", hash = "sha256:06cf46bdff72f58645434d467bf5228080801298fbba19fe268a01b4534467f5"}, + {file = "charset_normalizer-3.3.1-cp39-cp39-musllinux_1_1_i686.whl", hash = "sha256:3c66df3f41abee950d6638adc7eac4730a306b022570f71dd0bd6ba53503ab57"}, + {file = "charset_normalizer-3.3.1-cp39-cp39-musllinux_1_1_ppc64le.whl", hash = "sha256:cd805513198304026bd379d1d516afbf6c3c13f4382134a2c526b8b854da1c2e"}, + {file = "charset_normalizer-3.3.1-cp39-cp39-musllinux_1_1_s390x.whl", hash = "sha256:9505dc359edb6a330efcd2be825fdb73ee3e628d9010597aa1aee5aa63442e97"}, + {file = "charset_normalizer-3.3.1-cp39-cp39-musllinux_1_1_x86_64.whl", hash = "sha256:31445f38053476a0c4e6d12b047b08ced81e2c7c712e5a1ad97bc913256f91b2"}, + {file = "charset_normalizer-3.3.1-cp39-cp39-win32.whl", hash = "sha256:bd28b31730f0e982ace8663d108e01199098432a30a4c410d06fe08fdb9e93f4"}, + {file = "charset_normalizer-3.3.1-cp39-cp39-win_amd64.whl", hash = "sha256:555fe186da0068d3354cdf4bbcbc609b0ecae4d04c921cc13e209eece7720727"}, + {file = "charset_normalizer-3.3.1-py3-none-any.whl", hash = "sha256:800561453acdecedaac137bf09cd719c7a440b6800ec182f077bb8e7025fb708"}, +] + +[[package]] +name = "click" +version = "8.1.7" +description = "Composable command line interface toolkit" +optional = false +python-versions = ">=3.7" +files = [ + {file = "click-8.1.7-py3-none-any.whl", hash = "sha256:ae74fb96c20a0277a1d615f1e4d73c8414f5a98db8b799a7931d1582f3390c28"}, + {file = "click-8.1.7.tar.gz", hash = "sha256:ca9853ad459e787e2192211578cc907e7594e294c7ccc834310722b41b9ca6de"}, +] + +[package.dependencies] +colorama = {version = "*", markers = "platform_system == \"Windows\""} +importlib-metadata = {version = "*", markers = "python_version < \"3.8\""} + +[[package]] +name = "click-default-group" +version = "1.2.4" +description = "click_default_group" +optional = false +python-versions = ">=2.7" +files = [ + {file = "click_default_group-1.2.4-py2.py3-none-any.whl", hash = "sha256:9b60486923720e7fc61731bdb32b617039aba820e22e1c88766b1125592eaa5f"}, + {file = "click_default_group-1.2.4.tar.gz", hash = "sha256:eb3f3c99ec0d456ca6cd2a7f08f7d4e91771bef51b01bdd9580cc6450fe1251e"}, +] + +[package.dependencies] +click = "*" + +[package.extras] +test = ["pytest"] + +[[package]] +name = "colorama" +version = "0.4.6" +description = "Cross-platform colored terminal text." +optional = false +python-versions = "!=3.0.*,!=3.1.*,!=3.2.*,!=3.3.*,!=3.4.*,!=3.5.*,!=3.6.*,>=2.7" +files = [ + {file = "colorama-0.4.6-py2.py3-none-any.whl", hash = "sha256:4f1d9991f5acc0ca119f9d443620b77f9d6b33703e51011c16baf57afb285fc6"}, + {file = "colorama-0.4.6.tar.gz", hash = "sha256:08695f5cb7ed6e0531a20572697297273c47b8cae5a63ffc6d6ed5c201be6e44"}, +] + +[[package]] +name = "coverage" +version = "7.2.7" +description = "Code coverage measurement for Python" +optional = false +python-versions = ">=3.7" +files = [ + {file = "coverage-7.2.7-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:d39b5b4f2a66ccae8b7263ac3c8170994b65266797fb96cbbfd3fb5b23921db8"}, + {file = "coverage-7.2.7-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:6d040ef7c9859bb11dfeb056ff5b3872436e3b5e401817d87a31e1750b9ae2fb"}, + {file = "coverage-7.2.7-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:ba90a9563ba44a72fda2e85302c3abc71c5589cea608ca16c22b9804262aaeb6"}, + {file = "coverage-7.2.7-cp310-cp310-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:e7d9405291c6928619403db1d10bd07888888ec1abcbd9748fdaa971d7d661b2"}, + {file = "coverage-7.2.7-cp310-cp310-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:31563e97dae5598556600466ad9beea39fb04e0229e61c12eaa206e0aa202063"}, + {file = "coverage-7.2.7-cp310-cp310-musllinux_1_1_aarch64.whl", hash = "sha256:ebba1cd308ef115925421d3e6a586e655ca5a77b5bf41e02eb0e4562a111f2d1"}, + {file = "coverage-7.2.7-cp310-cp310-musllinux_1_1_i686.whl", hash = "sha256:cb017fd1b2603ef59e374ba2063f593abe0fc45f2ad9abdde5b4d83bd922a353"}, + {file = "coverage-7.2.7-cp310-cp310-musllinux_1_1_x86_64.whl", hash = "sha256:d62a5c7dad11015c66fbb9d881bc4caa5b12f16292f857842d9d1871595f4495"}, + {file = "coverage-7.2.7-cp310-cp310-win32.whl", hash = "sha256:ee57190f24fba796e36bb6d3aa8a8783c643d8fa9760c89f7a98ab5455fbf818"}, + {file = "coverage-7.2.7-cp310-cp310-win_amd64.whl", hash = "sha256:f75f7168ab25dd93110c8a8117a22450c19976afbc44234cbf71481094c1b850"}, + {file = "coverage-7.2.7-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:06a9a2be0b5b576c3f18f1a241f0473575c4a26021b52b2a85263a00f034d51f"}, + {file = "coverage-7.2.7-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:5baa06420f837184130752b7c5ea0808762083bf3487b5038d68b012e5937dbe"}, + {file = "coverage-7.2.7-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:fdec9e8cbf13a5bf63290fc6013d216a4c7232efb51548594ca3631a7f13c3a3"}, + {file = "coverage-7.2.7-cp311-cp311-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:52edc1a60c0d34afa421c9c37078817b2e67a392cab17d97283b64c5833f427f"}, + {file = "coverage-7.2.7-cp311-cp311-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:63426706118b7f5cf6bb6c895dc215d8a418d5952544042c8a2d9fe87fcf09cb"}, + {file = "coverage-7.2.7-cp311-cp311-musllinux_1_1_aarch64.whl", hash = "sha256:afb17f84d56068a7c29f5fa37bfd38d5aba69e3304af08ee94da8ed5b0865833"}, + {file = "coverage-7.2.7-cp311-cp311-musllinux_1_1_i686.whl", hash = "sha256:48c19d2159d433ccc99e729ceae7d5293fbffa0bdb94952d3579983d1c8c9d97"}, + {file = "coverage-7.2.7-cp311-cp311-musllinux_1_1_x86_64.whl", hash = "sha256:0e1f928eaf5469c11e886fe0885ad2bf1ec606434e79842a879277895a50942a"}, + {file = "coverage-7.2.7-cp311-cp311-win32.whl", hash = "sha256:33d6d3ea29d5b3a1a632b3c4e4f4ecae24ef170b0b9ee493883f2df10039959a"}, + {file = "coverage-7.2.7-cp311-cp311-win_amd64.whl", hash = "sha256:5b7540161790b2f28143191f5f8ec02fb132660ff175b7747b95dcb77ac26562"}, + {file = "coverage-7.2.7-cp312-cp312-macosx_10_9_x86_64.whl", hash = "sha256:f2f67fe12b22cd130d34d0ef79206061bfb5eda52feb6ce0dba0644e20a03cf4"}, + {file = "coverage-7.2.7-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:a342242fe22407f3c17f4b499276a02b01e80f861f1682ad1d95b04018e0c0d4"}, + {file = "coverage-7.2.7-cp312-cp312-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:171717c7cb6b453aebac9a2ef603699da237f341b38eebfee9be75d27dc38e01"}, + {file = "coverage-7.2.7-cp312-cp312-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:49969a9f7ffa086d973d91cec8d2e31080436ef0fb4a359cae927e742abfaaa6"}, + {file = "coverage-7.2.7-cp312-cp312-musllinux_1_1_aarch64.whl", hash = "sha256:b46517c02ccd08092f4fa99f24c3b83d8f92f739b4657b0f146246a0ca6a831d"}, + {file = "coverage-7.2.7-cp312-cp312-musllinux_1_1_i686.whl", hash = "sha256:a3d33a6b3eae87ceaefa91ffdc130b5e8536182cd6dfdbfc1aa56b46ff8c86de"}, + {file = "coverage-7.2.7-cp312-cp312-musllinux_1_1_x86_64.whl", hash = "sha256:976b9c42fb2a43ebf304fa7d4a310e5f16cc99992f33eced91ef6f908bd8f33d"}, + {file = "coverage-7.2.7-cp312-cp312-win32.whl", hash = "sha256:8de8bb0e5ad103888d65abef8bca41ab93721647590a3f740100cd65c3b00511"}, + {file = "coverage-7.2.7-cp312-cp312-win_amd64.whl", hash = "sha256:9e31cb64d7de6b6f09702bb27c02d1904b3aebfca610c12772452c4e6c21a0d3"}, + {file = "coverage-7.2.7-cp37-cp37m-macosx_10_9_x86_64.whl", hash = "sha256:58c2ccc2f00ecb51253cbe5d8d7122a34590fac9646a960d1430d5b15321d95f"}, + {file = "coverage-7.2.7-cp37-cp37m-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:d22656368f0e6189e24722214ed8d66b8022db19d182927b9a248a2a8a2f67eb"}, + {file = "coverage-7.2.7-cp37-cp37m-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:a895fcc7b15c3fc72beb43cdcbdf0ddb7d2ebc959edac9cef390b0d14f39f8a9"}, + {file = "coverage-7.2.7-cp37-cp37m-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:e84606b74eb7de6ff581a7915e2dab7a28a0517fbe1c9239eb227e1354064dcd"}, + {file = "coverage-7.2.7-cp37-cp37m-musllinux_1_1_aarch64.whl", hash = "sha256:0a5f9e1dbd7fbe30196578ca36f3fba75376fb99888c395c5880b355e2875f8a"}, + {file = "coverage-7.2.7-cp37-cp37m-musllinux_1_1_i686.whl", hash = "sha256:419bfd2caae268623dd469eff96d510a920c90928b60f2073d79f8fe2bbc5959"}, + {file = "coverage-7.2.7-cp37-cp37m-musllinux_1_1_x86_64.whl", hash = "sha256:2aee274c46590717f38ae5e4650988d1af340fe06167546cc32fe2f58ed05b02"}, + {file = "coverage-7.2.7-cp37-cp37m-win32.whl", hash = "sha256:61b9a528fb348373c433e8966535074b802c7a5d7f23c4f421e6c6e2f1697a6f"}, + {file = "coverage-7.2.7-cp37-cp37m-win_amd64.whl", hash = "sha256:b1c546aca0ca4d028901d825015dc8e4d56aac4b541877690eb76490f1dc8ed0"}, + {file = "coverage-7.2.7-cp38-cp38-macosx_10_9_x86_64.whl", hash = "sha256:54b896376ab563bd38453cecb813c295cf347cf5906e8b41d340b0321a5433e5"}, + {file = "coverage-7.2.7-cp38-cp38-macosx_11_0_arm64.whl", hash = "sha256:3d376df58cc111dc8e21e3b6e24606b5bb5dee6024f46a5abca99124b2229ef5"}, + {file = "coverage-7.2.7-cp38-cp38-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:5e330fc79bd7207e46c7d7fd2bb4af2963f5f635703925543a70b99574b0fea9"}, + {file = "coverage-7.2.7-cp38-cp38-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:1e9d683426464e4a252bf70c3498756055016f99ddaec3774bf368e76bbe02b6"}, + {file = "coverage-7.2.7-cp38-cp38-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:8d13c64ee2d33eccf7437961b6ea7ad8673e2be040b4f7fd4fd4d4d28d9ccb1e"}, + {file = "coverage-7.2.7-cp38-cp38-musllinux_1_1_aarch64.whl", hash = "sha256:b7aa5f8a41217360e600da646004f878250a0d6738bcdc11a0a39928d7dc2050"}, + {file = "coverage-7.2.7-cp38-cp38-musllinux_1_1_i686.whl", hash = "sha256:8fa03bce9bfbeeef9f3b160a8bed39a221d82308b4152b27d82d8daa7041fee5"}, + {file = "coverage-7.2.7-cp38-cp38-musllinux_1_1_x86_64.whl", hash = "sha256:245167dd26180ab4c91d5e1496a30be4cd721a5cf2abf52974f965f10f11419f"}, + {file = "coverage-7.2.7-cp38-cp38-win32.whl", hash = "sha256:d2c2db7fd82e9b72937969bceac4d6ca89660db0a0967614ce2481e81a0b771e"}, + {file = "coverage-7.2.7-cp38-cp38-win_amd64.whl", hash = "sha256:2e07b54284e381531c87f785f613b833569c14ecacdcb85d56b25c4622c16c3c"}, + {file = "coverage-7.2.7-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:537891ae8ce59ef63d0123f7ac9e2ae0fc8b72c7ccbe5296fec45fd68967b6c9"}, + {file = "coverage-7.2.7-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:06fb182e69f33f6cd1d39a6c597294cff3143554b64b9825d1dc69d18cc2fff2"}, + {file = "coverage-7.2.7-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:201e7389591af40950a6480bd9edfa8ed04346ff80002cec1a66cac4549c1ad7"}, + {file = "coverage-7.2.7-cp39-cp39-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:f6951407391b639504e3b3be51b7ba5f3528adbf1a8ac3302b687ecababf929e"}, + {file = "coverage-7.2.7-cp39-cp39-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:6f48351d66575f535669306aa7d6d6f71bc43372473b54a832222803eb956fd1"}, + {file = "coverage-7.2.7-cp39-cp39-musllinux_1_1_aarch64.whl", hash = "sha256:b29019c76039dc3c0fd815c41392a044ce555d9bcdd38b0fb60fb4cd8e475ba9"}, + {file = "coverage-7.2.7-cp39-cp39-musllinux_1_1_i686.whl", hash = "sha256:81c13a1fc7468c40f13420732805a4c38a105d89848b7c10af65a90beff25250"}, + {file = "coverage-7.2.7-cp39-cp39-musllinux_1_1_x86_64.whl", hash = "sha256:975d70ab7e3c80a3fe86001d8751f6778905ec723f5b110aed1e450da9d4b7f2"}, + {file = "coverage-7.2.7-cp39-cp39-win32.whl", hash = "sha256:7ee7d9d4822c8acc74a5e26c50604dff824710bc8de424904c0982e25c39c6cb"}, + {file = "coverage-7.2.7-cp39-cp39-win_amd64.whl", hash = "sha256:eb393e5ebc85245347950143969b241d08b52b88a3dc39479822e073a1a8eb27"}, + {file = "coverage-7.2.7-pp37.pp38.pp39-none-any.whl", hash = "sha256:b7b4c971f05e6ae490fef852c218b0e79d4e52f79ef0c8475566584a8fb3e01d"}, + {file = "coverage-7.2.7.tar.gz", hash = "sha256:924d94291ca674905fe9481f12294eb11f2d3d3fd1adb20314ba89e94f44ed59"}, +] + +[package.dependencies] +tomli = {version = "*", optional = true, markers = "python_full_version <= \"3.11.0a6\" and extra == \"toml\""} + +[package.extras] +toml = ["tomli"] + +[[package]] +name = "crashtest" +version = "0.4.1" +description = "Manage Python errors with ease" +optional = false +python-versions = ">=3.7,<4.0" +files = [ + {file = "crashtest-0.4.1-py3-none-any.whl", hash = "sha256:8d23eac5fa660409f57472e3851dab7ac18aba459a8d19cbbba86d3d5aecd2a5"}, + {file = "crashtest-0.4.1.tar.gz", hash = "sha256:80d7b1f316ebfbd429f648076d6275c877ba30ba48979de4191714a75266f0ce"}, +] + +[[package]] +name = "distlib" +version = "0.3.7" +description = "Distribution utilities" +optional = false +python-versions = "*" +files = [ + {file = "distlib-0.3.7-py2.py3-none-any.whl", hash = "sha256:2e24928bc811348f0feb63014e97aaae3037f2cf48712d51ae61df7fd6075057"}, + {file = "distlib-0.3.7.tar.gz", hash = "sha256:9dafe54b34a028eafd95039d5e5d4851a13734540f1331060d31c9916e7147a8"}, +] + +[[package]] +name = "docutils" +version = "0.18.1" +description = "Docutils -- Python Documentation Utilities" +optional = false +python-versions = ">=2.7, !=3.0.*, !=3.1.*, !=3.2.*, !=3.3.*, !=3.4.*" +files = [ + {file = "docutils-0.18.1-py2.py3-none-any.whl", hash = "sha256:23010f129180089fbcd3bc08cfefccb3b890b0050e1ca00c867036e9d161b98c"}, + {file = "docutils-0.18.1.tar.gz", hash = "sha256:679987caf361a7539d76e584cbeddc311e3aee937877c87346f31debc63e9d06"}, +] + +[[package]] +name = "exceptiongroup" +version = "1.1.3" +description = "Backport of PEP 654 (exception groups)" +optional = false +python-versions = ">=3.7" +files = [ + {file = "exceptiongroup-1.1.3-py3-none-any.whl", hash = "sha256:343280667a4585d195ca1cf9cef84a4e178c4b6cf2274caef9859782b567d5e3"}, + {file = "exceptiongroup-1.1.3.tar.gz", hash = "sha256:097acd85d473d75af5bb98e41b61ff7fe35efe6675e4f9370ec6ec5126d160e9"}, +] + +[package.extras] +test = ["pytest (>=6)"] + +[[package]] +name = "filelock" +version = "3.12.2" +description = "A platform independent file lock." +optional = false +python-versions = ">=3.7" +files = [ + {file = "filelock-3.12.2-py3-none-any.whl", hash = "sha256:cbb791cdea2a72f23da6ac5b5269ab0a0d161e9ef0100e653b69049a7706d1ec"}, + {file = "filelock-3.12.2.tar.gz", hash = "sha256:002740518d8aa59a26b0c76e10fb8c6e15eae825d34b6fdf670333fd7b938d81"}, +] + +[package.extras] +docs = ["furo (>=2023.5.20)", "sphinx (>=7.0.1)", "sphinx-autodoc-typehints (>=1.23,!=1.23.4)"] +testing = ["covdefaults (>=2.3)", "coverage (>=7.2.7)", "diff-cover (>=7.5)", "pytest (>=7.3.1)", "pytest-cov (>=4.1)", "pytest-mock (>=3.10)", "pytest-timeout (>=2.1)"] + +[[package]] +name = "filelock" +version = "3.12.4" +description = "A platform independent file lock." +optional = false +python-versions = ">=3.8" +files = [ + {file = "filelock-3.12.4-py3-none-any.whl", hash = "sha256:08c21d87ded6e2b9da6728c3dff51baf1dcecf973b768ef35bcbc3447edb9ad4"}, + {file = "filelock-3.12.4.tar.gz", hash = "sha256:2e6f249f1f3654291606e046b09f1fd5eac39b360664c27f5aad072012f8bcbd"}, +] + +[package.extras] +docs = ["furo (>=2023.7.26)", "sphinx (>=7.1.2)", "sphinx-autodoc-typehints (>=1.24)"] +testing = ["covdefaults (>=2.3)", "coverage (>=7.3)", "diff-cover (>=7.7)", "pytest (>=7.4)", "pytest-cov (>=4.1)", "pytest-mock (>=3.11.1)", "pytest-timeout (>=2.1)"] +typing = ["typing-extensions (>=4.7.1)"] + +[[package]] +name = "identify" +version = "2.5.24" +description = "File identification library for Python" +optional = false +python-versions = ">=3.7" +files = [ + {file = "identify-2.5.24-py2.py3-none-any.whl", hash = "sha256:986dbfb38b1140e763e413e6feb44cd731faf72d1909543178aa79b0e258265d"}, + {file = "identify-2.5.24.tar.gz", hash = "sha256:0aac67d5b4812498056d28a9a512a483f5085cc28640b02b258a59dac34301d4"}, +] + +[package.extras] +license = ["ukkonen"] + +[[package]] +name = "identify" +version = "2.5.30" +description = "File identification library for Python" +optional = false +python-versions = ">=3.8" +files = [ + {file = "identify-2.5.30-py2.py3-none-any.whl", hash = "sha256:afe67f26ae29bab007ec21b03d4114f41316ab9dd15aa8736a167481e108da54"}, + {file = "identify-2.5.30.tar.gz", hash = "sha256:f302a4256a15c849b91cfcdcec052a8ce914634b2f77ae87dad29cd749f2d88d"}, +] + +[package.extras] +license = ["ukkonen"] + +[[package]] +name = "idna" +version = "3.4" +description = "Internationalized Domain Names in Applications (IDNA)" +optional = false +python-versions = ">=3.5" +files = [ + {file = "idna-3.4-py3-none-any.whl", hash = "sha256:90b77e79eaa3eba6de819a0c442c0b4ceefc341a7a2ab77d7562bf49f425c5c2"}, + {file = "idna-3.4.tar.gz", hash = "sha256:814f528e8dead7d329833b91c5faa87d60bf71824cd12a7530b5526063d02cb4"}, +] + +[[package]] +name = "imagesize" +version = "1.4.1" +description = "Getting image size from png/jpeg/jpeg2000/gif file" +optional = false +python-versions = ">=2.7, !=3.0.*, !=3.1.*, !=3.2.*, !=3.3.*" +files = [ + {file = "imagesize-1.4.1-py2.py3-none-any.whl", hash = "sha256:0d8d18d08f840c19d0ee7ca1fd82490fdc3729b7ac93f49870406ddde8ef8d8b"}, + {file = "imagesize-1.4.1.tar.gz", hash = "sha256:69150444affb9cb0d5cc5a92b3676f0b2fb7cd9ae39e947a5e11a36b4497cd4a"}, +] + +[[package]] +name = "importlib-metadata" +version = "6.7.0" +description = "Read metadata from Python packages" +optional = false +python-versions = ">=3.7" +files = [ + {file = "importlib_metadata-6.7.0-py3-none-any.whl", hash = "sha256:cb52082e659e97afc5dac71e79de97d8681de3aa07ff18578330904a9d18e5b5"}, + {file = "importlib_metadata-6.7.0.tar.gz", hash = "sha256:1aaf550d4f73e5d6783e7acb77aec43d49da8017410afae93822cc9cca98c4d4"}, +] + +[package.dependencies] +typing-extensions = {version = ">=3.6.4", markers = "python_version < \"3.8\""} +zipp = ">=0.5" + +[package.extras] +docs = ["furo", "jaraco.packaging (>=9)", "jaraco.tidelift (>=1.4)", "rst.linker (>=1.9)", "sphinx (>=3.5)", "sphinx-lint"] +perf = ["ipython"] +testing = ["flufl.flake8", "importlib-resources (>=1.3)", "packaging", "pyfakefs", "pytest (>=6)", "pytest-black (>=0.3.7)", "pytest-checkdocs (>=2.4)", "pytest-cov", "pytest-enabler (>=1.3)", "pytest-mypy (>=0.9.1)", "pytest-perf (>=0.9.2)", "pytest-ruff"] + +[[package]] +name = "importlib-resources" +version = "5.12.0" +description = "Read resources from Python packages" +optional = false +python-versions = ">=3.7" +files = [ + {file = "importlib_resources-5.12.0-py3-none-any.whl", hash = "sha256:7b1deeebbf351c7578e09bf2f63fa2ce8b5ffec296e0d349139d43cca061a81a"}, + {file = "importlib_resources-5.12.0.tar.gz", hash = "sha256:4be82589bf5c1d7999aedf2a45159d10cb3ca4f19b2271f8792bc8e6da7b22f6"}, +] + +[package.dependencies] +zipp = {version = ">=3.1.0", markers = "python_version < \"3.10\""} + +[package.extras] +docs = ["furo", "jaraco.packaging (>=9)", "jaraco.tidelift (>=1.4)", "rst.linker (>=1.9)", "sphinx (>=3.5)", "sphinx-lint"] +testing = ["flake8 (<5)", "pytest (>=6)", "pytest-black (>=0.3.7)", "pytest-checkdocs (>=2.4)", "pytest-cov", "pytest-enabler (>=1.3)", "pytest-flake8", "pytest-mypy (>=0.9.1)"] + +[[package]] +name = "incremental" +version = "22.10.0" +description = "\"A small library that versions your Python projects.\"" +optional = false +python-versions = "*" +files = [ + {file = "incremental-22.10.0-py2.py3-none-any.whl", hash = "sha256:b864a1f30885ee72c5ac2835a761b8fe8aa9c28b9395cacf27286602688d3e51"}, + {file = "incremental-22.10.0.tar.gz", hash = "sha256:912feeb5e0f7e0188e6f42241d2f450002e11bbc0937c65865045854c24c0bd0"}, +] + +[package.extras] +mypy = ["click (>=6.0)", "mypy (==0.812)", "twisted (>=16.4.0)"] +scripts = ["click (>=6.0)", "twisted (>=16.4.0)"] + +[[package]] +name = "iniconfig" +version = "2.0.0" +description = "brain-dead simple config-ini parsing" +optional = false +python-versions = ">=3.7" +files = [ + {file = "iniconfig-2.0.0-py3-none-any.whl", hash = "sha256:b6a85871a79d2e3b22d2d1b94ac2824226a63c6b741c88f7ae975f18b6778374"}, + {file = "iniconfig-2.0.0.tar.gz", hash = "sha256:2d91e135bf72d31a410b17c16da610a82cb55f6b0477d1a902134b24a455b8b3"}, +] + +[[package]] +name = "jinja2" +version = "3.1.2" +description = "A very fast and expressive template engine." +optional = false +python-versions = ">=3.7" +files = [ + {file = "Jinja2-3.1.2-py3-none-any.whl", hash = "sha256:6088930bfe239f0e6710546ab9c19c9ef35e29792895fed6e6e31a023a182a61"}, + {file = "Jinja2-3.1.2.tar.gz", hash = "sha256:31351a702a408a9e7595a8fc6150fc3f43bb6bf7e319770cbc0db9df9437e852"}, +] + +[package.dependencies] +MarkupSafe = ">=2.0" + +[package.extras] +i18n = ["Babel (>=2.7)"] + +[[package]] +name = "markupsafe" +version = "2.1.3" +description = "Safely add untrusted strings to HTML/XML markup." +optional = false +python-versions = ">=3.7" +files = [ + {file = "MarkupSafe-2.1.3-cp310-cp310-macosx_10_9_universal2.whl", hash = "sha256:cd0f502fe016460680cd20aaa5a76d241d6f35a1c3350c474bac1273803893fa"}, + {file = "MarkupSafe-2.1.3-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:e09031c87a1e51556fdcb46e5bd4f59dfb743061cf93c4d6831bf894f125eb57"}, + {file = "MarkupSafe-2.1.3-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:68e78619a61ecf91e76aa3e6e8e33fc4894a2bebe93410754bd28fce0a8a4f9f"}, + {file = "MarkupSafe-2.1.3-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:65c1a9bcdadc6c28eecee2c119465aebff8f7a584dd719facdd9e825ec61ab52"}, + {file = "MarkupSafe-2.1.3-cp310-cp310-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:525808b8019e36eb524b8c68acdd63a37e75714eac50e988180b169d64480a00"}, + {file = "MarkupSafe-2.1.3-cp310-cp310-musllinux_1_1_aarch64.whl", hash = "sha256:962f82a3086483f5e5f64dbad880d31038b698494799b097bc59c2edf392fce6"}, + {file = "MarkupSafe-2.1.3-cp310-cp310-musllinux_1_1_i686.whl", hash = "sha256:aa7bd130efab1c280bed0f45501b7c8795f9fdbeb02e965371bbef3523627779"}, + {file = "MarkupSafe-2.1.3-cp310-cp310-musllinux_1_1_x86_64.whl", hash = "sha256:c9c804664ebe8f83a211cace637506669e7890fec1b4195b505c214e50dd4eb7"}, + {file = "MarkupSafe-2.1.3-cp310-cp310-win32.whl", hash = "sha256:10bbfe99883db80bdbaff2dcf681dfc6533a614f700da1287707e8a5d78a8431"}, + {file = "MarkupSafe-2.1.3-cp310-cp310-win_amd64.whl", hash = "sha256:1577735524cdad32f9f694208aa75e422adba74f1baee7551620e43a3141f559"}, + {file = "MarkupSafe-2.1.3-cp311-cp311-macosx_10_9_universal2.whl", hash = "sha256:ad9e82fb8f09ade1c3e1b996a6337afac2b8b9e365f926f5a61aacc71adc5b3c"}, + {file = "MarkupSafe-2.1.3-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:3c0fae6c3be832a0a0473ac912810b2877c8cb9d76ca48de1ed31e1c68386575"}, + {file = "MarkupSafe-2.1.3-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:b076b6226fb84157e3f7c971a47ff3a679d837cf338547532ab866c57930dbee"}, + {file = "MarkupSafe-2.1.3-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:bfce63a9e7834b12b87c64d6b155fdd9b3b96191b6bd334bf37db7ff1fe457f2"}, + {file = "MarkupSafe-2.1.3-cp311-cp311-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:338ae27d6b8745585f87218a3f23f1512dbf52c26c28e322dbe54bcede54ccb9"}, + {file = "MarkupSafe-2.1.3-cp311-cp311-musllinux_1_1_aarch64.whl", hash = "sha256:e4dd52d80b8c83fdce44e12478ad2e85c64ea965e75d66dbeafb0a3e77308fcc"}, + {file = "MarkupSafe-2.1.3-cp311-cp311-musllinux_1_1_i686.whl", hash = "sha256:df0be2b576a7abbf737b1575f048c23fb1d769f267ec4358296f31c2479db8f9"}, + {file = "MarkupSafe-2.1.3-cp311-cp311-musllinux_1_1_x86_64.whl", hash = "sha256:5bbe06f8eeafd38e5d0a4894ffec89378b6c6a625ff57e3028921f8ff59318ac"}, + {file = "MarkupSafe-2.1.3-cp311-cp311-win32.whl", hash = "sha256:dd15ff04ffd7e05ffcb7fe79f1b98041b8ea30ae9234aed2a9168b5797c3effb"}, + {file = "MarkupSafe-2.1.3-cp311-cp311-win_amd64.whl", hash = "sha256:134da1eca9ec0ae528110ccc9e48041e0828d79f24121a1a146161103c76e686"}, + {file = "MarkupSafe-2.1.3-cp37-cp37m-macosx_10_9_x86_64.whl", hash = "sha256:8e254ae696c88d98da6555f5ace2279cf7cd5b3f52be2b5cf97feafe883b58d2"}, + {file = "MarkupSafe-2.1.3-cp37-cp37m-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:cb0932dc158471523c9637e807d9bfb93e06a95cbf010f1a38b98623b929ef2b"}, + {file = "MarkupSafe-2.1.3-cp37-cp37m-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:9402b03f1a1b4dc4c19845e5c749e3ab82d5078d16a2a4c2cd2df62d57bb0707"}, + {file = "MarkupSafe-2.1.3-cp37-cp37m-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:ca379055a47383d02a5400cb0d110cef0a776fc644cda797db0c5696cfd7e18e"}, + {file = "MarkupSafe-2.1.3-cp37-cp37m-musllinux_1_1_aarch64.whl", hash = "sha256:b7ff0f54cb4ff66dd38bebd335a38e2c22c41a8ee45aa608efc890ac3e3931bc"}, + {file = "MarkupSafe-2.1.3-cp37-cp37m-musllinux_1_1_i686.whl", hash = "sha256:c011a4149cfbcf9f03994ec2edffcb8b1dc2d2aede7ca243746df97a5d41ce48"}, + {file = "MarkupSafe-2.1.3-cp37-cp37m-musllinux_1_1_x86_64.whl", hash = "sha256:56d9f2ecac662ca1611d183feb03a3fa4406469dafe241673d521dd5ae92a155"}, + {file = "MarkupSafe-2.1.3-cp37-cp37m-win32.whl", hash = "sha256:8758846a7e80910096950b67071243da3e5a20ed2546e6392603c096778d48e0"}, + {file = "MarkupSafe-2.1.3-cp37-cp37m-win_amd64.whl", hash = "sha256:787003c0ddb00500e49a10f2844fac87aa6ce977b90b0feaaf9de23c22508b24"}, + {file = "MarkupSafe-2.1.3-cp38-cp38-macosx_10_9_universal2.whl", hash = "sha256:2ef12179d3a291be237280175b542c07a36e7f60718296278d8593d21ca937d4"}, + {file = "MarkupSafe-2.1.3-cp38-cp38-macosx_10_9_x86_64.whl", hash = "sha256:2c1b19b3aaacc6e57b7e25710ff571c24d6c3613a45e905b1fde04d691b98ee0"}, + {file = "MarkupSafe-2.1.3-cp38-cp38-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:8afafd99945ead6e075b973fefa56379c5b5c53fd8937dad92c662da5d8fd5ee"}, + {file = "MarkupSafe-2.1.3-cp38-cp38-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:8c41976a29d078bb235fea9b2ecd3da465df42a562910f9022f1a03107bd02be"}, + {file = "MarkupSafe-2.1.3-cp38-cp38-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:d080e0a5eb2529460b30190fcfcc4199bd7f827663f858a226a81bc27beaa97e"}, + {file = "MarkupSafe-2.1.3-cp38-cp38-musllinux_1_1_aarch64.whl", hash = "sha256:69c0f17e9f5a7afdf2cc9fb2d1ce6aabdb3bafb7f38017c0b77862bcec2bbad8"}, + {file = "MarkupSafe-2.1.3-cp38-cp38-musllinux_1_1_i686.whl", hash = "sha256:504b320cd4b7eff6f968eddf81127112db685e81f7e36e75f9f84f0df46041c3"}, + {file = "MarkupSafe-2.1.3-cp38-cp38-musllinux_1_1_x86_64.whl", hash = "sha256:42de32b22b6b804f42c5d98be4f7e5e977ecdd9ee9b660fda1a3edf03b11792d"}, + {file = "MarkupSafe-2.1.3-cp38-cp38-win32.whl", hash = "sha256:ceb01949af7121f9fc39f7d27f91be8546f3fb112c608bc4029aef0bab86a2a5"}, + {file = "MarkupSafe-2.1.3-cp38-cp38-win_amd64.whl", hash = "sha256:1b40069d487e7edb2676d3fbdb2b0829ffa2cd63a2ec26c4938b2d34391b4ecc"}, + {file = "MarkupSafe-2.1.3-cp39-cp39-macosx_10_9_universal2.whl", hash = "sha256:8023faf4e01efadfa183e863fefde0046de576c6f14659e8782065bcece22198"}, + {file = "MarkupSafe-2.1.3-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:6b2b56950d93e41f33b4223ead100ea0fe11f8e6ee5f641eb753ce4b77a7042b"}, + {file = "MarkupSafe-2.1.3-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:9dcdfd0eaf283af041973bff14a2e143b8bd64e069f4c383416ecd79a81aab58"}, + {file = "MarkupSafe-2.1.3-cp39-cp39-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:05fb21170423db021895e1ea1e1f3ab3adb85d1c2333cbc2310f2a26bc77272e"}, + {file = "MarkupSafe-2.1.3-cp39-cp39-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:282c2cb35b5b673bbcadb33a585408104df04f14b2d9b01d4c345a3b92861c2c"}, + {file = "MarkupSafe-2.1.3-cp39-cp39-musllinux_1_1_aarch64.whl", hash = "sha256:ab4a0df41e7c16a1392727727e7998a467472d0ad65f3ad5e6e765015df08636"}, + {file = "MarkupSafe-2.1.3-cp39-cp39-musllinux_1_1_i686.whl", hash = "sha256:7ef3cb2ebbf91e330e3bb937efada0edd9003683db6b57bb108c4001f37a02ea"}, + {file = "MarkupSafe-2.1.3-cp39-cp39-musllinux_1_1_x86_64.whl", hash = "sha256:0a4e4a1aff6c7ac4cd55792abf96c915634c2b97e3cc1c7129578aa68ebd754e"}, + {file = "MarkupSafe-2.1.3-cp39-cp39-win32.whl", hash = "sha256:fec21693218efe39aa7f8599346e90c705afa52c5b31ae019b2e57e8f6542bb2"}, + {file = "MarkupSafe-2.1.3-cp39-cp39-win_amd64.whl", hash = "sha256:3fd4abcb888d15a94f32b75d8fd18ee162ca0c064f35b11134be77050296d6ba"}, + {file = "MarkupSafe-2.1.3.tar.gz", hash = "sha256:af598ed32d6ae86f1b747b82783958b1a4ab8f617b06fe68795c7f026abbdcad"}, +] + +[[package]] +name = "mypy" +version = "1.4.1" +description = "Optional static typing for Python" +optional = false +python-versions = ">=3.7" +files = [ + {file = "mypy-1.4.1-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:566e72b0cd6598503e48ea610e0052d1b8168e60a46e0bfd34b3acf2d57f96a8"}, + {file = "mypy-1.4.1-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:ca637024ca67ab24a7fd6f65d280572c3794665eaf5edcc7e90a866544076878"}, + {file = "mypy-1.4.1-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:0dde1d180cd84f0624c5dcaaa89c89775550a675aff96b5848de78fb11adabcd"}, + {file = "mypy-1.4.1-cp310-cp310-musllinux_1_1_x86_64.whl", hash = "sha256:8c4d8e89aa7de683e2056a581ce63c46a0c41e31bd2b6d34144e2c80f5ea53dc"}, + {file = "mypy-1.4.1-cp310-cp310-win_amd64.whl", hash = "sha256:bfdca17c36ae01a21274a3c387a63aa1aafe72bff976522886869ef131b937f1"}, + {file = "mypy-1.4.1-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:7549fbf655e5825d787bbc9ecf6028731973f78088fbca3a1f4145c39ef09462"}, + {file = "mypy-1.4.1-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:98324ec3ecf12296e6422939e54763faedbfcc502ea4a4c38502082711867258"}, + {file = "mypy-1.4.1-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:141dedfdbfe8a04142881ff30ce6e6653c9685b354876b12e4fe6c78598b45e2"}, + {file = "mypy-1.4.1-cp311-cp311-musllinux_1_1_x86_64.whl", hash = "sha256:8207b7105829eca6f3d774f64a904190bb2231de91b8b186d21ffd98005f14a7"}, + {file = "mypy-1.4.1-cp311-cp311-win_amd64.whl", hash = "sha256:16f0db5b641ba159eff72cff08edc3875f2b62b2fa2bc24f68c1e7a4e8232d01"}, + {file = "mypy-1.4.1-cp37-cp37m-macosx_10_9_x86_64.whl", hash = "sha256:470c969bb3f9a9efcedbadcd19a74ffb34a25f8e6b0e02dae7c0e71f8372f97b"}, + {file = "mypy-1.4.1-cp37-cp37m-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:e5952d2d18b79f7dc25e62e014fe5a23eb1a3d2bc66318df8988a01b1a037c5b"}, + {file = "mypy-1.4.1-cp37-cp37m-musllinux_1_1_x86_64.whl", hash = "sha256:190b6bab0302cec4e9e6767d3eb66085aef2a1cc98fe04936d8a42ed2ba77bb7"}, + {file = "mypy-1.4.1-cp37-cp37m-win_amd64.whl", hash = "sha256:9d40652cc4fe33871ad3338581dca3297ff5f2213d0df345bcfbde5162abf0c9"}, + {file = "mypy-1.4.1-cp38-cp38-macosx_10_9_x86_64.whl", hash = "sha256:01fd2e9f85622d981fd9063bfaef1aed6e336eaacca00892cd2d82801ab7c042"}, + {file = "mypy-1.4.1-cp38-cp38-macosx_11_0_arm64.whl", hash = "sha256:2460a58faeea905aeb1b9b36f5065f2dc9a9c6e4c992a6499a2360c6c74ceca3"}, + {file = "mypy-1.4.1-cp38-cp38-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:a2746d69a8196698146a3dbe29104f9eb6a2a4d8a27878d92169a6c0b74435b6"}, + {file = "mypy-1.4.1-cp38-cp38-musllinux_1_1_x86_64.whl", hash = "sha256:ae704dcfaa180ff7c4cfbad23e74321a2b774f92ca77fd94ce1049175a21c97f"}, + {file = "mypy-1.4.1-cp38-cp38-win_amd64.whl", hash = "sha256:43d24f6437925ce50139a310a64b2ab048cb2d3694c84c71c3f2a1626d8101dc"}, + {file = "mypy-1.4.1-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:c482e1246726616088532b5e964e39765b6d1520791348e6c9dc3af25b233828"}, + {file = "mypy-1.4.1-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:43b592511672017f5b1a483527fd2684347fdffc041c9ef53428c8dc530f79a3"}, + {file = "mypy-1.4.1-cp39-cp39-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:34a9239d5b3502c17f07fd7c0b2ae6b7dd7d7f6af35fbb5072c6208e76295816"}, + {file = "mypy-1.4.1-cp39-cp39-musllinux_1_1_x86_64.whl", hash = "sha256:5703097c4936bbb9e9bce41478c8d08edd2865e177dc4c52be759f81ee4dd26c"}, + {file = "mypy-1.4.1-cp39-cp39-win_amd64.whl", hash = "sha256:e02d700ec8d9b1859790c0475df4e4092c7bf3272a4fd2c9f33d87fac4427b8f"}, + {file = "mypy-1.4.1-py3-none-any.whl", hash = "sha256:45d32cec14e7b97af848bddd97d85ea4f0db4d5a149ed9676caa4eb2f7402bb4"}, + {file = "mypy-1.4.1.tar.gz", hash = "sha256:9bbcd9ab8ea1f2e1c8031c21445b511442cc45c89951e49bbf852cbb70755b1b"}, +] + +[package.dependencies] +mypy-extensions = ">=1.0.0" +tomli = {version = ">=1.1.0", markers = "python_version < \"3.11\""} +typed-ast = {version = ">=1.4.0,<2", markers = "python_version < \"3.8\""} +typing-extensions = ">=4.1.0" + +[package.extras] +dmypy = ["psutil (>=4.0)"] +install-types = ["pip"] +python2 = ["typed-ast (>=1.4.0,<2)"] +reports = ["lxml"] + +[[package]] +name = "mypy" +version = "1.6.1" +description = "Optional static typing for Python" +optional = false +python-versions = ">=3.8" +files = [ + {file = "mypy-1.6.1-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:e5012e5cc2ac628177eaac0e83d622b2dd499e28253d4107a08ecc59ede3fc2c"}, + {file = "mypy-1.6.1-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:d8fbb68711905f8912e5af474ca8b78d077447d8f3918997fecbf26943ff3cbb"}, + {file = "mypy-1.6.1-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:21a1ad938fee7d2d96ca666c77b7c494c3c5bd88dff792220e1afbebb2925b5e"}, + {file = "mypy-1.6.1-cp310-cp310-musllinux_1_1_x86_64.whl", hash = "sha256:b96ae2c1279d1065413965c607712006205a9ac541895004a1e0d4f281f2ff9f"}, + {file = "mypy-1.6.1-cp310-cp310-win_amd64.whl", hash = "sha256:40b1844d2e8b232ed92e50a4bd11c48d2daa351f9deee6c194b83bf03e418b0c"}, + {file = "mypy-1.6.1-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:81af8adaa5e3099469e7623436881eff6b3b06db5ef75e6f5b6d4871263547e5"}, + {file = "mypy-1.6.1-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:8c223fa57cb154c7eab5156856c231c3f5eace1e0bed9b32a24696b7ba3c3245"}, + {file = "mypy-1.6.1-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:a8032e00ce71c3ceb93eeba63963b864bf635a18f6c0c12da6c13c450eedb183"}, + {file = "mypy-1.6.1-cp311-cp311-musllinux_1_1_x86_64.whl", hash = "sha256:4c46b51de523817a0045b150ed11b56f9fff55f12b9edd0f3ed35b15a2809de0"}, + {file = "mypy-1.6.1-cp311-cp311-win_amd64.whl", hash = "sha256:19f905bcfd9e167159b3d63ecd8cb5e696151c3e59a1742e79bc3bcb540c42c7"}, + {file = "mypy-1.6.1-cp312-cp312-macosx_10_9_x86_64.whl", hash = "sha256:82e469518d3e9a321912955cc702d418773a2fd1e91c651280a1bda10622f02f"}, + {file = "mypy-1.6.1-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:d4473c22cc296425bbbce7e9429588e76e05bc7342da359d6520b6427bf76660"}, + {file = "mypy-1.6.1-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:59a0d7d24dfb26729e0a068639a6ce3500e31d6655df8557156c51c1cb874ce7"}, + {file = "mypy-1.6.1-cp312-cp312-musllinux_1_1_x86_64.whl", hash = "sha256:cfd13d47b29ed3bbaafaff7d8b21e90d827631afda134836962011acb5904b71"}, + {file = "mypy-1.6.1-cp312-cp312-win_amd64.whl", hash = "sha256:eb4f18589d196a4cbe5290b435d135dee96567e07c2b2d43b5c4621b6501531a"}, + {file = "mypy-1.6.1-cp38-cp38-macosx_10_9_x86_64.whl", hash = "sha256:41697773aa0bf53ff917aa077e2cde7aa50254f28750f9b88884acea38a16169"}, + {file = "mypy-1.6.1-cp38-cp38-macosx_11_0_arm64.whl", hash = "sha256:7274b0c57737bd3476d2229c6389b2ec9eefeb090bbaf77777e9d6b1b5a9d143"}, + {file = "mypy-1.6.1-cp38-cp38-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:bbaf4662e498c8c2e352da5f5bca5ab29d378895fa2d980630656178bd607c46"}, + {file = "mypy-1.6.1-cp38-cp38-musllinux_1_1_x86_64.whl", hash = "sha256:bb8ccb4724f7d8601938571bf3f24da0da791fe2db7be3d9e79849cb64e0ae85"}, + {file = "mypy-1.6.1-cp38-cp38-win_amd64.whl", hash = "sha256:68351911e85145f582b5aa6cd9ad666c8958bcae897a1bfda8f4940472463c45"}, + {file = "mypy-1.6.1-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:49ae115da099dcc0922a7a895c1eec82c1518109ea5c162ed50e3b3594c71208"}, + {file = "mypy-1.6.1-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:8b27958f8c76bed8edaa63da0739d76e4e9ad4ed325c814f9b3851425582a3cd"}, + {file = "mypy-1.6.1-cp39-cp39-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:925cd6a3b7b55dfba252b7c4561892311c5358c6b5a601847015a1ad4eb7d332"}, + {file = "mypy-1.6.1-cp39-cp39-musllinux_1_1_x86_64.whl", hash = "sha256:8f57e6b6927a49550da3d122f0cb983d400f843a8a82e65b3b380d3d7259468f"}, + {file = "mypy-1.6.1-cp39-cp39-win_amd64.whl", hash = "sha256:a43ef1c8ddfdb9575691720b6352761f3f53d85f1b57d7745701041053deff30"}, + {file = "mypy-1.6.1-py3-none-any.whl", hash = "sha256:4cbe68ef919c28ea561165206a2dcb68591c50f3bcf777932323bc208d949cf1"}, + {file = "mypy-1.6.1.tar.gz", hash = "sha256:4d01c00d09a0be62a4ca3f933e315455bde83f37f892ba4b08ce92f3cf44bcc1"}, +] + +[package.dependencies] +mypy-extensions = ">=1.0.0" +tomli = {version = ">=1.1.0", markers = "python_version < \"3.11\""} +typing-extensions = ">=4.1.0" + +[package.extras] +dmypy = ["psutil (>=4.0)"] +install-types = ["pip"] +reports = ["lxml"] + +[[package]] +name = "mypy-extensions" +version = "1.0.0" +description = "Type system extensions for programs checked with the mypy type checker." +optional = false +python-versions = ">=3.5" +files = [ + {file = "mypy_extensions-1.0.0-py3-none-any.whl", hash = "sha256:4392f6c0eb8a5668a69e23d168ffa70f0be9ccfd32b5cc2d26a34ae5b844552d"}, + {file = "mypy_extensions-1.0.0.tar.gz", hash = "sha256:75dbf8955dc00442a438fc4d0666508a9a97b6bd41aa2f0ffe9d2f2725af0782"}, +] + +[[package]] +name = "nodeenv" +version = "1.8.0" +description = "Node.js virtual environment builder" +optional = false +python-versions = ">=2.7,!=3.0.*,!=3.1.*,!=3.2.*,!=3.3.*,!=3.4.*,!=3.5.*,!=3.6.*" +files = [ + {file = "nodeenv-1.8.0-py2.py3-none-any.whl", hash = "sha256:df865724bb3c3adc86b3876fa209771517b0cfe596beff01a92700e0e8be4cec"}, + {file = "nodeenv-1.8.0.tar.gz", hash = "sha256:d51e0c37e64fbf47d017feac3145cdbb58836d7eee8c6f6d3b6880c5456227d2"}, +] + +[package.dependencies] +setuptools = "*" + +[[package]] +name = "packaging" +version = "23.2" +description = "Core utilities for Python packages" +optional = false +python-versions = ">=3.7" +files = [ + {file = "packaging-23.2-py3-none-any.whl", hash = "sha256:8c491190033a9af7e1d931d0b5dacc2ef47509b34dd0de67ed209b5203fc88c7"}, + {file = "packaging-23.2.tar.gz", hash = "sha256:048fb0e9405036518eaaf48a55953c750c11e1a1b68e0dd1a9d62ed0c092cfc5"}, +] + +[[package]] +name = "platformdirs" +version = "3.11.0" +description = "A small Python package for determining appropriate platform-specific dirs, e.g. a \"user data dir\"." +optional = false +python-versions = ">=3.7" +files = [ + {file = "platformdirs-3.11.0-py3-none-any.whl", hash = "sha256:e9d171d00af68be50e9202731309c4e658fd8bc76f55c11c7dd760d023bda68e"}, + {file = "platformdirs-3.11.0.tar.gz", hash = "sha256:cf8ee52a3afdb965072dcc652433e0c7e3e40cf5ea1477cd4b3b1d2eb75495b3"}, +] + +[package.dependencies] +typing-extensions = {version = ">=4.7.1", markers = "python_version < \"3.8\""} + +[package.extras] +docs = ["furo (>=2023.7.26)", "proselint (>=0.13)", "sphinx (>=7.1.1)", "sphinx-autodoc-typehints (>=1.24)"] +test = ["appdirs (==1.4.4)", "covdefaults (>=2.3)", "pytest (>=7.4)", "pytest-cov (>=4.1)", "pytest-mock (>=3.11.1)"] + +[[package]] +name = "pluggy" +version = "1.2.0" +description = "plugin and hook calling mechanisms for python" +optional = false +python-versions = ">=3.7" +files = [ + {file = "pluggy-1.2.0-py3-none-any.whl", hash = "sha256:c2fd55a7d7a3863cba1a013e4e2414658b1d07b6bc57b3919e0c63c9abb99849"}, + {file = "pluggy-1.2.0.tar.gz", hash = "sha256:d12f0c4b579b15f5e054301bb226ee85eeeba08ffec228092f8defbaa3a4c4b3"}, +] + +[package.dependencies] +importlib-metadata = {version = ">=0.12", markers = "python_version < \"3.8\""} + +[package.extras] +dev = ["pre-commit", "tox"] +testing = ["pytest", "pytest-benchmark"] + +[[package]] +name = "pre-commit" +version = "2.21.0" +description = "A framework for managing and maintaining multi-language pre-commit hooks." +optional = false +python-versions = ">=3.7" +files = [ + {file = "pre_commit-2.21.0-py2.py3-none-any.whl", hash = "sha256:e2f91727039fc39a92f58a588a25b87f936de6567eed4f0e673e0507edc75bad"}, + {file = "pre_commit-2.21.0.tar.gz", hash = "sha256:31ef31af7e474a8d8995027fefdfcf509b5c913ff31f2015b4ec4beb26a6f658"}, +] + +[package.dependencies] +cfgv = ">=2.0.0" +identify = ">=1.0.0" +importlib-metadata = {version = "*", markers = "python_version < \"3.8\""} +nodeenv = ">=0.11.1" +pyyaml = ">=5.1" +virtualenv = ">=20.10.0" + +[[package]] +name = "pre-commit" +version = "3.5.0" +description = "A framework for managing and maintaining multi-language pre-commit hooks." +optional = false +python-versions = ">=3.8" +files = [ + {file = "pre_commit-3.5.0-py2.py3-none-any.whl", hash = "sha256:841dc9aef25daba9a0238cd27984041fa0467b4199fc4852e27950664919f660"}, + {file = "pre_commit-3.5.0.tar.gz", hash = "sha256:5804465c675b659b0862f07907f96295d490822a450c4c40e747d0b1c6ebcb32"}, +] + +[package.dependencies] +cfgv = ">=2.0.0" +identify = ">=1.0.0" +nodeenv = ">=0.11.1" +pyyaml = ">=5.1" +virtualenv = ">=20.10.0" + +[[package]] +name = "pygments" +version = "2.16.1" +description = "Pygments is a syntax highlighting package written in Python." +optional = false +python-versions = ">=3.7" +files = [ + {file = "Pygments-2.16.1-py3-none-any.whl", hash = "sha256:13fc09fa63bc8d8671a6d247e1eb303c4b343eaee81d861f3404db2935653692"}, + {file = "Pygments-2.16.1.tar.gz", hash = "sha256:1daff0494820c69bc8941e407aa20f577374ee88364ee10a98fdbe0aece96e29"}, +] + +[package.extras] +plugins = ["importlib-metadata"] + +[[package]] +name = "pytest" +version = "7.4.3" +description = "pytest: simple powerful testing with Python" +optional = false +python-versions = ">=3.7" +files = [ + {file = "pytest-7.4.3-py3-none-any.whl", hash = "sha256:0d009c083ea859a71b76adf7c1d502e4bc170b80a8ef002da5806527b9591fac"}, + {file = "pytest-7.4.3.tar.gz", hash = "sha256:d989d136982de4e3b29dabcc838ad581c64e8ed52c11fbe86ddebd9da0818cd5"}, +] + +[package.dependencies] +colorama = {version = "*", markers = "sys_platform == \"win32\""} +exceptiongroup = {version = ">=1.0.0rc8", markers = "python_version < \"3.11\""} +importlib-metadata = {version = ">=0.12", markers = "python_version < \"3.8\""} +iniconfig = "*" +packaging = "*" +pluggy = ">=0.12,<2.0" +tomli = {version = ">=1.0.0", markers = "python_version < \"3.11\""} + +[package.extras] +testing = ["argcomplete", "attrs (>=19.2.0)", "hypothesis (>=3.56)", "mock", "nose", "pygments (>=2.7.2)", "requests", "setuptools", "xmlschema"] + +[[package]] +name = "pytest-cov" +version = "4.1.0" +description = "Pytest plugin for measuring coverage." +optional = false +python-versions = ">=3.7" +files = [ + {file = "pytest-cov-4.1.0.tar.gz", hash = "sha256:3904b13dfbfec47f003b8e77fd5b589cd11904a21ddf1ab38a64f204d6a10ef6"}, + {file = "pytest_cov-4.1.0-py3-none-any.whl", hash = "sha256:6ba70b9e97e69fcc3fb45bfeab2d0a138fb65c4d0d6a41ef33983ad114be8c3a"}, +] + +[package.dependencies] +coverage = {version = ">=5.2.1", extras = ["toml"]} +pytest = ">=4.6" + +[package.extras] +testing = ["fields", "hunter", "process-tests", "pytest-xdist", "six", "virtualenv"] + +[[package]] +name = "pytest-mock" +version = "3.11.1" +description = "Thin-wrapper around the mock package for easier use with pytest" +optional = false +python-versions = ">=3.7" +files = [ + {file = "pytest-mock-3.11.1.tar.gz", hash = "sha256:7f6b125602ac6d743e523ae0bfa71e1a697a2f5534064528c6ff84c2f7c2fc7f"}, + {file = "pytest_mock-3.11.1-py3-none-any.whl", hash = "sha256:21c279fff83d70763b05f8874cc9cfb3fcacd6d354247a976f9529d19f9acf39"}, +] + +[package.dependencies] +pytest = ">=5.0" + +[package.extras] +dev = ["pre-commit", "pytest-asyncio", "tox"] + +[[package]] +name = "pytz" +version = "2023.3.post1" +description = "World timezone definitions, modern and historical" +optional = false +python-versions = "*" +files = [ + {file = "pytz-2023.3.post1-py2.py3-none-any.whl", hash = "sha256:ce42d816b81b68506614c11e8937d3aa9e41007ceb50bfdcb0749b921bf646c7"}, + {file = "pytz-2023.3.post1.tar.gz", hash = "sha256:7b4fddbeb94a1eba4b557da24f19fdf9db575192544270a9101d8509f9f43d7b"}, +] + +[[package]] +name = "pyyaml" +version = "6.0.1" +description = "YAML parser and emitter for Python" +optional = false +python-versions = ">=3.6" +files = [ + {file = "PyYAML-6.0.1-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:d858aa552c999bc8a8d57426ed01e40bef403cd8ccdd0fc5f6f04a00414cac2a"}, + {file = "PyYAML-6.0.1-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:fd66fc5d0da6d9815ba2cebeb4205f95818ff4b79c3ebe268e75d961704af52f"}, + {file = "PyYAML-6.0.1-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:69b023b2b4daa7548bcfbd4aa3da05b3a74b772db9e23b982788168117739938"}, + {file = "PyYAML-6.0.1-cp310-cp310-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:81e0b275a9ecc9c0c0c07b4b90ba548307583c125f54d5b6946cfee6360c733d"}, + {file = "PyYAML-6.0.1-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:ba336e390cd8e4d1739f42dfe9bb83a3cc2e80f567d8805e11b46f4a943f5515"}, + {file = "PyYAML-6.0.1-cp310-cp310-win32.whl", hash = "sha256:bd4af7373a854424dabd882decdc5579653d7868b8fb26dc7d0e99f823aa5924"}, + {file = "PyYAML-6.0.1-cp310-cp310-win_amd64.whl", hash = "sha256:fd1592b3fdf65fff2ad0004b5e363300ef59ced41c2e6b3a99d4089fa8c5435d"}, + {file = "PyYAML-6.0.1-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:6965a7bc3cf88e5a1c3bd2e0b5c22f8d677dc88a455344035f03399034eb3007"}, + {file = "PyYAML-6.0.1-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:f003ed9ad21d6a4713f0a9b5a7a0a79e08dd0f221aff4525a2be4c346ee60aab"}, + {file = "PyYAML-6.0.1-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:42f8152b8dbc4fe7d96729ec2b99c7097d656dc1213a3229ca5383f973a5ed6d"}, + {file = "PyYAML-6.0.1-cp311-cp311-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:062582fca9fabdd2c8b54a3ef1c978d786e0f6b3a1510e0ac93ef59e0ddae2bc"}, + {file = "PyYAML-6.0.1-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:d2b04aac4d386b172d5b9692e2d2da8de7bfb6c387fa4f801fbf6fb2e6ba4673"}, + {file = "PyYAML-6.0.1-cp311-cp311-win32.whl", hash = "sha256:1635fd110e8d85d55237ab316b5b011de701ea0f29d07611174a1b42f1444741"}, + {file = "PyYAML-6.0.1-cp311-cp311-win_amd64.whl", hash = "sha256:bf07ee2fef7014951eeb99f56f39c9bb4af143d8aa3c21b1677805985307da34"}, + {file = "PyYAML-6.0.1-cp36-cp36m-macosx_10_9_x86_64.whl", hash = "sha256:50550eb667afee136e9a77d6dc71ae76a44df8b3e51e41b77f6de2932bfe0f47"}, + {file = "PyYAML-6.0.1-cp36-cp36m-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:1fe35611261b29bd1de0070f0b2f47cb6ff71fa6595c077e42bd0c419fa27b98"}, + {file = "PyYAML-6.0.1-cp36-cp36m-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:704219a11b772aea0d8ecd7058d0082713c3562b4e271b849ad7dc4a5c90c13c"}, + {file = "PyYAML-6.0.1-cp36-cp36m-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:afd7e57eddb1a54f0f1a974bc4391af8bcce0b444685d936840f125cf046d5bd"}, + {file = "PyYAML-6.0.1-cp36-cp36m-win32.whl", hash = "sha256:fca0e3a251908a499833aa292323f32437106001d436eca0e6e7833256674585"}, + {file = "PyYAML-6.0.1-cp36-cp36m-win_amd64.whl", hash = "sha256:f22ac1c3cac4dbc50079e965eba2c1058622631e526bd9afd45fedd49ba781fa"}, + {file = "PyYAML-6.0.1-cp37-cp37m-macosx_10_9_x86_64.whl", hash = "sha256:b1275ad35a5d18c62a7220633c913e1b42d44b46ee12554e5fd39c70a243d6a3"}, + {file = "PyYAML-6.0.1-cp37-cp37m-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:18aeb1bf9a78867dc38b259769503436b7c72f7a1f1f4c93ff9a17de54319b27"}, + {file = "PyYAML-6.0.1-cp37-cp37m-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:596106435fa6ad000c2991a98fa58eeb8656ef2325d7e158344fb33864ed87e3"}, + {file = "PyYAML-6.0.1-cp37-cp37m-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:baa90d3f661d43131ca170712d903e6295d1f7a0f595074f151c0aed377c9b9c"}, + {file = "PyYAML-6.0.1-cp37-cp37m-win32.whl", hash = "sha256:9046c58c4395dff28dd494285c82ba00b546adfc7ef001486fbf0324bc174fba"}, + {file = "PyYAML-6.0.1-cp37-cp37m-win_amd64.whl", hash = "sha256:4fb147e7a67ef577a588a0e2c17b6db51dda102c71de36f8549b6816a96e1867"}, + {file = "PyYAML-6.0.1-cp38-cp38-macosx_10_9_x86_64.whl", hash = "sha256:1d4c7e777c441b20e32f52bd377e0c409713e8bb1386e1099c2415f26e479595"}, + {file = "PyYAML-6.0.1-cp38-cp38-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:a0cd17c15d3bb3fa06978b4e8958dcdc6e0174ccea823003a106c7d4d7899ac5"}, + {file = "PyYAML-6.0.1-cp38-cp38-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:28c119d996beec18c05208a8bd78cbe4007878c6dd15091efb73a30e90539696"}, + {file = "PyYAML-6.0.1-cp38-cp38-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:7e07cbde391ba96ab58e532ff4803f79c4129397514e1413a7dc761ccd755735"}, + {file = "PyYAML-6.0.1-cp38-cp38-win32.whl", hash = "sha256:184c5108a2aca3c5b3d3bf9395d50893a7ab82a38004c8f61c258d4428e80206"}, + {file = "PyYAML-6.0.1-cp38-cp38-win_amd64.whl", hash = "sha256:1e2722cc9fbb45d9b87631ac70924c11d3a401b2d7f410cc0e3bbf249f2dca62"}, + {file = "PyYAML-6.0.1-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:9eb6caa9a297fc2c2fb8862bc5370d0303ddba53ba97e71f08023b6cd73d16a8"}, + {file = "PyYAML-6.0.1-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:c8098ddcc2a85b61647b2590f825f3db38891662cfc2fc776415143f599bb859"}, + {file = "PyYAML-6.0.1-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:5773183b6446b2c99bb77e77595dd486303b4faab2b086e7b17bc6bef28865f6"}, + {file = "PyYAML-6.0.1-cp39-cp39-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:b786eecbdf8499b9ca1d697215862083bd6d2a99965554781d0d8d1ad31e13a0"}, + {file = "PyYAML-6.0.1-cp39-cp39-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:bc1bf2925a1ecd43da378f4db9e4f799775d6367bdb94671027b73b393a7c42c"}, + {file = "PyYAML-6.0.1-cp39-cp39-win32.whl", hash = "sha256:faca3bdcf85b2fc05d06ff3fbc1f83e1391b3e724afa3feba7d13eeab355484c"}, + {file = "PyYAML-6.0.1-cp39-cp39-win_amd64.whl", hash = "sha256:510c9deebc5c0225e8c96813043e62b680ba2f9c50a08d3724c7f28a747d1486"}, + {file = "PyYAML-6.0.1.tar.gz", hash = "sha256:bfdf460b1736c775f2ba9f6a92bca30bc2095067b8a9d77876d1fad6cc3b4a43"}, +] + +[[package]] +name = "rapidfuzz" +version = "3.4.0" +description = "rapid fuzzy string matching" +optional = false +python-versions = ">=3.7" +files = [ + {file = "rapidfuzz-3.4.0-cp310-cp310-macosx_10_9_universal2.whl", hash = "sha256:1438e68fe8869fe6819a313140e98641b34bfc89234b82486d8fd02044a067e8"}, + {file = "rapidfuzz-3.4.0-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:59f851c7a54a9652b9598553547e0940244bfce7c9b672bac728efa0b9028d03"}, + {file = "rapidfuzz-3.4.0-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:6286510910fcd649471a7f5b77fcc971e673729e7c84216dbf321bead580d5a1"}, + {file = "rapidfuzz-3.4.0-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:87409e12f9a82aa33a5b845c49dd8d5d4264f2f171f0a69ddc638e100fcc50de"}, + {file = "rapidfuzz-3.4.0-cp310-cp310-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:d1d81d380ceabc8297880525c9d8b9e93fead38d3d2254e558c36c18aaf2553f"}, + {file = "rapidfuzz-3.4.0-cp310-cp310-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:a716efcfc92659d8695291f07da4fa60f42a131dc4ceab583931452dd5662e92"}, + {file = "rapidfuzz-3.4.0-cp310-cp310-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:83387fb81c4c0234b199110655779762dd5982cdf9de4f7c321110713193133e"}, + {file = "rapidfuzz-3.4.0-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:55efb3231bb954f3597313ebdf104289b8d139d5429ad517051855f84e12b94e"}, + {file = "rapidfuzz-3.4.0-cp310-cp310-musllinux_1_1_aarch64.whl", hash = "sha256:51d47d52c890cbdb2d8b2085d747e557f15efd9c990cb6ae624c8f6948c4aa3a"}, + {file = "rapidfuzz-3.4.0-cp310-cp310-musllinux_1_1_i686.whl", hash = "sha256:3db79070888d0dcd4f6a20fd30b8184dd975d6b0f7818acff5d7e07eba19b71f"}, + {file = "rapidfuzz-3.4.0-cp310-cp310-musllinux_1_1_ppc64le.whl", hash = "sha256:46efc5e4675e2bd5118427513f86eaf3689e1482ebd309ad4532bcefae78179d"}, + {file = "rapidfuzz-3.4.0-cp310-cp310-musllinux_1_1_s390x.whl", hash = "sha256:d15c364c5aa8f032dadf5b82fa02b7a4bd9688a961a27961cd5b985203f58037"}, + {file = "rapidfuzz-3.4.0-cp310-cp310-musllinux_1_1_x86_64.whl", hash = "sha256:f1e91460baa42f5408f3c062913456a24b2fc1a181959b58a9c06b5eef700ca6"}, + {file = "rapidfuzz-3.4.0-cp310-cp310-win32.whl", hash = "sha256:c7f4f6dac25c120de8845a65a97090658c8a976827ac22b6b86e2a16a60bb820"}, + {file = "rapidfuzz-3.4.0-cp310-cp310-win_amd64.whl", hash = "sha256:124578029d926b2be32d60b748be95ee0de6cb2753eb49d6d1d6146269b428b9"}, + {file = "rapidfuzz-3.4.0-cp310-cp310-win_arm64.whl", hash = "sha256:3af0384132e79fe6f6370d49347649382e04f689277525903bef84d30f3992fd"}, + {file = "rapidfuzz-3.4.0-cp311-cp311-macosx_10_9_universal2.whl", hash = "sha256:66ff93b81b382269dc7c2d46c839ce72e2d2331ad46a06321770bc94016fe236"}, + {file = "rapidfuzz-3.4.0-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:da2764604a31fd1e3f1cacf226b43a871cc9f28844a3196c2a6b1ba52ae12922"}, + {file = "rapidfuzz-3.4.0-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:8eb33895353bfcc33ccf4b4bae837c0afb4eaf20a0361aa6f0800cef12505e91"}, + {file = "rapidfuzz-3.4.0-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:ed3da08830c08c8bcd49414cc06b704a760d3067804775facc0df725b52085a4"}, + {file = "rapidfuzz-3.4.0-cp311-cp311-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:b38c7021f6114cfacba5717192fb3e1e50053261d49a774e645021a2f77e20a3"}, + {file = "rapidfuzz-3.4.0-cp311-cp311-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:f7f5ea97886d2ec7b2b9a8172812a76e1d243f2ce705c2f24baf46f9ef5d3951"}, + {file = "rapidfuzz-3.4.0-cp311-cp311-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:5b9a7ab061c1b75b274fc2ebd1d29cfa2e510c36e2f4cd9518a6d56d589003c8"}, + {file = "rapidfuzz-3.4.0-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:23b07685c21c93cdf6d68b49eccacfe975651b8d99ea8a02687400c60315e5bc"}, + {file = "rapidfuzz-3.4.0-cp311-cp311-musllinux_1_1_aarch64.whl", hash = "sha256:c2a564f748497b6a5e08a1dc0ac06655f65377cf072c4f0e2c73818acc655d36"}, + {file = "rapidfuzz-3.4.0-cp311-cp311-musllinux_1_1_i686.whl", hash = "sha256:ef30b5f2720f0acbcfba0e0661a4cc118621c47cf69b5fe92531dfed1e369e1c"}, + {file = "rapidfuzz-3.4.0-cp311-cp311-musllinux_1_1_ppc64le.whl", hash = "sha256:ab981f9091ae8bd32bca9289fa1019b4ec656543489e7e13e64882d57d989282"}, + {file = "rapidfuzz-3.4.0-cp311-cp311-musllinux_1_1_s390x.whl", hash = "sha256:a80f9aa4245a49e0677896d1b51b2b3bc36472aff7cec31c4a96f789135f03fe"}, + {file = "rapidfuzz-3.4.0-cp311-cp311-musllinux_1_1_x86_64.whl", hash = "sha256:0d8c6cb80b5d2edf88bf6a88ac6827a353c974405c2d7e3025ed9527a5dbe1a6"}, + {file = "rapidfuzz-3.4.0-cp311-cp311-win32.whl", hash = "sha256:c0150d521199277b5ad8bd3b060a5f3c1dbdf11df0533b4d79f458ef11d07e8c"}, + {file = "rapidfuzz-3.4.0-cp311-cp311-win_amd64.whl", hash = "sha256:bd50bc90167601963e2a90b820fb862d239ecb096a991bf3ce33ffaa1d6eedee"}, + {file = "rapidfuzz-3.4.0-cp311-cp311-win_arm64.whl", hash = "sha256:bd10d68baabb63a3bb36b683f98fc481fcc62230e493e4b31e316bd5b299ef68"}, + {file = "rapidfuzz-3.4.0-cp312-cp312-macosx_10_9_universal2.whl", hash = "sha256:7f497f850d46c5e08f3340343842a28ede5d3997e5d1cadbd265793cf47417e5"}, + {file = "rapidfuzz-3.4.0-cp312-cp312-macosx_10_9_x86_64.whl", hash = "sha256:a7d6a9f04ea1277add8943d4e144e59215009f54f2668124ff26dee18a875343"}, + {file = "rapidfuzz-3.4.0-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:b6fe2aff0d9b35191701714e05afe08f79eaea376a3a6ca802b72d9e5b48b545"}, + {file = "rapidfuzz-3.4.0-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:b81b8bc29114ca861fed23da548a837832b85495b0c1b2600e6060e3cf4d50aa"}, + {file = "rapidfuzz-3.4.0-cp312-cp312-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:805dc2aa3ac295dcbf2df8c1e420e8a73b1f632d6820a5a1c8506d22c11e0f27"}, + {file = "rapidfuzz-3.4.0-cp312-cp312-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:1276c7f50cd90a48b00084feb25256135c9ace6c599295dd5932949ec30c0e70"}, + {file = "rapidfuzz-3.4.0-cp312-cp312-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:0b9197656a6d71483959bf7d216e7fb7a6b80ca507433bcb3015fb92abc266f8"}, + {file = "rapidfuzz-3.4.0-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:3456f4df5b8800315fd161045c996479016c112228e4da370d09ed80c24853e5"}, + {file = "rapidfuzz-3.4.0-cp312-cp312-musllinux_1_1_aarch64.whl", hash = "sha256:734046d557550589edb83d5ad1468a1341d1092f1c64f26fd0b1fc50f9efdce1"}, + {file = "rapidfuzz-3.4.0-cp312-cp312-musllinux_1_1_i686.whl", hash = "sha256:37d5f0fbad6c092c89840eea2c4c845564d40849785de74c5e6ff48b47b0ecf6"}, + {file = "rapidfuzz-3.4.0-cp312-cp312-musllinux_1_1_ppc64le.whl", hash = "sha256:bfe14711b9a7b744e242a482c6cabb696517a1a9946fc1e88d353cd3eb384788"}, + {file = "rapidfuzz-3.4.0-cp312-cp312-musllinux_1_1_s390x.whl", hash = "sha256:1a733c10b1fcc47f837c23ab4a255cc4021a88939ff81baa64d6738231cba33d"}, + {file = "rapidfuzz-3.4.0-cp312-cp312-musllinux_1_1_x86_64.whl", hash = "sha256:929e6b71e5b36caee2ee11c209e75a0fcbd716a1b76ae6162b89ee9b591b63b1"}, + {file = "rapidfuzz-3.4.0-cp312-cp312-win32.whl", hash = "sha256:c56073ba1d1b25585359ad9769163cb2f3183e7a03c03b914a0667fcbd95dc5c"}, + {file = "rapidfuzz-3.4.0-cp312-cp312-win_amd64.whl", hash = "sha256:bf58ba21df06fc8aeef3056fd137eca0a593c2f5c82923a4524d251dc5f3df5d"}, + {file = "rapidfuzz-3.4.0-cp312-cp312-win_arm64.whl", hash = "sha256:f3effbe9c677658b3149da0d2778a740a6b7d8190c1407fd0c0770a4e223cfe0"}, + {file = "rapidfuzz-3.4.0-cp37-cp37m-macosx_10_9_x86_64.whl", hash = "sha256:ed0d5761b44d9dd87278d5c32903bb55632346e4d84ea67ba2e4a84afc3b7d45"}, + {file = "rapidfuzz-3.4.0-cp37-cp37m-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:1bafbd3e2e9e0b5f740f66155cc7e1e23eee1e1f2c44eff12daf14f90af0e8ab"}, + {file = "rapidfuzz-3.4.0-cp37-cp37m-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:2543fd8d0fb3b1ac065bf94ee54c0ea33343c62481d8e54b6117a88c92c9b721"}, + {file = "rapidfuzz-3.4.0-cp37-cp37m-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:93ceb62ade1a0e62696487274002157a58bb751fc82cd25016fc5523ba558ca5"}, + {file = "rapidfuzz-3.4.0-cp37-cp37m-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:76f4162ce5fe08609455d318936ed4aa709f40784be61fb4e200a378137b0230"}, + {file = "rapidfuzz-3.4.0-cp37-cp37m-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:f723197f2dbce508a7030dcf6d3fc940117aa54fc876021bf6f6feeaf3825ba1"}, + {file = "rapidfuzz-3.4.0-cp37-cp37m-musllinux_1_1_aarch64.whl", hash = "sha256:cfdc74afd93ac71270b5be5c25cb864b733b9ae32b07495705a6ac294ac4c390"}, + {file = "rapidfuzz-3.4.0-cp37-cp37m-musllinux_1_1_i686.whl", hash = "sha256:273c7c7f5b405f2f54d41e805883572d57e1f0a56861f93ca5a6733672088acb"}, + {file = "rapidfuzz-3.4.0-cp37-cp37m-musllinux_1_1_ppc64le.whl", hash = "sha256:712dd91d429afaddbf7e86662155f2ad9bc8135fca5803a01035a3c1d76c5977"}, + {file = "rapidfuzz-3.4.0-cp37-cp37m-musllinux_1_1_s390x.whl", hash = "sha256:9814905414696080d8448d6e6df788a0148954ab34d7cd8d75bcb85ba30e0b25"}, + {file = "rapidfuzz-3.4.0-cp37-cp37m-musllinux_1_1_x86_64.whl", hash = "sha256:01013ee67fb15608c8c5961af3bc2b1f242cff94c19f53237c9b3f0edb8e0a2d"}, + {file = "rapidfuzz-3.4.0-cp37-cp37m-win32.whl", hash = "sha256:8f5d2adc48c181486125d42230e80479a1e0568942e883d1ebdeb76cd3f83470"}, + {file = "rapidfuzz-3.4.0-cp37-cp37m-win_amd64.whl", hash = "sha256:c92d847c997c384670e3b4cf6727cb73a4d7a7ba6457310e2083cf06d56013c4"}, + {file = "rapidfuzz-3.4.0-cp38-cp38-macosx_10_9_universal2.whl", hash = "sha256:d0bda173b0ec1fa546f123088c0d42c9096304771b4c0555d4e08a66a246b3f6"}, + {file = "rapidfuzz-3.4.0-cp38-cp38-macosx_10_9_x86_64.whl", hash = "sha256:bbb05b1203f683b341f44ebe8fe38afed6e56f606094f9840d6406e4a7bf0eab"}, + {file = "rapidfuzz-3.4.0-cp38-cp38-macosx_11_0_arm64.whl", hash = "sha256:f0075ff8990437923da42202b60cf04b5c122ee2856f0cf2344fb890cadecf57"}, + {file = "rapidfuzz-3.4.0-cp38-cp38-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:9f295842c282fe7fe93bfe7a20e78f33f43418f47fb601f2f0a05df8a8282b43"}, + {file = "rapidfuzz-3.4.0-cp38-cp38-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:1ebee7313719dfe652debb74bdd4024e8cf381a59adc6d065520ff927f3445f4"}, + {file = "rapidfuzz-3.4.0-cp38-cp38-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:f71454249ddd29d8ba5415ed7307e7b7493fc7e9018f1ff496127b8b9a8df94b"}, + {file = "rapidfuzz-3.4.0-cp38-cp38-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:52c6b7a178f0e800488fa1aede17b00f6397cab0b79d48531504b0d89e45315f"}, + {file = "rapidfuzz-3.4.0-cp38-cp38-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:6d38596c804a9f2bd49360c15e1f4afbf016f181fe37fc4f1a4ddd247d3e91e5"}, + {file = "rapidfuzz-3.4.0-cp38-cp38-musllinux_1_1_aarch64.whl", hash = "sha256:8756461e7ee79723b8f762fc6db226e65eb453bf9fa64b14fc0274d4aaaf9e21"}, + {file = "rapidfuzz-3.4.0-cp38-cp38-musllinux_1_1_i686.whl", hash = "sha256:e14799297f194a4480f373e45142ef16d5dc68a42084c0e2018e0bdba56a8fef"}, + {file = "rapidfuzz-3.4.0-cp38-cp38-musllinux_1_1_ppc64le.whl", hash = "sha256:f813fb663d90038c1171d30ea1b6b275e09fced32f1d12b972c6045d9d4233f2"}, + {file = "rapidfuzz-3.4.0-cp38-cp38-musllinux_1_1_s390x.whl", hash = "sha256:0df66e07e42e2831fae84dea481f7803bec7cfa53c31d770e86ac47bb18dcd57"}, + {file = "rapidfuzz-3.4.0-cp38-cp38-musllinux_1_1_x86_64.whl", hash = "sha256:b05c7d4b4ddb617e977d648689013e50e5688140ee03538d3760a3a11d4fa8a2"}, + {file = "rapidfuzz-3.4.0-cp38-cp38-win32.whl", hash = "sha256:74b9a1c1fc139d325fb0b89ccc85527d27096a76f6ed690ee3378143cc38e91d"}, + {file = "rapidfuzz-3.4.0-cp38-cp38-win_amd64.whl", hash = "sha256:5fe3ef7daecd79f852936528e37528fd88818bc000991e0fea23b9ac5b79e875"}, + {file = "rapidfuzz-3.4.0-cp39-cp39-macosx_10_9_universal2.whl", hash = "sha256:61f16bb0f3026853500e7968261831a2e1a35d56947752bb6cf6953afd70b9de"}, + {file = "rapidfuzz-3.4.0-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:d188e8fb5a9709931c6a48cc62c4ac9b9d163969333711e426d9dbd134c1489b"}, + {file = "rapidfuzz-3.4.0-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:c006aa481d1b91c2600920ce16e42d208a4b6f318d393aef4dd2172d568f2641"}, + {file = "rapidfuzz-3.4.0-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:02afbe7ed12e9191082ed7bda43398baced1d9d805302b7b010d397de3ae973f"}, + {file = "rapidfuzz-3.4.0-cp39-cp39-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:01d64710060bc3241c08ac1f1a9012c7184f3f4c3d6e2eebb16c6093a03f6a67"}, + {file = "rapidfuzz-3.4.0-cp39-cp39-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:d3198f70b97127e52a4f96bb2f7de447f89baa338ff398eb126930c8e3137ad1"}, + {file = "rapidfuzz-3.4.0-cp39-cp39-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:50ad7bac98a0f00492687eddda73d2c0bdf71c78b52fddaa5901634ae323d3ce"}, + {file = "rapidfuzz-3.4.0-cp39-cp39-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:cc3efc06db79e818f4a6783a4e001b3c8b2c61bd05c0d5c4d333adaf64ed1b34"}, + {file = "rapidfuzz-3.4.0-cp39-cp39-musllinux_1_1_aarch64.whl", hash = "sha256:75d1365387ec8ef2128fd7e2f7436aa1a04a1953bc6d7068835bb769cd07c146"}, + {file = "rapidfuzz-3.4.0-cp39-cp39-musllinux_1_1_i686.whl", hash = "sha256:a0750278693525b5ce58d3b313e432dfa5d90f00d06ae54fa8cde87f2a397eb0"}, + {file = "rapidfuzz-3.4.0-cp39-cp39-musllinux_1_1_ppc64le.whl", hash = "sha256:2e49151572b842d290dcee2cc6f9ce7a7b40b77cc20d0f6d6b54e7afb7bafa5c"}, + {file = "rapidfuzz-3.4.0-cp39-cp39-musllinux_1_1_s390x.whl", hash = "sha256:8b38d7677b2f20b137bb7aaf0dcd3d8ac2a2cde65f09f5621bf3f57d9a1e5d6e"}, + {file = "rapidfuzz-3.4.0-cp39-cp39-musllinux_1_1_x86_64.whl", hash = "sha256:d904ac97f2e370f91e8170802669c8ad68641bf84d742968416b53c5960410c6"}, + {file = "rapidfuzz-3.4.0-cp39-cp39-win32.whl", hash = "sha256:53bbef345644eac1c2d7cc21ade4fe9554fa289f60eb2c576f7fdc454dbc0641"}, + {file = "rapidfuzz-3.4.0-cp39-cp39-win_amd64.whl", hash = "sha256:233bf022938c38060a93863ec548e624d69a56d7384634d8bea435b915b88e52"}, + {file = "rapidfuzz-3.4.0-cp39-cp39-win_arm64.whl", hash = "sha256:63933792146f3d333680d415cecc237e6275b42ad948d0a798f9a81325517666"}, + {file = "rapidfuzz-3.4.0-pp37-pypy37_pp73-macosx_10_9_x86_64.whl", hash = "sha256:e182ea5c809e7ed36ebfbcef4bb1808e213d27b33c036007a33bcbb7ba498356"}, + {file = "rapidfuzz-3.4.0-pp37-pypy37_pp73-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:9e1142c8d35fa6f3af8150d02ff8edcbea3723c851d889e8b2172e0d1b99f3f7"}, + {file = "rapidfuzz-3.4.0-pp37-pypy37_pp73-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:6b8258846e56b03230fa733d29bb4f9fb1f4790ac97d1ebe9faa3ff9d2850999"}, + {file = "rapidfuzz-3.4.0-pp37-pypy37_pp73-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:950d1dfd2927cd45c9bb2927933926718f0a17792841e651d42f4d1cb04a5c1d"}, + {file = "rapidfuzz-3.4.0-pp37-pypy37_pp73-win_amd64.whl", hash = "sha256:dd54dd0355225dc3c1d55e233d510adcccee9bb25d656b4cf1136114b92e7bf3"}, + {file = "rapidfuzz-3.4.0-pp38-pypy38_pp73-macosx_10_9_x86_64.whl", hash = "sha256:f5921780e7995e9ac3cea41fa57b623159d7295788618d3f2946d61328c25c25"}, + {file = "rapidfuzz-3.4.0-pp38-pypy38_pp73-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:fc4b1b69a64d337c40fa07a721dae1b1550d90f17973fb348055f6440d597e26"}, + {file = "rapidfuzz-3.4.0-pp38-pypy38_pp73-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:6f5c8b901b6d3be63591c68e2612f76ad85af27193d0a88d4d87bb047aeafcb3"}, + {file = "rapidfuzz-3.4.0-pp38-pypy38_pp73-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:c67f5ced39aff6277dd772b239ef8aa8fc810200a3b42f69ddbb085ea0e18232"}, + {file = "rapidfuzz-3.4.0-pp38-pypy38_pp73-win_amd64.whl", hash = "sha256:4fd94acab871afbc845400814134a83512a711e824dc2c9a9776d6123464a221"}, + {file = "rapidfuzz-3.4.0-pp39-pypy39_pp73-macosx_10_9_x86_64.whl", hash = "sha256:437508ec1ea6e71a77126715ac6208cb9c3e74272536ebfa79be9dd008cfb85f"}, + {file = "rapidfuzz-3.4.0-pp39-pypy39_pp73-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:a7215f7c5de912b364d5cf7c4c66915ccf4acf71aafbb8da62ad346569196e15"}, + {file = "rapidfuzz-3.4.0-pp39-pypy39_pp73-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:698488002eb7be2f737e48679ed0cd310b76291f26d8ec792db8345d13eb6573"}, + {file = "rapidfuzz-3.4.0-pp39-pypy39_pp73-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:e77873126eb07e7461f0b675263e6c5d42c8a952e88e4a44eeff96f237b2b024"}, + {file = "rapidfuzz-3.4.0-pp39-pypy39_pp73-win_amd64.whl", hash = "sha256:28d03cd33817f6e0bea9b618b460f85ff9c9c3fedc6c19cfa0992f719a0d1801"}, + {file = "rapidfuzz-3.4.0.tar.gz", hash = "sha256:a74112e2126b428c77db5e96f7ce34e91e750552147305b2d361122cbede2955"}, +] + +[package.extras] +full = ["numpy"] + +[[package]] +name = "requests" +version = "2.31.0" +description = "Python HTTP for Humans." +optional = false +python-versions = ">=3.7" +files = [ + {file = "requests-2.31.0-py3-none-any.whl", hash = "sha256:58cd2187c01e70e6e26505bca751777aa9f2ee0b7f4300988b709f44e013003f"}, + {file = "requests-2.31.0.tar.gz", hash = "sha256:942c5a758f98d790eaed1a29cb6eefc7ffb0d1cf7af05c3d2791656dbd6ad1e1"}, +] + +[package.dependencies] +certifi = ">=2017.4.17" +charset-normalizer = ">=2,<4" +idna = ">=2.5,<4" +urllib3 = ">=1.21.1,<3" + +[package.extras] +socks = ["PySocks (>=1.5.6,!=1.5.7)"] +use-chardet-on-py3 = ["chardet (>=3.0.2,<6)"] + +[[package]] +name = "setuptools" +version = "68.2.2" +description = "Easily download, build, install, upgrade, and uninstall Python packages" +optional = false +python-versions = ">=3.8" +files = [ + {file = "setuptools-68.2.2-py3-none-any.whl", hash = "sha256:b454a35605876da60632df1a60f736524eb73cc47bbc9f3f1ef1b644de74fd2a"}, + {file = "setuptools-68.2.2.tar.gz", hash = "sha256:4ac1475276d2f1c48684874089fefcd83bd7162ddaafb81fac866ba0db282a87"}, +] + +[package.extras] +docs = ["furo", "jaraco.packaging (>=9.3)", "jaraco.tidelift (>=1.4)", "pygments-github-lexers (==0.0.5)", "rst.linker (>=1.9)", "sphinx (>=3.5)", "sphinx-favicon", "sphinx-hoverxref (<2)", "sphinx-inline-tabs", "sphinx-lint", "sphinx-notfound-page (>=1,<2)", "sphinx-reredirects", "sphinxcontrib-towncrier"] +testing = ["build[virtualenv]", "filelock (>=3.4.0)", "flake8-2020", "ini2toml[lite] (>=0.9)", "jaraco.develop (>=7.21)", "jaraco.envs (>=2.2)", "jaraco.path (>=3.2.0)", "pip (>=19.1)", "pytest (>=6)", "pytest-black (>=0.3.7)", "pytest-checkdocs (>=2.4)", "pytest-cov", "pytest-enabler (>=2.2)", "pytest-mypy (>=0.9.1)", "pytest-perf", "pytest-ruff", "pytest-timeout", "pytest-xdist", "tomli-w (>=1.0.0)", "virtualenv (>=13.0.0)", "wheel"] +testing-integration = ["build[virtualenv] (>=1.0.3)", "filelock (>=3.4.0)", "jaraco.envs (>=2.2)", "jaraco.path (>=3.2.0)", "packaging (>=23.1)", "pytest", "pytest-enabler", "pytest-xdist", "tomli", "virtualenv (>=13.0.0)", "wheel"] + +[[package]] +name = "snowballstemmer" +version = "2.2.0" +description = "This package provides 29 stemmers for 28 languages generated from Snowball algorithms." +optional = false +python-versions = "*" +files = [ + {file = "snowballstemmer-2.2.0-py2.py3-none-any.whl", hash = "sha256:c8e1716e83cc398ae16824e5572ae04e0d9fc2c6b985fb0f900f5f0c96ecba1a"}, + {file = "snowballstemmer-2.2.0.tar.gz", hash = "sha256:09b16deb8547d3412ad7b590689584cd0fe25ec8db3be37788be3810cbf19cb1"}, +] + +[[package]] +name = "sphinx" +version = "5.3.0" +description = "Python documentation generator" +optional = false +python-versions = ">=3.6" +files = [ + {file = "Sphinx-5.3.0.tar.gz", hash = "sha256:51026de0a9ff9fc13c05d74913ad66047e104f56a129ff73e174eb5c3ee794b5"}, + {file = "sphinx-5.3.0-py3-none-any.whl", hash = "sha256:060ca5c9f7ba57a08a1219e547b269fadf125ae25b06b9fa7f66768efb652d6d"}, +] + +[package.dependencies] +alabaster = ">=0.7,<0.8" +babel = ">=2.9" +colorama = {version = ">=0.4.5", markers = "sys_platform == \"win32\""} +docutils = ">=0.14,<0.20" +imagesize = ">=1.3" +importlib-metadata = {version = ">=4.8", markers = "python_version < \"3.10\""} +Jinja2 = ">=3.0" +packaging = ">=21.0" +Pygments = ">=2.12" +requests = ">=2.5.0" +snowballstemmer = ">=2.0" +sphinxcontrib-applehelp = "*" +sphinxcontrib-devhelp = "*" +sphinxcontrib-htmlhelp = ">=2.0.0" +sphinxcontrib-jsmath = "*" +sphinxcontrib-qthelp = "*" +sphinxcontrib-serializinghtml = ">=1.1.5" + +[package.extras] +docs = ["sphinxcontrib-websupport"] +lint = ["docutils-stubs", "flake8 (>=3.5.0)", "flake8-bugbear", "flake8-comprehensions", "flake8-simplify", "isort", "mypy (>=0.981)", "sphinx-lint", "types-requests", "types-typed-ast"] +test = ["cython", "html5lib", "pytest (>=4.6)", "typed_ast"] + +[[package]] +name = "sphinx-rtd-theme" +version = "1.3.0" +description = "Read the Docs theme for Sphinx" +optional = false +python-versions = "!=3.0.*,!=3.1.*,!=3.2.*,!=3.3.*,!=3.4.*,!=3.5.*,>=2.7" +files = [ + {file = "sphinx_rtd_theme-1.3.0-py2.py3-none-any.whl", hash = "sha256:46ddef89cc2416a81ecfbeaceab1881948c014b1b6e4450b815311a89fb977b0"}, + {file = "sphinx_rtd_theme-1.3.0.tar.gz", hash = "sha256:590b030c7abb9cf038ec053b95e5380b5c70d61591eb0b552063fbe7c41f0931"}, +] + +[package.dependencies] +docutils = "<0.19" +sphinx = ">=1.6,<8" +sphinxcontrib-jquery = ">=4,<5" + +[package.extras] +dev = ["bump2version", "sphinxcontrib-httpdomain", "transifex-client", "wheel"] + +[[package]] +name = "sphinxcontrib-applehelp" +version = "1.0.2" +description = "sphinxcontrib-applehelp is a sphinx extension which outputs Apple help books" +optional = false +python-versions = ">=3.5" +files = [ + {file = "sphinxcontrib-applehelp-1.0.2.tar.gz", hash = "sha256:a072735ec80e7675e3f432fcae8610ecf509c5f1869d17e2eecff44389cdbc58"}, + {file = "sphinxcontrib_applehelp-1.0.2-py2.py3-none-any.whl", hash = "sha256:806111e5e962be97c29ec4c1e7fe277bfd19e9652fb1a4392105b43e01af885a"}, +] + +[package.extras] +lint = ["docutils-stubs", "flake8", "mypy"] +test = ["pytest"] + +[[package]] +name = "sphinxcontrib-devhelp" +version = "1.0.2" +description = "sphinxcontrib-devhelp is a sphinx extension which outputs Devhelp document." +optional = false +python-versions = ">=3.5" +files = [ + {file = "sphinxcontrib-devhelp-1.0.2.tar.gz", hash = "sha256:ff7f1afa7b9642e7060379360a67e9c41e8f3121f2ce9164266f61b9f4b338e4"}, + {file = "sphinxcontrib_devhelp-1.0.2-py2.py3-none-any.whl", hash = "sha256:8165223f9a335cc1af7ffe1ed31d2871f325254c0423bc0c4c7cd1c1e4734a2e"}, +] + +[package.extras] +lint = ["docutils-stubs", "flake8", "mypy"] +test = ["pytest"] + +[[package]] +name = "sphinxcontrib-htmlhelp" +version = "2.0.0" +description = "sphinxcontrib-htmlhelp is a sphinx extension which renders HTML help files" +optional = false +python-versions = ">=3.6" +files = [ + {file = "sphinxcontrib-htmlhelp-2.0.0.tar.gz", hash = "sha256:f5f8bb2d0d629f398bf47d0d69c07bc13b65f75a81ad9e2f71a63d4b7a2f6db2"}, + {file = "sphinxcontrib_htmlhelp-2.0.0-py2.py3-none-any.whl", hash = "sha256:d412243dfb797ae3ec2b59eca0e52dac12e75a241bf0e4eb861e450d06c6ed07"}, +] + +[package.extras] +lint = ["docutils-stubs", "flake8", "mypy"] +test = ["html5lib", "pytest"] + +[[package]] +name = "sphinxcontrib-jquery" +version = "4.1" +description = "Extension to include jQuery on newer Sphinx releases" +optional = false +python-versions = ">=2.7" +files = [ + {file = "sphinxcontrib-jquery-4.1.tar.gz", hash = "sha256:1620739f04e36a2c779f1a131a2dfd49b2fd07351bf1968ced074365933abc7a"}, + {file = "sphinxcontrib_jquery-4.1-py2.py3-none-any.whl", hash = "sha256:f936030d7d0147dd026a4f2b5a57343d233f1fc7b363f68b3d4f1cb0993878ae"}, +] + +[package.dependencies] +Sphinx = ">=1.8" + +[[package]] +name = "sphinxcontrib-jsmath" +version = "1.0.1" +description = "A sphinx extension which renders display math in HTML via JavaScript" +optional = false +python-versions = ">=3.5" +files = [ + {file = "sphinxcontrib-jsmath-1.0.1.tar.gz", hash = "sha256:a9925e4a4587247ed2191a22df5f6970656cb8ca2bd6284309578f2153e0c4b8"}, + {file = "sphinxcontrib_jsmath-1.0.1-py2.py3-none-any.whl", hash = "sha256:2ec2eaebfb78f3f2078e73666b1415417a116cc848b72e5172e596c871103178"}, +] + +[package.extras] +test = ["flake8", "mypy", "pytest"] + +[[package]] +name = "sphinxcontrib-qthelp" +version = "1.0.3" +description = "sphinxcontrib-qthelp is a sphinx extension which outputs QtHelp document." +optional = false +python-versions = ">=3.5" +files = [ + {file = "sphinxcontrib-qthelp-1.0.3.tar.gz", hash = "sha256:4c33767ee058b70dba89a6fc5c1892c0d57a54be67ddd3e7875a18d14cba5a72"}, + {file = "sphinxcontrib_qthelp-1.0.3-py2.py3-none-any.whl", hash = "sha256:bd9fc24bcb748a8d51fd4ecaade681350aa63009a347a8c14e637895444dfab6"}, +] + +[package.extras] +lint = ["docutils-stubs", "flake8", "mypy"] +test = ["pytest"] + +[[package]] +name = "sphinxcontrib-serializinghtml" +version = "1.1.5" +description = "sphinxcontrib-serializinghtml is a sphinx extension which outputs \"serialized\" HTML files (json and pickle)." +optional = false +python-versions = ">=3.5" +files = [ + {file = "sphinxcontrib-serializinghtml-1.1.5.tar.gz", hash = "sha256:aa5f6de5dfdf809ef505c4895e51ef5c9eac17d0f287933eb49ec495280b6952"}, + {file = "sphinxcontrib_serializinghtml-1.1.5-py2.py3-none-any.whl", hash = "sha256:352a9a00ae864471d3a7ead8d7d79f5fc0b57e8b3f95e9867eb9eb28999b92fd"}, +] + +[package.extras] +lint = ["docutils-stubs", "flake8", "mypy"] +test = ["pytest"] + +[[package]] +name = "tomli" +version = "2.0.1" +description = "A lil' TOML parser" +optional = false +python-versions = ">=3.7" +files = [ + {file = "tomli-2.0.1-py3-none-any.whl", hash = "sha256:939de3e7a6161af0c887ef91b7d41a53e7c5a1ca976325f429cb46ea9bc30ecc"}, + {file = "tomli-2.0.1.tar.gz", hash = "sha256:de526c12914f0c550d15924c62d72abc48d6fe7364aa87328337a31007fe8a4f"}, +] + +[[package]] +name = "towncrier" +version = "23.6.0" +description = "Building newsfiles for your project." +optional = false +python-versions = ">=3.7" +files = [ + {file = "towncrier-23.6.0-py3-none-any.whl", hash = "sha256:da552f29192b3c2b04d630133f194c98e9f14f0558669d427708e203fea4d0a5"}, + {file = "towncrier-23.6.0.tar.gz", hash = "sha256:fc29bd5ab4727c8dacfbe636f7fb5dc53b99805b62da1c96b214836159ff70c1"}, +] + +[package.dependencies] +click = "*" +click-default-group = "*" +importlib-resources = {version = ">=5", markers = "python_version < \"3.10\""} +incremental = "*" +jinja2 = "*" +tomli = {version = "*", markers = "python_version < \"3.11\""} + +[package.extras] +dev = ["furo", "packaging", "sphinx (>=5)", "twisted"] + +[[package]] +name = "typed-ast" +version = "1.5.5" +description = "a fork of Python 2 and 3 ast modules with type comment support" +optional = false +python-versions = ">=3.6" +files = [ + {file = "typed_ast-1.5.5-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:4bc1efe0ce3ffb74784e06460f01a223ac1f6ab31c6bc0376a21184bf5aabe3b"}, + {file = "typed_ast-1.5.5-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:5f7a8c46a8b333f71abd61d7ab9255440d4a588f34a21f126bbfc95f6049e686"}, + {file = "typed_ast-1.5.5-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:597fc66b4162f959ee6a96b978c0435bd63791e31e4f410622d19f1686d5e769"}, + {file = "typed_ast-1.5.5-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:d41b7a686ce653e06c2609075d397ebd5b969d821b9797d029fccd71fdec8e04"}, + {file = "typed_ast-1.5.5-cp310-cp310-musllinux_1_1_aarch64.whl", hash = "sha256:5fe83a9a44c4ce67c796a1b466c270c1272e176603d5e06f6afbc101a572859d"}, + {file = "typed_ast-1.5.5-cp310-cp310-musllinux_1_1_x86_64.whl", hash = "sha256:d5c0c112a74c0e5db2c75882a0adf3133adedcdbfd8cf7c9d6ed77365ab90a1d"}, + {file = "typed_ast-1.5.5-cp310-cp310-win_amd64.whl", hash = "sha256:e1a976ed4cc2d71bb073e1b2a250892a6e968ff02aa14c1f40eba4f365ffec02"}, + {file = "typed_ast-1.5.5-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:c631da9710271cb67b08bd3f3813b7af7f4c69c319b75475436fcab8c3d21bee"}, + {file = "typed_ast-1.5.5-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:b445c2abfecab89a932b20bd8261488d574591173d07827c1eda32c457358b18"}, + {file = "typed_ast-1.5.5-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:cc95ffaaab2be3b25eb938779e43f513e0e538a84dd14a5d844b8f2932593d88"}, + {file = "typed_ast-1.5.5-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:61443214d9b4c660dcf4b5307f15c12cb30bdfe9588ce6158f4a005baeb167b2"}, + {file = "typed_ast-1.5.5-cp311-cp311-musllinux_1_1_aarch64.whl", hash = "sha256:6eb936d107e4d474940469e8ec5b380c9b329b5f08b78282d46baeebd3692dc9"}, + {file = "typed_ast-1.5.5-cp311-cp311-musllinux_1_1_x86_64.whl", hash = "sha256:e48bf27022897577d8479eaed64701ecaf0467182448bd95759883300ca818c8"}, + {file = "typed_ast-1.5.5-cp311-cp311-win_amd64.whl", hash = "sha256:83509f9324011c9a39faaef0922c6f720f9623afe3fe220b6d0b15638247206b"}, + {file = "typed_ast-1.5.5-cp36-cp36m-macosx_10_9_x86_64.whl", hash = "sha256:44f214394fc1af23ca6d4e9e744804d890045d1643dd7e8229951e0ef39429b5"}, + {file = "typed_ast-1.5.5-cp36-cp36m-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:118c1ce46ce58fda78503eae14b7664163aa735b620b64b5b725453696f2a35c"}, + {file = "typed_ast-1.5.5-cp36-cp36m-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:be4919b808efa61101456e87f2d4c75b228f4e52618621c77f1ddcaae15904fa"}, + {file = "typed_ast-1.5.5-cp36-cp36m-musllinux_1_1_aarch64.whl", hash = "sha256:fc2b8c4e1bc5cd96c1a823a885e6b158f8451cf6f5530e1829390b4d27d0807f"}, + {file = "typed_ast-1.5.5-cp36-cp36m-musllinux_1_1_x86_64.whl", hash = "sha256:16f7313e0a08c7de57f2998c85e2a69a642e97cb32f87eb65fbfe88381a5e44d"}, + {file = "typed_ast-1.5.5-cp36-cp36m-win_amd64.whl", hash = "sha256:2b946ef8c04f77230489f75b4b5a4a6f24c078be4aed241cfabe9cbf4156e7e5"}, + {file = "typed_ast-1.5.5-cp37-cp37m-macosx_10_9_x86_64.whl", hash = "sha256:2188bc33d85951ea4ddad55d2b35598b2709d122c11c75cffd529fbc9965508e"}, + {file = "typed_ast-1.5.5-cp37-cp37m-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:0635900d16ae133cab3b26c607586131269f88266954eb04ec31535c9a12ef1e"}, + {file = "typed_ast-1.5.5-cp37-cp37m-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:57bfc3cf35a0f2fdf0a88a3044aafaec1d2f24d8ae8cd87c4f58d615fb5b6311"}, + {file = "typed_ast-1.5.5-cp37-cp37m-musllinux_1_1_aarch64.whl", hash = "sha256:fe58ef6a764de7b4b36edfc8592641f56e69b7163bba9f9c8089838ee596bfb2"}, + {file = "typed_ast-1.5.5-cp37-cp37m-musllinux_1_1_x86_64.whl", hash = "sha256:d09d930c2d1d621f717bb217bf1fe2584616febb5138d9b3e8cdd26506c3f6d4"}, + {file = "typed_ast-1.5.5-cp37-cp37m-win_amd64.whl", hash = "sha256:d40c10326893ecab8a80a53039164a224984339b2c32a6baf55ecbd5b1df6431"}, + {file = "typed_ast-1.5.5-cp38-cp38-macosx_10_9_x86_64.whl", hash = "sha256:fd946abf3c31fb50eee07451a6aedbfff912fcd13cf357363f5b4e834cc5e71a"}, + {file = "typed_ast-1.5.5-cp38-cp38-macosx_11_0_arm64.whl", hash = "sha256:ed4a1a42df8a3dfb6b40c3d2de109e935949f2f66b19703eafade03173f8f437"}, + {file = "typed_ast-1.5.5-cp38-cp38-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:045f9930a1550d9352464e5149710d56a2aed23a2ffe78946478f7b5416f1ede"}, + {file = "typed_ast-1.5.5-cp38-cp38-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:381eed9c95484ceef5ced626355fdc0765ab51d8553fec08661dce654a935db4"}, + {file = "typed_ast-1.5.5-cp38-cp38-musllinux_1_1_aarch64.whl", hash = "sha256:bfd39a41c0ef6f31684daff53befddae608f9daf6957140228a08e51f312d7e6"}, + {file = "typed_ast-1.5.5-cp38-cp38-musllinux_1_1_x86_64.whl", hash = "sha256:8c524eb3024edcc04e288db9541fe1f438f82d281e591c548903d5b77ad1ddd4"}, + {file = "typed_ast-1.5.5-cp38-cp38-win_amd64.whl", hash = "sha256:7f58fabdde8dcbe764cef5e1a7fcb440f2463c1bbbec1cf2a86ca7bc1f95184b"}, + {file = "typed_ast-1.5.5-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:042eb665ff6bf020dd2243307d11ed626306b82812aba21836096d229fdc6a10"}, + {file = "typed_ast-1.5.5-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:622e4a006472b05cf6ef7f9f2636edc51bda670b7bbffa18d26b255269d3d814"}, + {file = "typed_ast-1.5.5-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:1efebbbf4604ad1283e963e8915daa240cb4bf5067053cf2f0baadc4d4fb51b8"}, + {file = "typed_ast-1.5.5-cp39-cp39-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:f0aefdd66f1784c58f65b502b6cf8b121544680456d1cebbd300c2c813899274"}, + {file = "typed_ast-1.5.5-cp39-cp39-musllinux_1_1_aarch64.whl", hash = "sha256:48074261a842acf825af1968cd912f6f21357316080ebaca5f19abbb11690c8a"}, + {file = "typed_ast-1.5.5-cp39-cp39-musllinux_1_1_x86_64.whl", hash = "sha256:429ae404f69dc94b9361bb62291885894b7c6fb4640d561179548c849f8492ba"}, + {file = "typed_ast-1.5.5-cp39-cp39-win_amd64.whl", hash = "sha256:335f22ccb244da2b5c296e6f96b06ee9bed46526db0de38d2f0e5a6597b81155"}, + {file = "typed_ast-1.5.5.tar.gz", hash = "sha256:94282f7a354f36ef5dbce0ef3467ebf6a258e370ab33d5b40c249fa996e590dd"}, +] + +[[package]] +name = "typing-extensions" +version = "4.7.1" +description = "Backported and Experimental Type Hints for Python 3.7+" +optional = false +python-versions = ">=3.7" +files = [ + {file = "typing_extensions-4.7.1-py3-none-any.whl", hash = "sha256:440d5dd3af93b060174bf433bccd69b0babc3b15b1a8dca43789fd7f61514b36"}, + {file = "typing_extensions-4.7.1.tar.gz", hash = "sha256:b75ddc264f0ba5615db7ba217daeb99701ad295353c45f9e95963337ceeeffb2"}, +] + +[[package]] +name = "urllib3" +version = "2.0.7" +description = "HTTP library with thread-safe connection pooling, file post, and more." +optional = false +python-versions = ">=3.7" +files = [ + {file = "urllib3-2.0.7-py3-none-any.whl", hash = "sha256:fdb6d215c776278489906c2f8916e6e7d4f5a9b602ccbcfdf7f016fc8da0596e"}, + {file = "urllib3-2.0.7.tar.gz", hash = "sha256:c97dfde1f7bd43a71c8d2a58e369e9b2bf692d1334ea9f9cae55add7d0dd0f84"}, +] + +[package.extras] +brotli = ["brotli (>=1.0.9)", "brotlicffi (>=0.8.0)"] +secure = ["certifi", "cryptography (>=1.9)", "idna (>=2.0.0)", "pyopenssl (>=17.1.0)", "urllib3-secure-extra"] +socks = ["pysocks (>=1.5.6,!=1.5.7,<2.0)"] +zstd = ["zstandard (>=0.18.0)"] + +[[package]] +name = "virtualenv" +version = "20.24.6" +description = "Virtual Python Environment builder" +optional = false +python-versions = ">=3.7" +files = [ + {file = "virtualenv-20.24.6-py3-none-any.whl", hash = "sha256:520d056652454c5098a00c0f073611ccbea4c79089331f60bf9d7ba247bb7381"}, + {file = "virtualenv-20.24.6.tar.gz", hash = "sha256:02ece4f56fbf939dbbc33c0715159951d6bf14aaf5457b092e4548e1382455af"}, +] + +[package.dependencies] +distlib = ">=0.3.7,<1" +filelock = ">=3.12.2,<4" +importlib-metadata = {version = ">=6.6", markers = "python_version < \"3.8\""} +platformdirs = ">=3.9.1,<4" + +[package.extras] +docs = ["furo (>=2023.7.26)", "proselint (>=0.13)", "sphinx (>=7.1.2)", "sphinx-argparse (>=0.4)", "sphinxcontrib-towncrier (>=0.2.1a0)", "towncrier (>=23.6)"] +test = ["covdefaults (>=2.3)", "coverage (>=7.2.7)", "coverage-enable-subprocess (>=1)", "flaky (>=3.7)", "packaging (>=23.1)", "pytest (>=7.4)", "pytest-env (>=0.8.2)", "pytest-freezer (>=0.4.8)", "pytest-mock (>=3.11.1)", "pytest-randomly (>=3.12)", "pytest-timeout (>=2.1)", "setuptools (>=68)", "time-machine (>=2.10)"] + +[[package]] +name = "zipp" +version = "3.15.0" +description = "Backport of pathlib-compatible object wrapper for zip files" +optional = false +python-versions = ">=3.7" +files = [ + {file = "zipp-3.15.0-py3-none-any.whl", hash = "sha256:48904fc76a60e542af151aded95726c1a5c34ed43ab4134b597665c86d7ad556"}, + {file = "zipp-3.15.0.tar.gz", hash = "sha256:112929ad649da941c23de50f356a2b5570c954b65150642bccdd66bf194d224b"}, +] + +[package.extras] +docs = ["furo", "jaraco.packaging (>=9)", "jaraco.tidelift (>=1.4)", "rst.linker (>=1.9)", "sphinx (>=3.5)", "sphinx-lint"] +testing = ["big-O", "flake8 (<5)", "jaraco.functools", "jaraco.itertools", "more-itertools", "pytest (>=6)", "pytest-black (>=0.3.7)", "pytest-checkdocs (>=2.4)", "pytest-cov", "pytest-enabler (>=1.3)", "pytest-flake8", "pytest-mypy (>=0.9.1)"] + +[metadata] +lock-version = "2.0" +python-versions = "^3.7" +content-hash = "99e188820ea9b593c65e9e260af8dfe70c19299938075726d30d1a3cc566c9fe" diff --git a/tests/fixtures/real-world-locks/poetry/cleo-2.1.0/pyproject.toml b/tests/fixtures/real-world-locks/poetry/cleo-2.1.0/pyproject.toml new file mode 100644 index 00000000..2e808325 --- /dev/null +++ b/tests/fixtures/real-world-locks/poetry/cleo-2.1.0/pyproject.toml @@ -0,0 +1,149 @@ +[build-system] +requires = ["poetry-core>=1.1.0"] +build-backend = "poetry.core.masonry.api" + +[tool.poetry] +name = "cleo" +version = "2.1.0" +description = "Cleo allows you to create beautiful and testable command-line interfaces." +authors = [ + "Sébastien Eustace " +] +maintainers = [ + "Branch Vincent ", + "Bartosz Sokorski ", +] +license = "MIT" +readme = "README.md" +packages = [{ include = "cleo", from = "src" }] +include = [{ path = "tests", format = "sdist" }] +repository = "https://github.com/python-poetry/cleo" +keywords = ["cli", "commands"] +classifiers = [ + "Development Status :: 5 - Production/Stable", + "Environment :: Console", + "Intended Audience :: Developers", + "Intended Audience :: Information Technology", + "Intended Audience :: System Administrators", + "Operating System :: OS Independent", + "Topic :: Software Development :: Libraries :: Application Frameworks", + "Topic :: Software Development :: Libraries :: Python Modules", + "Topic :: Software Development :: Libraries", + "Topic :: Software Development", +] + +[tool.poetry.dependencies] +python = "^3.7" +crashtest = "^0.4.1" +rapidfuzz = "^3.0.0" + +[tool.poetry.group.dev.dependencies] +mypy = [ + { version = "^1.0", python = "<3.8" }, + { version = "^1.5", python = ">=3.8" }, +] +pre-commit = [ + { version = "^2.0", python = "<3.8" }, + { version = "^3.0", python = ">=3.8" }, +] +pytest = "^7.1.2" +pytest-cov = "^4.0" +pytest-mock = "^3.8.2" +towncrier = ">=22.12.0" + +[tool.poetry.group.doc.dependencies] +Sphinx = "^5.2.3" +sphinx-rtd-theme = "^1.0.0" + +[tool.ruff] +fix = true +unfixable = [ + "ERA", # do not autoremove commented out code +] +target-version = "py37" +line-length = 88 +extend-select = [ + "B", # flake8-bugbear + "C4", # flake8-comprehensions + "ERA", # flake8-eradicate/eradicate + "I", # isort + "N", # pep8-naming + "PIE", # flake8-pie + "PGH", # pygrep + "RUF", # ruff checks + "SIM", # flake8-simplify + "TCH", # flake8-type-checking + "TID", # flake8-tidy-imports + "UP", # pyupgrade +] +extend-exclude = [ + "docs/*", + "tests/fixtures/exceptions/*" +] + +[tool.ruff.flake8-tidy-imports] +ban-relative-imports = "all" + +[tool.ruff.isort] +force-single-line = true +lines-between-types = 1 +lines-after-imports = 2 +known-first-party = ["cleo"] +required-imports = ["from __future__ import annotations"] + +[tool.mypy] +strict = true +files = ["src", "tests"] +pretty = true + +[tool.pytest.ini_options] +addopts = "-q" +testpaths = ["tests"] + +[tool.coverage.report] +omit = [ + "src/cleo/_compat.py", +] +exclude_lines = [ + "pragma: no cover", + "if TYPE_CHECKING:", + "raise NotImplementedError" +] + +[tool.towncrier] +package = "cleo" +filename = "CHANGELOG.md" +issue_format = "([#{issue}](https://github.com/python-poetry/cleo/pull/{issue}))" +directory = "news/" +title_format = "{version} ({project_date})" +template = "news/news_template.jinja2" +underlines = "-~^" +start_string = "\n" + +[tool.towncrier.fragment.break] +name = "Breaking Changes" +showcontent = true + +[tool.towncrier.fragment.feat] +name = "Features & Improvements" +showcontent = true + +[tool.towncrier.fragment.bugfix] +name = "Bug Fixes" +showcontent = true + +[tool.towncrier.fragment.docs] +name = "Documentation" +showcontent = true + +[tool.towncrier.fragment.deps] +name = "Dependencies" +showcontent = true + +[tool.towncrier.fragment.removal] +name = "Removals and Deprecations" +showcontent = true + +[tool.towncrier.fragment.misc] +name = "Miscellaneous" +showcontent = true diff --git a/tests/fixtures/real-world-locks/poetry/pastel-0.2.1/poetry.lock b/tests/fixtures/real-world-locks/poetry/pastel-0.2.1/poetry.lock new file mode 100644 index 00000000..e682838a --- /dev/null +++ b/tests/fixtures/real-world-locks/poetry/pastel-0.2.1/poetry.lock @@ -0,0 +1,572 @@ +[[package]] +name = "appdirs" +version = "1.4.4" +description = "A small Python module for determining appropriate platform-specific dirs, e.g. a \"user data dir\"." +category = "dev" +optional = false +python-versions = "*" + +[[package]] +name = "atomicwrites" +version = "1.4.0" +description = "Atomic file writes." +category = "dev" +optional = false +python-versions = ">=2.7, !=3.0.*, !=3.1.*, !=3.2.*, !=3.3.*" + +[[package]] +name = "attrs" +version = "20.1.0" +description = "Classes Without Boilerplate" +category = "dev" +optional = false +python-versions = ">=2.7, !=3.0.*, !=3.1.*, !=3.2.*, !=3.3.*" + +[package.extras] +dev = ["coverage (>=5.0.2)", "hypothesis", "pympler", "pytest (>=4.3.0)", "six", "zope.interface", "sphinx", "sphinx-rtd-theme", "pre-commit"] +docs = ["sphinx", "sphinx-rtd-theme", "zope.interface"] +tests = ["coverage (>=5.0.2)", "hypothesis", "pympler", "pytest (>=4.3.0)", "six", "zope.interface"] + +[[package]] +name = "backports.functools-lru-cache" +version = "1.6.1" +description = "Backport of functools.lru_cache" +category = "dev" +optional = false +python-versions = ">=2.6" + +[package.extras] +docs = ["sphinx", "jaraco.packaging (>=3.2)", "rst.linker (>=1.9)"] +testing = ["pytest (>=3.5,<3.7.3 || >3.7.3)", "pytest-checkdocs (>=1.2.3)", "pytest-flake8", "pytest-black-multipy", "pytest-cov"] + +[[package]] +name = "colorama" +version = "0.4.1" +description = "Cross-platform colored terminal text." +category = "dev" +optional = false +python-versions = ">=2.7, !=3.0.*, !=3.1.*, !=3.2.*, !=3.3.*" + +[[package]] +name = "colorama" +version = "0.4.3" +description = "Cross-platform colored terminal text." +category = "dev" +optional = false +python-versions = ">=2.7, !=3.0.*, !=3.1.*, !=3.2.*, !=3.3.*, !=3.4.*" + +[[package]] +name = "configparser" +version = "4.0.2" +description = "Updated configparser from Python 3.7 for Python 2.6+." +category = "dev" +optional = false +python-versions = ">=2.6" + +[package.extras] +docs = ["sphinx", "jaraco.packaging (>=3.2)", "rst.linker (>=1.9)"] +testing = ["pytest (>=3.5,<3.7.3 || >3.7.3)", "pytest-checkdocs (>=1.2)", "pytest-flake8", "pytest-black-multipy"] + +[[package]] +name = "contextlib2" +version = "0.6.0.post1" +description = "Backports and enhancements for the contextlib module" +category = "dev" +optional = false +python-versions = ">=2.7, !=3.0.*, !=3.1.*, !=3.2.*, !=3.3.*" + +[[package]] +name = "coverage" +version = "4.5.4" +description = "Code coverage measurement for Python" +category = "dev" +optional = false +python-versions = ">=2.6, !=3.0.*, !=3.1.*, !=3.2.*, <4" + +[[package]] +name = "distlib" +version = "0.3.1" +description = "Distribution utilities" +category = "dev" +optional = false +python-versions = "*" + +[[package]] +name = "filelock" +version = "3.0.12" +description = "A platform independent file lock." +category = "dev" +optional = false +python-versions = "*" + +[[package]] +name = "funcsigs" +version = "1.0.2" +description = "Python function signatures from PEP362 for Python 2.6, 2.7 and 3.2+" +category = "dev" +optional = false +python-versions = "*" + +[[package]] +name = "importlib-metadata" +version = "0.23" +description = "Read metadata from Python packages" +category = "dev" +optional = false +python-versions = ">=2.7,!=3.0,!=3.1,!=3.2,!=3.3" + +[package.extras] +docs = ["sphinx", "rst.linker"] +testing = ["packaging", "importlib-resources"] + +[package.dependencies] +zipp = ">=0.5" +configparser = {version = ">=3.5", markers = "python_version < \"3\""} +contextlib2 = {version = "*", markers = "python_version < \"3\""} + +[[package]] +name = "importlib-resources" +version = "1.0.2" +description = "Read resources from Python packages" +category = "dev" +optional = false +python-versions = ">=2.7,!=3.0,!=3.1,!=3.2,!=3.3" + +[package.dependencies] +pathlib2 = {version = "*", markers = "python_version < \"3\""} +typing = {version = "*", markers = "python_version < \"3.5\""} + +[[package]] +name = "mock" +version = "3.0.5" +description = "Rolling backport of unittest.mock for all Pythons" +category = "dev" +optional = false +python-versions = ">=2.7, !=3.0.*, !=3.1.*, !=3.2.*, !=3.3.*" + +[package.extras] +build = ["twine", "wheel", "blurb"] +docs = ["sphinx"] +test = ["pytest", "pytest-cov"] + +[package.dependencies] +six = "*" +funcsigs = {version = ">=1", markers = "python_version < \"3.3\""} + +[[package]] +name = "more-itertools" +version = "5.0.0" +description = "More routines for operating on iterables, beyond itertools" +category = "dev" +optional = false +python-versions = "*" + +[package.dependencies] +six = ">=1.0.0,<2.0.0" + +[[package]] +name = "more-itertools" +version = "7.2.0" +description = "More routines for operating on iterables, beyond itertools" +category = "dev" +optional = false +python-versions = ">=3.4" + +[[package]] +name = "packaging" +version = "20.4" +description = "Core utilities for Python packages" +category = "dev" +optional = false +python-versions = ">=2.7, !=3.0.*, !=3.1.*, !=3.2.*, !=3.3.*" + +[package.dependencies] +pyparsing = ">=2.0.2" +six = "*" + +[[package]] +name = "pathlib2" +version = "2.3.5" +description = "Object-oriented filesystem paths" +category = "dev" +optional = false +python-versions = "*" + +[package.dependencies] +six = "*" +scandir = {version = "*", markers = "python_version < \"3.5\""} + +[[package]] +name = "pluggy" +version = "0.13.1" +description = "plugin and hook calling mechanisms for python" +category = "dev" +optional = false +python-versions = ">=2.7, !=3.0.*, !=3.1.*, !=3.2.*, !=3.3.*" + +[package.extras] +dev = ["pre-commit", "tox"] + +[package.dependencies] +importlib-metadata = {version = ">=0.12", markers = "python_version < \"3.8\""} + +[[package]] +name = "py" +version = "1.9.0" +description = "library with cross-python path, ini-parsing, io, code, log facilities" +category = "dev" +optional = false +python-versions = ">=2.7, !=3.0.*, !=3.1.*, !=3.2.*, !=3.3.*" + +[[package]] +name = "pyparsing" +version = "2.4.7" +description = "Python parsing module" +category = "dev" +optional = false +python-versions = ">=2.6, !=3.0.*, !=3.1.*, !=3.2.*" + +[[package]] +name = "pytest" +version = "4.6.11" +description = "pytest: simple powerful testing with Python" +category = "dev" +optional = false +python-versions = "!=3.0.*,!=3.1.*,!=3.2.*,!=3.3.*,>=2.7" + +[package.extras] +testing = ["argcomplete", "hypothesis (>=3.56)", "nose", "requests", "mock"] + +[package.dependencies] +atomicwrites = ">=1.0" +attrs = ">=17.4.0" +packaging = "*" +pluggy = ">=0.12,<1.0" +py = ">=1.5.0" +six = ">=1.10.0" +wcwidth = "*" +funcsigs = {version = ">=1.0", markers = "python_version < \"3.0\""} +importlib-metadata = {version = ">=0.12", markers = "python_version < \"3.8\""} +pathlib2 = {version = ">=2.2.0", markers = "python_version < \"3.6\""} + +[[package.dependencies.colorama]] +version = "*" +markers = "sys_platform == \"win32\" and python_version != \"3.4\"" + +[[package.dependencies.colorama]] +version = "<=0.4.1" +markers = "sys_platform == \"win32\" and python_version == \"3.4\"" + +[[package.dependencies.more-itertools]] +version = ">=4.0.0,<6.0.0" +markers = "python_version <= \"2.7\"" + +[[package.dependencies.more-itertools]] +version = ">=4.0.0" +markers = "python_version > \"2.7\"" + +[[package]] +name = "pytest-cov" +version = "2.8.1" +description = "Pytest plugin for measuring coverage." +category = "dev" +optional = false +python-versions = ">=2.7, !=3.0.*, !=3.1.*, !=3.2.*, !=3.3.*" + +[package.extras] +testing = ["fields", "hunter", "process-tests (2.0.2)", "six", "virtualenv"] + +[package.dependencies] +coverage = ">=4.4" +pytest = ">=3.6" + +[[package]] +name = "pytest-mock" +version = "1.13.0" +description = "Thin-wrapper around the mock package for easier use with py.test" +category = "dev" +optional = false +python-versions = ">=2.7, !=3.0.*, !=3.1.*, !=3.2.*, !=3.3.*" + +[package.extras] +dev = ["pre-commit", "tox"] + +[package.dependencies] +pytest = ">=2.7" +mock = {version = "*", markers = "python_version < \"3.0\""} + +[[package]] +name = "scandir" +version = "1.10.0" +description = "scandir, a better directory iterator and faster os.walk()" +category = "dev" +optional = false +python-versions = "*" + +[[package]] +name = "six" +version = "1.15.0" +description = "Python 2 and 3 compatibility utilities" +category = "dev" +optional = false +python-versions = ">=2.7, !=3.0.*, !=3.1.*, !=3.2.*" + +[[package]] +name = "toml" +version = "0.10.1" +description = "Python Library for Tom's Obvious, Minimal Language" +category = "dev" +optional = false +python-versions = "*" + +[[package]] +name = "tox" +version = "3.14.0" +description = "tox is a generic virtualenv management and test command line tool" +category = "dev" +optional = false +python-versions = ">=2.7, !=3.0.*, !=3.1.*, !=3.2.*, !=3.3.*" + +[package.extras] +docs = ["sphinx (>=2.0.0,<3)", "towncrier (>=18.5.0)", "pygments-github-lexers (>=0.0.5)", "sphinxcontrib-autoprogram (>=0.1.5)"] +testing = ["freezegun (>=0.3.11,<1)", "pathlib2 (>=2.3.3,<3)", "pytest (>=4.0.0,<6)", "pytest-cov (>=2.5.1,<3)", "pytest-mock (>=1.10.0,<2)", "pytest-xdist (>=1.22.2,<2)", "pytest-randomly (>=1.2.3,<2)", "flaky (>=3.4.0,<4)", "psutil (>=5.6.1,<6)"] + +[package.dependencies] +filelock = ">=3.0.0,<4" +packaging = ">=14" +pluggy = ">=0.12.0,<1" +py = ">=1.4.17,<2" +six = ">=1.0.0,<2" +toml = ">=0.9.4" +virtualenv = ">=14.0.0" +importlib-metadata = {version = ">=0.12,<1", markers = "python_version < \"3.8\""} + +[[package]] +name = "typing" +version = "3.7.4.3" +description = "Type Hints for Python" +category = "dev" +optional = false +python-versions = ">=2.7, !=3.0.*, !=3.1.*, !=3.2.*, !=3.3.*" + +[[package]] +name = "virtualenv" +version = "20.0.31" +description = "Virtual Python Environment builder" +category = "dev" +optional = false +python-versions = "!=3.0.*,!=3.1.*,!=3.2.*,!=3.3.*,>=2.7" + +[package.extras] +docs = ["proselint (>=0.10.2)", "sphinx (>=3)", "sphinx-argparse (>=0.2.5)", "sphinx-rtd-theme (>=0.4.3)", "towncrier (>=19.9.0rc1)"] +testing = ["coverage (>=5)", "coverage-enable-subprocess (>=1)", "flaky (>=3)", "pytest (>=4)", "pytest-env (>=0.6.2)", "pytest-freezegun (>=0.4.1)", "pytest-mock (>=2)", "pytest-randomly (>=1)", "pytest-timeout (>=1)", "pytest-xdist (>=1.31.0)", "packaging (>=20.0)", "xonsh (>=0.9.16)"] + +[package.dependencies] +appdirs = ">=1.4.3,<2" +distlib = ">=0.3.1,<1" +filelock = ">=3.0.0,<4" +six = ">=1.9.0,<2" +importlib-metadata = {version = ">=0.12,<2", markers = "python_version < \"3.8\""} +importlib-resources = {version = ">=1.0", markers = "python_version < \"3.7\""} +pathlib2 = {version = ">=2.3.3,<3", markers = "python_version < \"3.4\" and sys_platform != \"win32\""} + +[[package]] +name = "wcwidth" +version = "0.2.5" +description = "Measures the displayed width of unicode strings in a terminal" +category = "dev" +optional = false +python-versions = "*" + +[package.dependencies] +"backports.functools-lru-cache" = {version = ">=1.2.1", markers = "python_version < \"3.2\""} + +[[package]] +name = "zipp" +version = "1.2.0" +description = "Backport of pathlib-compatible object wrapper for zip files" +category = "dev" +optional = false +python-versions = ">=2.7" + +[package.extras] +docs = ["sphinx", "jaraco.packaging (>=3.2)", "rst.linker (>=1.9)"] +testing = ["pathlib2", "unittest2", "jaraco.itertools", "func-timeout"] + +[package.dependencies] +contextlib2 = {version = "*", markers = "python_version < \"3.4\""} + +[metadata] +lock-version = "1.1" +python-versions = "~2.7 || ^3.4" +content-hash = "569e785f5e774e4b45a66c713dbcc990048f57afce8c24a7a5a46760d225c8db" + +[metadata.files] +appdirs = [ + {file = "appdirs-1.4.4-py2.py3-none-any.whl", hash = "sha256:a841dacd6b99318a741b166adb07e19ee71a274450e68237b4650ca1055ab128"}, + {file = "appdirs-1.4.4.tar.gz", hash = "sha256:7d5d0167b2b1ba821647616af46a749d1c653740dd0d2415100fe26e27afdf41"}, +] +atomicwrites = [ + {file = "atomicwrites-1.4.0-py2.py3-none-any.whl", hash = "sha256:6d1784dea7c0c8d4a5172b6c620f40b6e4cbfdf96d783691f2e1302a7b88e197"}, + {file = "atomicwrites-1.4.0.tar.gz", hash = "sha256:ae70396ad1a434f9c7046fd2dd196fc04b12f9e91ffb859164193be8b6168a7a"}, +] +attrs = [ + {file = "attrs-20.1.0-py2.py3-none-any.whl", hash = "sha256:2867b7b9f8326499ab5b0e2d12801fa5c98842d2cbd22b35112ae04bf85b4dff"}, + {file = "attrs-20.1.0.tar.gz", hash = "sha256:0ef97238856430dcf9228e07f316aefc17e8939fc8507e18c6501b761ef1a42a"}, +] +"backports.functools-lru-cache" = [ + {file = "backports.functools_lru_cache-1.6.1-py2.py3-none-any.whl", hash = "sha256:0bada4c2f8a43d533e4ecb7a12214d9420e66eb206d54bf2d682581ca4b80848"}, + {file = "backports.functools_lru_cache-1.6.1.tar.gz", hash = "sha256:8fde5f188da2d593bd5bc0be98d9abc46c95bb8a9dde93429570192ee6cc2d4a"}, +] +colorama = [ + {file = "colorama-0.4.1-py2.py3-none-any.whl", hash = "sha256:f8ac84de7840f5b9c4e3347b3c1eaa50f7e49c2b07596221daec5edaabbd7c48"}, + {file = "colorama-0.4.1.tar.gz", hash = "sha256:05eed71e2e327246ad6b38c540c4a3117230b19679b875190486ddd2d721422d"}, + {file = "colorama-0.4.3-py2.py3-none-any.whl", hash = "sha256:7d73d2a99753107a36ac6b455ee49046802e59d9d076ef8e47b61499fa29afff"}, + {file = "colorama-0.4.3.tar.gz", hash = "sha256:e96da0d330793e2cb9485e9ddfd918d456036c7149416295932478192f4436a1"}, +] +configparser = [ + {file = "configparser-4.0.2-py2.py3-none-any.whl", hash = "sha256:254c1d9c79f60c45dfde850850883d5aaa7f19a23f13561243a050d5a7c3fe4c"}, + {file = "configparser-4.0.2.tar.gz", hash = "sha256:c7d282687a5308319bf3d2e7706e575c635b0a470342641c93bea0ea3b5331df"}, +] +contextlib2 = [ + {file = "contextlib2-0.6.0.post1-py2.py3-none-any.whl", hash = "sha256:3355078a159fbb44ee60ea80abd0d87b80b78c248643b49aa6d94673b413609b"}, + {file = "contextlib2-0.6.0.post1.tar.gz", hash = "sha256:01f490098c18b19d2bd5bb5dc445b2054d2fa97f09a4280ba2c5f3c394c8162e"}, +] +coverage = [ + {file = "coverage-4.5.4-cp26-cp26m-macosx_10_12_x86_64.whl", hash = "sha256:eee64c616adeff7db37cc37da4180a3a5b6177f5c46b187894e633f088fb5b28"}, + {file = "coverage-4.5.4-cp27-cp27m-macosx_10_12_x86_64.whl", hash = "sha256:ef824cad1f980d27f26166f86856efe11eff9912c4fed97d3804820d43fa550c"}, + {file = "coverage-4.5.4-cp27-cp27m-macosx_10_13_intel.whl", hash = "sha256:9a334d6c83dfeadae576b4d633a71620d40d1c379129d587faa42ee3e2a85cce"}, + {file = "coverage-4.5.4-cp27-cp27m-manylinux1_i686.whl", hash = "sha256:7494b0b0274c5072bddbfd5b4a6c6f18fbbe1ab1d22a41e99cd2d00c8f96ecfe"}, + {file = "coverage-4.5.4-cp27-cp27m-manylinux1_x86_64.whl", hash = "sha256:826f32b9547c8091679ff292a82aca9c7b9650f9fda3e2ca6bf2ac905b7ce888"}, + {file = "coverage-4.5.4-cp27-cp27m-win32.whl", hash = "sha256:63a9a5fc43b58735f65ed63d2cf43508f462dc49857da70b8980ad78d41d52fc"}, + {file = "coverage-4.5.4-cp27-cp27m-win_amd64.whl", hash = "sha256:e2ede7c1d45e65e209d6093b762e98e8318ddeff95317d07a27a2140b80cfd24"}, + {file = "coverage-4.5.4-cp27-cp27mu-manylinux1_i686.whl", hash = "sha256:dd579709a87092c6dbee09d1b7cfa81831040705ffa12a1b248935274aee0437"}, + {file = "coverage-4.5.4-cp27-cp27mu-manylinux1_x86_64.whl", hash = "sha256:08907593569fe59baca0bf152c43f3863201efb6113ecb38ce7e97ce339805a6"}, + {file = "coverage-4.5.4-cp33-cp33m-macosx_10_10_x86_64.whl", hash = "sha256:6b62544bb68106e3f00b21c8930e83e584fdca005d4fffd29bb39fb3ffa03cb5"}, + {file = "coverage-4.5.4-cp34-cp34m-macosx_10_12_x86_64.whl", hash = "sha256:331cb5115673a20fb131dadd22f5bcaf7677ef758741312bee4937d71a14b2ef"}, + {file = "coverage-4.5.4-cp34-cp34m-manylinux1_i686.whl", hash = "sha256:bf1ef9eb901113a9805287e090452c05547578eaab1b62e4ad456fcc049a9b7e"}, + {file = "coverage-4.5.4-cp34-cp34m-manylinux1_x86_64.whl", hash = "sha256:386e2e4090f0bc5df274e720105c342263423e77ee8826002dcffe0c9533dbca"}, + {file = "coverage-4.5.4-cp34-cp34m-win32.whl", hash = "sha256:fa964bae817babece5aa2e8c1af841bebb6d0b9add8e637548809d040443fee0"}, + {file = "coverage-4.5.4-cp34-cp34m-win_amd64.whl", hash = "sha256:df6712284b2e44a065097846488f66840445eb987eb81b3cc6e4149e7b6982e1"}, + {file = "coverage-4.5.4-cp35-cp35m-macosx_10_12_x86_64.whl", hash = "sha256:efc89291bd5a08855829a3c522df16d856455297cf35ae827a37edac45f466a7"}, + {file = "coverage-4.5.4-cp35-cp35m-manylinux1_i686.whl", hash = "sha256:e4ef9c164eb55123c62411f5936b5c2e521b12356037b6e1c2617cef45523d47"}, + {file = "coverage-4.5.4-cp35-cp35m-manylinux1_x86_64.whl", hash = "sha256:ff37757e068ae606659c28c3bd0d923f9d29a85de79bf25b2b34b148473b5025"}, + {file = "coverage-4.5.4-cp35-cp35m-win32.whl", hash = "sha256:bf0a7aed7f5521c7ca67febd57db473af4762b9622254291fbcbb8cd0ba5e33e"}, + {file = "coverage-4.5.4-cp35-cp35m-win_amd64.whl", hash = "sha256:19e4df788a0581238e9390c85a7a09af39c7b539b29f25c89209e6c3e371270d"}, + {file = "coverage-4.5.4-cp36-cp36m-macosx_10_13_x86_64.whl", hash = "sha256:60851187677b24c6085248f0a0b9b98d49cba7ecc7ec60ba6b9d2e5574ac1ee9"}, + {file = "coverage-4.5.4-cp36-cp36m-manylinux1_i686.whl", hash = "sha256:245388cda02af78276b479f299bbf3783ef0a6a6273037d7c60dc73b8d8d7755"}, + {file = "coverage-4.5.4-cp36-cp36m-manylinux1_x86_64.whl", hash = "sha256:c0afd27bc0e307a1ffc04ca5ec010a290e49e3afbe841c5cafc5c5a80ecd81c9"}, + {file = "coverage-4.5.4-cp36-cp36m-win32.whl", hash = "sha256:6ba744056423ef8d450cf627289166da65903885272055fb4b5e113137cfa14f"}, + {file = "coverage-4.5.4-cp36-cp36m-win_amd64.whl", hash = "sha256:af7ed8a8aa6957aac47b4268631fa1df984643f07ef00acd374e456364b373f5"}, + {file = "coverage-4.5.4-cp37-cp37m-macosx_10_13_x86_64.whl", hash = "sha256:3a794ce50daee01c74a494919d5ebdc23d58873747fa0e288318728533a3e1ca"}, + {file = "coverage-4.5.4-cp37-cp37m-manylinux1_i686.whl", hash = "sha256:0be0f1ed45fc0c185cfd4ecc19a1d6532d72f86a2bac9de7e24541febad72650"}, + {file = "coverage-4.5.4-cp37-cp37m-manylinux1_x86_64.whl", hash = "sha256:eca2b7343524e7ba246cab8ff00cab47a2d6d54ada3b02772e908a45675722e2"}, + {file = "coverage-4.5.4-cp37-cp37m-win32.whl", hash = "sha256:93715dffbcd0678057f947f496484e906bf9509f5c1c38fc9ba3922893cda5f5"}, + {file = "coverage-4.5.4-cp37-cp37m-win_amd64.whl", hash = "sha256:23cc09ed395b03424d1ae30dcc292615c1372bfba7141eb85e11e50efaa6b351"}, + {file = "coverage-4.5.4-cp38-cp38-macosx_10_13_x86_64.whl", hash = "sha256:141f08ed3c4b1847015e2cd62ec06d35e67a3ac185c26f7635f4406b90afa9c5"}, + {file = "coverage-4.5.4.tar.gz", hash = "sha256:e07d9f1a23e9e93ab5c62902833bf3e4b1f65502927379148b6622686223125c"}, +] +distlib = [ + {file = "distlib-0.3.1-py2.py3-none-any.whl", hash = "sha256:8c09de2c67b3e7deef7184574fc060ab8a793e7adbb183d942c389c8b13c52fb"}, + {file = "distlib-0.3.1.zip", hash = "sha256:edf6116872c863e1aa9d5bb7cb5e05a022c519a4594dc703843343a9ddd9bff1"}, +] +filelock = [ + {file = "filelock-3.0.12-py3-none-any.whl", hash = "sha256:929b7d63ec5b7d6b71b0fa5ac14e030b3f70b75747cef1b10da9b879fef15836"}, + {file = "filelock-3.0.12.tar.gz", hash = "sha256:18d82244ee114f543149c66a6e0c14e9c4f8a1044b5cdaadd0f82159d6a6ff59"}, +] +funcsigs = [ + {file = "funcsigs-1.0.2-py2.py3-none-any.whl", hash = "sha256:330cc27ccbf7f1e992e69fef78261dc7c6569012cf397db8d3de0234e6c937ca"}, + {file = "funcsigs-1.0.2.tar.gz", hash = "sha256:a7bb0f2cf3a3fd1ab2732cb49eba4252c2af4240442415b4abce3b87022a8f50"}, +] +importlib-metadata = [ + {file = "importlib_metadata-0.23-py2.py3-none-any.whl", hash = "sha256:d5f18a79777f3aa179c145737780282e27b508fc8fd688cb17c7a813e8bd39af"}, + {file = "importlib_metadata-0.23.tar.gz", hash = "sha256:aa18d7378b00b40847790e7c27e11673d7fed219354109d0e7b9e5b25dc3ad26"}, +] +importlib-resources = [ + {file = "importlib_resources-1.0.2-py2.py3-none-any.whl", hash = "sha256:6e2783b2538bd5a14678284a3962b0660c715e5a0f10243fd5e00a4b5974f50b"}, + {file = "importlib_resources-1.0.2.tar.gz", hash = "sha256:d3279fd0f6f847cced9f7acc19bd3e5df54d34f93a2e7bb5f238f81545787078"}, +] +mock = [ + {file = "mock-3.0.5-py2.py3-none-any.whl", hash = "sha256:d157e52d4e5b938c550f39eb2fd15610db062441a9c2747d3dbfa9298211d0f8"}, + {file = "mock-3.0.5.tar.gz", hash = "sha256:83657d894c90d5681d62155c82bda9c1187827525880eda8ff5df4ec813437c3"}, +] +more-itertools = [ + {file = "more-itertools-5.0.0.tar.gz", hash = "sha256:38a936c0a6d98a38bcc2d03fdaaedaba9f412879461dd2ceff8d37564d6522e4"}, + {file = "more_itertools-5.0.0-py2-none-any.whl", hash = "sha256:c0a5785b1109a6bd7fac76d6837fd1feca158e54e521ccd2ae8bfe393cc9d4fc"}, + {file = "more_itertools-5.0.0-py3-none-any.whl", hash = "sha256:fe7a7cae1ccb57d33952113ff4fa1bc5f879963600ed74918f1236e212ee50b9"}, + {file = "more-itertools-7.2.0.tar.gz", hash = "sha256:409cd48d4db7052af495b09dec721011634af3753ae1ef92d2b32f73a745f832"}, + {file = "more_itertools-7.2.0-py3-none-any.whl", hash = "sha256:92b8c4b06dac4f0611c0729b2f2ede52b2e1bac1ab48f089c7ddc12e26bb60c4"}, +] +packaging = [ + {file = "packaging-20.4-py2.py3-none-any.whl", hash = "sha256:998416ba6962ae7fbd6596850b80e17859a5753ba17c32284f67bfff33784181"}, + {file = "packaging-20.4.tar.gz", hash = "sha256:4357f74f47b9c12db93624a82154e9b120fa8293699949152b22065d556079f8"}, +] +pathlib2 = [ + {file = "pathlib2-2.3.5-py2.py3-none-any.whl", hash = "sha256:0ec8205a157c80d7acc301c0b18fbd5d44fe655968f5d947b6ecef5290fc35db"}, + {file = "pathlib2-2.3.5.tar.gz", hash = "sha256:6cd9a47b597b37cc57de1c05e56fb1a1c9cc9fab04fe78c29acd090418529868"}, +] +pluggy = [ + {file = "pluggy-0.13.1-py2.py3-none-any.whl", hash = "sha256:966c145cd83c96502c3c3868f50408687b38434af77734af1e9ca461a4081d2d"}, + {file = "pluggy-0.13.1.tar.gz", hash = "sha256:15b2acde666561e1298d71b523007ed7364de07029219b604cf808bfa1c765b0"}, +] +py = [ + {file = "py-1.9.0-py2.py3-none-any.whl", hash = "sha256:366389d1db726cd2fcfc79732e75410e5fe4d31db13692115529d34069a043c2"}, + {file = "py-1.9.0.tar.gz", hash = "sha256:9ca6883ce56b4e8da7e79ac18787889fa5206c79dcc67fb065376cd2fe03f342"}, +] +pyparsing = [ + {file = "pyparsing-2.4.7-py2.py3-none-any.whl", hash = "sha256:ef9d7589ef3c200abe66653d3f1ab1033c3c419ae9b9bdb1240a85b024efc88b"}, + {file = "pyparsing-2.4.7.tar.gz", hash = "sha256:c203ec8783bf771a155b207279b9bccb8dea02d8f0c9e5f8ead507bc3246ecc1"}, +] +pytest = [ + {file = "pytest-4.6.11-py2.py3-none-any.whl", hash = "sha256:a00a7d79cbbdfa9d21e7d0298392a8dd4123316bfac545075e6f8f24c94d8c97"}, + {file = "pytest-4.6.11.tar.gz", hash = "sha256:50fa82392f2120cc3ec2ca0a75ee615be4c479e66669789771f1758332be4353"}, +] +pytest-cov = [ + {file = "pytest-cov-2.8.1.tar.gz", hash = "sha256:cc6742d8bac45070217169f5f72ceee1e0e55b0221f54bcf24845972d3a47f2b"}, + {file = "pytest_cov-2.8.1-py2.py3-none-any.whl", hash = "sha256:cdbdef4f870408ebdbfeb44e63e07eb18bb4619fae852f6e760645fa36172626"}, +] +pytest-mock = [ + {file = "pytest-mock-1.13.0.tar.gz", hash = "sha256:e24a911ec96773022ebcc7030059b57cd3480b56d4f5d19b7c370ec635e6aed5"}, + {file = "pytest_mock-1.13.0-py2.py3-none-any.whl", hash = "sha256:67e414b3caef7bff6fc6bd83b22b5bc39147e4493f483c2679bc9d4dc485a94d"}, +] +scandir = [ + {file = "scandir-1.10.0-cp27-cp27m-win32.whl", hash = "sha256:92c85ac42f41ffdc35b6da57ed991575bdbe69db895507af88b9f499b701c188"}, + {file = "scandir-1.10.0-cp27-cp27m-win_amd64.whl", hash = "sha256:cb925555f43060a1745d0a321cca94bcea927c50114b623d73179189a4e100ac"}, + {file = "scandir-1.10.0-cp34-cp34m-win32.whl", hash = "sha256:2c712840c2e2ee8dfaf36034080108d30060d759c7b73a01a52251cc8989f11f"}, + {file = "scandir-1.10.0-cp34-cp34m-win_amd64.whl", hash = "sha256:2586c94e907d99617887daed6c1d102b5ca28f1085f90446554abf1faf73123e"}, + {file = "scandir-1.10.0-cp35-cp35m-win32.whl", hash = "sha256:2b8e3888b11abb2217a32af0766bc06b65cc4a928d8727828ee68af5a967fa6f"}, + {file = "scandir-1.10.0-cp35-cp35m-win_amd64.whl", hash = "sha256:8c5922863e44ffc00c5c693190648daa6d15e7c1207ed02d6f46a8dcc2869d32"}, + {file = "scandir-1.10.0-cp36-cp36m-win32.whl", hash = "sha256:2ae41f43797ca0c11591c0c35f2f5875fa99f8797cb1a1fd440497ec0ae4b022"}, + {file = "scandir-1.10.0-cp36-cp36m-win_amd64.whl", hash = "sha256:7d2d7a06a252764061a020407b997dd036f7bd6a175a5ba2b345f0a357f0b3f4"}, + {file = "scandir-1.10.0-cp37-cp37m-win32.whl", hash = "sha256:67f15b6f83e6507fdc6fca22fedf6ef8b334b399ca27c6b568cbfaa82a364173"}, + {file = "scandir-1.10.0-cp37-cp37m-win_amd64.whl", hash = "sha256:b24086f2375c4a094a6b51e78b4cf7ca16c721dcee2eddd7aa6494b42d6d519d"}, + {file = "scandir-1.10.0.tar.gz", hash = "sha256:4d4631f6062e658e9007ab3149a9b914f3548cb38bfb021c64f39a025ce578ae"}, +] +six = [ + {file = "six-1.15.0-py2.py3-none-any.whl", hash = "sha256:8b74bedcbbbaca38ff6d7491d76f2b06b3592611af620f8426e82dddb04a5ced"}, + {file = "six-1.15.0.tar.gz", hash = "sha256:30639c035cdb23534cd4aa2dd52c3bf48f06e5f4a941509c8bafd8ce11080259"}, +] +toml = [ + {file = "toml-0.10.1-py2.py3-none-any.whl", hash = "sha256:bda89d5935c2eac546d648028b9901107a595863cb36bae0c73ac804a9b4ce88"}, + {file = "toml-0.10.1.tar.gz", hash = "sha256:926b612be1e5ce0634a2ca03470f95169cf16f939018233a670519cb4ac58b0f"}, +] +tox = [ + {file = "tox-3.14.0-py2.py3-none-any.whl", hash = "sha256:0bc216b6a2e6afe764476b4a07edf2c1dab99ed82bb146a1130b2e828f5bff5e"}, + {file = "tox-3.14.0.tar.gz", hash = "sha256:c4f6b319c20ba4913dbfe71ebfd14ff95d1853c4231493608182f66e566ecfe1"}, +] +typing = [ + {file = "typing-3.7.4.3-py2-none-any.whl", hash = "sha256:283d868f5071ab9ad873e5e52268d611e851c870a2ba354193026f2dfb29d8b5"}, + {file = "typing-3.7.4.3.tar.gz", hash = "sha256:1187fb9c82fd670d10aa07bbb6cfcfe4bdda42d6fab8d5134f04e8c4d0b71cc9"}, +] +virtualenv = [ + {file = "virtualenv-20.0.31-py2.py3-none-any.whl", hash = "sha256:e0305af10299a7fb0d69393d8f04cb2965dda9351140d11ac8db4e5e3970451b"}, + {file = "virtualenv-20.0.31.tar.gz", hash = "sha256:43add625c53c596d38f971a465553f6318decc39d98512bc100fa1b1e839c8dc"}, +] +wcwidth = [ + {file = "wcwidth-0.2.5-py2.py3-none-any.whl", hash = "sha256:beb4802a9cebb9144e99086eff703a642a13d6a0052920003a230f3294bbe784"}, + {file = "wcwidth-0.2.5.tar.gz", hash = "sha256:c4d647b99872929fdb7bdcaa4fbe7f01413ed3d98077df798530e5b04f116c83"}, +] +zipp = [ + {file = "zipp-1.2.0-py2.py3-none-any.whl", hash = "sha256:e0d9e63797e483a30d27e09fffd308c59a700d365ec34e93cc100844168bf921"}, + {file = "zipp-1.2.0.tar.gz", hash = "sha256:c70410551488251b0fee67b460fb9a536af8d6f9f008ad10ac51f615b6a521b1"}, +] diff --git a/tests/fixtures/real-world-locks/poetry/pastel-0.2.1/pyproject.toml b/tests/fixtures/real-world-locks/poetry/pastel-0.2.1/pyproject.toml new file mode 100644 index 00000000..ec0874b8 --- /dev/null +++ b/tests/fixtures/real-world-locks/poetry/pastel-0.2.1/pyproject.toml @@ -0,0 +1,27 @@ +[tool.poetry] +name = "pastel" +version = "0.2.1" +description = "Bring colors to your terminal." +authors = ["Sébastien Eustace "] +license = "MIT" +readme = "README.rst" +homepage = "https://github.com/sdispater/pastel" +repository = "https://github.com/sdispater/pastel" + +packages = [ + {include = "pastel"}, + {include = "tests", format = "sdist"}, +] + +[tool.poetry.dependencies] +python = "~2.7 || ^3.4" + +[tool.poetry.dev-dependencies] +pytest = "^4.6.4" +pytest-cov = "^2.7.1" +pytest-mock = "^1.10.4" +tox = "^3.13.2" + +[build-system] +requires = ["poetry-core>=1.0.0a9"] +build-backend = "poetry.core.masonry.api" diff --git a/tests/fixtures/real-world-locks/poetry/pendulum-3.2.0/poetry.lock b/tests/fixtures/real-world-locks/poetry/pendulum-3.2.0/poetry.lock new file mode 100644 index 00000000..8411da19 --- /dev/null +++ b/tests/fixtures/real-world-locks/poetry/pendulum-3.2.0/poetry.lock @@ -0,0 +1,1366 @@ +# This file is automatically @generated by Poetry 2.2.1 and should not be changed by hand. + +[[package]] +name = "babel" +version = "2.16.0" +description = "Internationalization utilities" +optional = false +python-versions = ">=3.8" +groups = ["dev"] +files = [ + {file = "babel-2.16.0-py3-none-any.whl", hash = "sha256:368b5b98b37c06b7daf6696391c3240c938b37767d4584413e8438c5c435fa8b"}, + {file = "babel-2.16.0.tar.gz", hash = "sha256:d1f3554ca26605fe173f3de0c65f750f5a42f924499bf134de6423582298e316"}, +] + +[package.extras] +dev = ["freezegun (>=1.0,<2.0)", "pytest (>=6.0)", "pytest-cov"] + +[[package]] +name = "cachetools" +version = "5.5.0" +description = "Extensible memoizing collections and decorators" +optional = false +python-versions = ">=3.7" +groups = ["dev"] +files = [ + {file = "cachetools-5.5.0-py3-none-any.whl", hash = "sha256:02134e8439cdc2ffb62023ce1debca2944c3f289d66bb17ead3ab3dede74b292"}, + {file = "cachetools-5.5.0.tar.gz", hash = "sha256:2cc24fb4cbe39633fb7badd9db9ca6295d766d9c2995f245725a46715d050f2a"}, +] + +[[package]] +name = "cffi" +version = "1.17.1" +description = "Foreign Function Interface for Python calling C code." +optional = false +python-versions = ">=3.8" +groups = ["benchmark"] +files = [ + {file = "cffi-1.17.1-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:df8b1c11f177bc2313ec4b2d46baec87a5f3e71fc8b45dab2ee7cae86d9aba14"}, + {file = "cffi-1.17.1-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:8f2cdc858323644ab277e9bb925ad72ae0e67f69e804f4898c070998d50b1a67"}, + {file = "cffi-1.17.1-cp310-cp310-manylinux_2_12_i686.manylinux2010_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:edae79245293e15384b51f88b00613ba9f7198016a5948b5dddf4917d4d26382"}, + {file = "cffi-1.17.1-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:45398b671ac6d70e67da8e4224a065cec6a93541bb7aebe1b198a61b58c7b702"}, + {file = "cffi-1.17.1-cp310-cp310-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:ad9413ccdeda48c5afdae7e4fa2192157e991ff761e7ab8fdd8926f40b160cc3"}, + {file = "cffi-1.17.1-cp310-cp310-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:5da5719280082ac6bd9aa7becb3938dc9f9cbd57fac7d2871717b1feb0902ab6"}, + {file = "cffi-1.17.1-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:2bb1a08b8008b281856e5971307cc386a8e9c5b625ac297e853d36da6efe9c17"}, + {file = "cffi-1.17.1-cp310-cp310-musllinux_1_1_aarch64.whl", hash = "sha256:045d61c734659cc045141be4bae381a41d89b741f795af1dd018bfb532fd0df8"}, + {file = "cffi-1.17.1-cp310-cp310-musllinux_1_1_i686.whl", hash = "sha256:6883e737d7d9e4899a8a695e00ec36bd4e5e4f18fabe0aca0efe0a4b44cdb13e"}, + {file = "cffi-1.17.1-cp310-cp310-musllinux_1_1_x86_64.whl", hash = "sha256:6b8b4a92e1c65048ff98cfe1f735ef8f1ceb72e3d5f0c25fdb12087a23da22be"}, + {file = "cffi-1.17.1-cp310-cp310-win32.whl", hash = "sha256:c9c3d058ebabb74db66e431095118094d06abf53284d9c81f27300d0e0d8bc7c"}, + {file = "cffi-1.17.1-cp310-cp310-win_amd64.whl", hash = "sha256:0f048dcf80db46f0098ccac01132761580d28e28bc0f78ae0d58048063317e15"}, + {file = "cffi-1.17.1-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:a45e3c6913c5b87b3ff120dcdc03f6131fa0065027d0ed7ee6190736a74cd401"}, + {file = "cffi-1.17.1-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:30c5e0cb5ae493c04c8b42916e52ca38079f1b235c2f8ae5f4527b963c401caf"}, + {file = "cffi-1.17.1-cp311-cp311-manylinux_2_12_i686.manylinux2010_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:f75c7ab1f9e4aca5414ed4d8e5c0e303a34f4421f8a0d47a4d019ceff0ab6af4"}, + {file = "cffi-1.17.1-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:a1ed2dd2972641495a3ec98445e09766f077aee98a1c896dcb4ad0d303628e41"}, + {file = "cffi-1.17.1-cp311-cp311-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:46bf43160c1a35f7ec506d254e5c890f3c03648a4dbac12d624e4490a7046cd1"}, + {file = "cffi-1.17.1-cp311-cp311-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:a24ed04c8ffd54b0729c07cee15a81d964e6fee0e3d4d342a27b020d22959dc6"}, + {file = "cffi-1.17.1-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:610faea79c43e44c71e1ec53a554553fa22321b65fae24889706c0a84d4ad86d"}, + {file = "cffi-1.17.1-cp311-cp311-musllinux_1_1_aarch64.whl", hash = "sha256:a9b15d491f3ad5d692e11f6b71f7857e7835eb677955c00cc0aefcd0669adaf6"}, + {file = "cffi-1.17.1-cp311-cp311-musllinux_1_1_i686.whl", hash = "sha256:de2ea4b5833625383e464549fec1bc395c1bdeeb5f25c4a3a82b5a8c756ec22f"}, + {file = "cffi-1.17.1-cp311-cp311-musllinux_1_1_x86_64.whl", hash = "sha256:fc48c783f9c87e60831201f2cce7f3b2e4846bf4d8728eabe54d60700b318a0b"}, + {file = "cffi-1.17.1-cp311-cp311-win32.whl", hash = "sha256:85a950a4ac9c359340d5963966e3e0a94a676bd6245a4b55bc43949eee26a655"}, + {file = "cffi-1.17.1-cp311-cp311-win_amd64.whl", hash = "sha256:caaf0640ef5f5517f49bc275eca1406b0ffa6aa184892812030f04c2abf589a0"}, + {file = "cffi-1.17.1-cp312-cp312-macosx_10_9_x86_64.whl", hash = "sha256:805b4371bf7197c329fcb3ead37e710d1bca9da5d583f5073b799d5c5bd1eee4"}, + {file = "cffi-1.17.1-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:733e99bc2df47476e3848417c5a4540522f234dfd4ef3ab7fafdf555b082ec0c"}, + {file = "cffi-1.17.1-cp312-cp312-manylinux_2_12_i686.manylinux2010_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:1257bdabf294dceb59f5e70c64a3e2f462c30c7ad68092d01bbbfb1c16b1ba36"}, + {file = "cffi-1.17.1-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:da95af8214998d77a98cc14e3a3bd00aa191526343078b530ceb0bd710fb48a5"}, + {file = "cffi-1.17.1-cp312-cp312-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:d63afe322132c194cf832bfec0dc69a99fb9bb6bbd550f161a49e9e855cc78ff"}, + {file = "cffi-1.17.1-cp312-cp312-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:f79fc4fc25f1c8698ff97788206bb3c2598949bfe0fef03d299eb1b5356ada99"}, + {file = "cffi-1.17.1-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:b62ce867176a75d03a665bad002af8e6d54644fad99a3c70905c543130e39d93"}, + {file = "cffi-1.17.1-cp312-cp312-musllinux_1_1_aarch64.whl", hash = "sha256:386c8bf53c502fff58903061338ce4f4950cbdcb23e2902d86c0f722b786bbe3"}, + {file = "cffi-1.17.1-cp312-cp312-musllinux_1_1_x86_64.whl", hash = "sha256:4ceb10419a9adf4460ea14cfd6bc43d08701f0835e979bf821052f1805850fe8"}, + {file = "cffi-1.17.1-cp312-cp312-win32.whl", hash = "sha256:a08d7e755f8ed21095a310a693525137cfe756ce62d066e53f502a83dc550f65"}, + {file = "cffi-1.17.1-cp312-cp312-win_amd64.whl", hash = "sha256:51392eae71afec0d0c8fb1a53b204dbb3bcabcb3c9b807eedf3e1e6ccf2de903"}, + {file = "cffi-1.17.1-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:f3a2b4222ce6b60e2e8b337bb9596923045681d71e5a082783484d845390938e"}, + {file = "cffi-1.17.1-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:0984a4925a435b1da406122d4d7968dd861c1385afe3b45ba82b750f229811e2"}, + {file = "cffi-1.17.1-cp313-cp313-manylinux_2_12_i686.manylinux2010_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:d01b12eeeb4427d3110de311e1774046ad344f5b1a7403101878976ecd7a10f3"}, + {file = "cffi-1.17.1-cp313-cp313-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:706510fe141c86a69c8ddc029c7910003a17353970cff3b904ff0686a5927683"}, + {file = "cffi-1.17.1-cp313-cp313-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:de55b766c7aa2e2a3092c51e0483d700341182f08e67c63630d5b6f200bb28e5"}, + {file = "cffi-1.17.1-cp313-cp313-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:c59d6e989d07460165cc5ad3c61f9fd8f1b4796eacbd81cee78957842b834af4"}, + {file = "cffi-1.17.1-cp313-cp313-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:dd398dbc6773384a17fe0d3e7eeb8d1a21c2200473ee6806bb5e6a8e62bb73dd"}, + {file = "cffi-1.17.1-cp313-cp313-musllinux_1_1_aarch64.whl", hash = "sha256:3edc8d958eb099c634dace3c7e16560ae474aa3803a5df240542b305d14e14ed"}, + {file = "cffi-1.17.1-cp313-cp313-musllinux_1_1_x86_64.whl", hash = "sha256:72e72408cad3d5419375fc87d289076ee319835bdfa2caad331e377589aebba9"}, + {file = "cffi-1.17.1-cp313-cp313-win32.whl", hash = "sha256:e03eab0a8677fa80d646b5ddece1cbeaf556c313dcfac435ba11f107ba117b5d"}, + {file = "cffi-1.17.1-cp313-cp313-win_amd64.whl", hash = "sha256:f6a16c31041f09ead72d69f583767292f750d24913dadacf5756b966aacb3f1a"}, + {file = "cffi-1.17.1-cp38-cp38-macosx_10_9_x86_64.whl", hash = "sha256:636062ea65bd0195bc012fea9321aca499c0504409f413dc88af450b57ffd03b"}, + {file = "cffi-1.17.1-cp38-cp38-manylinux_2_12_i686.manylinux2010_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:c7eac2ef9b63c79431bc4b25f1cd649d7f061a28808cbc6c47b534bd789ef964"}, + {file = "cffi-1.17.1-cp38-cp38-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:e221cf152cff04059d011ee126477f0d9588303eb57e88923578ace7baad17f9"}, + {file = "cffi-1.17.1-cp38-cp38-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:31000ec67d4221a71bd3f67df918b1f88f676f1c3b535a7eb473255fdc0b83fc"}, + {file = "cffi-1.17.1-cp38-cp38-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:6f17be4345073b0a7b8ea599688f692ac3ef23ce28e5df79c04de519dbc4912c"}, + {file = "cffi-1.17.1-cp38-cp38-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:0e2b1fac190ae3ebfe37b979cc1ce69c81f4e4fe5746bb401dca63a9062cdaf1"}, + {file = "cffi-1.17.1-cp38-cp38-win32.whl", hash = "sha256:7596d6620d3fa590f677e9ee430df2958d2d6d6de2feeae5b20e82c00b76fbf8"}, + {file = "cffi-1.17.1-cp38-cp38-win_amd64.whl", hash = "sha256:78122be759c3f8a014ce010908ae03364d00a1f81ab5c7f4a7a5120607ea56e1"}, + {file = "cffi-1.17.1-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:b2ab587605f4ba0bf81dc0cb08a41bd1c0a5906bd59243d56bad7668a6fc6c16"}, + {file = "cffi-1.17.1-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:28b16024becceed8c6dfbc75629e27788d8a3f9030691a1dbf9821a128b22c36"}, + {file = "cffi-1.17.1-cp39-cp39-manylinux_2_12_i686.manylinux2010_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:1d599671f396c4723d016dbddb72fe8e0397082b0a77a4fab8028923bec050e8"}, + {file = "cffi-1.17.1-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:ca74b8dbe6e8e8263c0ffd60277de77dcee6c837a3d0881d8c1ead7268c9e576"}, + {file = "cffi-1.17.1-cp39-cp39-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:f7f5baafcc48261359e14bcd6d9bff6d4b28d9103847c9e136694cb0501aef87"}, + {file = "cffi-1.17.1-cp39-cp39-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:98e3969bcff97cae1b2def8ba499ea3d6f31ddfdb7635374834cf89a1a08ecf0"}, + {file = "cffi-1.17.1-cp39-cp39-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:cdf5ce3acdfd1661132f2a9c19cac174758dc2352bfe37d98aa7512c6b7178b3"}, + {file = "cffi-1.17.1-cp39-cp39-musllinux_1_1_aarch64.whl", hash = "sha256:9755e4345d1ec879e3849e62222a18c7174d65a6a92d5b346b1863912168b595"}, + {file = "cffi-1.17.1-cp39-cp39-musllinux_1_1_i686.whl", hash = "sha256:f1e22e8c4419538cb197e4dd60acc919d7696e5ef98ee4da4e01d3f8cfa4cc5a"}, + {file = "cffi-1.17.1-cp39-cp39-musllinux_1_1_x86_64.whl", hash = "sha256:c03e868a0b3bc35839ba98e74211ed2b05d2119be4e8a0f224fba9384f1fe02e"}, + {file = "cffi-1.17.1-cp39-cp39-win32.whl", hash = "sha256:e31ae45bc2e29f6b2abd0de1cc3b9d5205aa847cafaecb8af1476a609a2f6eb7"}, + {file = "cffi-1.17.1-cp39-cp39-win_amd64.whl", hash = "sha256:d016c76bdd850f3c626af19b0542c9677ba156e4ee4fccfdd7848803533ef662"}, + {file = "cffi-1.17.1.tar.gz", hash = "sha256:1c39c6016c32bc48dd54561950ebd6836e1670f2ae46128f67cf49e789c52824"}, +] + +[package.dependencies] +pycparser = "*" + +[[package]] +name = "cfgv" +version = "3.4.0" +description = "Validate configuration and produce human readable error messages." +optional = false +python-versions = ">=3.8" +groups = ["lint"] +files = [ + {file = "cfgv-3.4.0-py2.py3-none-any.whl", hash = "sha256:b7265b1f29fd3316bfcd2b330d63d024f2bfd8bcb8b0272f8e19a504856c48f9"}, + {file = "cfgv-3.4.0.tar.gz", hash = "sha256:e52591d4c5f5dead8e0f673fb16db7949d2cfb3f7da4582893288f0ded8fe560"}, +] + +[[package]] +name = "chardet" +version = "5.2.0" +description = "Universal encoding detector for Python 3" +optional = false +python-versions = ">=3.7" +groups = ["dev"] +files = [ + {file = "chardet-5.2.0-py3-none-any.whl", hash = "sha256:e1cf59446890a00105fe7b7912492ea04b6e6f06d4b742b2c788469e34c82970"}, + {file = "chardet-5.2.0.tar.gz", hash = "sha256:1b3b6ff479a8c414bc3fa2c0852995695c4a026dcd6d0633b2dd092ca39c1cf7"}, +] + +[[package]] +name = "cleo" +version = "2.1.0" +description = "Cleo allows you to create beautiful and testable command-line interfaces." +optional = false +python-versions = ">=3.7,<4.0" +groups = ["dev"] +markers = "python_version < \"4.0\"" +files = [ + {file = "cleo-2.1.0-py3-none-any.whl", hash = "sha256:4a31bd4dd45695a64ee3c4758f583f134267c2bc518d8ae9a29cf237d009b07e"}, + {file = "cleo-2.1.0.tar.gz", hash = "sha256:0b2c880b5d13660a7ea651001fb4acb527696c01f15c9ee650f377aa543fd523"}, +] + +[package.dependencies] +crashtest = ">=0.4.1,<0.5.0" +rapidfuzz = ">=3.0.0,<4.0.0" + +[[package]] +name = "click" +version = "8.1.7" +description = "Composable command line interface toolkit" +optional = false +python-versions = ">=3.7" +groups = ["doc"] +files = [ + {file = "click-8.1.7-py3-none-any.whl", hash = "sha256:ae74fb96c20a0277a1d615f1e4d73c8414f5a98db8b799a7931d1582f3390c28"}, + {file = "click-8.1.7.tar.gz", hash = "sha256:ca9853ad459e787e2192211578cc907e7594e294c7ccc834310722b41b9ca6de"}, +] + +[package.dependencies] +colorama = {version = "*", markers = "platform_system == \"Windows\""} + +[[package]] +name = "colorama" +version = "0.4.6" +description = "Cross-platform colored terminal text." +optional = false +python-versions = "!=3.0.*,!=3.1.*,!=3.2.*,!=3.3.*,!=3.4.*,!=3.5.*,!=3.6.*,>=2.7" +groups = ["benchmark", "dev", "doc", "test"] +files = [ + {file = "colorama-0.4.6-py2.py3-none-any.whl", hash = "sha256:4f1d9991f5acc0ca119f9d443620b77f9d6b33703e51011c16baf57afb285fc6"}, + {file = "colorama-0.4.6.tar.gz", hash = "sha256:08695f5cb7ed6e0531a20572697297273c47b8cae5a63ffc6d6ed5c201be6e44"}, +] +markers = {benchmark = "sys_platform == \"win32\"", doc = "platform_system == \"Windows\"", test = "sys_platform == \"win32\""} + +[[package]] +name = "crashtest" +version = "0.4.1" +description = "Manage Python errors with ease" +optional = false +python-versions = ">=3.7,<4.0" +groups = ["dev"] +markers = "python_version < \"4.0\"" +files = [ + {file = "crashtest-0.4.1-py3-none-any.whl", hash = "sha256:8d23eac5fa660409f57472e3851dab7ac18aba459a8d19cbbba86d3d5aecd2a5"}, + {file = "crashtest-0.4.1.tar.gz", hash = "sha256:80d7b1f316ebfbd429f648076d6275c877ba30ba48979de4191714a75266f0ce"}, +] + +[[package]] +name = "distlib" +version = "0.3.9" +description = "Distribution utilities" +optional = false +python-versions = "*" +groups = ["dev", "lint"] +files = [ + {file = "distlib-0.3.9-py2.py3-none-any.whl", hash = "sha256:47f8c22fd27c27e25a65601af709b38e4f0a45ea4fc2e710f65755fa8caaaf87"}, + {file = "distlib-0.3.9.tar.gz", hash = "sha256:a60f20dea646b8a33f3e7772f74dc0b2d0772d2837ee1342a00645c81edf9403"}, +] + +[[package]] +name = "exceptiongroup" +version = "1.2.2" +description = "Backport of PEP 654 (exception groups)" +optional = false +python-versions = ">=3.7" +groups = ["benchmark", "test"] +markers = "python_version < \"3.11\"" +files = [ + {file = "exceptiongroup-1.2.2-py3-none-any.whl", hash = "sha256:3111b9d131c238bec2f8f516e123e14ba243563fb135d3fe885990585aa7795b"}, + {file = "exceptiongroup-1.2.2.tar.gz", hash = "sha256:47c2edf7c6738fafb49fd34290706d1a1a2f4d1c6df275526b62cbb4aa5393cc"}, +] + +[package.extras] +test = ["pytest (>=6)"] + +[[package]] +name = "filelock" +version = "3.16.1" +description = "A platform independent file lock." +optional = false +python-versions = ">=3.8" +groups = ["dev", "lint"] +files = [ + {file = "filelock-3.16.1-py3-none-any.whl", hash = "sha256:2082e5703d51fbf98ea75855d9d5527e33d8ff23099bec374a134febee6946b0"}, + {file = "filelock-3.16.1.tar.gz", hash = "sha256:c249fbfcd5db47e5e2d6d62198e565475ee65e4831e2561c8e313fa7eb961435"}, +] + +[package.extras] +docs = ["furo (>=2024.8.6)", "sphinx (>=8.0.2)", "sphinx-autodoc-typehints (>=2.4.1)"] +testing = ["covdefaults (>=2.3)", "coverage (>=7.6.1)", "diff-cover (>=9.2)", "pytest (>=8.3.3)", "pytest-asyncio (>=0.24)", "pytest-cov (>=5)", "pytest-mock (>=3.14)", "pytest-timeout (>=2.3.1)", "virtualenv (>=20.26.4)"] +typing = ["typing-extensions (>=4.12.2) ; python_version < \"3.11\""] + +[[package]] +name = "ghp-import" +version = "2.1.0" +description = "Copy your docs directly to the gh-pages branch." +optional = false +python-versions = "*" +groups = ["doc"] +files = [ + {file = "ghp-import-2.1.0.tar.gz", hash = "sha256:9c535c4c61193c2df8871222567d7fd7e5014d835f97dc7b7439069e2413d343"}, + {file = "ghp_import-2.1.0-py3-none-any.whl", hash = "sha256:8337dd7b50877f163d4c0289bc1f1c7f127550241988d568c1db512c4324a619"}, +] + +[package.dependencies] +python-dateutil = ">=2.8.1" + +[package.extras] +dev = ["flake8", "markdown", "twine", "wheel"] + +[[package]] +name = "identify" +version = "2.6.3" +description = "File identification library for Python" +optional = false +python-versions = ">=3.9" +groups = ["lint"] +files = [ + {file = "identify-2.6.3-py2.py3-none-any.whl", hash = "sha256:9edba65473324c2ea9684b1f944fe3191db3345e50b6d04571d10ed164f8d7bd"}, + {file = "identify-2.6.3.tar.gz", hash = "sha256:62f5dae9b5fef52c84cc188514e9ea4f3f636b1d8799ab5ebc475471f9e47a02"}, +] + +[package.extras] +license = ["ukkonen"] + +[[package]] +name = "importlib-metadata" +version = "8.5.0" +description = "Read metadata from Python packages" +optional = false +python-versions = ">=3.8" +groups = ["benchmark", "doc"] +markers = "python_version == \"3.9\"" +files = [ + {file = "importlib_metadata-8.5.0-py3-none-any.whl", hash = "sha256:45e54197d28b7a7f1559e60b95e7c567032b602131fbd588f1497f47880aa68b"}, + {file = "importlib_metadata-8.5.0.tar.gz", hash = "sha256:71522656f0abace1d072b9e5481a48f07c138e00f079c38c8f883823f9c26bd7"}, +] + +[package.dependencies] +zipp = ">=3.20" + +[package.extras] +check = ["pytest-checkdocs (>=2.4)", "pytest-ruff (>=0.2.1) ; sys_platform != \"cygwin\""] +cover = ["pytest-cov"] +doc = ["furo", "jaraco.packaging (>=9.3)", "jaraco.tidelift (>=1.4)", "rst.linker (>=1.9)", "sphinx (>=3.5)", "sphinx-lint"] +enabler = ["pytest-enabler (>=2.2)"] +perf = ["ipython"] +test = ["flufl.flake8", "importlib-resources (>=1.3) ; python_version < \"3.9\"", "jaraco.test (>=5.4)", "packaging", "pyfakefs", "pytest (>=6,!=8.1.*)", "pytest-perf (>=0.9.2)"] +type = ["pytest-mypy"] + +[[package]] +name = "iniconfig" +version = "2.0.0" +description = "brain-dead simple config-ini parsing" +optional = false +python-versions = ">=3.7" +groups = ["benchmark", "test"] +files = [ + {file = "iniconfig-2.0.0-py3-none-any.whl", hash = "sha256:b6a85871a79d2e3b22d2d1b94ac2824226a63c6b741c88f7ae975f18b6778374"}, + {file = "iniconfig-2.0.0.tar.gz", hash = "sha256:2d91e135bf72d31a410b17c16da610a82cb55f6b0477d1a902134b24a455b8b3"}, +] + +[[package]] +name = "jinja2" +version = "3.1.4" +description = "A very fast and expressive template engine." +optional = false +python-versions = ">=3.7" +groups = ["doc"] +files = [ + {file = "jinja2-3.1.4-py3-none-any.whl", hash = "sha256:bc5dd2abb727a5319567b7a813e6a2e7318c39f4f487cfe6c89c6f9c7d25197d"}, + {file = "jinja2-3.1.4.tar.gz", hash = "sha256:4a3aee7acbbe7303aede8e9648d13b8bf88a429282aa6122a993f0ac800cb369"}, +] + +[package.dependencies] +MarkupSafe = ">=2.0" + +[package.extras] +i18n = ["Babel (>=2.7)"] + +[[package]] +name = "markdown" +version = "3.7" +description = "Python implementation of John Gruber's Markdown." +optional = false +python-versions = ">=3.8" +groups = ["doc"] +files = [ + {file = "Markdown-3.7-py3-none-any.whl", hash = "sha256:7eb6df5690b81a1d7942992c97fad2938e956e79df20cbc6186e9c3a77b1c803"}, + {file = "markdown-3.7.tar.gz", hash = "sha256:2ae2471477cfd02dbbf038d5d9bc226d40def84b4fe2986e49b59b6b472bbed2"}, +] + +[package.dependencies] +importlib-metadata = {version = ">=4.4", markers = "python_version < \"3.10\""} + +[package.extras] +docs = ["mdx-gh-links (>=0.2)", "mkdocs (>=1.5)", "mkdocs-gen-files", "mkdocs-literate-nav", "mkdocs-nature (>=0.6)", "mkdocs-section-index", "mkdocstrings[python]"] +testing = ["coverage", "pyyaml"] + +[[package]] +name = "markdown-include" +version = "0.8.1" +description = "A Python-Markdown extension which provides an 'include' function" +optional = false +python-versions = ">=3.7" +groups = ["doc"] +files = [ + {file = "markdown-include-0.8.1.tar.gz", hash = "sha256:1d0623e0fc2757c38d35df53752768356162284259d259c486b4ab6285cdbbe3"}, + {file = "markdown_include-0.8.1-py3-none-any.whl", hash = "sha256:32f0635b9cfef46997b307e2430022852529f7a5b87c0075c504283e7cc7db53"}, +] + +[package.dependencies] +markdown = ">=3.0" + +[package.extras] +tests = ["pytest"] + +[[package]] +name = "markdown-it-py" +version = "3.0.0" +description = "Python port of markdown-it. Markdown parsing, done right!" +optional = false +python-versions = ">=3.8" +groups = ["benchmark"] +files = [ + {file = "markdown-it-py-3.0.0.tar.gz", hash = "sha256:e3f60a94fa066dc52ec76661e37c851cb232d92f9886b15cb560aaada2df8feb"}, + {file = "markdown_it_py-3.0.0-py3-none-any.whl", hash = "sha256:355216845c60bd96232cd8d8c40e8f9765cc86f46880e43a8fd22dc1a1a8cab1"}, +] + +[package.dependencies] +mdurl = ">=0.1,<1.0" + +[package.extras] +benchmarking = ["psutil", "pytest", "pytest-benchmark"] +code-style = ["pre-commit (>=3.0,<4.0)"] +compare = ["commonmark (>=0.9,<1.0)", "markdown (>=3.4,<4.0)", "mistletoe (>=1.0,<2.0)", "mistune (>=2.0,<3.0)", "panflute (>=2.3,<3.0)"] +linkify = ["linkify-it-py (>=1,<3)"] +plugins = ["mdit-py-plugins"] +profiling = ["gprof2dot"] +rtd = ["jupyter_sphinx", "mdit-py-plugins", "myst-parser", "pyyaml", "sphinx", "sphinx-copybutton", "sphinx-design", "sphinx_book_theme"] +testing = ["coverage", "pytest", "pytest-cov", "pytest-regressions"] + +[[package]] +name = "markupsafe" +version = "3.0.2" +description = "Safely add untrusted strings to HTML/XML markup." +optional = false +python-versions = ">=3.9" +groups = ["doc"] +files = [ + {file = "MarkupSafe-3.0.2-cp310-cp310-macosx_10_9_universal2.whl", hash = "sha256:7e94c425039cde14257288fd61dcfb01963e658efbc0ff54f5306b06054700f8"}, + {file = "MarkupSafe-3.0.2-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:9e2d922824181480953426608b81967de705c3cef4d1af983af849d7bd619158"}, + {file = "MarkupSafe-3.0.2-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:38a9ef736c01fccdd6600705b09dc574584b89bea478200c5fbf112a6b0d5579"}, + {file = "MarkupSafe-3.0.2-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:bbcb445fa71794da8f178f0f6d66789a28d7319071af7a496d4d507ed566270d"}, + {file = "MarkupSafe-3.0.2-cp310-cp310-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:57cb5a3cf367aeb1d316576250f65edec5bb3be939e9247ae594b4bcbc317dfb"}, + {file = "MarkupSafe-3.0.2-cp310-cp310-musllinux_1_2_aarch64.whl", hash = "sha256:3809ede931876f5b2ec92eef964286840ed3540dadf803dd570c3b7e13141a3b"}, + {file = "MarkupSafe-3.0.2-cp310-cp310-musllinux_1_2_i686.whl", hash = "sha256:e07c3764494e3776c602c1e78e298937c3315ccc9043ead7e685b7f2b8d47b3c"}, + {file = "MarkupSafe-3.0.2-cp310-cp310-musllinux_1_2_x86_64.whl", hash = "sha256:b424c77b206d63d500bcb69fa55ed8d0e6a3774056bdc4839fc9298a7edca171"}, + {file = "MarkupSafe-3.0.2-cp310-cp310-win32.whl", hash = "sha256:fcabf5ff6eea076f859677f5f0b6b5c1a51e70a376b0579e0eadef8db48c6b50"}, + {file = "MarkupSafe-3.0.2-cp310-cp310-win_amd64.whl", hash = "sha256:6af100e168aa82a50e186c82875a5893c5597a0c1ccdb0d8b40240b1f28b969a"}, + {file = "MarkupSafe-3.0.2-cp311-cp311-macosx_10_9_universal2.whl", hash = "sha256:9025b4018f3a1314059769c7bf15441064b2207cb3f065e6ea1e7359cb46db9d"}, + {file = "MarkupSafe-3.0.2-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:93335ca3812df2f366e80509ae119189886b0f3c2b81325d39efdb84a1e2ae93"}, + {file = "MarkupSafe-3.0.2-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:2cb8438c3cbb25e220c2ab33bb226559e7afb3baec11c4f218ffa7308603c832"}, + {file = "MarkupSafe-3.0.2-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:a123e330ef0853c6e822384873bef7507557d8e4a082961e1defa947aa59ba84"}, + {file = "MarkupSafe-3.0.2-cp311-cp311-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:1e084f686b92e5b83186b07e8a17fc09e38fff551f3602b249881fec658d3eca"}, + {file = "MarkupSafe-3.0.2-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:d8213e09c917a951de9d09ecee036d5c7d36cb6cb7dbaece4c71a60d79fb9798"}, + {file = "MarkupSafe-3.0.2-cp311-cp311-musllinux_1_2_i686.whl", hash = "sha256:5b02fb34468b6aaa40dfc198d813a641e3a63b98c2b05a16b9f80b7ec314185e"}, + {file = "MarkupSafe-3.0.2-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:0bff5e0ae4ef2e1ae4fdf2dfd5b76c75e5c2fa4132d05fc1b0dabcd20c7e28c4"}, + {file = "MarkupSafe-3.0.2-cp311-cp311-win32.whl", hash = "sha256:6c89876f41da747c8d3677a2b540fb32ef5715f97b66eeb0c6b66f5e3ef6f59d"}, + {file = "MarkupSafe-3.0.2-cp311-cp311-win_amd64.whl", hash = "sha256:70a87b411535ccad5ef2f1df5136506a10775d267e197e4cf531ced10537bd6b"}, + {file = "MarkupSafe-3.0.2-cp312-cp312-macosx_10_13_universal2.whl", hash = "sha256:9778bd8ab0a994ebf6f84c2b949e65736d5575320a17ae8984a77fab08db94cf"}, + {file = "MarkupSafe-3.0.2-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:846ade7b71e3536c4e56b386c2a47adf5741d2d8b94ec9dc3e92e5e1ee1e2225"}, + {file = "MarkupSafe-3.0.2-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:1c99d261bd2d5f6b59325c92c73df481e05e57f19837bdca8413b9eac4bd8028"}, + {file = "MarkupSafe-3.0.2-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:e17c96c14e19278594aa4841ec148115f9c7615a47382ecb6b82bd8fea3ab0c8"}, + {file = "MarkupSafe-3.0.2-cp312-cp312-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:88416bd1e65dcea10bc7569faacb2c20ce071dd1f87539ca2ab364bf6231393c"}, + {file = "MarkupSafe-3.0.2-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:2181e67807fc2fa785d0592dc2d6206c019b9502410671cc905d132a92866557"}, + {file = "MarkupSafe-3.0.2-cp312-cp312-musllinux_1_2_i686.whl", hash = "sha256:52305740fe773d09cffb16f8ed0427942901f00adedac82ec8b67752f58a1b22"}, + {file = "MarkupSafe-3.0.2-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:ad10d3ded218f1039f11a75f8091880239651b52e9bb592ca27de44eed242a48"}, + {file = "MarkupSafe-3.0.2-cp312-cp312-win32.whl", hash = "sha256:0f4ca02bea9a23221c0182836703cbf8930c5e9454bacce27e767509fa286a30"}, + {file = "MarkupSafe-3.0.2-cp312-cp312-win_amd64.whl", hash = "sha256:8e06879fc22a25ca47312fbe7c8264eb0b662f6db27cb2d3bbbc74b1df4b9b87"}, + {file = "MarkupSafe-3.0.2-cp313-cp313-macosx_10_13_universal2.whl", hash = "sha256:ba9527cdd4c926ed0760bc301f6728ef34d841f405abf9d4f959c478421e4efd"}, + {file = "MarkupSafe-3.0.2-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:f8b3d067f2e40fe93e1ccdd6b2e1d16c43140e76f02fb1319a05cf2b79d99430"}, + {file = "MarkupSafe-3.0.2-cp313-cp313-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:569511d3b58c8791ab4c2e1285575265991e6d8f8700c7be0e88f86cb0672094"}, + {file = "MarkupSafe-3.0.2-cp313-cp313-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:15ab75ef81add55874e7ab7055e9c397312385bd9ced94920f2802310c930396"}, + {file = "MarkupSafe-3.0.2-cp313-cp313-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:f3818cb119498c0678015754eba762e0d61e5b52d34c8b13d770f0719f7b1d79"}, + {file = "MarkupSafe-3.0.2-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:cdb82a876c47801bb54a690c5ae105a46b392ac6099881cdfb9f6e95e4014c6a"}, + {file = "MarkupSafe-3.0.2-cp313-cp313-musllinux_1_2_i686.whl", hash = "sha256:cabc348d87e913db6ab4aa100f01b08f481097838bdddf7c7a84b7575b7309ca"}, + {file = "MarkupSafe-3.0.2-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:444dcda765c8a838eaae23112db52f1efaf750daddb2d9ca300bcae1039adc5c"}, + {file = "MarkupSafe-3.0.2-cp313-cp313-win32.whl", hash = "sha256:bcf3e58998965654fdaff38e58584d8937aa3096ab5354d493c77d1fdd66d7a1"}, + {file = "MarkupSafe-3.0.2-cp313-cp313-win_amd64.whl", hash = "sha256:e6a2a455bd412959b57a172ce6328d2dd1f01cb2135efda2e4576e8a23fa3b0f"}, + {file = "MarkupSafe-3.0.2-cp313-cp313t-macosx_10_13_universal2.whl", hash = "sha256:b5a6b3ada725cea8a5e634536b1b01c30bcdcd7f9c6fff4151548d5bf6b3a36c"}, + {file = "MarkupSafe-3.0.2-cp313-cp313t-macosx_11_0_arm64.whl", hash = "sha256:a904af0a6162c73e3edcb969eeeb53a63ceeb5d8cf642fade7d39e7963a22ddb"}, + {file = "MarkupSafe-3.0.2-cp313-cp313t-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:4aa4e5faecf353ed117801a068ebab7b7e09ffb6e1d5e412dc852e0da018126c"}, + {file = "MarkupSafe-3.0.2-cp313-cp313t-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:c0ef13eaeee5b615fb07c9a7dadb38eac06a0608b41570d8ade51c56539e509d"}, + {file = "MarkupSafe-3.0.2-cp313-cp313t-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:d16a81a06776313e817c951135cf7340a3e91e8c1ff2fac444cfd75fffa04afe"}, + {file = "MarkupSafe-3.0.2-cp313-cp313t-musllinux_1_2_aarch64.whl", hash = "sha256:6381026f158fdb7c72a168278597a5e3a5222e83ea18f543112b2662a9b699c5"}, + {file = "MarkupSafe-3.0.2-cp313-cp313t-musllinux_1_2_i686.whl", hash = "sha256:3d79d162e7be8f996986c064d1c7c817f6df3a77fe3d6859f6f9e7be4b8c213a"}, + {file = "MarkupSafe-3.0.2-cp313-cp313t-musllinux_1_2_x86_64.whl", hash = "sha256:131a3c7689c85f5ad20f9f6fb1b866f402c445b220c19fe4308c0b147ccd2ad9"}, + {file = "MarkupSafe-3.0.2-cp313-cp313t-win32.whl", hash = "sha256:ba8062ed2cf21c07a9e295d5b8a2a5ce678b913b45fdf68c32d95d6c1291e0b6"}, + {file = "MarkupSafe-3.0.2-cp313-cp313t-win_amd64.whl", hash = "sha256:e444a31f8db13eb18ada366ab3cf45fd4b31e4db1236a4448f68778c1d1a5a2f"}, + {file = "MarkupSafe-3.0.2-cp39-cp39-macosx_10_9_universal2.whl", hash = "sha256:eaa0a10b7f72326f1372a713e73c3f739b524b3af41feb43e4921cb529f5929a"}, + {file = "MarkupSafe-3.0.2-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:48032821bbdf20f5799ff537c7ac3d1fba0ba032cfc06194faffa8cda8b560ff"}, + {file = "MarkupSafe-3.0.2-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:1a9d3f5f0901fdec14d8d2f66ef7d035f2157240a433441719ac9a3fba440b13"}, + {file = "MarkupSafe-3.0.2-cp39-cp39-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:88b49a3b9ff31e19998750c38e030fc7bb937398b1f78cfa599aaef92d693144"}, + {file = "MarkupSafe-3.0.2-cp39-cp39-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:cfad01eed2c2e0c01fd0ecd2ef42c492f7f93902e39a42fc9ee1692961443a29"}, + {file = "MarkupSafe-3.0.2-cp39-cp39-musllinux_1_2_aarch64.whl", hash = "sha256:1225beacc926f536dc82e45f8a4d68502949dc67eea90eab715dea3a21c1b5f0"}, + {file = "MarkupSafe-3.0.2-cp39-cp39-musllinux_1_2_i686.whl", hash = "sha256:3169b1eefae027567d1ce6ee7cae382c57fe26e82775f460f0b2778beaad66c0"}, + {file = "MarkupSafe-3.0.2-cp39-cp39-musllinux_1_2_x86_64.whl", hash = "sha256:eb7972a85c54febfb25b5c4b4f3af4dcc731994c7da0d8a0b4a6eb0640e1d178"}, + {file = "MarkupSafe-3.0.2-cp39-cp39-win32.whl", hash = "sha256:8c4e8c3ce11e1f92f6536ff07154f9d49677ebaaafc32db9db4620bc11ed480f"}, + {file = "MarkupSafe-3.0.2-cp39-cp39-win_amd64.whl", hash = "sha256:6e296a513ca3d94054c2c881cc913116e90fd030ad1c656b3869762b754f5f8a"}, + {file = "markupsafe-3.0.2.tar.gz", hash = "sha256:ee55d3edf80167e48ea11a923c7386f4669df67d7994554387f84e7d8b0a2bf0"}, +] + +[[package]] +name = "maturin" +version = "1.7.6" +description = "Build and publish crates with pyo3, cffi and uniffi bindings as well as rust binaries as python packages" +optional = false +python-versions = ">=3.7" +groups = ["build"] +files = [ + {file = "maturin-1.7.6-py3-none-linux_armv6l.whl", hash = "sha256:8c23309b75624cf4dc76682bbfe587ce42c9ba595bdc954c1c0b35ef3869470e"}, + {file = "maturin-1.7.6-py3-none-macosx_10_12_x86_64.macosx_11_0_arm64.macosx_10_12_universal2.whl", hash = "sha256:85eb76c502f3d9923371623fa153f67afc07b81aa3a28a2620340564bf521e6a"}, + {file = "maturin-1.7.6-py3-none-macosx_10_12_x86_64.whl", hash = "sha256:37f42a6e15cd49e12a13475b105239e1da20763d50213d541ad56c78d900df9d"}, + {file = "maturin-1.7.6-py3-none-manylinux_2_12_i686.manylinux2010_i686.musllinux_1_1_i686.whl", hash = "sha256:f64b3a30f3af59fbdbeba980508c7a8294b5f5202a292f41800d22cb8ab69238"}, + {file = "maturin-1.7.6-py3-none-manylinux_2_12_x86_64.manylinux2010_x86_64.musllinux_1_1_x86_64.whl", hash = "sha256:41d3f0af4a15ee328aa16ba5581f1bfdf0ad88f2a3e1ee9ebf77d2fe269d05af"}, + {file = "maturin-1.7.6-py3-none-manylinux_2_17_aarch64.manylinux2014_aarch64.musllinux_1_1_aarch64.whl", hash = "sha256:41395b4b4d8c35fb2c86143bc3a8808024076a60ed72bfa0002f032f2913ee3d"}, + {file = "maturin-1.7.6-py3-none-manylinux_2_17_armv7l.manylinux2014_armv7l.musllinux_1_1_armv7l.whl", hash = "sha256:534c0663c10b590f9c1de8c49f06c0d7da7e1d3078f3975b0191b139a73f051b"}, + {file = "maturin-1.7.6-py3-none-manylinux_2_17_ppc64le.manylinux2014_ppc64le.musllinux_1_1_ppc64le.whl", hash = "sha256:517a0b469199fab8a5e05a2f2477e156c90f80ed160e28e6ee42d5315c2c424b"}, + {file = "maturin-1.7.6-py3-none-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:44c39226a22c2c587e3b886890c76b6ba950ab0f7b129932f8f0498441d47981"}, + {file = "maturin-1.7.6-py3-none-win32.whl", hash = "sha256:8455cecb948c01ff20689a953a2fd034d4ef94f2bf256cf817beb12572e3051c"}, + {file = "maturin-1.7.6-py3-none-win_amd64.whl", hash = "sha256:84382c7a10d3c84cdfeb230d9b88f78fd99c2aebbd121fd8f04efc706ff65507"}, + {file = "maturin-1.7.6-py3-none-win_arm64.whl", hash = "sha256:cc5a14f42d6f2cf3eff944f2d00d0ce45fc6060d61e51aa8b8c407efbea4dea8"}, + {file = "maturin-1.7.6.tar.gz", hash = "sha256:18c3f192c0f48e820fe684c9b89cc099f0107fd93845d39d6001610e3b1b94c4"}, +] + +[package.dependencies] +tomli = {version = ">=1.1.0", markers = "python_version < \"3.11\""} + +[package.extras] +patchelf = ["patchelf"] +zig = ["ziglang (>=0.10.0,<0.13.0)"] + +[[package]] +name = "mdurl" +version = "0.1.2" +description = "Markdown URL utilities" +optional = false +python-versions = ">=3.7" +groups = ["benchmark"] +files = [ + {file = "mdurl-0.1.2-py3-none-any.whl", hash = "sha256:84008a41e51615a49fc9966191ff91509e3c40b939176e643fd50a5c2196b8f8"}, + {file = "mdurl-0.1.2.tar.gz", hash = "sha256:bb413d29f5eea38f31dd4754dd7377d4465116fb207585f97bf925588687c1ba"}, +] + +[[package]] +name = "mergedeep" +version = "1.3.4" +description = "A deep merge function for 🐍." +optional = false +python-versions = ">=3.6" +groups = ["doc"] +files = [ + {file = "mergedeep-1.3.4-py3-none-any.whl", hash = "sha256:70775750742b25c0d8f36c55aed03d24c3384d17c951b3175d898bd778ef0307"}, + {file = "mergedeep-1.3.4.tar.gz", hash = "sha256:0096d52e9dad9939c3d975a774666af186eda617e6ca84df4c94dec30004f2a8"}, +] + +[[package]] +name = "mkdocs" +version = "1.6.1" +description = "Project documentation with Markdown." +optional = false +python-versions = ">=3.8" +groups = ["doc"] +files = [ + {file = "mkdocs-1.6.1-py3-none-any.whl", hash = "sha256:db91759624d1647f3f34aa0c3f327dd2601beae39a366d6e064c03468d35c20e"}, + {file = "mkdocs-1.6.1.tar.gz", hash = "sha256:7b432f01d928c084353ab39c57282f29f92136665bdd6abf7c1ec8d822ef86f2"}, +] + +[package.dependencies] +click = ">=7.0" +colorama = {version = ">=0.4", markers = "platform_system == \"Windows\""} +ghp-import = ">=1.0" +importlib-metadata = {version = ">=4.4", markers = "python_version < \"3.10\""} +jinja2 = ">=2.11.1" +markdown = ">=3.3.6" +markupsafe = ">=2.0.1" +mergedeep = ">=1.3.4" +mkdocs-get-deps = ">=0.2.0" +packaging = ">=20.5" +pathspec = ">=0.11.1" +pyyaml = ">=5.1" +pyyaml-env-tag = ">=0.1" +watchdog = ">=2.0" + +[package.extras] +i18n = ["babel (>=2.9.0)"] +min-versions = ["babel (==2.9.0)", "click (==7.0)", "colorama (==0.4) ; platform_system == \"Windows\"", "ghp-import (==1.0)", "importlib-metadata (==4.4) ; python_version < \"3.10\"", "jinja2 (==2.11.1)", "markdown (==3.3.6)", "markupsafe (==2.0.1)", "mergedeep (==1.3.4)", "mkdocs-get-deps (==0.2.0)", "packaging (==20.5)", "pathspec (==0.11.1)", "pyyaml (==5.1)", "pyyaml-env-tag (==0.1)", "watchdog (==2.0)"] + +[[package]] +name = "mkdocs-get-deps" +version = "0.2.0" +description = "MkDocs extension that lists all dependencies according to a mkdocs.yml file" +optional = false +python-versions = ">=3.8" +groups = ["doc"] +files = [ + {file = "mkdocs_get_deps-0.2.0-py3-none-any.whl", hash = "sha256:2bf11d0b133e77a0dd036abeeb06dec8775e46efa526dc70667d8863eefc6134"}, + {file = "mkdocs_get_deps-0.2.0.tar.gz", hash = "sha256:162b3d129c7fad9b19abfdcb9c1458a651628e4b1dea628ac68790fb3061c60c"}, +] + +[package.dependencies] +importlib-metadata = {version = ">=4.3", markers = "python_version < \"3.10\""} +mergedeep = ">=1.3.4" +platformdirs = ">=2.2.0" +pyyaml = ">=5.1" + +[[package]] +name = "mypy" +version = "1.13.0" +description = "Optional static typing for Python" +optional = false +python-versions = ">=3.8" +groups = ["typing"] +files = [ + {file = "mypy-1.13.0-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:6607e0f1dd1fb7f0aca14d936d13fd19eba5e17e1cd2a14f808fa5f8f6d8f60a"}, + {file = "mypy-1.13.0-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:8a21be69bd26fa81b1f80a61ee7ab05b076c674d9b18fb56239d72e21d9f4c80"}, + {file = "mypy-1.13.0-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:7b2353a44d2179846a096e25691d54d59904559f4232519d420d64da6828a3a7"}, + {file = "mypy-1.13.0-cp310-cp310-musllinux_1_1_x86_64.whl", hash = "sha256:0730d1c6a2739d4511dc4253f8274cdd140c55c32dfb0a4cf8b7a43f40abfa6f"}, + {file = "mypy-1.13.0-cp310-cp310-win_amd64.whl", hash = "sha256:c5fc54dbb712ff5e5a0fca797e6e0aa25726c7e72c6a5850cfd2adbc1eb0a372"}, + {file = "mypy-1.13.0-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:581665e6f3a8a9078f28d5502f4c334c0c8d802ef55ea0e7276a6e409bc0d82d"}, + {file = "mypy-1.13.0-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:3ddb5b9bf82e05cc9a627e84707b528e5c7caaa1c55c69e175abb15a761cec2d"}, + {file = "mypy-1.13.0-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:20c7ee0bc0d5a9595c46f38beb04201f2620065a93755704e141fcac9f59db2b"}, + {file = "mypy-1.13.0-cp311-cp311-musllinux_1_1_x86_64.whl", hash = "sha256:3790ded76f0b34bc9c8ba4def8f919dd6a46db0f5a6610fb994fe8efdd447f73"}, + {file = "mypy-1.13.0-cp311-cp311-win_amd64.whl", hash = "sha256:51f869f4b6b538229c1d1bcc1dd7d119817206e2bc54e8e374b3dfa202defcca"}, + {file = "mypy-1.13.0-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:5c7051a3461ae84dfb5dd15eff5094640c61c5f22257c8b766794e6dd85e72d5"}, + {file = "mypy-1.13.0-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:39bb21c69a5d6342f4ce526e4584bc5c197fd20a60d14a8624d8743fffb9472e"}, + {file = "mypy-1.13.0-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:164f28cb9d6367439031f4c81e84d3ccaa1e19232d9d05d37cb0bd880d3f93c2"}, + {file = "mypy-1.13.0-cp312-cp312-musllinux_1_1_x86_64.whl", hash = "sha256:a4c1bfcdbce96ff5d96fc9b08e3831acb30dc44ab02671eca5953eadad07d6d0"}, + {file = "mypy-1.13.0-cp312-cp312-win_amd64.whl", hash = "sha256:a0affb3a79a256b4183ba09811e3577c5163ed06685e4d4b46429a271ba174d2"}, + {file = "mypy-1.13.0-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:a7b44178c9760ce1a43f544e595d35ed61ac2c3de306599fa59b38a6048e1aa7"}, + {file = "mypy-1.13.0-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:5d5092efb8516d08440e36626f0153b5006d4088c1d663d88bf79625af3d1d62"}, + {file = "mypy-1.13.0-cp313-cp313-manylinux_2_17_x86_64.manylinux2014_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:de2904956dac40ced10931ac967ae63c5089bd498542194b436eb097a9f77bc8"}, + {file = "mypy-1.13.0-cp313-cp313-musllinux_1_1_x86_64.whl", hash = "sha256:7bfd8836970d33c2105562650656b6846149374dc8ed77d98424b40b09340ba7"}, + {file = "mypy-1.13.0-cp313-cp313-win_amd64.whl", hash = "sha256:9f73dba9ec77acb86457a8fc04b5239822df0c14a082564737833d2963677dbc"}, + {file = "mypy-1.13.0-cp38-cp38-macosx_10_9_x86_64.whl", hash = "sha256:100fac22ce82925f676a734af0db922ecfea991e1d7ec0ceb1e115ebe501301a"}, + {file = "mypy-1.13.0-cp38-cp38-macosx_11_0_arm64.whl", hash = "sha256:7bcb0bb7f42a978bb323a7c88f1081d1b5dee77ca86f4100735a6f541299d8fb"}, + {file = "mypy-1.13.0-cp38-cp38-manylinux_2_17_x86_64.manylinux2014_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:bde31fc887c213e223bbfc34328070996061b0833b0a4cfec53745ed61f3519b"}, + {file = "mypy-1.13.0-cp38-cp38-musllinux_1_1_x86_64.whl", hash = "sha256:07de989f89786f62b937851295ed62e51774722e5444a27cecca993fc3f9cd74"}, + {file = "mypy-1.13.0-cp38-cp38-win_amd64.whl", hash = "sha256:4bde84334fbe19bad704b3f5b78c4abd35ff1026f8ba72b29de70dda0916beb6"}, + {file = "mypy-1.13.0-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:0246bcb1b5de7f08f2826451abd947bf656945209b140d16ed317f65a17dc7dc"}, + {file = "mypy-1.13.0-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:7f5b7deae912cf8b77e990b9280f170381fdfbddf61b4ef80927edd813163732"}, + {file = "mypy-1.13.0-cp39-cp39-manylinux_2_17_x86_64.manylinux2014_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:7029881ec6ffb8bc233a4fa364736789582c738217b133f1b55967115288a2bc"}, + {file = "mypy-1.13.0-cp39-cp39-musllinux_1_1_x86_64.whl", hash = "sha256:3e38b980e5681f28f033f3be86b099a247b13c491f14bb8b1e1e134d23bb599d"}, + {file = "mypy-1.13.0-cp39-cp39-win_amd64.whl", hash = "sha256:a6789be98a2017c912ae6ccb77ea553bbaf13d27605d2ca20a76dfbced631b24"}, + {file = "mypy-1.13.0-py3-none-any.whl", hash = "sha256:9c250883f9fd81d212e0952c92dbfcc96fc237f4b7c92f56ac81fd48460b3e5a"}, + {file = "mypy-1.13.0.tar.gz", hash = "sha256:0291a61b6fbf3e6673e3405cfcc0e7650bebc7939659fdca2702958038bd835e"}, +] + +[package.dependencies] +mypy-extensions = ">=1.0.0" +tomli = {version = ">=1.1.0", markers = "python_version < \"3.11\""} +typing-extensions = ">=4.6.0" + +[package.extras] +dmypy = ["psutil (>=4.0)"] +faster-cache = ["orjson"] +install-types = ["pip"] +mypyc = ["setuptools (>=50)"] +reports = ["lxml"] + +[[package]] +name = "mypy-extensions" +version = "1.0.0" +description = "Type system extensions for programs checked with the mypy type checker." +optional = false +python-versions = ">=3.5" +groups = ["typing"] +files = [ + {file = "mypy_extensions-1.0.0-py3-none-any.whl", hash = "sha256:4392f6c0eb8a5668a69e23d168ffa70f0be9ccfd32b5cc2d26a34ae5b844552d"}, + {file = "mypy_extensions-1.0.0.tar.gz", hash = "sha256:75dbf8955dc00442a438fc4d0666508a9a97b6bd41aa2f0ffe9d2f2725af0782"}, +] + +[[package]] +name = "nodeenv" +version = "1.9.1" +description = "Node.js virtual environment builder" +optional = false +python-versions = "!=3.0.*,!=3.1.*,!=3.2.*,!=3.3.*,!=3.4.*,!=3.5.*,!=3.6.*,>=2.7" +groups = ["lint"] +files = [ + {file = "nodeenv-1.9.1-py2.py3-none-any.whl", hash = "sha256:ba11c9782d29c27c70ffbdda2d7415098754709be8a7056d79a737cd901155c9"}, + {file = "nodeenv-1.9.1.tar.gz", hash = "sha256:6ec12890a2dab7946721edbfbcd91f3319c6ccc9aec47be7c7e6b7011ee6645f"}, +] + +[[package]] +name = "packaging" +version = "24.2" +description = "Core utilities for Python packages" +optional = false +python-versions = ">=3.8" +groups = ["benchmark", "dev", "doc", "test"] +files = [ + {file = "packaging-24.2-py3-none-any.whl", hash = "sha256:09abb1bccd265c01f4a3aa3f7a7db064b36514d2cba19a2f694fe6150451a759"}, + {file = "packaging-24.2.tar.gz", hash = "sha256:c228a6dc5e932d346bc5739379109d49e8853dd8223571c7c5b55260edc0b97f"}, +] + +[[package]] +name = "pathspec" +version = "0.12.1" +description = "Utility library for gitignore style pattern matching of file paths." +optional = false +python-versions = ">=3.8" +groups = ["doc"] +files = [ + {file = "pathspec-0.12.1-py3-none-any.whl", hash = "sha256:a0d503e138a4c123b27490a4f7beda6a01c6f288df0e4a8b79c7eb0dc7b4cc08"}, + {file = "pathspec-0.12.1.tar.gz", hash = "sha256:a482d51503a1ab33b1c67a6c3813a26953dbdc71c31dacaef9a838c4e29f5712"}, +] + +[[package]] +name = "platformdirs" +version = "4.3.6" +description = "A small Python package for determining appropriate platform-specific dirs, e.g. a `user data dir`." +optional = false +python-versions = ">=3.8" +groups = ["dev", "doc", "lint"] +files = [ + {file = "platformdirs-4.3.6-py3-none-any.whl", hash = "sha256:73e575e1408ab8103900836b97580d5307456908a03e92031bab39e4554cc3fb"}, + {file = "platformdirs-4.3.6.tar.gz", hash = "sha256:357fb2acbc885b0419afd3ce3ed34564c13c9b95c89360cd9563f73aa5e2b907"}, +] + +[package.extras] +docs = ["furo (>=2024.8.6)", "proselint (>=0.14)", "sphinx (>=8.0.2)", "sphinx-autodoc-typehints (>=2.4)"] +test = ["appdirs (==1.4.4)", "covdefaults (>=2.3)", "pytest (>=8.3.2)", "pytest-cov (>=5)", "pytest-mock (>=3.14)"] +type = ["mypy (>=1.11.2)"] + +[[package]] +name = "pluggy" +version = "1.5.0" +description = "plugin and hook calling mechanisms for python" +optional = false +python-versions = ">=3.8" +groups = ["benchmark", "dev", "test"] +files = [ + {file = "pluggy-1.5.0-py3-none-any.whl", hash = "sha256:44e1ad92c8ca002de6377e165f3e0f1be63266ab4d554740532335b9d75ea669"}, + {file = "pluggy-1.5.0.tar.gz", hash = "sha256:2cffa88e94fdc978c4c574f15f9e59b7f4201d439195c3715ca9e2486f1d0cf1"}, +] + +[package.extras] +dev = ["pre-commit", "tox"] +testing = ["pytest", "pytest-benchmark"] + +[[package]] +name = "pre-commit" +version = "3.8.0" +description = "A framework for managing and maintaining multi-language pre-commit hooks." +optional = false +python-versions = ">=3.9" +groups = ["lint"] +files = [ + {file = "pre_commit-3.8.0-py2.py3-none-any.whl", hash = "sha256:9a90a53bf82fdd8778d58085faf8d83df56e40dfe18f45b19446e26bf1b3a63f"}, + {file = "pre_commit-3.8.0.tar.gz", hash = "sha256:8bb6494d4a20423842e198980c9ecf9f96607a07ea29549e180eef9ae80fe7af"}, +] + +[package.dependencies] +cfgv = ">=2.0.0" +identify = ">=1.0.0" +nodeenv = ">=0.11.1" +pyyaml = ">=5.1" +virtualenv = ">=20.10.0" + +[[package]] +name = "py-cpuinfo" +version = "9.0.0" +description = "Get CPU info with pure Python" +optional = false +python-versions = "*" +groups = ["test"] +files = [ + {file = "py-cpuinfo-9.0.0.tar.gz", hash = "sha256:3cdbbf3fac90dc6f118bfd64384f309edeadd902d7c8fb17f02ffa1fc3f49690"}, + {file = "py_cpuinfo-9.0.0-py3-none-any.whl", hash = "sha256:859625bc251f64e21f077d099d4162689c762b5d6a4c3c97553d56241c9674d5"}, +] + +[[package]] +name = "pycparser" +version = "2.22" +description = "C parser in Python" +optional = false +python-versions = ">=3.8" +groups = ["benchmark"] +files = [ + {file = "pycparser-2.22-py3-none-any.whl", hash = "sha256:c3702b6d3dd8c7abc1afa565d7e63d53a1d0bd86cdc24edd75470f4de499cfcc"}, + {file = "pycparser-2.22.tar.gz", hash = "sha256:491c8be9c040f5390f5bf44a5b07752bd07f56edf992381b05c701439eec10f6"}, +] + +[[package]] +name = "pygments" +version = "2.18.0" +description = "Pygments is a syntax highlighting package written in Python." +optional = false +python-versions = ">=3.8" +groups = ["benchmark", "doc"] +files = [ + {file = "pygments-2.18.0-py3-none-any.whl", hash = "sha256:b8e6aca0523f3ab76fee51799c488e38782ac06eafcf95e7ba832985c8e7b13a"}, + {file = "pygments-2.18.0.tar.gz", hash = "sha256:786ff802f32e91311bff3889f6e9a86e81505fe99f2735bb6d60ae0c5004f199"}, +] + +[package.extras] +windows-terminal = ["colorama (>=0.4.6)"] + +[[package]] +name = "pymdown-extensions" +version = "10.12" +description = "Extension pack for Python Markdown." +optional = false +python-versions = ">=3.8" +groups = ["doc"] +files = [ + {file = "pymdown_extensions-10.12-py3-none-any.whl", hash = "sha256:49f81412242d3527b8b4967b990df395c89563043bc51a3d2d7d500e52123b77"}, + {file = "pymdown_extensions-10.12.tar.gz", hash = "sha256:b0ee1e0b2bef1071a47891ab17003bfe5bf824a398e13f49f8ed653b699369a7"}, +] + +[package.dependencies] +markdown = ">=3.6" +pyyaml = "*" + +[package.extras] +extra = ["pygments (>=2.12)"] + +[[package]] +name = "pyproject-api" +version = "1.8.0" +description = "API to interact with the python pyproject.toml based projects" +optional = false +python-versions = ">=3.8" +groups = ["dev"] +files = [ + {file = "pyproject_api-1.8.0-py3-none-any.whl", hash = "sha256:3d7d347a047afe796fd5d1885b1e391ba29be7169bd2f102fcd378f04273d228"}, + {file = "pyproject_api-1.8.0.tar.gz", hash = "sha256:77b8049f2feb5d33eefcc21b57f1e279636277a8ac8ad6b5871037b243778496"}, +] + +[package.dependencies] +packaging = ">=24.1" +tomli = {version = ">=2.0.1", markers = "python_version < \"3.11\""} + +[package.extras] +docs = ["furo (>=2024.8.6)", "sphinx-autodoc-typehints (>=2.4.1)"] +testing = ["covdefaults (>=2.3)", "pytest (>=8.3.3)", "pytest-cov (>=5)", "pytest-mock (>=3.14)", "setuptools (>=75.1)"] + +[[package]] +name = "pytest" +version = "7.4.4" +description = "pytest: simple powerful testing with Python" +optional = false +python-versions = ">=3.7" +groups = ["benchmark", "test"] +files = [ + {file = "pytest-7.4.4-py3-none-any.whl", hash = "sha256:b090cdf5ed60bf4c45261be03239c2c1c22df034fbffe691abe93cd80cea01d8"}, + {file = "pytest-7.4.4.tar.gz", hash = "sha256:2cf0005922c6ace4a3e2ec8b4080eb0d9753fdc93107415332f50ce9e7994280"}, +] + +[package.dependencies] +colorama = {version = "*", markers = "sys_platform == \"win32\""} +exceptiongroup = {version = ">=1.0.0rc8", markers = "python_version < \"3.11\""} +iniconfig = "*" +packaging = "*" +pluggy = ">=0.12,<2.0" +tomli = {version = ">=1.0.0", markers = "python_version < \"3.11\""} + +[package.extras] +testing = ["argcomplete", "attrs (>=19.2.0)", "hypothesis (>=3.56)", "mock", "nose", "pygments (>=2.7.2)", "requests", "setuptools", "xmlschema"] + +[[package]] +name = "pytest-benchmark" +version = "4.0.0" +description = "A ``pytest`` fixture for benchmarking code. It will group the tests into rounds that are calibrated to the chosen timer." +optional = false +python-versions = ">=3.7" +groups = ["test"] +files = [ + {file = "pytest-benchmark-4.0.0.tar.gz", hash = "sha256:fb0785b83efe599a6a956361c0691ae1dbb5318018561af10f3e915caa0048d1"}, + {file = "pytest_benchmark-4.0.0-py3-none-any.whl", hash = "sha256:fdb7db64e31c8b277dff9850d2a2556d8b60bcb0ea6524e36e28ffd7c87f71d6"}, +] + +[package.dependencies] +py-cpuinfo = "*" +pytest = ">=3.8" + +[package.extras] +aspect = ["aspectlib"] +elasticsearch = ["elasticsearch"] +histogram = ["pygal", "pygaljs"] + +[[package]] +name = "pytest-codspeed" +version = "3.2.0" +description = "Pytest plugin to create CodSpeed benchmarks" +optional = false +python-versions = ">=3.9" +groups = ["benchmark"] +files = [ + {file = "pytest_codspeed-3.2.0-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:c5165774424c7ab8db7e7acdb539763a0e5657996effefdf0664d7fd95158d34"}, + {file = "pytest_codspeed-3.2.0-cp310-cp310-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:9bd55f92d772592c04a55209950c50880413ae46876e66bd349ef157075ca26c"}, + {file = "pytest_codspeed-3.2.0-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:4cf6f56067538f4892baa8d7ab5ef4e45bb59033be1ef18759a2c7fc55b32035"}, + {file = "pytest_codspeed-3.2.0-cp311-cp311-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:39a687b05c3d145642061b45ea78e47e12f13ce510104d1a2cda00eee0e36f58"}, + {file = "pytest_codspeed-3.2.0-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:46a1afaaa1ac4c2ca5b0700d31ac46d80a27612961d031067d73c6ccbd8d3c2b"}, + {file = "pytest_codspeed-3.2.0-cp312-cp312-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:c48ce3af3dfa78413ed3d69d1924043aa1519048dbff46edccf8f35a25dab3c2"}, + {file = "pytest_codspeed-3.2.0-cp313-cp313-manylinux_2_17_aarch64.manylinux2014_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:66692506d33453df48b36a84703448cb8b22953eea51f03fbb2eb758dc2bdc4f"}, + {file = "pytest_codspeed-3.2.0-cp313-cp313-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:479774f80d0bdfafa16112700df4dbd31bf2a6757fac74795fd79c0a7b3c389b"}, + {file = "pytest_codspeed-3.2.0-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:109f9f4dd1088019c3b3f887d003b7d65f98a7736ca1d457884f5aa293e8e81c"}, + {file = "pytest_codspeed-3.2.0-cp39-cp39-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:e2f69a03b52c9bb041aec1b8ee54b7b6c37a6d0a948786effa4c71157765b6da"}, + {file = "pytest_codspeed-3.2.0-py3-none-any.whl", hash = "sha256:54b5c2e986d6a28e7b0af11d610ea57bd5531cec8326abe486f1b55b09d91c39"}, + {file = "pytest_codspeed-3.2.0.tar.gz", hash = "sha256:f9d1b1a3b2c69cdc0490a1e8b1ced44bffbd0e8e21d81a7160cfdd923f6e8155"}, +] + +[package.dependencies] +cffi = ">=1.17.1" +importlib-metadata = {version = ">=8.5.0", markers = "python_version < \"3.10\""} +pytest = ">=3.8" +rich = ">=13.8.1" + +[package.extras] +compat = ["pytest-benchmark (>=5.0.0,<5.1.0)", "pytest-xdist (>=3.6.1,<3.7.0)"] +lint = ["mypy (>=1.11.2,<1.12.0)", "ruff (>=0.6.5,<0.7.0)"] +test = ["pytest (>=7.0,<8.0)", "pytest-cov (>=4.0.0,<4.1.0)"] + +[[package]] +name = "python-dateutil" +version = "2.9.0.post0" +description = "Extensions to the standard Python datetime module" +optional = false +python-versions = "!=3.0.*,!=3.1.*,!=3.2.*,>=2.7" +groups = ["main", "doc", "test"] +files = [ + {file = "python-dateutil-2.9.0.post0.tar.gz", hash = "sha256:37dd54208da7e1cd875388217d5e00ebd4179249f90fb72437e91a35459a0ad3"}, + {file = "python_dateutil-2.9.0.post0-py2.py3-none-any.whl", hash = "sha256:a8b2bc7bffae282281c8140a97d3aa9c14da0b136dfe83f850eea9a5f7470427"}, +] + +[package.dependencies] +six = ">=1.5" + +[[package]] +name = "pyyaml" +version = "6.0.2" +description = "YAML parser and emitter for Python" +optional = false +python-versions = ">=3.8" +groups = ["doc", "lint"] +files = [ + {file = "PyYAML-6.0.2-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:0a9a2848a5b7feac301353437eb7d5957887edbf81d56e903999a75a3d743086"}, + {file = "PyYAML-6.0.2-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:29717114e51c84ddfba879543fb232a6ed60086602313ca38cce623c1d62cfbf"}, + {file = "PyYAML-6.0.2-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:8824b5a04a04a047e72eea5cec3bc266db09e35de6bdfe34c9436ac5ee27d237"}, + {file = "PyYAML-6.0.2-cp310-cp310-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:7c36280e6fb8385e520936c3cb3b8042851904eba0e58d277dca80a5cfed590b"}, + {file = "PyYAML-6.0.2-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:ec031d5d2feb36d1d1a24380e4db6d43695f3748343d99434e6f5f9156aaa2ed"}, + {file = "PyYAML-6.0.2-cp310-cp310-musllinux_1_1_aarch64.whl", hash = "sha256:936d68689298c36b53b29f23c6dbb74de12b4ac12ca6cfe0e047bedceea56180"}, + {file = "PyYAML-6.0.2-cp310-cp310-musllinux_1_1_x86_64.whl", hash = "sha256:23502f431948090f597378482b4812b0caae32c22213aecf3b55325e049a6c68"}, + {file = "PyYAML-6.0.2-cp310-cp310-win32.whl", hash = "sha256:2e99c6826ffa974fe6e27cdb5ed0021786b03fc98e5ee3c5bfe1fd5015f42b99"}, + {file = "PyYAML-6.0.2-cp310-cp310-win_amd64.whl", hash = "sha256:a4d3091415f010369ae4ed1fc6b79def9416358877534caf6a0fdd2146c87a3e"}, + {file = "PyYAML-6.0.2-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:cc1c1159b3d456576af7a3e4d1ba7e6924cb39de8f67111c735f6fc832082774"}, + {file = "PyYAML-6.0.2-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:1e2120ef853f59c7419231f3bf4e7021f1b936f6ebd222406c3b60212205d2ee"}, + {file = "PyYAML-6.0.2-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:5d225db5a45f21e78dd9358e58a98702a0302f2659a3c6cd320564b75b86f47c"}, + {file = "PyYAML-6.0.2-cp311-cp311-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:5ac9328ec4831237bec75defaf839f7d4564be1e6b25ac710bd1a96321cc8317"}, + {file = "PyYAML-6.0.2-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:3ad2a3decf9aaba3d29c8f537ac4b243e36bef957511b4766cb0057d32b0be85"}, + {file = "PyYAML-6.0.2-cp311-cp311-musllinux_1_1_aarch64.whl", hash = "sha256:ff3824dc5261f50c9b0dfb3be22b4567a6f938ccce4587b38952d85fd9e9afe4"}, + {file = "PyYAML-6.0.2-cp311-cp311-musllinux_1_1_x86_64.whl", hash = "sha256:797b4f722ffa07cc8d62053e4cff1486fa6dc094105d13fea7b1de7d8bf71c9e"}, + {file = "PyYAML-6.0.2-cp311-cp311-win32.whl", hash = "sha256:11d8f3dd2b9c1207dcaf2ee0bbbfd5991f571186ec9cc78427ba5bd32afae4b5"}, + {file = "PyYAML-6.0.2-cp311-cp311-win_amd64.whl", hash = "sha256:e10ce637b18caea04431ce14fabcf5c64a1c61ec9c56b071a4b7ca131ca52d44"}, + {file = "PyYAML-6.0.2-cp312-cp312-macosx_10_9_x86_64.whl", hash = "sha256:c70c95198c015b85feafc136515252a261a84561b7b1d51e3384e0655ddf25ab"}, + {file = "PyYAML-6.0.2-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:ce826d6ef20b1bc864f0a68340c8b3287705cae2f8b4b1d932177dcc76721725"}, + {file = "PyYAML-6.0.2-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:1f71ea527786de97d1a0cc0eacd1defc0985dcf6b3f17bb77dcfc8c34bec4dc5"}, + {file = "PyYAML-6.0.2-cp312-cp312-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:9b22676e8097e9e22e36d6b7bda33190d0d400f345f23d4065d48f4ca7ae0425"}, + {file = "PyYAML-6.0.2-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:80bab7bfc629882493af4aa31a4cfa43a4c57c83813253626916b8c7ada83476"}, + {file = "PyYAML-6.0.2-cp312-cp312-musllinux_1_1_aarch64.whl", hash = "sha256:0833f8694549e586547b576dcfaba4a6b55b9e96098b36cdc7ebefe667dfed48"}, + {file = "PyYAML-6.0.2-cp312-cp312-musllinux_1_1_x86_64.whl", hash = "sha256:8b9c7197f7cb2738065c481a0461e50ad02f18c78cd75775628afb4d7137fb3b"}, + {file = "PyYAML-6.0.2-cp312-cp312-win32.whl", hash = "sha256:ef6107725bd54b262d6dedcc2af448a266975032bc85ef0172c5f059da6325b4"}, + {file = "PyYAML-6.0.2-cp312-cp312-win_amd64.whl", hash = "sha256:7e7401d0de89a9a855c839bc697c079a4af81cf878373abd7dc625847d25cbd8"}, + {file = "PyYAML-6.0.2-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:efdca5630322a10774e8e98e1af481aad470dd62c3170801852d752aa7a783ba"}, + {file = "PyYAML-6.0.2-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:50187695423ffe49e2deacb8cd10510bc361faac997de9efef88badc3bb9e2d1"}, + {file = "PyYAML-6.0.2-cp313-cp313-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:0ffe8360bab4910ef1b9e87fb812d8bc0a308b0d0eef8c8f44e0254ab3b07133"}, + {file = "PyYAML-6.0.2-cp313-cp313-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:17e311b6c678207928d649faa7cb0d7b4c26a0ba73d41e99c4fff6b6c3276484"}, + {file = "PyYAML-6.0.2-cp313-cp313-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:70b189594dbe54f75ab3a1acec5f1e3faa7e8cf2f1e08d9b561cb41b845f69d5"}, + {file = "PyYAML-6.0.2-cp313-cp313-musllinux_1_1_aarch64.whl", hash = "sha256:41e4e3953a79407c794916fa277a82531dd93aad34e29c2a514c2c0c5fe971cc"}, + {file = "PyYAML-6.0.2-cp313-cp313-musllinux_1_1_x86_64.whl", hash = "sha256:68ccc6023a3400877818152ad9a1033e3db8625d899c72eacb5a668902e4d652"}, + {file = "PyYAML-6.0.2-cp313-cp313-win32.whl", hash = "sha256:bc2fa7c6b47d6bc618dd7fb02ef6fdedb1090ec036abab80d4681424b84c1183"}, + {file = "PyYAML-6.0.2-cp313-cp313-win_amd64.whl", hash = "sha256:8388ee1976c416731879ac16da0aff3f63b286ffdd57cdeb95f3f2e085687563"}, + {file = "PyYAML-6.0.2-cp38-cp38-macosx_10_9_x86_64.whl", hash = "sha256:24471b829b3bf607e04e88d79542a9d48bb037c2267d7927a874e6c205ca7e9a"}, + {file = "PyYAML-6.0.2-cp38-cp38-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:d7fded462629cfa4b685c5416b949ebad6cec74af5e2d42905d41e257e0869f5"}, + {file = "PyYAML-6.0.2-cp38-cp38-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:d84a1718ee396f54f3a086ea0a66d8e552b2ab2017ef8b420e92edbc841c352d"}, + {file = "PyYAML-6.0.2-cp38-cp38-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:9056c1ecd25795207ad294bcf39f2db3d845767be0ea6e6a34d856f006006083"}, + {file = "PyYAML-6.0.2-cp38-cp38-musllinux_1_1_x86_64.whl", hash = "sha256:82d09873e40955485746739bcb8b4586983670466c23382c19cffecbf1fd8706"}, + {file = "PyYAML-6.0.2-cp38-cp38-win32.whl", hash = "sha256:43fa96a3ca0d6b1812e01ced1044a003533c47f6ee8aca31724f78e93ccc089a"}, + {file = "PyYAML-6.0.2-cp38-cp38-win_amd64.whl", hash = "sha256:01179a4a8559ab5de078078f37e5c1a30d76bb88519906844fd7bdea1b7729ff"}, + {file = "PyYAML-6.0.2-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:688ba32a1cffef67fd2e9398a2efebaea461578b0923624778664cc1c914db5d"}, + {file = "PyYAML-6.0.2-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:a8786accb172bd8afb8be14490a16625cbc387036876ab6ba70912730faf8e1f"}, + {file = "PyYAML-6.0.2-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:d8e03406cac8513435335dbab54c0d385e4a49e4945d2909a581c83647ca0290"}, + {file = "PyYAML-6.0.2-cp39-cp39-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:f753120cb8181e736c57ef7636e83f31b9c0d1722c516f7e86cf15b7aa57ff12"}, + {file = "PyYAML-6.0.2-cp39-cp39-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:3b1fdb9dc17f5a7677423d508ab4f243a726dea51fa5e70992e59a7411c89d19"}, + {file = "PyYAML-6.0.2-cp39-cp39-musllinux_1_1_aarch64.whl", hash = "sha256:0b69e4ce7a131fe56b7e4d770c67429700908fc0752af059838b1cfb41960e4e"}, + {file = "PyYAML-6.0.2-cp39-cp39-musllinux_1_1_x86_64.whl", hash = "sha256:a9f8c2e67970f13b16084e04f134610fd1d374bf477b17ec1599185cf611d725"}, + {file = "PyYAML-6.0.2-cp39-cp39-win32.whl", hash = "sha256:6395c297d42274772abc367baaa79683958044e5d3835486c16da75d2a694631"}, + {file = "PyYAML-6.0.2-cp39-cp39-win_amd64.whl", hash = "sha256:39693e1f8320ae4f43943590b49779ffb98acb81f788220ea932a6b6c51004d8"}, + {file = "pyyaml-6.0.2.tar.gz", hash = "sha256:d584d9ec91ad65861cc08d42e834324ef890a082e591037abe114850ff7bbc3e"}, +] + +[[package]] +name = "pyyaml-env-tag" +version = "0.1" +description = "A custom YAML tag for referencing environment variables in YAML files. " +optional = false +python-versions = ">=3.6" +groups = ["doc"] +files = [ + {file = "pyyaml_env_tag-0.1-py3-none-any.whl", hash = "sha256:af31106dec8a4d68c60207c1886031cbf839b68aa7abccdb19868200532c2069"}, + {file = "pyyaml_env_tag-0.1.tar.gz", hash = "sha256:70092675bda14fdec33b31ba77e7543de9ddc88f2e5b99160396572d11525bdb"}, +] + +[package.dependencies] +pyyaml = "*" + +[[package]] +name = "rapidfuzz" +version = "3.10.1" +description = "rapid fuzzy string matching" +optional = false +python-versions = ">=3.9" +groups = ["dev"] +markers = "python_version < \"4.0\"" +files = [ + {file = "rapidfuzz-3.10.1-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:f17d9f21bf2f2f785d74f7b0d407805468b4c173fa3e52c86ec94436b338e74a"}, + {file = "rapidfuzz-3.10.1-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:b31f358a70efc143909fb3d75ac6cd3c139cd41339aa8f2a3a0ead8315731f2b"}, + {file = "rapidfuzz-3.10.1-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:7f4f43f2204b56a61448ec2dd061e26fd344c404da99fb19f3458200c5874ba2"}, + {file = "rapidfuzz-3.10.1-cp310-cp310-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:9d81bf186a453a2757472133b24915768abc7c3964194406ed93e170e16c21cb"}, + {file = "rapidfuzz-3.10.1-cp310-cp310-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:3611c8f45379a12063d70075c75134f2a8bd2e4e9b8a7995112ddae95ca1c982"}, + {file = "rapidfuzz-3.10.1-cp310-cp310-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:3c3b537b97ac30da4b73930fa8a4fe2f79c6d1c10ad535c5c09726612cd6bed9"}, + {file = "rapidfuzz-3.10.1-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:231ef1ec9cf7b59809ce3301006500b9d564ddb324635f4ea8f16b3e2a1780da"}, + {file = "rapidfuzz-3.10.1-cp310-cp310-musllinux_1_2_aarch64.whl", hash = "sha256:ed4f3adc1294834955b7e74edd3c6bd1aad5831c007f2d91ea839e76461a5879"}, + {file = "rapidfuzz-3.10.1-cp310-cp310-musllinux_1_2_i686.whl", hash = "sha256:7b6015da2e707bf632a71772a2dbf0703cff6525732c005ad24987fe86e8ec32"}, + {file = "rapidfuzz-3.10.1-cp310-cp310-musllinux_1_2_ppc64le.whl", hash = "sha256:1b35a118d61d6f008e8e3fb3a77674d10806a8972c7b8be433d6598df4d60b01"}, + {file = "rapidfuzz-3.10.1-cp310-cp310-musllinux_1_2_s390x.whl", hash = "sha256:bc308d79a7e877226f36bdf4e149e3ed398d8277c140be5c1fd892ec41739e6d"}, + {file = "rapidfuzz-3.10.1-cp310-cp310-musllinux_1_2_x86_64.whl", hash = "sha256:f017dbfecc172e2d0c37cf9e3d519179d71a7f16094b57430dffc496a098aa17"}, + {file = "rapidfuzz-3.10.1-cp310-cp310-win32.whl", hash = "sha256:36c0e1483e21f918d0f2f26799fe5ac91c7b0c34220b73007301c4f831a9c4c7"}, + {file = "rapidfuzz-3.10.1-cp310-cp310-win_amd64.whl", hash = "sha256:10746c1d4c8cd8881c28a87fd7ba0c9c102346dfe7ff1b0d021cdf093e9adbff"}, + {file = "rapidfuzz-3.10.1-cp310-cp310-win_arm64.whl", hash = "sha256:dfa64b89dcb906835e275187569e51aa9d546a444489e97aaf2cc84011565fbe"}, + {file = "rapidfuzz-3.10.1-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:92958ae075c87fef393f835ed02d4fe8d5ee2059a0934c6c447ea3417dfbf0e8"}, + {file = "rapidfuzz-3.10.1-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:ba7521e072c53e33c384e78615d0718e645cab3c366ecd3cc8cb732befd94967"}, + {file = "rapidfuzz-3.10.1-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:00d02cbd75d283c287471b5b3738b3e05c9096150f93f2d2dfa10b3d700f2db9"}, + {file = "rapidfuzz-3.10.1-cp311-cp311-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:efa1582a397da038e2f2576c9cd49b842f56fde37d84a6b0200ffebc08d82350"}, + {file = "rapidfuzz-3.10.1-cp311-cp311-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:f12912acee1f506f974f58de9fdc2e62eea5667377a7e9156de53241c05fdba8"}, + {file = "rapidfuzz-3.10.1-cp311-cp311-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:666d5d8b17becc3f53447bcb2b6b33ce6c2df78792495d1fa82b2924cd48701a"}, + {file = "rapidfuzz-3.10.1-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:26f71582c0d62445067ee338ddad99b655a8f4e4ed517a90dcbfbb7d19310474"}, + {file = "rapidfuzz-3.10.1-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:8a2ef08b27167bcff230ffbfeedd4c4fa6353563d6aaa015d725dd3632fc3de7"}, + {file = "rapidfuzz-3.10.1-cp311-cp311-musllinux_1_2_i686.whl", hash = "sha256:365e4fc1a2b95082c890f5e98489b894e6bf8c338c6ac89bb6523c2ca6e9f086"}, + {file = "rapidfuzz-3.10.1-cp311-cp311-musllinux_1_2_ppc64le.whl", hash = "sha256:1996feb7a61609fa842e6b5e0c549983222ffdedaf29644cc67e479902846dfe"}, + {file = "rapidfuzz-3.10.1-cp311-cp311-musllinux_1_2_s390x.whl", hash = "sha256:cf654702f144beaa093103841a2ea6910d617d0bb3fccb1d1fd63c54dde2cd49"}, + {file = "rapidfuzz-3.10.1-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:ec108bf25de674781d0a9a935030ba090c78d49def3d60f8724f3fc1e8e75024"}, + {file = "rapidfuzz-3.10.1-cp311-cp311-win32.whl", hash = "sha256:031f8b367e5d92f7a1e27f7322012f3c321c3110137b43cc3bf678505583ef48"}, + {file = "rapidfuzz-3.10.1-cp311-cp311-win_amd64.whl", hash = "sha256:f98f36c6a1bb9a6c8bbec99ad87c8c0e364f34761739b5ea9adf7b48129ae8cf"}, + {file = "rapidfuzz-3.10.1-cp311-cp311-win_arm64.whl", hash = "sha256:f1da2028cb4e41be55ee797a82d6c1cf589442504244249dfeb32efc608edee7"}, + {file = "rapidfuzz-3.10.1-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:1340b56340896bede246f612b6ecf685f661a56aabef3d2512481bfe23ac5835"}, + {file = "rapidfuzz-3.10.1-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:2316515169b7b5a453f0ce3adbc46c42aa332cae9f2edb668e24d1fc92b2f2bb"}, + {file = "rapidfuzz-3.10.1-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:8e06fe6a12241ec1b72c0566c6b28cda714d61965d86569595ad24793d1ab259"}, + {file = "rapidfuzz-3.10.1-cp312-cp312-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:d99c1cd9443b19164ec185a7d752f4b4db19c066c136f028991a480720472e23"}, + {file = "rapidfuzz-3.10.1-cp312-cp312-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:a1d9aa156ed52d3446388ba4c2f335e312191d1ca9d1f5762ee983cf23e4ecf6"}, + {file = "rapidfuzz-3.10.1-cp312-cp312-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:54bcf4efaaee8e015822be0c2c28214815f4f6b4f70d8362cfecbd58a71188ac"}, + {file = "rapidfuzz-3.10.1-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:c0c955e32afdbfdf6e9ee663d24afb25210152d98c26d22d399712d29a9b976b"}, + {file = "rapidfuzz-3.10.1-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:191633722203f5b7717efcb73a14f76f3b124877d0608c070b827c5226d0b972"}, + {file = "rapidfuzz-3.10.1-cp312-cp312-musllinux_1_2_i686.whl", hash = "sha256:195baad28057ec9609e40385991004e470af9ef87401e24ebe72c064431524ab"}, + {file = "rapidfuzz-3.10.1-cp312-cp312-musllinux_1_2_ppc64le.whl", hash = "sha256:0fff4a6b87c07366662b62ae994ffbeadc472e72f725923f94b72a3db49f4671"}, + {file = "rapidfuzz-3.10.1-cp312-cp312-musllinux_1_2_s390x.whl", hash = "sha256:4ffed25f9fdc0b287f30a98467493d1e1ce5b583f6317f70ec0263b3c97dbba6"}, + {file = "rapidfuzz-3.10.1-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:d02cf8e5af89a9ac8f53c438ddff6d773f62c25c6619b29db96f4aae248177c0"}, + {file = "rapidfuzz-3.10.1-cp312-cp312-win32.whl", hash = "sha256:f3bb81d4fe6a5d20650f8c0afcc8f6e1941f6fecdb434f11b874c42467baded0"}, + {file = "rapidfuzz-3.10.1-cp312-cp312-win_amd64.whl", hash = "sha256:aaf83e9170cb1338922ae42d320699dccbbdca8ffed07faeb0b9257822c26e24"}, + {file = "rapidfuzz-3.10.1-cp312-cp312-win_arm64.whl", hash = "sha256:c5da802a0d085ad81b0f62828fb55557996c497b2d0b551bbdfeafd6d447892f"}, + {file = "rapidfuzz-3.10.1-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:fc22d69a1c9cccd560a5c434c0371b2df0f47c309c635a01a913e03bbf183710"}, + {file = "rapidfuzz-3.10.1-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:38b0dac2c8e057562b8f0d8ae5b663d2d6a28c5ab624de5b73cef9abb6129a24"}, + {file = "rapidfuzz-3.10.1-cp313-cp313-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:6fde3bbb14e92ce8fcb5c2edfff72e474d0080cadda1c97785bf4822f037a309"}, + {file = "rapidfuzz-3.10.1-cp313-cp313-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:9141fb0592e55f98fe9ac0f3ce883199b9c13e262e0bf40c5b18cdf926109d16"}, + {file = "rapidfuzz-3.10.1-cp313-cp313-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:237bec5dd1bfc9b40bbd786cd27949ef0c0eb5fab5eb491904c6b5df59d39d3c"}, + {file = "rapidfuzz-3.10.1-cp313-cp313-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:18123168cba156ab5794ea6de66db50f21bb3c66ae748d03316e71b27d907b95"}, + {file = "rapidfuzz-3.10.1-cp313-cp313-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:0b75fe506c8e02769cc47f5ab21ce3e09b6211d3edaa8f8f27331cb6988779be"}, + {file = "rapidfuzz-3.10.1-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:9da82aa4b46973aaf9e03bb4c3d6977004648c8638febfc0f9d237e865761270"}, + {file = "rapidfuzz-3.10.1-cp313-cp313-musllinux_1_2_i686.whl", hash = "sha256:c34c022d5ad564f1a5a57a4a89793bd70d7bad428150fb8ff2760b223407cdcf"}, + {file = "rapidfuzz-3.10.1-cp313-cp313-musllinux_1_2_ppc64le.whl", hash = "sha256:1e96c84d6c2a0ca94e15acb5399118fff669f4306beb98a6d8ec6f5dccab4412"}, + {file = "rapidfuzz-3.10.1-cp313-cp313-musllinux_1_2_s390x.whl", hash = "sha256:e8e154b84a311263e1aca86818c962e1fa9eefdd643d1d5d197fcd2738f88cb9"}, + {file = "rapidfuzz-3.10.1-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:335fee93188f8cd585552bb8057228ce0111bd227fa81bfd40b7df6b75def8ab"}, + {file = "rapidfuzz-3.10.1-cp313-cp313-win32.whl", hash = "sha256:6729b856166a9e95c278410f73683957ea6100c8a9d0a8dbe434c49663689255"}, + {file = "rapidfuzz-3.10.1-cp313-cp313-win_amd64.whl", hash = "sha256:0e06d99ad1ad97cb2ef7f51ec6b1fedd74a3a700e4949353871cf331d07b382a"}, + {file = "rapidfuzz-3.10.1-cp313-cp313-win_arm64.whl", hash = "sha256:8d1b7082104d596a3eb012e0549b2634ed15015b569f48879701e9d8db959dbb"}, + {file = "rapidfuzz-3.10.1-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:779027d3307e1a2b1dc0c03c34df87a470a368a1a0840a9d2908baf2d4067956"}, + {file = "rapidfuzz-3.10.1-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:440b5608ab12650d0390128d6858bc839ae77ffe5edf0b33a1551f2fa9860651"}, + {file = "rapidfuzz-3.10.1-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:82cac41a411e07a6f3dc80dfbd33f6be70ea0abd72e99c59310819d09f07d945"}, + {file = "rapidfuzz-3.10.1-cp39-cp39-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:958473c9f0bca250590200fd520b75be0dbdbc4a7327dc87a55b6d7dc8d68552"}, + {file = "rapidfuzz-3.10.1-cp39-cp39-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:9ef60dfa73749ef91cb6073be1a3e135f4846ec809cc115f3cbfc6fe283a5584"}, + {file = "rapidfuzz-3.10.1-cp39-cp39-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:a7fbac18f2c19fc983838a60611e67e3262e36859994c26f2ee85bb268de2355"}, + {file = "rapidfuzz-3.10.1-cp39-cp39-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:9a0d519ff39db887cd73f4e297922786d548f5c05d6b51f4e6754f452a7f4296"}, + {file = "rapidfuzz-3.10.1-cp39-cp39-musllinux_1_2_aarch64.whl", hash = "sha256:bebb7bc6aeb91cc57e4881b222484c26759ca865794187217c9dcea6c33adae6"}, + {file = "rapidfuzz-3.10.1-cp39-cp39-musllinux_1_2_i686.whl", hash = "sha256:fe07f8b9c3bb5c5ad1d2c66884253e03800f4189a60eb6acd6119ebaf3eb9894"}, + {file = "rapidfuzz-3.10.1-cp39-cp39-musllinux_1_2_ppc64le.whl", hash = "sha256:bfa48a4a2d45a41457f0840c48e579db157a927f4e97acf6e20df8fc521c79de"}, + {file = "rapidfuzz-3.10.1-cp39-cp39-musllinux_1_2_s390x.whl", hash = "sha256:2cf44d01bfe8ee605b7eaeecbc2b9ca64fc55765f17b304b40ed8995f69d7716"}, + {file = "rapidfuzz-3.10.1-cp39-cp39-musllinux_1_2_x86_64.whl", hash = "sha256:1e6bbca9246d9eedaa1c84e04a7f555493ba324d52ae4d9f3d9ddd1b740dcd87"}, + {file = "rapidfuzz-3.10.1-cp39-cp39-win32.whl", hash = "sha256:567f88180f2c1423b4fe3f3ad6e6310fc97b85bdba574801548597287fc07028"}, + {file = "rapidfuzz-3.10.1-cp39-cp39-win_amd64.whl", hash = "sha256:6b2cd7c29d6ecdf0b780deb587198f13213ac01c430ada6913452fd0c40190fc"}, + {file = "rapidfuzz-3.10.1-cp39-cp39-win_arm64.whl", hash = "sha256:9f912d459e46607ce276128f52bea21ebc3e9a5ccf4cccfef30dd5bddcf47be8"}, + {file = "rapidfuzz-3.10.1-pp310-pypy310_pp73-macosx_10_15_x86_64.whl", hash = "sha256:ac4452f182243cfab30ba4668ef2de101effaedc30f9faabb06a095a8c90fd16"}, + {file = "rapidfuzz-3.10.1-pp310-pypy310_pp73-macosx_11_0_arm64.whl", hash = "sha256:565c2bd4f7d23c32834652b27b51dd711814ab614b4e12add8476be4e20d1cf5"}, + {file = "rapidfuzz-3.10.1-pp310-pypy310_pp73-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:187d9747149321607be4ccd6f9f366730078bed806178ec3eeb31d05545e9e8f"}, + {file = "rapidfuzz-3.10.1-pp310-pypy310_pp73-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:616290fb9a8fa87e48cb0326d26f98d4e29f17c3b762c2d586f2b35c1fd2034b"}, + {file = "rapidfuzz-3.10.1-pp310-pypy310_pp73-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:073a5b107e17ebd264198b78614c0206fa438cce749692af5bc5f8f484883f50"}, + {file = "rapidfuzz-3.10.1-pp310-pypy310_pp73-win_amd64.whl", hash = "sha256:39c4983e2e2ccb9732f3ac7d81617088822f4a12291d416b09b8a1eadebb3e29"}, + {file = "rapidfuzz-3.10.1-pp39-pypy39_pp73-macosx_10_15_x86_64.whl", hash = "sha256:ac7adee6bcf0c6fee495d877edad1540a7e0f5fc208da03ccb64734b43522d7a"}, + {file = "rapidfuzz-3.10.1-pp39-pypy39_pp73-macosx_11_0_arm64.whl", hash = "sha256:425f4ac80b22153d391ee3f94bc854668a0c6c129f05cf2eaf5ee74474ddb69e"}, + {file = "rapidfuzz-3.10.1-pp39-pypy39_pp73-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:65a2fa13e8a219f9b5dcb9e74abe3ced5838a7327e629f426d333dfc8c5a6e66"}, + {file = "rapidfuzz-3.10.1-pp39-pypy39_pp73-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:75561f3df9a906aaa23787e9992b228b1ab69007932dc42070f747103e177ba8"}, + {file = "rapidfuzz-3.10.1-pp39-pypy39_pp73-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:edd062490537e97ca125bc6c7f2b7331c2b73d21dc304615afe61ad1691e15d5"}, + {file = "rapidfuzz-3.10.1-pp39-pypy39_pp73-win_amd64.whl", hash = "sha256:cfcc8feccf63245a22dfdd16e222f1a39771a44b870beb748117a0e09cbb4a62"}, + {file = "rapidfuzz-3.10.1.tar.gz", hash = "sha256:5a15546d847a915b3f42dc79ef9b0c78b998b4e2c53b252e7166284066585979"}, +] + +[package.extras] +all = ["numpy"] + +[[package]] +name = "rich" +version = "13.9.4" +description = "Render rich text, tables, progress bars, syntax highlighting, markdown and more to the terminal" +optional = false +python-versions = ">=3.8.0" +groups = ["benchmark"] +files = [ + {file = "rich-13.9.4-py3-none-any.whl", hash = "sha256:6049d5e6ec054bf2779ab3358186963bac2ea89175919d699e378b99738c2a90"}, + {file = "rich-13.9.4.tar.gz", hash = "sha256:439594978a49a09530cff7ebc4b5c7103ef57baf48d5ea3184f21d9a2befa098"}, +] + +[package.dependencies] +markdown-it-py = ">=2.2.0" +pygments = ">=2.13.0,<3.0.0" +typing-extensions = {version = ">=4.0.0,<5.0", markers = "python_version < \"3.11\""} + +[package.extras] +jupyter = ["ipywidgets (>=7.5.1,<9)"] + +[[package]] +name = "six" +version = "1.16.0" +description = "Python 2 and 3 compatibility utilities" +optional = false +python-versions = ">=2.7, !=3.0.*, !=3.1.*, !=3.2.*" +groups = ["main", "doc", "test"] +files = [ + {file = "six-1.16.0-py2.py3-none-any.whl", hash = "sha256:8abb2f1d86890a2dfb989f9a77cfcfd3e47c2a354b01111771326f8aa26e0254"}, + {file = "six-1.16.0.tar.gz", hash = "sha256:1e61c37477a1626458e36f7b1d82aa5c9b094fa4802892072e49de9c60c4c926"}, +] + +[[package]] +name = "time-machine" +version = "2.16.0" +description = "Travel through time in your tests." +optional = false +python-versions = ">=3.9" +groups = ["main", "test"] +files = [ + {file = "time_machine-2.16.0-cp310-cp310-macosx_10_9_universal2.whl", hash = "sha256:09531af59fdfb39bfd24d28bd1e837eff5a5d98318509a31b6cfd57d27801e52"}, + {file = "time_machine-2.16.0-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:92d0b0f3c49f34dd76eb462f0afdc61ed1cb318c06c46d03e99b44ebb489bdad"}, + {file = "time_machine-2.16.0-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:7c29616e18e2349a8766d5b6817920fc74e39c00fa375d202231e9d525a1b882"}, + {file = "time_machine-2.16.0-cp310-cp310-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:c1ceb6035a64cb00650e3ab203cf3faffac18576a3f3125c24df468b784077c7"}, + {file = "time_machine-2.16.0-cp310-cp310-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:64c205ea37b8c4ba232645335fc3b75bc2d03ce30f0a34649e36cae85652ee96"}, + {file = "time_machine-2.16.0-cp310-cp310-musllinux_1_2_aarch64.whl", hash = "sha256:dfe92412bd11104c4f0fb2da68653e6c45b41f7217319a83a8b66ed4f20148b3"}, + {file = "time_machine-2.16.0-cp310-cp310-musllinux_1_2_i686.whl", hash = "sha256:d5fe7a6284e3dce87ae13a25029c53542dd27a28d151f3ef362ec4dd9c3e45fd"}, + {file = "time_machine-2.16.0-cp310-cp310-musllinux_1_2_x86_64.whl", hash = "sha256:c0fca3025266d88d1b48be162a43b7c2d91c81cc5b3bee9f01194678ffb9969a"}, + {file = "time_machine-2.16.0-cp310-cp310-win32.whl", hash = "sha256:4149e17018af07a5756a1df84aea71e6e178598c358c860c6bfec42170fa7970"}, + {file = "time_machine-2.16.0-cp310-cp310-win_amd64.whl", hash = "sha256:01bc257e9418980a4922de94775be42a966e1a082fb01a1635917f9afc7b84ca"}, + {file = "time_machine-2.16.0-cp310-cp310-win_arm64.whl", hash = "sha256:6895e3e84119594ab12847c928f619d40ae9cedd0755515dc154a5b5dc6edd9f"}, + {file = "time_machine-2.16.0-cp311-cp311-macosx_10_9_universal2.whl", hash = "sha256:8f936566ef9f09136a3d5db305961ef6d897b76b240c9ff4199144aed6dd4fe5"}, + {file = "time_machine-2.16.0-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:5886e23ede3478ca2a3e0a641f5d09dd784dfa9e48c96e8e5e31fc4fe77b6dc0"}, + {file = "time_machine-2.16.0-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:c76caf539fa4941e1817b7c482c87c65c52a1903fea761e84525955c6106fafb"}, + {file = "time_machine-2.16.0-cp311-cp311-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:298aa423e07c8b21b991782f01d7749c871c792319c2af3e9755f9ab49033212"}, + {file = "time_machine-2.16.0-cp311-cp311-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:e3391ae9c484736850bb44ef125cbad52fe2d1b69e42c95dc88c43af8ead2cc7"}, + {file = "time_machine-2.16.0-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:503e7ff507c2089699d91885fc5b9c8ff16774a7b6aff48b4dcee0c0a0685b61"}, + {file = "time_machine-2.16.0-cp311-cp311-musllinux_1_2_i686.whl", hash = "sha256:eee7b0fc4fbab2c6585ea17606c6548be83919c70deea0865409fe9fc2d8cdce"}, + {file = "time_machine-2.16.0-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:9db5e5b3ccdadaafa5730c2f9db44c38b013234c9ad01f87738907e19bdba268"}, + {file = "time_machine-2.16.0-cp311-cp311-win32.whl", hash = "sha256:2552f0767bc10c9d668f108fef9b487809cdeb772439ce932e74136365c69baf"}, + {file = "time_machine-2.16.0-cp311-cp311-win_amd64.whl", hash = "sha256:12474fcdbc475aa6fe5275fe7224e685c5b9777f5939647f35980e9614ae7558"}, + {file = "time_machine-2.16.0-cp311-cp311-win_arm64.whl", hash = "sha256:ac2df0fa564356384515ed62cb6679f33f1f529435b16b0ec0f88414635dbe39"}, + {file = "time_machine-2.16.0-cp312-cp312-macosx_10_9_universal2.whl", hash = "sha256:84788f4d62a8b1bf5e499bb9b0e23ceceea21c415ad6030be6267ce3d639842f"}, + {file = "time_machine-2.16.0-cp312-cp312-macosx_10_9_x86_64.whl", hash = "sha256:15ec236b6571730236a193d9d6c11d472432fc6ab54e85eac1c16d98ddcd71bf"}, + {file = "time_machine-2.16.0-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:cedc989717c8b44a3881ac3d68ab5a95820448796c550de6a2149ed1525157f0"}, + {file = "time_machine-2.16.0-cp312-cp312-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:9d26d79de1c63a8c6586c75967e09b0ff306aa7e944a1eaddb74595c9b1839ca"}, + {file = "time_machine-2.16.0-cp312-cp312-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:317b68b56a9c3731e0cf8886e0f94230727159e375988b36c60edce0ddbcb44a"}, + {file = "time_machine-2.16.0-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:43e1e18279759897be3293a255d53e6b1cb0364b69d9591d0b80c51e461c94b0"}, + {file = "time_machine-2.16.0-cp312-cp312-musllinux_1_2_i686.whl", hash = "sha256:e43adb22def972a29d2b147999b56897116085777a0fea182fd93ee45730611e"}, + {file = "time_machine-2.16.0-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:0c766bea27a0600e36806d628ebc4b47178b12fcdfb6c24dc0a566a9c06bfe7f"}, + {file = "time_machine-2.16.0-cp312-cp312-win32.whl", hash = "sha256:6dae82ab647d107817e013db82223e20a9853fa88543fec853ae326382d03c2e"}, + {file = "time_machine-2.16.0-cp312-cp312-win_amd64.whl", hash = "sha256:265462c77dc9576267c3c7f20707780a171a9fdbac93ac22e608c309efd68c33"}, + {file = "time_machine-2.16.0-cp312-cp312-win_arm64.whl", hash = "sha256:ef768e14768eebe3bb1196c0dece8e14c1c6991605721214a0c3c68cf77eb216"}, + {file = "time_machine-2.16.0-cp313-cp313-macosx_10_13_universal2.whl", hash = "sha256:7751bf745d54e9e8b358c0afa332815da9b8a6194b26d0fd62876ab6c4d5c9c0"}, + {file = "time_machine-2.16.0-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:1784edf173ca840ba154de6eed000b5727f65ab92972c2f88cec5c4d6349c5f2"}, + {file = "time_machine-2.16.0-cp313-cp313-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:2f5876a5682ce1f517e55d7ace2383432627889f6f7e338b961f99d684fd9e8d"}, + {file = "time_machine-2.16.0-cp313-cp313-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:806672529a2e255cd901f244c9033767dc1fa53466d0d3e3e49565a1572a64fe"}, + {file = "time_machine-2.16.0-cp313-cp313-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:667b150fedb54acdca2a4bea5bf6da837b43e6dd12857301b48191f8803ba93f"}, + {file = "time_machine-2.16.0-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:da3ae1028af240c0c46c79adf9c1acffecc6ed1701f2863b8132f5ceae6ae4b5"}, + {file = "time_machine-2.16.0-cp313-cp313-musllinux_1_2_i686.whl", hash = "sha256:520a814ea1b2706c89ab260a54023033d3015abef25c77873b83e3d7c1fafbb2"}, + {file = "time_machine-2.16.0-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:8243664438bb468408b29c6865958662d75e51f79c91842d2794fa22629eb697"}, + {file = "time_machine-2.16.0-cp313-cp313-win32.whl", hash = "sha256:32d445ce20d25c60ab92153c073942b0bac9815bfbfd152ce3dcc225d15ce988"}, + {file = "time_machine-2.16.0-cp313-cp313-win_amd64.whl", hash = "sha256:f6927dda86425f97ffda36131f297b1a601c64a6ee6838bfa0e6d3149c2f0d9f"}, + {file = "time_machine-2.16.0-cp313-cp313-win_arm64.whl", hash = "sha256:4d3843143c46dddca6491a954bbd0abfd435681512ac343169560e9bab504129"}, + {file = "time_machine-2.16.0-cp39-cp39-macosx_10_9_universal2.whl", hash = "sha256:23c5283c01b4f80b7dfbc88f3d8088c06c301b94b7c35366be498c2d7b308549"}, + {file = "time_machine-2.16.0-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:ac95ae4529d7d85b251f9cf0f961a8a408ba285875811268f469d824a3b0b15a"}, + {file = "time_machine-2.16.0-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:dfb76674db946a74f0ca6e3b81caa8265e35dafe9b7005c7d2b8dd5bbd3825cf"}, + {file = "time_machine-2.16.0-cp39-cp39-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:d0b6ff3ccde9b16bbc694a2b5facf2d8890554f3135ff626ed1429e270e3cc4f"}, + {file = "time_machine-2.16.0-cp39-cp39-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:c1906ec6e26e6b803cd6aab28d420c87285b9c209ff2a69f82d12f82278f78bb"}, + {file = "time_machine-2.16.0-cp39-cp39-musllinux_1_2_aarch64.whl", hash = "sha256:e46bd09c944ec7a20868abd2b83d7d7abdaf427775e9df3089b9226a122b340f"}, + {file = "time_machine-2.16.0-cp39-cp39-musllinux_1_2_i686.whl", hash = "sha256:cac3e2b4101db296b150cb665e5461c03621e6ede6117fc9d5048c0ec96d6e7c"}, + {file = "time_machine-2.16.0-cp39-cp39-musllinux_1_2_x86_64.whl", hash = "sha256:1e0dcc97cfec12ae306e3036746e7631cc7ef65c31889f7264c25217d4938367"}, + {file = "time_machine-2.16.0-cp39-cp39-win32.whl", hash = "sha256:c761d32d0c5d1fe5b71ac502e1bd5edec4598a7fc6f607b9b906b98e911148ce"}, + {file = "time_machine-2.16.0-cp39-cp39-win_amd64.whl", hash = "sha256:ddfab1c622342f2945942c5c2d6be327656980e8f2d2b2ce0c022d0aa3711361"}, + {file = "time_machine-2.16.0-cp39-cp39-win_arm64.whl", hash = "sha256:2e08a4015d5d1aab2cb46c780e85b33efcd5cbe880bb363b282a6972e617b8bb"}, + {file = "time_machine-2.16.0.tar.gz", hash = "sha256:4a99acc273d2f98add23a89b94d4dd9e14969c01214c8514bfa78e4e9364c7e2"}, +] + +[package.dependencies] +python-dateutil = "*" + +[[package]] +name = "tomli" +version = "2.2.1" +description = "A lil' TOML parser" +optional = false +python-versions = ">=3.8" +groups = ["benchmark", "build", "dev", "test", "typing"] +markers = "python_version < \"3.11\"" +files = [ + {file = "tomli-2.2.1-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:678e4fa69e4575eb77d103de3df8a895e1591b48e740211bd1067378c69e8249"}, + {file = "tomli-2.2.1-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:023aa114dd824ade0100497eb2318602af309e5a55595f76b626d6d9f3b7b0a6"}, + {file = "tomli-2.2.1-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:ece47d672db52ac607a3d9599a9d48dcb2f2f735c6c2d1f34130085bb12b112a"}, + {file = "tomli-2.2.1-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:6972ca9c9cc9f0acaa56a8ca1ff51e7af152a9f87fb64623e31d5c83700080ee"}, + {file = "tomli-2.2.1-cp311-cp311-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:c954d2250168d28797dd4e3ac5cf812a406cd5a92674ee4c8f123c889786aa8e"}, + {file = "tomli-2.2.1-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:8dd28b3e155b80f4d54beb40a441d366adcfe740969820caf156c019fb5c7ec4"}, + {file = "tomli-2.2.1-cp311-cp311-musllinux_1_2_i686.whl", hash = "sha256:e59e304978767a54663af13c07b3d1af22ddee3bb2fb0618ca1593e4f593a106"}, + {file = "tomli-2.2.1-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:33580bccab0338d00994d7f16f4c4ec25b776af3ffaac1ed74e0b3fc95e885a8"}, + {file = "tomli-2.2.1-cp311-cp311-win32.whl", hash = "sha256:465af0e0875402f1d226519c9904f37254b3045fc5084697cefb9bdde1ff99ff"}, + {file = "tomli-2.2.1-cp311-cp311-win_amd64.whl", hash = "sha256:2d0f2fdd22b02c6d81637a3c95f8cd77f995846af7414c5c4b8d0545afa1bc4b"}, + {file = "tomli-2.2.1-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:4a8f6e44de52d5e6c657c9fe83b562f5f4256d8ebbfe4ff922c495620a7f6cea"}, + {file = "tomli-2.2.1-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:8d57ca8095a641b8237d5b079147646153d22552f1c637fd3ba7f4b0b29167a8"}, + {file = "tomli-2.2.1-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:4e340144ad7ae1533cb897d406382b4b6fede8890a03738ff1683af800d54192"}, + {file = "tomli-2.2.1-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:db2b95f9de79181805df90bedc5a5ab4c165e6ec3fe99f970d0e302f384ad222"}, + {file = "tomli-2.2.1-cp312-cp312-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:40741994320b232529c802f8bc86da4e1aa9f413db394617b9a256ae0f9a7f77"}, + {file = "tomli-2.2.1-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:400e720fe168c0f8521520190686ef8ef033fb19fc493da09779e592861b78c6"}, + {file = "tomli-2.2.1-cp312-cp312-musllinux_1_2_i686.whl", hash = "sha256:02abe224de6ae62c19f090f68da4e27b10af2b93213d36cf44e6e1c5abd19fdd"}, + {file = "tomli-2.2.1-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:b82ebccc8c8a36f2094e969560a1b836758481f3dc360ce9a3277c65f374285e"}, + {file = "tomli-2.2.1-cp312-cp312-win32.whl", hash = "sha256:889f80ef92701b9dbb224e49ec87c645ce5df3fa2cc548664eb8a25e03127a98"}, + {file = "tomli-2.2.1-cp312-cp312-win_amd64.whl", hash = "sha256:7fc04e92e1d624a4a63c76474610238576942d6b8950a2d7f908a340494e67e4"}, + {file = "tomli-2.2.1-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:f4039b9cbc3048b2416cc57ab3bda989a6fcf9b36cf8937f01a6e731b64f80d7"}, + {file = "tomli-2.2.1-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:286f0ca2ffeeb5b9bd4fcc8d6c330534323ec51b2f52da063b11c502da16f30c"}, + {file = "tomli-2.2.1-cp313-cp313-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:a92ef1a44547e894e2a17d24e7557a5e85a9e1d0048b0b5e7541f76c5032cb13"}, + {file = "tomli-2.2.1-cp313-cp313-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:9316dc65bed1684c9a98ee68759ceaed29d229e985297003e494aa825ebb0281"}, + {file = "tomli-2.2.1-cp313-cp313-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:e85e99945e688e32d5a35c1ff38ed0b3f41f43fad8df0bdf79f72b2ba7bc5272"}, + {file = "tomli-2.2.1-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:ac065718db92ca818f8d6141b5f66369833d4a80a9d74435a268c52bdfa73140"}, + {file = "tomli-2.2.1-cp313-cp313-musllinux_1_2_i686.whl", hash = "sha256:d920f33822747519673ee656a4b6ac33e382eca9d331c87770faa3eef562aeb2"}, + {file = "tomli-2.2.1-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:a198f10c4d1b1375d7687bc25294306e551bf1abfa4eace6650070a5c1ae2744"}, + {file = "tomli-2.2.1-cp313-cp313-win32.whl", hash = "sha256:d3f5614314d758649ab2ab3a62d4f2004c825922f9e370b29416484086b264ec"}, + {file = "tomli-2.2.1-cp313-cp313-win_amd64.whl", hash = "sha256:a38aa0308e754b0e3c67e344754dff64999ff9b513e691d0e786265c93583c69"}, + {file = "tomli-2.2.1-py3-none-any.whl", hash = "sha256:cb55c73c5f4408779d0cf3eef9f762b9c9f147a77de7b258bef0a5628adc85cc"}, + {file = "tomli-2.2.1.tar.gz", hash = "sha256:cd45e1dc79c835ce60f7404ec8119f2eb06d38b1deba146f07ced3bbc44505ff"}, +] + +[[package]] +name = "tox" +version = "4.23.2" +description = "tox is a generic virtualenv management and test command line tool" +optional = false +python-versions = ">=3.8" +groups = ["dev"] +files = [ + {file = "tox-4.23.2-py3-none-any.whl", hash = "sha256:452bc32bb031f2282881a2118923176445bac783ab97c874b8770ab4c3b76c38"}, + {file = "tox-4.23.2.tar.gz", hash = "sha256:86075e00e555df6e82e74cfc333917f91ecb47ffbc868dcafbd2672e332f4a2c"}, +] + +[package.dependencies] +cachetools = ">=5.5" +chardet = ">=5.2" +colorama = ">=0.4.6" +filelock = ">=3.16.1" +packaging = ">=24.1" +platformdirs = ">=4.3.6" +pluggy = ">=1.5" +pyproject-api = ">=1.8" +tomli = {version = ">=2.0.1", markers = "python_version < \"3.11\""} +typing-extensions = {version = ">=4.12.2", markers = "python_version < \"3.11\""} +virtualenv = ">=20.26.6" + +[package.extras] +test = ["devpi-process (>=1.0.2)", "pytest (>=8.3.3)", "pytest-mock (>=3.14)"] + +[[package]] +name = "types-python-dateutil" +version = "2.9.0.20241003" +description = "Typing stubs for python-dateutil" +optional = false +python-versions = ">=3.8" +groups = ["typing"] +files = [ + {file = "types-python-dateutil-2.9.0.20241003.tar.gz", hash = "sha256:58cb85449b2a56d6684e41aeefb4c4280631246a0da1a719bdbe6f3fb0317446"}, + {file = "types_python_dateutil-2.9.0.20241003-py3-none-any.whl", hash = "sha256:250e1d8e80e7bbc3a6c99b907762711d1a1cdd00e978ad39cb5940f6f0a87f3d"}, +] + +[[package]] +name = "typing-extensions" +version = "4.12.2" +description = "Backported and Experimental Type Hints for Python 3.8+" +optional = false +python-versions = ">=3.8" +groups = ["benchmark", "dev", "typing"] +files = [ + {file = "typing_extensions-4.12.2-py3-none-any.whl", hash = "sha256:04e5ca0351e0f3f85c6853954072df659d0d13fac324d0072316b67d7794700d"}, + {file = "typing_extensions-4.12.2.tar.gz", hash = "sha256:1a7ead55c7e559dd4dee8856e3a88b41225abfe1ce8df57b7c13915fe121ffb8"}, +] +markers = {benchmark = "python_version < \"3.11\"", dev = "python_version < \"3.11\""} + +[[package]] +name = "tzdata" +version = "2024.2" +description = "Provider of IANA time zone data" +optional = false +python-versions = ">=2" +groups = ["main"] +files = [ + {file = "tzdata-2024.2-py2.py3-none-any.whl", hash = "sha256:a48093786cdcde33cad18c2555e8532f34422074448fbc874186f0abd79565cd"}, + {file = "tzdata-2024.2.tar.gz", hash = "sha256:7d85cc416e9382e69095b7bdf4afd9e3880418a2413feec7069d533d6b4e31cc"}, +] + +[[package]] +name = "virtualenv" +version = "20.28.0" +description = "Virtual Python Environment builder" +optional = false +python-versions = ">=3.8" +groups = ["dev", "lint"] +files = [ + {file = "virtualenv-20.28.0-py3-none-any.whl", hash = "sha256:23eae1b4516ecd610481eda647f3a7c09aea295055337331bb4e6892ecce47b0"}, + {file = "virtualenv-20.28.0.tar.gz", hash = "sha256:2c9c3262bb8e7b87ea801d715fae4495e6032450c71d2309be9550e7364049aa"}, +] + +[package.dependencies] +distlib = ">=0.3.7,<1" +filelock = ">=3.12.2,<4" +platformdirs = ">=3.9.1,<5" + +[package.extras] +docs = ["furo (>=2023.7.26)", "proselint (>=0.13)", "sphinx (>=7.1.2,!=7.3)", "sphinx-argparse (>=0.4)", "sphinxcontrib-towncrier (>=0.2.1a0)", "towncrier (>=23.6)"] +test = ["covdefaults (>=2.3)", "coverage (>=7.2.7)", "coverage-enable-subprocess (>=1)", "flaky (>=3.7)", "packaging (>=23.1)", "pytest (>=7.4)", "pytest-env (>=0.8.2)", "pytest-freezer (>=0.4.8) ; platform_python_implementation == \"PyPy\" or platform_python_implementation == \"CPython\" and sys_platform == \"win32\" and python_version >= \"3.13\"", "pytest-mock (>=3.11.1)", "pytest-randomly (>=3.12)", "pytest-timeout (>=2.1)", "setuptools (>=68)", "time-machine (>=2.10) ; platform_python_implementation == \"CPython\""] + +[[package]] +name = "watchdog" +version = "6.0.0" +description = "Filesystem events monitoring" +optional = false +python-versions = ">=3.9" +groups = ["doc"] +files = [ + {file = "watchdog-6.0.0-cp310-cp310-macosx_10_9_universal2.whl", hash = "sha256:d1cdb490583ebd691c012b3d6dae011000fe42edb7a82ece80965b42abd61f26"}, + {file = "watchdog-6.0.0-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:bc64ab3bdb6a04d69d4023b29422170b74681784ffb9463ed4870cf2f3e66112"}, + {file = "watchdog-6.0.0-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:c897ac1b55c5a1461e16dae288d22bb2e412ba9807df8397a635d88f671d36c3"}, + {file = "watchdog-6.0.0-cp311-cp311-macosx_10_9_universal2.whl", hash = "sha256:6eb11feb5a0d452ee41f824e271ca311a09e250441c262ca2fd7ebcf2461a06c"}, + {file = "watchdog-6.0.0-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:ef810fbf7b781a5a593894e4f439773830bdecb885e6880d957d5b9382a960d2"}, + {file = "watchdog-6.0.0-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:afd0fe1b2270917c5e23c2a65ce50c2a4abb63daafb0d419fde368e272a76b7c"}, + {file = "watchdog-6.0.0-cp312-cp312-macosx_10_13_universal2.whl", hash = "sha256:bdd4e6f14b8b18c334febb9c4425a878a2ac20efd1e0b231978e7b150f92a948"}, + {file = "watchdog-6.0.0-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:c7c15dda13c4eb00d6fb6fc508b3c0ed88b9d5d374056b239c4ad1611125c860"}, + {file = "watchdog-6.0.0-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:6f10cb2d5902447c7d0da897e2c6768bca89174d0c6e1e30abec5421af97a5b0"}, + {file = "watchdog-6.0.0-cp313-cp313-macosx_10_13_universal2.whl", hash = "sha256:490ab2ef84f11129844c23fb14ecf30ef3d8a6abafd3754a6f75ca1e6654136c"}, + {file = "watchdog-6.0.0-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:76aae96b00ae814b181bb25b1b98076d5fc84e8a53cd8885a318b42b6d3a5134"}, + {file = "watchdog-6.0.0-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:a175f755fc2279e0b7312c0035d52e27211a5bc39719dd529625b1930917345b"}, + {file = "watchdog-6.0.0-cp39-cp39-macosx_10_9_universal2.whl", hash = "sha256:e6f0e77c9417e7cd62af82529b10563db3423625c5fce018430b249bf977f9e8"}, + {file = "watchdog-6.0.0-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:90c8e78f3b94014f7aaae121e6b909674df5b46ec24d6bebc45c44c56729af2a"}, + {file = "watchdog-6.0.0-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:e7631a77ffb1f7d2eefa4445ebbee491c720a5661ddf6df3498ebecae5ed375c"}, + {file = "watchdog-6.0.0-pp310-pypy310_pp73-macosx_10_15_x86_64.whl", hash = "sha256:c7ac31a19f4545dd92fc25d200694098f42c9a8e391bc00bdd362c5736dbf881"}, + {file = "watchdog-6.0.0-pp310-pypy310_pp73-macosx_11_0_arm64.whl", hash = "sha256:9513f27a1a582d9808cf21a07dae516f0fab1cf2d7683a742c498b93eedabb11"}, + {file = "watchdog-6.0.0-pp39-pypy39_pp73-macosx_10_15_x86_64.whl", hash = "sha256:7a0e56874cfbc4b9b05c60c8a1926fedf56324bb08cfbc188969777940aef3aa"}, + {file = "watchdog-6.0.0-pp39-pypy39_pp73-macosx_11_0_arm64.whl", hash = "sha256:e6439e374fc012255b4ec786ae3c4bc838cd7309a540e5fe0952d03687d8804e"}, + {file = "watchdog-6.0.0-py3-none-manylinux2014_aarch64.whl", hash = "sha256:7607498efa04a3542ae3e05e64da8202e58159aa1fa4acddf7678d34a35d4f13"}, + {file = "watchdog-6.0.0-py3-none-manylinux2014_armv7l.whl", hash = "sha256:9041567ee8953024c83343288ccc458fd0a2d811d6a0fd68c4c22609e3490379"}, + {file = "watchdog-6.0.0-py3-none-manylinux2014_i686.whl", hash = "sha256:82dc3e3143c7e38ec49d61af98d6558288c415eac98486a5c581726e0737c00e"}, + {file = "watchdog-6.0.0-py3-none-manylinux2014_ppc64.whl", hash = "sha256:212ac9b8bf1161dc91bd09c048048a95ca3a4c4f5e5d4a7d1b1a7d5752a7f96f"}, + {file = "watchdog-6.0.0-py3-none-manylinux2014_ppc64le.whl", hash = "sha256:e3df4cbb9a450c6d49318f6d14f4bbc80d763fa587ba46ec86f99f9e6876bb26"}, + {file = "watchdog-6.0.0-py3-none-manylinux2014_s390x.whl", hash = "sha256:2cce7cfc2008eb51feb6aab51251fd79b85d9894e98ba847408f662b3395ca3c"}, + {file = "watchdog-6.0.0-py3-none-manylinux2014_x86_64.whl", hash = "sha256:20ffe5b202af80ab4266dcd3e91aae72bf2da48c0d33bdb15c66658e685e94e2"}, + {file = "watchdog-6.0.0-py3-none-win32.whl", hash = "sha256:07df1fdd701c5d4c8e55ef6cf55b8f0120fe1aef7ef39a1c6fc6bc2e606d517a"}, + {file = "watchdog-6.0.0-py3-none-win_amd64.whl", hash = "sha256:cbafb470cf848d93b5d013e2ecb245d4aa1c8fd0504e863ccefa32445359d680"}, + {file = "watchdog-6.0.0-py3-none-win_ia64.whl", hash = "sha256:a1914259fa9e1454315171103c6a30961236f508b9b623eae470268bbcc6a22f"}, + {file = "watchdog-6.0.0.tar.gz", hash = "sha256:9ddf7c82fda3ae8e24decda1338ede66e1c99883db93711d8fb941eaa2d8c282"}, +] + +[package.extras] +watchmedo = ["PyYAML (>=3.10)"] + +[[package]] +name = "zipp" +version = "3.21.0" +description = "Backport of pathlib-compatible object wrapper for zip files" +optional = false +python-versions = ">=3.9" +groups = ["benchmark", "doc"] +markers = "python_version == \"3.9\"" +files = [ + {file = "zipp-3.21.0-py3-none-any.whl", hash = "sha256:ac1bbe05fd2991f160ebce24ffbac5f6d11d83dc90891255885223d42b3cd931"}, + {file = "zipp-3.21.0.tar.gz", hash = "sha256:2c9958f6430a2040341a52eb608ed6dd93ef4392e02ffe219417c1b28b5dd1f4"}, +] + +[package.extras] +check = ["pytest-checkdocs (>=2.4)", "pytest-ruff (>=0.2.1) ; sys_platform != \"cygwin\""] +cover = ["pytest-cov"] +doc = ["furo", "jaraco.packaging (>=9.3)", "jaraco.tidelift (>=1.4)", "rst.linker (>=1.9)", "sphinx (>=3.5)", "sphinx-lint"] +enabler = ["pytest-enabler (>=2.2)"] +test = ["big-O", "importlib-resources ; python_version < \"3.9\"", "jaraco.functools", "jaraco.itertools", "jaraco.test", "more-itertools", "pytest (>=6,!=8.1.*)", "pytest-ignore-flaky"] +type = ["pytest-mypy"] + +[extras] +test = ["time-machine"] + +[metadata] +lock-version = "2.1" +python-versions = ">=3.9" +content-hash = "491ee653cad58327fa9ff27388a6d0402bc8d5202b08bca616042f5d5ae12176" diff --git a/tests/fixtures/real-world-locks/poetry/pendulum-3.2.0/pyproject.toml b/tests/fixtures/real-world-locks/poetry/pendulum-3.2.0/pyproject.toml new file mode 100644 index 00000000..345c0e52 --- /dev/null +++ b/tests/fixtures/real-world-locks/poetry/pendulum-3.2.0/pyproject.toml @@ -0,0 +1,227 @@ +[project] +name = "pendulum" +version = "3.2.0" +description = "Python datetimes made easy" +readme = "README.rst" +requires-python = ">=3.9" +license = { text = "MIT License" } +authors = [{ name = "Sébastien Eustace", email = "sebastien@eustace.io" }] +keywords = ['datetime', 'date', 'time'] +classifiers = [ + "License :: OSI Approved :: MIT License", + "Programming Language :: Python :: 3", + "Programming Language :: Python :: 3.9", + "Programming Language :: Python :: 3.10", + "Programming Language :: Python :: 3.11", + "Programming Language :: Python :: 3.12", + "Programming Language :: Python :: 3.13", +] + +dependencies = [ + "python-dateutil>=2.6", + "tzdata>=2020.1", +] + +[project.optional-dependencies] +test = [ + 'time-machine>=2.6.0,<3.0.0; implementation_name != "pypy"', +] + +[project.urls] +Homepage = "https://pendulum.eustace.io" +Documentation = "https://pendulum.eustace.io/docs" +Repository = "https://github.com/sdispater/pendulum" + + +[tool.poetry.group.test.dependencies] +pytest = "^7.1.2" +time-machine = ">=2.16.0" +pytest-benchmark = "^4.0.0" + +[tool.poetry.group.doc.dependencies] +mkdocs = "^1.0" +pymdown-extensions = ">=6,<11" +pygments = "^2.2" +markdown-include = "^0.8.1" + +[tool.poetry.group.lint.dependencies] +pre-commit = "^3.0.0" + +[tool.poetry.group.typing.dependencies] +mypy = "^1.3.0" +types-python-dateutil = "^2.8.19" + +[tool.poetry.group.dev.dependencies] +babel = "^2.10.3" +cleo = { version = "^2.0.1", python = ">=3.8,<4.0" } +tox = "^4.0.0" + +[tool.poetry.group.benchmark.dependencies] +pytest-codspeed = "^3.0.0" + +[tool.poetry.group.build.dependencies] +maturin = ">=1.0,<2.0" + +[tool.maturin] +module-name = "pendulum._pendulum" +features = ["pyo3/extension-module"] +python-packages = ["pendulum"] +include = [ + { path = "LICENSE", format = "sdist" }, +] +[tool.ruff] +fix = true +line-length = 88 +target-version = "py39" +extend-exclude = [ + # External to the project's coding standards: + "docs/*", + # Machine-generated, too many false-positives + "src/pendulum/locales/*", + # ruff disagrees with black when it comes to formatting + "*.pyi", +] + +[tool.ruff.lint] +extend-select = [ + "B", # flake8-bugbear + "C4", # flake8-comprehensions + "ERA", # flake8-eradicate/eradicate + "I", # isort + "N", # pep8-naming + "PIE", # flake8-pie + "PGH", # pygrep + "RUF", # ruff checks + "SIM", # flake8-simplify + "T20", # flake8-print + "TCH", # flake8-type-checking + "TID", # flake8-tidy-imports + "UP", # pyupgrade +] +ignore = [ + "B904", # use 'raise ... from err' + "B905", # use explicit 'strict=' parameter with 'zip()' + "N818", + "RUF001" +] +extend-safe-fixes = [ + "TCH", # move import from and to TYPE_CHECKING blocks +] +unfixable = [ + "ERA", # do not autoremove commented out code +] + +[tool.ruff.lint.flake8-tidy-imports] +ban-relative-imports = "all" + +[tool.ruff.lint.isort] +force-single-line = true +lines-between-types = 1 +lines-after-imports = 2 +known-first-party = ["pendulum"] +known-third-party = [ + "babel", + "cleo", + "dateutil", + "time_machine", + "pytzdata", +] +required-imports = ["from __future__ import annotations"] + +[tool.ruff.lint.extend-per-file-ignores] +"build.py" = ["I002"] +"clock" = ["RUF012"] + +[tool.mypy] +strict = true +files = "src, tests" +show_error_codes = true +pretty = true +warn_unused_ignores = true +exclude = [ + "^build\\.py$" +] + +# The following whitelist is used to allow for incremental adoption +# of Mypy. Modules should be removed from this whitelist as and when +# their respective type errors have been addressed. No new modules +# should be added to this whitelist. + +[[tool.mypy.overrides]] +module = [ + "pendulum.mixins.default", + "tests.test_parsing", + "tests.date.test_add", + "tests.date.test_behavior", + "tests.date.test_construct", + "tests.date.test_comparison", + "tests.date.test_day_of_week_modifiers", + "tests.date.test_diff", + "tests.date.test_fluent_setters", + "tests.date.test_getters", + "tests.date.test_start_end_of", + "tests.date.test_strings", + "tests.date.test_sub", + "tests.datetime.test_add", + "tests.datetime.test_behavior", + "tests.datetime.test_construct", + "tests.datetime.test_comparison", + "tests.datetime.test_create_from_timestamp", + "tests.datetime.test_day_of_week_modifiers", + "tests.datetime.test_diff", + "tests.datetime.test_fluent_setters", + "tests.datetime.test_from_format", + "tests.datetime.test_getters", + "tests.datetime.test_naive", + "tests.datetime.test_replace", + "tests.datetime.test_start_end_of", + "tests.datetime.test_strings", + "tests.datetime.test_sub", + "tests.datetime.test_timezone", + "tests.duration.test_add_sub", + "tests.duration.test_arithmetic", + "tests.duration.test_behavior", + "tests.duration.test_construct", + "tests.duration.test_in_methods", + "tests.duration.test_in_words", + "tests.duration.test_total_methods", + "tests.formatting.test_formatter", + "tests.helpers.test_local_time", + "tests.localization.*", + "tests.parsing.test_parsing", + "tests.parsing.test_parsing_duration", + "tests.parsing.test_parse_iso8601", + "tests.interval.test_add_subtract", + "tests.interval.test_arithmetic", + "tests.interval.test_behavior", + "tests.interval.test_construct", + "tests.interval.test_hashing", + "tests.interval.test_in_words", + "tests.interval.test_range", + "tests.time.test_add", + "tests.time.test_behavior", + "tests.time.test_comparison", + "tests.time.test_construct", + "tests.time.test_diff", + "tests.time.test_fluent_setters", + "tests.time.test_strings", + "tests.time.test_sub", + "tests.tz.test_helpers", + "tests.tz.test_local_timezone", + "tests.tz.test_timezone", + "tests.tz.test_timezones", +] +ignore_errors = true + +[tool.coverage.run] +omit = [ + "pendulum/locales/*", + "pendulum/__version__.py,", + "pendulum/_extensions/*", + "pendulum/parsing/iso8601.py", + "pendulum/utils/_compat.py", +] + +[build-system] +requires = ["maturin>=1.0,<2.0"] +build-backend = "maturin" diff --git a/tests/fixtures/real-world-locks/poetry/tomlkit-0.15.1/poetry.lock b/tests/fixtures/real-world-locks/poetry/tomlkit-0.15.1/poetry.lock new file mode 100644 index 00000000..6bb97349 --- /dev/null +++ b/tests/fixtures/real-world-locks/poetry/tomlkit-0.15.1/poetry.lock @@ -0,0 +1,1205 @@ +# This file is automatically @generated by Poetry 2.4.1 and should not be changed by hand. + +[[package]] +name = "alabaster" +version = "0.7.13" +description = "A configurable sidebar-enabled Sphinx theme" +optional = false +python-versions = ">=3.6" +groups = ["dev"] +files = [ + {file = "alabaster-0.7.13-py3-none-any.whl", hash = "sha256:1ee19aca801bbabb5ba3f5f258e4422dfa86f82f3e9cefb0859b283cdd7f62a3"}, + {file = "alabaster-0.7.13.tar.gz", hash = "sha256:a27a4a084d5e690e16e01e03ad2b2e552c61a65469419b907243193de1a84ae2"}, +] + +[[package]] +name = "babel" +version = "2.14.0" +description = "Internationalization utilities" +optional = false +python-versions = ">=3.7" +groups = ["dev"] +files = [ + {file = "Babel-2.14.0-py3-none-any.whl", hash = "sha256:efb1a25b7118e67ce3a259bed20545c29cb68be8ad2c784c83689981b7a57287"}, + {file = "Babel-2.14.0.tar.gz", hash = "sha256:6919867db036398ba21eb5c7a0f6b28ab8cbc3ae7a73a44ebe34ae74a4e7d363"}, +] + +[package.extras] +dev = ["freezegun (>=1.0,<2.0)", "pytest (>=6.0)", "pytest-cov"] + +[[package]] +name = "beautifulsoup4" +version = "4.12.3" +description = "Screen-scraping library" +optional = false +python-versions = ">=3.6.0" +groups = ["dev"] +files = [ + {file = "beautifulsoup4-4.12.3-py3-none-any.whl", hash = "sha256:b80878c9f40111313e55da8ba20bdba06d8fa3969fc68304167741bbf9e082ed"}, + {file = "beautifulsoup4-4.12.3.tar.gz", hash = "sha256:74e3d1928edc070d21748185c46e3fb33490f22f52a3addee9aee0f4f7781051"}, +] + +[package.dependencies] +soupsieve = ">1.2" + +[package.extras] +cchardet = ["cchardet"] +chardet = ["chardet"] +charset-normalizer = ["charset-normalizer"] +html5lib = ["html5lib"] +lxml = ["lxml"] + +[[package]] +name = "certifi" +version = "2024.7.4" +description = "Python package for providing Mozilla's CA Bundle." +optional = false +python-versions = ">=3.6" +groups = ["dev"] +files = [ + {file = "certifi-2024.7.4-py3-none-any.whl", hash = "sha256:c198e21b1289c2ab85ee4e67bb4b4ef3ead0892059901a8d5b622f24a1101e90"}, + {file = "certifi-2024.7.4.tar.gz", hash = "sha256:5a1e7645bc0ec61a09e26c36f6106dd4cf40c6db3a1fb6352b0244e7fb057c7b"}, +] + +[[package]] +name = "cfgv" +version = "3.3.1" +description = "Validate configuration and produce human readable error messages." +optional = false +python-versions = ">=3.6.1" +groups = ["dev"] +files = [ + {file = "cfgv-3.3.1-py2.py3-none-any.whl", hash = "sha256:c6a0883f3917a037485059700b9e75da2464e6c27051014ad85ba6aaa5884426"}, + {file = "cfgv-3.3.1.tar.gz", hash = "sha256:f5a830efb9ce7a445376bb66ec94c638a9787422f96264c98edc6bdeed8ab736"}, +] + +[[package]] +name = "charset-normalizer" +version = "3.3.2" +description = "The Real First Universal Charset Detector. Open, modern and actively maintained alternative to Chardet." +optional = false +python-versions = ">=3.7.0" +groups = ["dev"] +files = [ + {file = "charset-normalizer-3.3.2.tar.gz", hash = "sha256:f30c3cb33b24454a82faecaf01b19c18562b1e89558fb6c56de4d9118a032fd5"}, + {file = "charset_normalizer-3.3.2-cp310-cp310-macosx_10_9_universal2.whl", hash = "sha256:25baf083bf6f6b341f4121c2f3c548875ee6f5339300e08be3f2b2ba1721cdd3"}, + {file = "charset_normalizer-3.3.2-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:06435b539f889b1f6f4ac1758871aae42dc3a8c0e24ac9e60c2384973ad73027"}, + {file = "charset_normalizer-3.3.2-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:9063e24fdb1e498ab71cb7419e24622516c4a04476b17a2dab57e8baa30d6e03"}, + {file = "charset_normalizer-3.3.2-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:6897af51655e3691ff853668779c7bad41579facacf5fd7253b0133308cf000d"}, + {file = "charset_normalizer-3.3.2-cp310-cp310-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:1d3193f4a680c64b4b6a9115943538edb896edc190f0b222e73761716519268e"}, + {file = "charset_normalizer-3.3.2-cp310-cp310-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:cd70574b12bb8a4d2aaa0094515df2463cb429d8536cfb6c7ce983246983e5a6"}, + {file = "charset_normalizer-3.3.2-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:8465322196c8b4d7ab6d1e049e4c5cb460d0394da4a27d23cc242fbf0034b6b5"}, + {file = "charset_normalizer-3.3.2-cp310-cp310-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:a9a8e9031d613fd2009c182b69c7b2c1ef8239a0efb1df3f7c8da66d5dd3d537"}, + {file = "charset_normalizer-3.3.2-cp310-cp310-musllinux_1_1_aarch64.whl", hash = "sha256:beb58fe5cdb101e3a055192ac291b7a21e3b7ef4f67fa1d74e331a7f2124341c"}, + {file = "charset_normalizer-3.3.2-cp310-cp310-musllinux_1_1_i686.whl", hash = "sha256:e06ed3eb3218bc64786f7db41917d4e686cc4856944f53d5bdf83a6884432e12"}, + {file = "charset_normalizer-3.3.2-cp310-cp310-musllinux_1_1_ppc64le.whl", hash = "sha256:2e81c7b9c8979ce92ed306c249d46894776a909505d8f5a4ba55b14206e3222f"}, + {file = "charset_normalizer-3.3.2-cp310-cp310-musllinux_1_1_s390x.whl", hash = "sha256:572c3763a264ba47b3cf708a44ce965d98555f618ca42c926a9c1616d8f34269"}, + {file = "charset_normalizer-3.3.2-cp310-cp310-musllinux_1_1_x86_64.whl", hash = "sha256:fd1abc0d89e30cc4e02e4064dc67fcc51bd941eb395c502aac3ec19fab46b519"}, + {file = "charset_normalizer-3.3.2-cp310-cp310-win32.whl", hash = "sha256:3d47fa203a7bd9c5b6cee4736ee84ca03b8ef23193c0d1ca99b5089f72645c73"}, + {file = "charset_normalizer-3.3.2-cp310-cp310-win_amd64.whl", hash = "sha256:10955842570876604d404661fbccbc9c7e684caf432c09c715ec38fbae45ae09"}, + {file = "charset_normalizer-3.3.2-cp311-cp311-macosx_10_9_universal2.whl", hash = "sha256:802fe99cca7457642125a8a88a084cef28ff0cf9407060f7b93dca5aa25480db"}, + {file = "charset_normalizer-3.3.2-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:573f6eac48f4769d667c4442081b1794f52919e7edada77495aaed9236d13a96"}, + {file = "charset_normalizer-3.3.2-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:549a3a73da901d5bc3ce8d24e0600d1fa85524c10287f6004fbab87672bf3e1e"}, + {file = "charset_normalizer-3.3.2-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:f27273b60488abe721a075bcca6d7f3964f9f6f067c8c4c605743023d7d3944f"}, + {file = "charset_normalizer-3.3.2-cp311-cp311-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:1ceae2f17a9c33cb48e3263960dc5fc8005351ee19db217e9b1bb15d28c02574"}, + {file = "charset_normalizer-3.3.2-cp311-cp311-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:65f6f63034100ead094b8744b3b97965785388f308a64cf8d7c34f2f2e5be0c4"}, + {file = "charset_normalizer-3.3.2-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:753f10e867343b4511128c6ed8c82f7bec3bd026875576dfd88483c5c73b2fd8"}, + {file = "charset_normalizer-3.3.2-cp311-cp311-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:4a78b2b446bd7c934f5dcedc588903fb2f5eec172f3d29e52a9096a43722adfc"}, + {file = "charset_normalizer-3.3.2-cp311-cp311-musllinux_1_1_aarch64.whl", hash = "sha256:e537484df0d8f426ce2afb2d0f8e1c3d0b114b83f8850e5f2fbea0e797bd82ae"}, + {file = "charset_normalizer-3.3.2-cp311-cp311-musllinux_1_1_i686.whl", hash = "sha256:eb6904c354526e758fda7167b33005998fb68c46fbc10e013ca97f21ca5c8887"}, + {file = "charset_normalizer-3.3.2-cp311-cp311-musllinux_1_1_ppc64le.whl", hash = "sha256:deb6be0ac38ece9ba87dea880e438f25ca3eddfac8b002a2ec3d9183a454e8ae"}, + {file = "charset_normalizer-3.3.2-cp311-cp311-musllinux_1_1_s390x.whl", hash = "sha256:4ab2fe47fae9e0f9dee8c04187ce5d09f48eabe611be8259444906793ab7cbce"}, + {file = "charset_normalizer-3.3.2-cp311-cp311-musllinux_1_1_x86_64.whl", hash = "sha256:80402cd6ee291dcb72644d6eac93785fe2c8b9cb30893c1af5b8fdd753b9d40f"}, + {file = "charset_normalizer-3.3.2-cp311-cp311-win32.whl", hash = "sha256:7cd13a2e3ddeed6913a65e66e94b51d80a041145a026c27e6bb76c31a853c6ab"}, + {file = "charset_normalizer-3.3.2-cp311-cp311-win_amd64.whl", hash = "sha256:663946639d296df6a2bb2aa51b60a2454ca1cb29835324c640dafb5ff2131a77"}, + {file = "charset_normalizer-3.3.2-cp312-cp312-macosx_10_9_universal2.whl", hash = "sha256:0b2b64d2bb6d3fb9112bafa732def486049e63de9618b5843bcdd081d8144cd8"}, + {file = "charset_normalizer-3.3.2-cp312-cp312-macosx_10_9_x86_64.whl", hash = "sha256:ddbb2551d7e0102e7252db79ba445cdab71b26640817ab1e3e3648dad515003b"}, + {file = "charset_normalizer-3.3.2-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:55086ee1064215781fff39a1af09518bc9255b50d6333f2e4c74ca09fac6a8f6"}, + {file = "charset_normalizer-3.3.2-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:8f4a014bc36d3c57402e2977dada34f9c12300af536839dc38c0beab8878f38a"}, + {file = "charset_normalizer-3.3.2-cp312-cp312-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:a10af20b82360ab00827f916a6058451b723b4e65030c5a18577c8b2de5b3389"}, + {file = "charset_normalizer-3.3.2-cp312-cp312-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:8d756e44e94489e49571086ef83b2bb8ce311e730092d2c34ca8f7d925cb20aa"}, + {file = "charset_normalizer-3.3.2-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:90d558489962fd4918143277a773316e56c72da56ec7aa3dc3dbbe20fdfed15b"}, + {file = "charset_normalizer-3.3.2-cp312-cp312-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:6ac7ffc7ad6d040517be39eb591cac5ff87416c2537df6ba3cba3bae290c0fed"}, + {file = "charset_normalizer-3.3.2-cp312-cp312-musllinux_1_1_aarch64.whl", hash = "sha256:7ed9e526742851e8d5cc9e6cf41427dfc6068d4f5a3bb03659444b4cabf6bc26"}, + {file = "charset_normalizer-3.3.2-cp312-cp312-musllinux_1_1_i686.whl", hash = "sha256:8bdb58ff7ba23002a4c5808d608e4e6c687175724f54a5dade5fa8c67b604e4d"}, + {file = "charset_normalizer-3.3.2-cp312-cp312-musllinux_1_1_ppc64le.whl", hash = "sha256:6b3251890fff30ee142c44144871185dbe13b11bab478a88887a639655be1068"}, + {file = "charset_normalizer-3.3.2-cp312-cp312-musllinux_1_1_s390x.whl", hash = "sha256:b4a23f61ce87adf89be746c8a8974fe1c823c891d8f86eb218bb957c924bb143"}, + {file = "charset_normalizer-3.3.2-cp312-cp312-musllinux_1_1_x86_64.whl", hash = "sha256:efcb3f6676480691518c177e3b465bcddf57cea040302f9f4e6e191af91174d4"}, + {file = "charset_normalizer-3.3.2-cp312-cp312-win32.whl", hash = "sha256:d965bba47ddeec8cd560687584e88cf699fd28f192ceb452d1d7ee807c5597b7"}, + {file = "charset_normalizer-3.3.2-cp312-cp312-win_amd64.whl", hash = "sha256:96b02a3dc4381e5494fad39be677abcb5e6634bf7b4fa83a6dd3112607547001"}, + {file = "charset_normalizer-3.3.2-cp37-cp37m-macosx_10_9_x86_64.whl", hash = "sha256:95f2a5796329323b8f0512e09dbb7a1860c46a39da62ecb2324f116fa8fdc85c"}, + {file = "charset_normalizer-3.3.2-cp37-cp37m-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:c002b4ffc0be611f0d9da932eb0f704fe2602a9a949d1f738e4c34c75b0863d5"}, + {file = "charset_normalizer-3.3.2-cp37-cp37m-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:a981a536974bbc7a512cf44ed14938cf01030a99e9b3a06dd59578882f06f985"}, + {file = "charset_normalizer-3.3.2-cp37-cp37m-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:3287761bc4ee9e33561a7e058c72ac0938c4f57fe49a09eae428fd88aafe7bb6"}, + {file = "charset_normalizer-3.3.2-cp37-cp37m-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:42cb296636fcc8b0644486d15c12376cb9fa75443e00fb25de0b8602e64c1714"}, + {file = "charset_normalizer-3.3.2-cp37-cp37m-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:0a55554a2fa0d408816b3b5cedf0045f4b8e1a6065aec45849de2d6f3f8e9786"}, + {file = "charset_normalizer-3.3.2-cp37-cp37m-musllinux_1_1_aarch64.whl", hash = "sha256:c083af607d2515612056a31f0a8d9e0fcb5876b7bfc0abad3ecd275bc4ebc2d5"}, + {file = "charset_normalizer-3.3.2-cp37-cp37m-musllinux_1_1_i686.whl", hash = "sha256:87d1351268731db79e0f8e745d92493ee2841c974128ef629dc518b937d9194c"}, + {file = "charset_normalizer-3.3.2-cp37-cp37m-musllinux_1_1_ppc64le.whl", hash = "sha256:bd8f7df7d12c2db9fab40bdd87a7c09b1530128315d047a086fa3ae3435cb3a8"}, + {file = "charset_normalizer-3.3.2-cp37-cp37m-musllinux_1_1_s390x.whl", hash = "sha256:c180f51afb394e165eafe4ac2936a14bee3eb10debc9d9e4db8958fe36afe711"}, + {file = "charset_normalizer-3.3.2-cp37-cp37m-musllinux_1_1_x86_64.whl", hash = "sha256:8c622a5fe39a48f78944a87d4fb8a53ee07344641b0562c540d840748571b811"}, + {file = "charset_normalizer-3.3.2-cp37-cp37m-win32.whl", hash = "sha256:db364eca23f876da6f9e16c9da0df51aa4f104a972735574842618b8c6d999d4"}, + {file = "charset_normalizer-3.3.2-cp37-cp37m-win_amd64.whl", hash = "sha256:86216b5cee4b06df986d214f664305142d9c76df9b6512be2738aa72a2048f99"}, + {file = "charset_normalizer-3.3.2-cp38-cp38-macosx_10_9_universal2.whl", hash = "sha256:6463effa3186ea09411d50efc7d85360b38d5f09b870c48e4600f63af490e56a"}, + {file = "charset_normalizer-3.3.2-cp38-cp38-macosx_10_9_x86_64.whl", hash = "sha256:6c4caeef8fa63d06bd437cd4bdcf3ffefe6738fb1b25951440d80dc7df8c03ac"}, + {file = "charset_normalizer-3.3.2-cp38-cp38-macosx_11_0_arm64.whl", hash = "sha256:37e55c8e51c236f95b033f6fb391d7d7970ba5fe7ff453dad675e88cf303377a"}, + {file = "charset_normalizer-3.3.2-cp38-cp38-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:fb69256e180cb6c8a894fee62b3afebae785babc1ee98b81cdf68bbca1987f33"}, + {file = "charset_normalizer-3.3.2-cp38-cp38-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:ae5f4161f18c61806f411a13b0310bea87f987c7d2ecdbdaad0e94eb2e404238"}, + {file = "charset_normalizer-3.3.2-cp38-cp38-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:b2b0a0c0517616b6869869f8c581d4eb2dd83a4d79e0ebcb7d373ef9956aeb0a"}, + {file = "charset_normalizer-3.3.2-cp38-cp38-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:45485e01ff4d3630ec0d9617310448a8702f70e9c01906b0d0118bdf9d124cf2"}, + {file = "charset_normalizer-3.3.2-cp38-cp38-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:eb00ed941194665c332bf8e078baf037d6c35d7c4f3102ea2d4f16ca94a26dc8"}, + {file = "charset_normalizer-3.3.2-cp38-cp38-musllinux_1_1_aarch64.whl", hash = "sha256:2127566c664442652f024c837091890cb1942c30937add288223dc895793f898"}, + {file = "charset_normalizer-3.3.2-cp38-cp38-musllinux_1_1_i686.whl", hash = "sha256:a50aebfa173e157099939b17f18600f72f84eed3049e743b68ad15bd69b6bf99"}, + {file = "charset_normalizer-3.3.2-cp38-cp38-musllinux_1_1_ppc64le.whl", hash = "sha256:4d0d1650369165a14e14e1e47b372cfcb31d6ab44e6e33cb2d4e57265290044d"}, + {file = "charset_normalizer-3.3.2-cp38-cp38-musllinux_1_1_s390x.whl", hash = "sha256:923c0c831b7cfcb071580d3f46c4baf50f174be571576556269530f4bbd79d04"}, + {file = "charset_normalizer-3.3.2-cp38-cp38-musllinux_1_1_x86_64.whl", hash = "sha256:06a81e93cd441c56a9b65d8e1d043daeb97a3d0856d177d5c90ba85acb3db087"}, + {file = "charset_normalizer-3.3.2-cp38-cp38-win32.whl", hash = "sha256:6ef1d82a3af9d3eecdba2321dc1b3c238245d890843e040e41e470ffa64c3e25"}, + {file = "charset_normalizer-3.3.2-cp38-cp38-win_amd64.whl", hash = "sha256:eb8821e09e916165e160797a6c17edda0679379a4be5c716c260e836e122f54b"}, + {file = "charset_normalizer-3.3.2-cp39-cp39-macosx_10_9_universal2.whl", hash = "sha256:c235ebd9baae02f1b77bcea61bce332cb4331dc3617d254df3323aa01ab47bd4"}, + {file = "charset_normalizer-3.3.2-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:5b4c145409bef602a690e7cfad0a15a55c13320ff7a3ad7ca59c13bb8ba4d45d"}, + {file = "charset_normalizer-3.3.2-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:68d1f8a9e9e37c1223b656399be5d6b448dea850bed7d0f87a8311f1ff3dabb0"}, + {file = "charset_normalizer-3.3.2-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:22afcb9f253dac0696b5a4be4a1c0f8762f8239e21b99680099abd9b2b1b2269"}, + {file = "charset_normalizer-3.3.2-cp39-cp39-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:e27ad930a842b4c5eb8ac0016b0a54f5aebbe679340c26101df33424142c143c"}, + {file = "charset_normalizer-3.3.2-cp39-cp39-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:1f79682fbe303db92bc2b1136016a38a42e835d932bab5b3b1bfcfbf0640e519"}, + {file = "charset_normalizer-3.3.2-cp39-cp39-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:b261ccdec7821281dade748d088bb6e9b69e6d15b30652b74cbbac25e280b796"}, + {file = "charset_normalizer-3.3.2-cp39-cp39-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:122c7fa62b130ed55f8f285bfd56d5f4b4a5b503609d181f9ad85e55c89f4185"}, + {file = "charset_normalizer-3.3.2-cp39-cp39-musllinux_1_1_aarch64.whl", hash = "sha256:d0eccceffcb53201b5bfebb52600a5fb483a20b61da9dbc885f8b103cbe7598c"}, + {file = "charset_normalizer-3.3.2-cp39-cp39-musllinux_1_1_i686.whl", hash = "sha256:9f96df6923e21816da7e0ad3fd47dd8f94b2a5ce594e00677c0013018b813458"}, + {file = "charset_normalizer-3.3.2-cp39-cp39-musllinux_1_1_ppc64le.whl", hash = "sha256:7f04c839ed0b6b98b1a7501a002144b76c18fb1c1850c8b98d458ac269e26ed2"}, + {file = "charset_normalizer-3.3.2-cp39-cp39-musllinux_1_1_s390x.whl", hash = "sha256:34d1c8da1e78d2e001f363791c98a272bb734000fcef47a491c1e3b0505657a8"}, + {file = "charset_normalizer-3.3.2-cp39-cp39-musllinux_1_1_x86_64.whl", hash = "sha256:ff8fa367d09b717b2a17a052544193ad76cd49979c805768879cb63d9ca50561"}, + {file = "charset_normalizer-3.3.2-cp39-cp39-win32.whl", hash = "sha256:aed38f6e4fb3f5d6bf81bfa990a07806be9d83cf7bacef998ab1a9bd660a581f"}, + {file = "charset_normalizer-3.3.2-cp39-cp39-win_amd64.whl", hash = "sha256:b01b88d45a6fcb69667cd6d2f7a9aeb4bf53760d7fc536bf679ec94fe9f3ff3d"}, + {file = "charset_normalizer-3.3.2-py3-none-any.whl", hash = "sha256:3e4d1f6587322d2788836a99c69062fbb091331ec940e02d12d179c1d53e25fc"}, +] + +[[package]] +name = "colorama" +version = "0.4.6" +description = "Cross-platform colored terminal text." +optional = false +python-versions = "!=3.0.*,!=3.1.*,!=3.2.*,!=3.3.*,!=3.4.*,!=3.5.*,!=3.6.*,>=2.7" +groups = ["dev"] +markers = "sys_platform == \"win32\"" +files = [ + {file = "colorama-0.4.6-py2.py3-none-any.whl", hash = "sha256:4f1d9991f5acc0ca119f9d443620b77f9d6b33703e51011c16baf57afb285fc6"}, + {file = "colorama-0.4.6.tar.gz", hash = "sha256:08695f5cb7ed6e0531a20572697297273c47b8cae5a63ffc6d6ed5c201be6e44"}, +] + +[[package]] +name = "coverage" +version = "7.2.7" +description = "Code coverage measurement for Python" +optional = false +python-versions = ">=3.7" +groups = ["dev"] +files = [ + {file = "coverage-7.2.7-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:d39b5b4f2a66ccae8b7263ac3c8170994b65266797fb96cbbfd3fb5b23921db8"}, + {file = "coverage-7.2.7-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:6d040ef7c9859bb11dfeb056ff5b3872436e3b5e401817d87a31e1750b9ae2fb"}, + {file = "coverage-7.2.7-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:ba90a9563ba44a72fda2e85302c3abc71c5589cea608ca16c22b9804262aaeb6"}, + {file = "coverage-7.2.7-cp310-cp310-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:e7d9405291c6928619403db1d10bd07888888ec1abcbd9748fdaa971d7d661b2"}, + {file = "coverage-7.2.7-cp310-cp310-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:31563e97dae5598556600466ad9beea39fb04e0229e61c12eaa206e0aa202063"}, + {file = "coverage-7.2.7-cp310-cp310-musllinux_1_1_aarch64.whl", hash = "sha256:ebba1cd308ef115925421d3e6a586e655ca5a77b5bf41e02eb0e4562a111f2d1"}, + {file = "coverage-7.2.7-cp310-cp310-musllinux_1_1_i686.whl", hash = "sha256:cb017fd1b2603ef59e374ba2063f593abe0fc45f2ad9abdde5b4d83bd922a353"}, + {file = "coverage-7.2.7-cp310-cp310-musllinux_1_1_x86_64.whl", hash = "sha256:d62a5c7dad11015c66fbb9d881bc4caa5b12f16292f857842d9d1871595f4495"}, + {file = "coverage-7.2.7-cp310-cp310-win32.whl", hash = "sha256:ee57190f24fba796e36bb6d3aa8a8783c643d8fa9760c89f7a98ab5455fbf818"}, + {file = "coverage-7.2.7-cp310-cp310-win_amd64.whl", hash = "sha256:f75f7168ab25dd93110c8a8117a22450c19976afbc44234cbf71481094c1b850"}, + {file = "coverage-7.2.7-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:06a9a2be0b5b576c3f18f1a241f0473575c4a26021b52b2a85263a00f034d51f"}, + {file = "coverage-7.2.7-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:5baa06420f837184130752b7c5ea0808762083bf3487b5038d68b012e5937dbe"}, + {file = "coverage-7.2.7-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:fdec9e8cbf13a5bf63290fc6013d216a4c7232efb51548594ca3631a7f13c3a3"}, + {file = "coverage-7.2.7-cp311-cp311-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:52edc1a60c0d34afa421c9c37078817b2e67a392cab17d97283b64c5833f427f"}, + {file = "coverage-7.2.7-cp311-cp311-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:63426706118b7f5cf6bb6c895dc215d8a418d5952544042c8a2d9fe87fcf09cb"}, + {file = "coverage-7.2.7-cp311-cp311-musllinux_1_1_aarch64.whl", hash = "sha256:afb17f84d56068a7c29f5fa37bfd38d5aba69e3304af08ee94da8ed5b0865833"}, + {file = "coverage-7.2.7-cp311-cp311-musllinux_1_1_i686.whl", hash = "sha256:48c19d2159d433ccc99e729ceae7d5293fbffa0bdb94952d3579983d1c8c9d97"}, + {file = "coverage-7.2.7-cp311-cp311-musllinux_1_1_x86_64.whl", hash = "sha256:0e1f928eaf5469c11e886fe0885ad2bf1ec606434e79842a879277895a50942a"}, + {file = "coverage-7.2.7-cp311-cp311-win32.whl", hash = "sha256:33d6d3ea29d5b3a1a632b3c4e4f4ecae24ef170b0b9ee493883f2df10039959a"}, + {file = "coverage-7.2.7-cp311-cp311-win_amd64.whl", hash = "sha256:5b7540161790b2f28143191f5f8ec02fb132660ff175b7747b95dcb77ac26562"}, + {file = "coverage-7.2.7-cp312-cp312-macosx_10_9_x86_64.whl", hash = "sha256:f2f67fe12b22cd130d34d0ef79206061bfb5eda52feb6ce0dba0644e20a03cf4"}, + {file = "coverage-7.2.7-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:a342242fe22407f3c17f4b499276a02b01e80f861f1682ad1d95b04018e0c0d4"}, + {file = "coverage-7.2.7-cp312-cp312-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:171717c7cb6b453aebac9a2ef603699da237f341b38eebfee9be75d27dc38e01"}, + {file = "coverage-7.2.7-cp312-cp312-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:49969a9f7ffa086d973d91cec8d2e31080436ef0fb4a359cae927e742abfaaa6"}, + {file = "coverage-7.2.7-cp312-cp312-musllinux_1_1_aarch64.whl", hash = "sha256:b46517c02ccd08092f4fa99f24c3b83d8f92f739b4657b0f146246a0ca6a831d"}, + {file = "coverage-7.2.7-cp312-cp312-musllinux_1_1_i686.whl", hash = "sha256:a3d33a6b3eae87ceaefa91ffdc130b5e8536182cd6dfdbfc1aa56b46ff8c86de"}, + {file = "coverage-7.2.7-cp312-cp312-musllinux_1_1_x86_64.whl", hash = "sha256:976b9c42fb2a43ebf304fa7d4a310e5f16cc99992f33eced91ef6f908bd8f33d"}, + {file = "coverage-7.2.7-cp312-cp312-win32.whl", hash = "sha256:8de8bb0e5ad103888d65abef8bca41ab93721647590a3f740100cd65c3b00511"}, + {file = "coverage-7.2.7-cp312-cp312-win_amd64.whl", hash = "sha256:9e31cb64d7de6b6f09702bb27c02d1904b3aebfca610c12772452c4e6c21a0d3"}, + {file = "coverage-7.2.7-cp37-cp37m-macosx_10_9_x86_64.whl", hash = "sha256:58c2ccc2f00ecb51253cbe5d8d7122a34590fac9646a960d1430d5b15321d95f"}, + {file = "coverage-7.2.7-cp37-cp37m-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:d22656368f0e6189e24722214ed8d66b8022db19d182927b9a248a2a8a2f67eb"}, + {file = "coverage-7.2.7-cp37-cp37m-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:a895fcc7b15c3fc72beb43cdcbdf0ddb7d2ebc959edac9cef390b0d14f39f8a9"}, + {file = "coverage-7.2.7-cp37-cp37m-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:e84606b74eb7de6ff581a7915e2dab7a28a0517fbe1c9239eb227e1354064dcd"}, + {file = "coverage-7.2.7-cp37-cp37m-musllinux_1_1_aarch64.whl", hash = "sha256:0a5f9e1dbd7fbe30196578ca36f3fba75376fb99888c395c5880b355e2875f8a"}, + {file = "coverage-7.2.7-cp37-cp37m-musllinux_1_1_i686.whl", hash = "sha256:419bfd2caae268623dd469eff96d510a920c90928b60f2073d79f8fe2bbc5959"}, + {file = "coverage-7.2.7-cp37-cp37m-musllinux_1_1_x86_64.whl", hash = "sha256:2aee274c46590717f38ae5e4650988d1af340fe06167546cc32fe2f58ed05b02"}, + {file = "coverage-7.2.7-cp37-cp37m-win32.whl", hash = "sha256:61b9a528fb348373c433e8966535074b802c7a5d7f23c4f421e6c6e2f1697a6f"}, + {file = "coverage-7.2.7-cp37-cp37m-win_amd64.whl", hash = "sha256:b1c546aca0ca4d028901d825015dc8e4d56aac4b541877690eb76490f1dc8ed0"}, + {file = "coverage-7.2.7-cp38-cp38-macosx_10_9_x86_64.whl", hash = "sha256:54b896376ab563bd38453cecb813c295cf347cf5906e8b41d340b0321a5433e5"}, + {file = "coverage-7.2.7-cp38-cp38-macosx_11_0_arm64.whl", hash = "sha256:3d376df58cc111dc8e21e3b6e24606b5bb5dee6024f46a5abca99124b2229ef5"}, + {file = "coverage-7.2.7-cp38-cp38-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:5e330fc79bd7207e46c7d7fd2bb4af2963f5f635703925543a70b99574b0fea9"}, + {file = "coverage-7.2.7-cp38-cp38-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:1e9d683426464e4a252bf70c3498756055016f99ddaec3774bf368e76bbe02b6"}, + {file = "coverage-7.2.7-cp38-cp38-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:8d13c64ee2d33eccf7437961b6ea7ad8673e2be040b4f7fd4fd4d4d28d9ccb1e"}, + {file = "coverage-7.2.7-cp38-cp38-musllinux_1_1_aarch64.whl", hash = "sha256:b7aa5f8a41217360e600da646004f878250a0d6738bcdc11a0a39928d7dc2050"}, + {file = "coverage-7.2.7-cp38-cp38-musllinux_1_1_i686.whl", hash = "sha256:8fa03bce9bfbeeef9f3b160a8bed39a221d82308b4152b27d82d8daa7041fee5"}, + {file = "coverage-7.2.7-cp38-cp38-musllinux_1_1_x86_64.whl", hash = "sha256:245167dd26180ab4c91d5e1496a30be4cd721a5cf2abf52974f965f10f11419f"}, + {file = "coverage-7.2.7-cp38-cp38-win32.whl", hash = "sha256:d2c2db7fd82e9b72937969bceac4d6ca89660db0a0967614ce2481e81a0b771e"}, + {file = "coverage-7.2.7-cp38-cp38-win_amd64.whl", hash = "sha256:2e07b54284e381531c87f785f613b833569c14ecacdcb85d56b25c4622c16c3c"}, + {file = "coverage-7.2.7-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:537891ae8ce59ef63d0123f7ac9e2ae0fc8b72c7ccbe5296fec45fd68967b6c9"}, + {file = "coverage-7.2.7-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:06fb182e69f33f6cd1d39a6c597294cff3143554b64b9825d1dc69d18cc2fff2"}, + {file = "coverage-7.2.7-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:201e7389591af40950a6480bd9edfa8ed04346ff80002cec1a66cac4549c1ad7"}, + {file = "coverage-7.2.7-cp39-cp39-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:f6951407391b639504e3b3be51b7ba5f3528adbf1a8ac3302b687ecababf929e"}, + {file = "coverage-7.2.7-cp39-cp39-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:6f48351d66575f535669306aa7d6d6f71bc43372473b54a832222803eb956fd1"}, + {file = "coverage-7.2.7-cp39-cp39-musllinux_1_1_aarch64.whl", hash = "sha256:b29019c76039dc3c0fd815c41392a044ce555d9bcdd38b0fb60fb4cd8e475ba9"}, + {file = "coverage-7.2.7-cp39-cp39-musllinux_1_1_i686.whl", hash = "sha256:81c13a1fc7468c40f13420732805a4c38a105d89848b7c10af65a90beff25250"}, + {file = "coverage-7.2.7-cp39-cp39-musllinux_1_1_x86_64.whl", hash = "sha256:975d70ab7e3c80a3fe86001d8751f6778905ec723f5b110aed1e450da9d4b7f2"}, + {file = "coverage-7.2.7-cp39-cp39-win32.whl", hash = "sha256:7ee7d9d4822c8acc74a5e26c50604dff824710bc8de424904c0982e25c39c6cb"}, + {file = "coverage-7.2.7-cp39-cp39-win_amd64.whl", hash = "sha256:eb393e5ebc85245347950143969b241d08b52b88a3dc39479822e073a1a8eb27"}, + {file = "coverage-7.2.7-pp37.pp38.pp39-none-any.whl", hash = "sha256:b7b4c971f05e6ae490fef852c218b0e79d4e52f79ef0c8475566584a8fb3e01d"}, + {file = "coverage-7.2.7.tar.gz", hash = "sha256:924d94291ca674905fe9481f12294eb11f2d3d3fd1adb20314ba89e94f44ed59"}, +] + +[package.dependencies] +tomli = {version = "*", optional = true, markers = "python_full_version <= \"3.11.0a6\" and extra == \"toml\""} + +[package.extras] +toml = ["tomli ; python_full_version <= \"3.11.0a6\""] + +[[package]] +name = "distlib" +version = "0.3.8" +description = "Distribution utilities" +optional = false +python-versions = "*" +groups = ["dev"] +files = [ + {file = "distlib-0.3.8-py2.py3-none-any.whl", hash = "sha256:034db59a0b96f8ca18035f36290806a9a6e6bd9d1ff91e45a7f172eb17e51784"}, + {file = "distlib-0.3.8.tar.gz", hash = "sha256:1530ea13e350031b6312d8580ddb6b27a104275a31106523b8f123787f494f64"}, +] + +[[package]] +name = "docutils" +version = "0.17.1" +description = "Docutils -- Python Documentation Utilities" +optional = false +python-versions = ">=2.7, !=3.0.*, !=3.1.*, !=3.2.*, !=3.3.*, !=3.4.*" +groups = ["dev"] +files = [ + {file = "docutils-0.17.1-py2.py3-none-any.whl", hash = "sha256:cf316c8370a737a022b72b56874f6602acf974a37a9fba42ec2876387549fc61"}, + {file = "docutils-0.17.1.tar.gz", hash = "sha256:686577d2e4c32380bb50cbb22f575ed742d58168cee37e99117a854bcd88f125"}, +] + +[[package]] +name = "exceptiongroup" +version = "1.2.1" +description = "Backport of PEP 654 (exception groups)" +optional = false +python-versions = ">=3.7" +groups = ["dev"] +markers = "python_version < \"3.11\"" +files = [ + {file = "exceptiongroup-1.2.1-py3-none-any.whl", hash = "sha256:5258b9ed329c5bbdd31a309f53cbfb0b155341807f6ff7606a1e801a891b29ad"}, + {file = "exceptiongroup-1.2.1.tar.gz", hash = "sha256:a4785e48b045528f5bfe627b6ad554ff32def154f42372786903b7abcfe1aa16"}, +] + +[package.extras] +test = ["pytest (>=6)"] + +[[package]] +name = "filelock" +version = "3.19.1" +description = "A platform independent file lock." +optional = false +python-versions = ">=3.9" +groups = ["dev"] +markers = "python_version == \"3.9\"" +files = [ + {file = "filelock-3.19.1-py3-none-any.whl", hash = "sha256:d38e30481def20772f5baf097c122c3babc4fcdb7e14e57049eb9d88c6dc017d"}, + {file = "filelock-3.19.1.tar.gz", hash = "sha256:66eda1888b0171c998b35be2bcc0f6d75c388a7ce20c3f3f37aa8e96c2dddf58"}, +] + +[[package]] +name = "filelock" +version = "3.20.3" +description = "A platform independent file lock." +optional = false +python-versions = ">=3.10" +groups = ["dev"] +markers = "python_version >= \"3.10\"" +files = [ + {file = "filelock-3.20.3-py3-none-any.whl", hash = "sha256:4b0dda527ee31078689fc205ec4f1c1bf7d56cf88b6dc9426c4f230e46c2dce1"}, + {file = "filelock-3.20.3.tar.gz", hash = "sha256:18c57ee915c7ec61cff0ecf7f0f869936c7c30191bb0cf406f1341778d0834e1"}, +] + +[[package]] +name = "furo" +version = "2022.9.29" +description = "A clean customisable Sphinx documentation theme." +optional = false +python-versions = ">=3.7" +groups = ["dev"] +files = [ + {file = "furo-2022.9.29-py3-none-any.whl", hash = "sha256:559ee17999c0f52728481dcf6b1b0cf8c9743e68c5e3a18cb45a7992747869a9"}, + {file = "furo-2022.9.29.tar.gz", hash = "sha256:d4238145629c623609c2deb5384f8d036e2a1ee2a101d64b67b4348112470dbd"}, +] + +[package.dependencies] +beautifulsoup4 = "*" +pygments = ">=2.7" +sphinx = ">=4.0,<6.0" +sphinx-basic-ng = "*" + +[[package]] +name = "identify" +version = "2.5.24" +description = "File identification library for Python" +optional = false +python-versions = ">=3.7" +groups = ["dev"] +files = [ + {file = "identify-2.5.24-py2.py3-none-any.whl", hash = "sha256:986dbfb38b1140e763e413e6feb44cd731faf72d1909543178aa79b0e258265d"}, + {file = "identify-2.5.24.tar.gz", hash = "sha256:0aac67d5b4812498056d28a9a512a483f5085cc28640b02b258a59dac34301d4"}, +] + +[package.extras] +license = ["ukkonen"] + +[[package]] +name = "idna" +version = "3.15" +description = "Internationalized Domain Names in Applications (IDNA)" +optional = false +python-versions = ">=3.8" +groups = ["dev"] +files = [ + {file = "idna-3.15-py3-none-any.whl", hash = "sha256:048adeaf8c2d788c40fee287673ccaa74c24ffd8dcf09ffa555a2fbb59f10ac8"}, + {file = "idna-3.15.tar.gz", hash = "sha256:ca962446ea538f7092a95e057da437618e886f4d349216d2b1e294abfdb65fdc"}, +] + +[package.extras] +all = ["mypy (>=1.11.2)", "pytest (>=8.3.2)", "ruff (>=0.6.2)"] + +[[package]] +name = "imagesize" +version = "1.4.1" +description = "Getting image size from png/jpeg/jpeg2000/gif file" +optional = false +python-versions = ">=2.7, !=3.0.*, !=3.1.*, !=3.2.*, !=3.3.*" +groups = ["dev"] +files = [ + {file = "imagesize-1.4.1-py2.py3-none-any.whl", hash = "sha256:0d8d18d08f840c19d0ee7ca1fd82490fdc3729b7ac93f49870406ddde8ef8d8b"}, + {file = "imagesize-1.4.1.tar.gz", hash = "sha256:69150444affb9cb0d5cc5a92b3676f0b2fb7cd9ae39e947a5e11a36b4497cd4a"}, +] + +[[package]] +name = "importlib-metadata" +version = "6.7.0" +description = "Read metadata from Python packages" +optional = false +python-versions = ">=3.7" +groups = ["dev"] +markers = "python_version == \"3.9\"" +files = [ + {file = "importlib_metadata-6.7.0-py3-none-any.whl", hash = "sha256:cb52082e659e97afc5dac71e79de97d8681de3aa07ff18578330904a9d18e5b5"}, + {file = "importlib_metadata-6.7.0.tar.gz", hash = "sha256:1aaf550d4f73e5d6783e7acb77aec43d49da8017410afae93822cc9cca98c4d4"}, +] + +[package.dependencies] +zipp = ">=0.5" + +[package.extras] +docs = ["furo", "jaraco.packaging (>=9)", "jaraco.tidelift (>=1.4)", "rst.linker (>=1.9)", "sphinx (>=3.5)", "sphinx-lint"] +perf = ["ipython"] +testing = ["flufl.flake8", "importlib-resources (>=1.3) ; python_version < \"3.9\"", "packaging", "pyfakefs", "pytest (>=6)", "pytest-black (>=0.3.7) ; platform_python_implementation != \"PyPy\"", "pytest-checkdocs (>=2.4)", "pytest-cov", "pytest-enabler (>=1.3)", "pytest-mypy (>=0.9.1) ; platform_python_implementation != \"PyPy\"", "pytest-perf (>=0.9.2)", "pytest-ruff"] + +[[package]] +name = "iniconfig" +version = "2.0.0" +description = "brain-dead simple config-ini parsing" +optional = false +python-versions = ">=3.7" +groups = ["dev"] +files = [ + {file = "iniconfig-2.0.0-py3-none-any.whl", hash = "sha256:b6a85871a79d2e3b22d2d1b94ac2824226a63c6b741c88f7ae975f18b6778374"}, + {file = "iniconfig-2.0.0.tar.gz", hash = "sha256:2d91e135bf72d31a410b17c16da610a82cb55f6b0477d1a902134b24a455b8b3"}, +] + +[[package]] +name = "jinja2" +version = "3.1.6" +description = "A very fast and expressive template engine." +optional = false +python-versions = ">=3.7" +groups = ["dev"] +files = [ + {file = "jinja2-3.1.6-py3-none-any.whl", hash = "sha256:85ece4451f492d0c13c5dd7c13a64681a86afae63a5f347908daf103ce6d2f67"}, + {file = "jinja2-3.1.6.tar.gz", hash = "sha256:0137fb05990d35f1275a587e9aee6d56da821fc83491a0fb838183be43f66d6d"}, +] + +[package.dependencies] +MarkupSafe = ">=2.0" + +[package.extras] +i18n = ["Babel (>=2.7)"] + +[[package]] +name = "librt" +version = "0.8.1" +description = "Mypyc runtime library" +optional = false +python-versions = ">=3.9" +groups = ["dev"] +markers = "platform_python_implementation != \"PyPy\"" +files = [ + {file = "librt-0.8.1-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:81fd938344fecb9373ba1b155968c8a329491d2ce38e7ddb76f30ffb938f12dc"}, + {file = "librt-0.8.1-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:5db05697c82b3a2ec53f6e72b2ed373132b0c2e05135f0696784e97d7f5d48e7"}, + {file = "librt-0.8.1-cp310-cp310-manylinux1_i686.manylinux_2_28_i686.manylinux_2_5_i686.whl", hash = "sha256:d56bc4011975f7460bea7b33e1ff425d2f1adf419935ff6707273c77f8a4ada6"}, + {file = "librt-0.8.1-cp310-cp310-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:5cdc0f588ff4b663ea96c26d2a230c525c6fc62b28314edaaaca8ed5af931ad0"}, + {file = "librt-0.8.1-cp310-cp310-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:97c2b54ff6717a7a563b72627990bec60d8029df17df423f0ed37d56a17a176b"}, + {file = "librt-0.8.1-cp310-cp310-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:8f1125e6bbf2f1657d9a2f3ccc4a2c9b0c8b176965bb565dd4d86be67eddb4b6"}, + {file = "librt-0.8.1-cp310-cp310-musllinux_1_2_aarch64.whl", hash = "sha256:8f4bb453f408137d7581be309b2fbc6868a80e7ef60c88e689078ee3a296ae71"}, + {file = "librt-0.8.1-cp310-cp310-musllinux_1_2_i686.whl", hash = "sha256:c336d61d2fe74a3195edc1646d53ff1cddd3a9600b09fa6ab75e5514ba4862a7"}, + {file = "librt-0.8.1-cp310-cp310-musllinux_1_2_riscv64.whl", hash = "sha256:eb5656019db7c4deacf0c1a55a898c5bb8f989be904597fcb5232a2f4828fa05"}, + {file = "librt-0.8.1-cp310-cp310-musllinux_1_2_x86_64.whl", hash = "sha256:c25d9e338d5bed46c1632f851babf3d13c78f49a225462017cf5e11e845c5891"}, + {file = "librt-0.8.1-cp310-cp310-win32.whl", hash = "sha256:aaab0e307e344cb28d800957ef3ec16605146ef0e59e059a60a176d19543d1b7"}, + {file = "librt-0.8.1-cp310-cp310-win_amd64.whl", hash = "sha256:56e04c14b696300d47b3bc5f1d10a00e86ae978886d0cee14e5714fafb5df5d2"}, + {file = "librt-0.8.1-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:681dc2451d6d846794a828c16c22dc452d924e9f700a485b7ecb887a30aad1fd"}, + {file = "librt-0.8.1-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:a3b4350b13cc0e6f5bec8fa7caf29a8fb8cdc051a3bae45cfbfd7ce64f009965"}, + {file = "librt-0.8.1-cp311-cp311-manylinux1_i686.manylinux_2_28_i686.manylinux_2_5_i686.whl", hash = "sha256:ac1e7817fd0ed3d14fd7c5df91daed84c48e4c2a11ee99c0547f9f62fdae13da"}, + {file = "librt-0.8.1-cp311-cp311-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:747328be0c5b7075cde86a0e09d7a9196029800ba75a1689332348e998fb85c0"}, + {file = "librt-0.8.1-cp311-cp311-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:f0af2bd2bc204fa27f3d6711d0f360e6b8c684a035206257a81673ab924aa11e"}, + {file = "librt-0.8.1-cp311-cp311-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:d480de377f5b687b6b1bc0c0407426da556e2a757633cc7e4d2e1a057aa688f3"}, + {file = "librt-0.8.1-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:d0ee06b5b5291f609ddb37b9750985b27bc567791bc87c76a569b3feed8481ac"}, + {file = "librt-0.8.1-cp311-cp311-musllinux_1_2_i686.whl", hash = "sha256:9e2c6f77b9ad48ce5603b83b7da9ee3e36b3ab425353f695cba13200c5d96596"}, + {file = "librt-0.8.1-cp311-cp311-musllinux_1_2_riscv64.whl", hash = "sha256:439352ba9373f11cb8e1933da194dcc6206daf779ff8df0ed69c5e39113e6a99"}, + {file = "librt-0.8.1-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:82210adabbc331dbb65d7868b105185464ef13f56f7f76688565ad79f648b0fe"}, + {file = "librt-0.8.1-cp311-cp311-win32.whl", hash = "sha256:52c224e14614b750c0a6d97368e16804a98c684657c7518752c356834fff83bb"}, + {file = "librt-0.8.1-cp311-cp311-win_amd64.whl", hash = "sha256:c00e5c884f528c9932d278d5c9cbbea38a6b81eb62c02e06ae53751a83a4d52b"}, + {file = "librt-0.8.1-cp311-cp311-win_arm64.whl", hash = "sha256:f7cdf7f26c2286ffb02e46d7bac56c94655540b26347673bea15fa52a6af17e9"}, + {file = "librt-0.8.1-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:a28f2612ab566b17f3698b0da021ff9960610301607c9a5e8eaca62f5e1c350a"}, + {file = "librt-0.8.1-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:60a78b694c9aee2a0f1aaeaa7d101cf713e92e8423a941d2897f4fa37908dab9"}, + {file = "librt-0.8.1-cp312-cp312-manylinux1_i686.manylinux_2_28_i686.manylinux_2_5_i686.whl", hash = "sha256:758509ea3f1eba2a57558e7e98f4659d0ea7670bff49673b0dde18a3c7e6c0eb"}, + {file = "librt-0.8.1-cp312-cp312-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:039b9f2c506bd0ab0f8725aa5ba339c6f0cd19d3b514b50d134789809c24285d"}, + {file = "librt-0.8.1-cp312-cp312-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:5bb54f1205a3a6ab41a6fd71dfcdcbd278670d3a90ca502a30d9da583105b6f7"}, + {file = "librt-0.8.1-cp312-cp312-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:05bd41cdee35b0c59c259f870f6da532a2c5ca57db95b5f23689fcb5c9e42440"}, + {file = "librt-0.8.1-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:adfab487facf03f0d0857b8710cf82d0704a309d8ffc33b03d9302b4c64e91a9"}, + {file = "librt-0.8.1-cp312-cp312-musllinux_1_2_i686.whl", hash = "sha256:153188fe98a72f206042be10a2c6026139852805215ed9539186312d50a8e972"}, + {file = "librt-0.8.1-cp312-cp312-musllinux_1_2_riscv64.whl", hash = "sha256:dd3c41254ee98604b08bd5b3af5bf0a89740d4ee0711de95b65166bf44091921"}, + {file = "librt-0.8.1-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:e0d138c7ae532908cbb342162b2611dbd4d90c941cd25ab82084aaf71d2c0bd0"}, + {file = "librt-0.8.1-cp312-cp312-win32.whl", hash = "sha256:43353b943613c5d9c49a25aaffdba46f888ec354e71e3529a00cca3f04d66a7a"}, + {file = "librt-0.8.1-cp312-cp312-win_amd64.whl", hash = "sha256:ff8baf1f8d3f4b6b7257fcb75a501f2a5499d0dda57645baa09d4d0d34b19444"}, + {file = "librt-0.8.1-cp312-cp312-win_arm64.whl", hash = "sha256:0f2ae3725904f7377e11cc37722d5d401e8b3d5851fb9273d7f4fe04f6b3d37d"}, + {file = "librt-0.8.1-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:7e6bad1cd94f6764e1e21950542f818a09316645337fd5ab9a7acc45d99a8f35"}, + {file = "librt-0.8.1-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:cf450f498c30af55551ba4f66b9123b7185362ec8b625a773b3d39aa1a717583"}, + {file = "librt-0.8.1-cp313-cp313-manylinux1_i686.manylinux_2_28_i686.manylinux_2_5_i686.whl", hash = "sha256:eca45e982fa074090057132e30585a7e8674e9e885d402eae85633e9f449ce6c"}, + {file = "librt-0.8.1-cp313-cp313-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:0c3811485fccfda840861905b8c70bba5ec094e02825598bb9d4ca3936857a04"}, + {file = "librt-0.8.1-cp313-cp313-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:5e4af413908f77294605e28cfd98063f54b2c790561383971d2f52d113d9c363"}, + {file = "librt-0.8.1-cp313-cp313-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:5212a5bd7fae98dae95710032902edcd2ec4dc994e883294f75c857b83f9aba0"}, + {file = "librt-0.8.1-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:e692aa2d1d604e6ca12d35e51fdc36f4cda6345e28e36374579f7ef3611b3012"}, + {file = "librt-0.8.1-cp313-cp313-musllinux_1_2_i686.whl", hash = "sha256:4be2a5c926b9770c9e08e717f05737a269b9d0ebc5d2f0060f0fe3fe9ce47acb"}, + {file = "librt-0.8.1-cp313-cp313-musllinux_1_2_riscv64.whl", hash = "sha256:fd1a720332ea335ceb544cf0a03f81df92abd4bb887679fd1e460976b0e6214b"}, + {file = "librt-0.8.1-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:93c2af9e01e0ef80d95ae3c720be101227edae5f2fe7e3dc63d8857fadfc5a1d"}, + {file = "librt-0.8.1-cp313-cp313-win32.whl", hash = "sha256:086a32dbb71336627e78cc1d6ee305a68d038ef7d4c39aaff41ae8c9aa46e91a"}, + {file = "librt-0.8.1-cp313-cp313-win_amd64.whl", hash = "sha256:e11769a1dbda4da7b00a76cfffa67aa47cfa66921d2724539eee4b9ede780b79"}, + {file = "librt-0.8.1-cp313-cp313-win_arm64.whl", hash = "sha256:924817ab3141aca17893386ee13261f1d100d1ef410d70afe4389f2359fea4f0"}, + {file = "librt-0.8.1-cp314-cp314-macosx_10_13_x86_64.whl", hash = "sha256:6cfa7fe54fd4d1f47130017351a959fe5804bda7a0bc7e07a2cdbc3fdd28d34f"}, + {file = "librt-0.8.1-cp314-cp314-macosx_11_0_arm64.whl", hash = "sha256:228c2409c079f8c11fb2e5d7b277077f694cb93443eb760e00b3b83cb8b3176c"}, + {file = "librt-0.8.1-cp314-cp314-manylinux1_i686.manylinux_2_28_i686.manylinux_2_5_i686.whl", hash = "sha256:7aae78ab5e3206181780e56912d1b9bb9f90a7249ce12f0e8bf531d0462dd0fc"}, + {file = "librt-0.8.1-cp314-cp314-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:172d57ec04346b047ca6af181e1ea4858086c80bdf455f61994c4aa6fc3f866c"}, + {file = "librt-0.8.1-cp314-cp314-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:6b1977c4ea97ce5eb7755a78fae68d87e4102e4aaf54985e8b56806849cc06a3"}, + {file = "librt-0.8.1-cp314-cp314-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:10c42e1f6fd06733ef65ae7bebce2872bcafd8d6e6b0a08fe0a05a23b044fb14"}, + {file = "librt-0.8.1-cp314-cp314-musllinux_1_2_aarch64.whl", hash = "sha256:4c8dfa264b9193c4ee19113c985c95f876fae5e51f731494fc4e0cf594990ba7"}, + {file = "librt-0.8.1-cp314-cp314-musllinux_1_2_i686.whl", hash = "sha256:01170b6729a438f0dedc4a26ed342e3dc4f02d1000b4b19f980e1877f0c297e6"}, + {file = "librt-0.8.1-cp314-cp314-musllinux_1_2_riscv64.whl", hash = "sha256:7b02679a0d783bdae30d443025b94465d8c3dc512f32f5b5031f93f57ac32071"}, + {file = "librt-0.8.1-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:190b109bb69592a3401fe1ffdea41a2e73370ace2ffdc4a0e8e2b39cdea81b78"}, + {file = "librt-0.8.1-cp314-cp314-win32.whl", hash = "sha256:e70a57ecf89a0f64c24e37f38d3fe217a58169d2fe6ed6d70554964042474023"}, + {file = "librt-0.8.1-cp314-cp314-win_amd64.whl", hash = "sha256:7e2f3edca35664499fbb36e4770650c4bd4a08abc1f4458eab9df4ec56389730"}, + {file = "librt-0.8.1-cp314-cp314-win_arm64.whl", hash = "sha256:0d2f82168e55ddefd27c01c654ce52379c0750ddc31ee86b4b266bcf4d65f2a3"}, + {file = "librt-0.8.1-cp314-cp314t-macosx_10_13_x86_64.whl", hash = "sha256:2c74a2da57a094bd48d03fa5d196da83d2815678385d2978657499063709abe1"}, + {file = "librt-0.8.1-cp314-cp314t-macosx_11_0_arm64.whl", hash = "sha256:a355d99c4c0d8e5b770313b8b247411ed40949ca44e33e46a4789b9293a907ee"}, + {file = "librt-0.8.1-cp314-cp314t-manylinux1_i686.manylinux_2_28_i686.manylinux_2_5_i686.whl", hash = "sha256:2eb345e8b33fb748227409c9f1233d4df354d6e54091f0e8fc53acdb2ffedeb7"}, + {file = "librt-0.8.1-cp314-cp314t-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:9be2f15e53ce4e83cc08adc29b26fb5978db62ef2a366fbdf716c8a6c8901040"}, + {file = "librt-0.8.1-cp314-cp314t-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:785ae29c1f5c6e7c2cde2c7c0e148147f4503da3abc5d44d482068da5322fd9e"}, + {file = "librt-0.8.1-cp314-cp314t-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:1d3a7da44baf692f0c6aeb5b2a09c5e6fc7a703bca9ffa337ddd2e2da53f7732"}, + {file = "librt-0.8.1-cp314-cp314t-musllinux_1_2_aarch64.whl", hash = "sha256:5fc48998000cbc39ec0d5311312dda93ecf92b39aaf184c5e817d5d440b29624"}, + {file = "librt-0.8.1-cp314-cp314t-musllinux_1_2_i686.whl", hash = "sha256:e96baa6820280077a78244b2e06e416480ed859bbd8e5d641cf5742919d8beb4"}, + {file = "librt-0.8.1-cp314-cp314t-musllinux_1_2_riscv64.whl", hash = "sha256:31362dbfe297b23590530007062c32c6f6176f6099646bb2c95ab1b00a57c382"}, + {file = "librt-0.8.1-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:cc3656283d11540ab0ea01978378e73e10002145117055e03722417aeab30994"}, + {file = "librt-0.8.1-cp314-cp314t-win32.whl", hash = "sha256:738f08021b3142c2918c03692608baed43bc51144c29e35807682f8070ee2a3a"}, + {file = "librt-0.8.1-cp314-cp314t-win_amd64.whl", hash = "sha256:89815a22daf9c51884fb5dbe4f1ef65ee6a146e0b6a8df05f753e2e4a9359bf4"}, + {file = "librt-0.8.1-cp314-cp314t-win_arm64.whl", hash = "sha256:bf512a71a23504ed08103a13c941f763db13fb11177beb3d9244c98c29fb4a61"}, + {file = "librt-0.8.1-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:3dff3d3ca8db20e783b1bc7de49c0a2ab0b8387f31236d6a026597d07fcd68ac"}, + {file = "librt-0.8.1-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:08eec3a1fc435f0d09c87b6bf1ec798986a3544f446b864e4099633a56fcd9ed"}, + {file = "librt-0.8.1-cp39-cp39-manylinux1_i686.manylinux_2_28_i686.manylinux_2_5_i686.whl", hash = "sha256:e3f0a41487fd5fad7e760b9e8a90e251e27c2816fbc2cff36a22a0e6bcbbd9dd"}, + {file = "librt-0.8.1-cp39-cp39-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:bacdb58d9939d95cc557b4dbaa86527c9db2ac1ed76a18bc8d26f6dc8647d851"}, + {file = "librt-0.8.1-cp39-cp39-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:b6d7ab1f01aa753188605b09a51faa44a3327400b00b8cce424c71910fc0a128"}, + {file = "librt-0.8.1-cp39-cp39-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:4998009e7cb9e896569f4be7004f09d0ed70d386fa99d42b6d363f6d200501ac"}, + {file = "librt-0.8.1-cp39-cp39-musllinux_1_2_aarch64.whl", hash = "sha256:2cc68eeeef5e906839c7bb0815748b5b0a974ec27125beefc0f942715785b551"}, + {file = "librt-0.8.1-cp39-cp39-musllinux_1_2_i686.whl", hash = "sha256:0bf69d79a23f4f40b8673a947a234baeeb133b5078b483b7297c5916539cf5d5"}, + {file = "librt-0.8.1-cp39-cp39-musllinux_1_2_riscv64.whl", hash = "sha256:22b46eabd76c1986ee7d231b0765ad387d7673bbd996aa0d0d054b38ac65d8f6"}, + {file = "librt-0.8.1-cp39-cp39-musllinux_1_2_x86_64.whl", hash = "sha256:237796479f4d0637d6b9cbcb926ff424a97735e68ade6facf402df4ec93375ed"}, + {file = "librt-0.8.1-cp39-cp39-win32.whl", hash = "sha256:4beb04b8c66c6ae62f8c1e0b2f097c1ebad9295c929a8d5286c05eae7c2fc7dc"}, + {file = "librt-0.8.1-cp39-cp39-win_amd64.whl", hash = "sha256:64548cde61b692dc0dc379f4b5f59a2f582c2ebe7890d09c1ae3b9e66fa015b7"}, + {file = "librt-0.8.1.tar.gz", hash = "sha256:be46a14693955b3bd96014ccbdb8339ee8c9346fbe11c1b78901b55125f14c73"}, +] + +[[package]] +name = "markupsafe" +version = "2.1.5" +description = "Safely add untrusted strings to HTML/XML markup." +optional = false +python-versions = ">=3.7" +groups = ["dev"] +files = [ + {file = "MarkupSafe-2.1.5-cp310-cp310-macosx_10_9_universal2.whl", hash = "sha256:a17a92de5231666cfbe003f0e4b9b3a7ae3afb1ec2845aadc2bacc93ff85febc"}, + {file = "MarkupSafe-2.1.5-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:72b6be590cc35924b02c78ef34b467da4ba07e4e0f0454a2c5907f473fc50ce5"}, + {file = "MarkupSafe-2.1.5-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:e61659ba32cf2cf1481e575d0462554625196a1f2fc06a1c777d3f48e8865d46"}, + {file = "MarkupSafe-2.1.5-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:2174c595a0d73a3080ca3257b40096db99799265e1c27cc5a610743acd86d62f"}, + {file = "MarkupSafe-2.1.5-cp310-cp310-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:ae2ad8ae6ebee9d2d94b17fb62763125f3f374c25618198f40cbb8b525411900"}, + {file = "MarkupSafe-2.1.5-cp310-cp310-musllinux_1_1_aarch64.whl", hash = "sha256:075202fa5b72c86ad32dc7d0b56024ebdbcf2048c0ba09f1cde31bfdd57bcfff"}, + {file = "MarkupSafe-2.1.5-cp310-cp310-musllinux_1_1_i686.whl", hash = "sha256:598e3276b64aff0e7b3451b72e94fa3c238d452e7ddcd893c3ab324717456bad"}, + {file = "MarkupSafe-2.1.5-cp310-cp310-musllinux_1_1_x86_64.whl", hash = "sha256:fce659a462a1be54d2ffcacea5e3ba2d74daa74f30f5f143fe0c58636e355fdd"}, + {file = "MarkupSafe-2.1.5-cp310-cp310-win32.whl", hash = "sha256:d9fad5155d72433c921b782e58892377c44bd6252b5af2f67f16b194987338a4"}, + {file = "MarkupSafe-2.1.5-cp310-cp310-win_amd64.whl", hash = "sha256:bf50cd79a75d181c9181df03572cdce0fbb75cc353bc350712073108cba98de5"}, + {file = "MarkupSafe-2.1.5-cp311-cp311-macosx_10_9_universal2.whl", hash = "sha256:629ddd2ca402ae6dbedfceeba9c46d5f7b2a61d9749597d4307f943ef198fc1f"}, + {file = "MarkupSafe-2.1.5-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:5b7b716f97b52c5a14bffdf688f971b2d5ef4029127f1ad7a513973cfd818df2"}, + {file = "MarkupSafe-2.1.5-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:6ec585f69cec0aa07d945b20805be741395e28ac1627333b1c5b0105962ffced"}, + {file = "MarkupSafe-2.1.5-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:b91c037585eba9095565a3556f611e3cbfaa42ca1e865f7b8015fe5c7336d5a5"}, + {file = "MarkupSafe-2.1.5-cp311-cp311-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:7502934a33b54030eaf1194c21c692a534196063db72176b0c4028e140f8f32c"}, + {file = "MarkupSafe-2.1.5-cp311-cp311-musllinux_1_1_aarch64.whl", hash = "sha256:0e397ac966fdf721b2c528cf028494e86172b4feba51d65f81ffd65c63798f3f"}, + {file = "MarkupSafe-2.1.5-cp311-cp311-musllinux_1_1_i686.whl", hash = "sha256:c061bb86a71b42465156a3ee7bd58c8c2ceacdbeb95d05a99893e08b8467359a"}, + {file = "MarkupSafe-2.1.5-cp311-cp311-musllinux_1_1_x86_64.whl", hash = "sha256:3a57fdd7ce31c7ff06cdfbf31dafa96cc533c21e443d57f5b1ecc6cdc668ec7f"}, + {file = "MarkupSafe-2.1.5-cp311-cp311-win32.whl", hash = "sha256:397081c1a0bfb5124355710fe79478cdbeb39626492b15d399526ae53422b906"}, + {file = "MarkupSafe-2.1.5-cp311-cp311-win_amd64.whl", hash = "sha256:2b7c57a4dfc4f16f7142221afe5ba4e093e09e728ca65c51f5620c9aaeb9a617"}, + {file = "MarkupSafe-2.1.5-cp312-cp312-macosx_10_9_universal2.whl", hash = "sha256:8dec4936e9c3100156f8a2dc89c4b88d5c435175ff03413b443469c7c8c5f4d1"}, + {file = "MarkupSafe-2.1.5-cp312-cp312-macosx_10_9_x86_64.whl", hash = "sha256:3c6b973f22eb18a789b1460b4b91bf04ae3f0c4234a0a6aa6b0a92f6f7b951d4"}, + {file = "MarkupSafe-2.1.5-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:ac07bad82163452a6884fe8fa0963fb98c2346ba78d779ec06bd7a6262132aee"}, + {file = "MarkupSafe-2.1.5-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:f5dfb42c4604dddc8e4305050aa6deb084540643ed5804d7455b5df8fe16f5e5"}, + {file = "MarkupSafe-2.1.5-cp312-cp312-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:ea3d8a3d18833cf4304cd2fc9cbb1efe188ca9b5efef2bdac7adc20594a0e46b"}, + {file = "MarkupSafe-2.1.5-cp312-cp312-musllinux_1_1_aarch64.whl", hash = "sha256:d050b3361367a06d752db6ead6e7edeb0009be66bc3bae0ee9d97fb326badc2a"}, + {file = "MarkupSafe-2.1.5-cp312-cp312-musllinux_1_1_i686.whl", hash = "sha256:bec0a414d016ac1a18862a519e54b2fd0fc8bbfd6890376898a6c0891dd82e9f"}, + {file = "MarkupSafe-2.1.5-cp312-cp312-musllinux_1_1_x86_64.whl", hash = "sha256:58c98fee265677f63a4385256a6d7683ab1832f3ddd1e66fe948d5880c21a169"}, + {file = "MarkupSafe-2.1.5-cp312-cp312-win32.whl", hash = "sha256:8590b4ae07a35970728874632fed7bd57b26b0102df2d2b233b6d9d82f6c62ad"}, + {file = "MarkupSafe-2.1.5-cp312-cp312-win_amd64.whl", hash = "sha256:823b65d8706e32ad2df51ed89496147a42a2a6e01c13cfb6ffb8b1e92bc910bb"}, + {file = "MarkupSafe-2.1.5-cp37-cp37m-macosx_10_9_x86_64.whl", hash = "sha256:c8b29db45f8fe46ad280a7294f5c3ec36dbac9491f2d1c17345be8e69cc5928f"}, + {file = "MarkupSafe-2.1.5-cp37-cp37m-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:ec6a563cff360b50eed26f13adc43e61bc0c04d94b8be985e6fb24b81f6dcfdf"}, + {file = "MarkupSafe-2.1.5-cp37-cp37m-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:a549b9c31bec33820e885335b451286e2969a2d9e24879f83fe904a5ce59d70a"}, + {file = "MarkupSafe-2.1.5-cp37-cp37m-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:4f11aa001c540f62c6166c7726f71f7573b52c68c31f014c25cc7901deea0b52"}, + {file = "MarkupSafe-2.1.5-cp37-cp37m-musllinux_1_1_aarch64.whl", hash = "sha256:7b2e5a267c855eea6b4283940daa6e88a285f5f2a67f2220203786dfa59b37e9"}, + {file = "MarkupSafe-2.1.5-cp37-cp37m-musllinux_1_1_i686.whl", hash = "sha256:2d2d793e36e230fd32babe143b04cec8a8b3eb8a3122d2aceb4a371e6b09b8df"}, + {file = "MarkupSafe-2.1.5-cp37-cp37m-musllinux_1_1_x86_64.whl", hash = "sha256:ce409136744f6521e39fd8e2a24c53fa18ad67aa5bc7c2cf83645cce5b5c4e50"}, + {file = "MarkupSafe-2.1.5-cp37-cp37m-win32.whl", hash = "sha256:4096e9de5c6fdf43fb4f04c26fb114f61ef0bf2e5604b6ee3019d51b69e8c371"}, + {file = "MarkupSafe-2.1.5-cp37-cp37m-win_amd64.whl", hash = "sha256:4275d846e41ecefa46e2015117a9f491e57a71ddd59bbead77e904dc02b1bed2"}, + {file = "MarkupSafe-2.1.5-cp38-cp38-macosx_10_9_universal2.whl", hash = "sha256:656f7526c69fac7f600bd1f400991cc282b417d17539a1b228617081106feb4a"}, + {file = "MarkupSafe-2.1.5-cp38-cp38-macosx_10_9_x86_64.whl", hash = "sha256:97cafb1f3cbcd3fd2b6fbfb99ae11cdb14deea0736fc2b0952ee177f2b813a46"}, + {file = "MarkupSafe-2.1.5-cp38-cp38-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:1f3fbcb7ef1f16e48246f704ab79d79da8a46891e2da03f8783a5b6fa41a9532"}, + {file = "MarkupSafe-2.1.5-cp38-cp38-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:fa9db3f79de01457b03d4f01b34cf91bc0048eb2c3846ff26f66687c2f6d16ab"}, + {file = "MarkupSafe-2.1.5-cp38-cp38-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:ffee1f21e5ef0d712f9033568f8344d5da8cc2869dbd08d87c84656e6a2d2f68"}, + {file = "MarkupSafe-2.1.5-cp38-cp38-musllinux_1_1_aarch64.whl", hash = "sha256:5dedb4db619ba5a2787a94d877bc8ffc0566f92a01c0ef214865e54ecc9ee5e0"}, + {file = "MarkupSafe-2.1.5-cp38-cp38-musllinux_1_1_i686.whl", hash = "sha256:30b600cf0a7ac9234b2638fbc0fb6158ba5bdcdf46aeb631ead21248b9affbc4"}, + {file = "MarkupSafe-2.1.5-cp38-cp38-musllinux_1_1_x86_64.whl", hash = "sha256:8dd717634f5a044f860435c1d8c16a270ddf0ef8588d4887037c5028b859b0c3"}, + {file = "MarkupSafe-2.1.5-cp38-cp38-win32.whl", hash = "sha256:daa4ee5a243f0f20d528d939d06670a298dd39b1ad5f8a72a4275124a7819eff"}, + {file = "MarkupSafe-2.1.5-cp38-cp38-win_amd64.whl", hash = "sha256:619bc166c4f2de5caa5a633b8b7326fbe98e0ccbfacabd87268a2b15ff73a029"}, + {file = "MarkupSafe-2.1.5-cp39-cp39-macosx_10_9_universal2.whl", hash = "sha256:7a68b554d356a91cce1236aa7682dc01df0edba8d043fd1ce607c49dd3c1edcf"}, + {file = "MarkupSafe-2.1.5-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:db0b55e0f3cc0be60c1f19efdde9a637c32740486004f20d1cff53c3c0ece4d2"}, + {file = "MarkupSafe-2.1.5-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:3e53af139f8579a6d5f7b76549125f0d94d7e630761a2111bc431fd820e163b8"}, + {file = "MarkupSafe-2.1.5-cp39-cp39-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:17b950fccb810b3293638215058e432159d2b71005c74371d784862b7e4683f3"}, + {file = "MarkupSafe-2.1.5-cp39-cp39-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:4c31f53cdae6ecfa91a77820e8b151dba54ab528ba65dfd235c80b086d68a465"}, + {file = "MarkupSafe-2.1.5-cp39-cp39-musllinux_1_1_aarch64.whl", hash = "sha256:bff1b4290a66b490a2f4719358c0cdcd9bafb6b8f061e45c7a2460866bf50c2e"}, + {file = "MarkupSafe-2.1.5-cp39-cp39-musllinux_1_1_i686.whl", hash = "sha256:bc1667f8b83f48511b94671e0e441401371dfd0f0a795c7daa4a3cd1dde55bea"}, + {file = "MarkupSafe-2.1.5-cp39-cp39-musllinux_1_1_x86_64.whl", hash = "sha256:5049256f536511ee3f7e1b3f87d1d1209d327e818e6ae1365e8653d7e3abb6a6"}, + {file = "MarkupSafe-2.1.5-cp39-cp39-win32.whl", hash = "sha256:00e046b6dd71aa03a41079792f8473dc494d564611a8f89bbbd7cb93295ebdcf"}, + {file = "MarkupSafe-2.1.5-cp39-cp39-win_amd64.whl", hash = "sha256:fa173ec60341d6bb97a89f5ea19c85c5643c1e7dedebc22f5181eb73573142c5"}, + {file = "MarkupSafe-2.1.5.tar.gz", hash = "sha256:d283d37a890ba4c1ae73ffadf8046435c76e7bc2247bbb63c00bd1a709c6544b"}, +] + +[[package]] +name = "mypy" +version = "1.19.1" +description = "Optional static typing for Python" +optional = false +python-versions = ">=3.9" +groups = ["dev"] +files = [ + {file = "mypy-1.19.1-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:5f05aa3d375b385734388e844bc01733bd33c644ab48e9684faa54e5389775ec"}, + {file = "mypy-1.19.1-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:022ea7279374af1a5d78dfcab853fe6a536eebfda4b59deab53cd21f6cd9f00b"}, + {file = "mypy-1.19.1-cp310-cp310-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:ee4c11e460685c3e0c64a4c5de82ae143622410950d6be863303a1c4ba0e36d6"}, + {file = "mypy-1.19.1-cp310-cp310-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:de759aafbae8763283b2ee5869c7255391fbc4de3ff171f8f030b5ec48381b74"}, + {file = "mypy-1.19.1-cp310-cp310-musllinux_1_2_x86_64.whl", hash = "sha256:ab43590f9cd5108f41aacf9fca31841142c786827a74ab7cc8a2eacb634e09a1"}, + {file = "mypy-1.19.1-cp310-cp310-win_amd64.whl", hash = "sha256:2899753e2f61e571b3971747e302d5f420c3fd09650e1951e99f823bc3089dac"}, + {file = "mypy-1.19.1-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:d8dfc6ab58ca7dda47d9237349157500468e404b17213d44fc1cb77bce532288"}, + {file = "mypy-1.19.1-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:e3f276d8493c3c97930e354b2595a44a21348b320d859fb4a2b9f66da9ed27ab"}, + {file = "mypy-1.19.1-cp311-cp311-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:2abb24cf3f17864770d18d673c85235ba52456b36a06b6afc1e07c1fdcd3d0e6"}, + {file = "mypy-1.19.1-cp311-cp311-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:a009ffa5a621762d0c926a078c2d639104becab69e79538a494bcccb62cc0331"}, + {file = "mypy-1.19.1-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:f7cee03c9a2e2ee26ec07479f38ea9c884e301d42c6d43a19d20fb014e3ba925"}, + {file = "mypy-1.19.1-cp311-cp311-win_amd64.whl", hash = "sha256:4b84a7a18f41e167f7995200a1d07a4a6810e89d29859df936f1c3923d263042"}, + {file = "mypy-1.19.1-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:a8174a03289288c1f6c46d55cef02379b478bfbc8e358e02047487cad44c6ca1"}, + {file = "mypy-1.19.1-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:ffcebe56eb09ff0c0885e750036a095e23793ba6c2e894e7e63f6d89ad51f22e"}, + {file = "mypy-1.19.1-cp312-cp312-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:b64d987153888790bcdb03a6473d321820597ab8dd9243b27a92153c4fa50fd2"}, + {file = "mypy-1.19.1-cp312-cp312-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:c35d298c2c4bba75feb2195655dfea8124d855dfd7343bf8b8c055421eaf0cf8"}, + {file = "mypy-1.19.1-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:34c81968774648ab5ac09c29a375fdede03ba253f8f8287847bd480782f73a6a"}, + {file = "mypy-1.19.1-cp312-cp312-win_amd64.whl", hash = "sha256:b10e7c2cd7870ba4ad9b2d8a6102eb5ffc1f16ca35e3de6bfa390c1113029d13"}, + {file = "mypy-1.19.1-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:e3157c7594ff2ef1634ee058aafc56a82db665c9438fd41b390f3bde1ab12250"}, + {file = "mypy-1.19.1-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:bdb12f69bcc02700c2b47e070238f42cb87f18c0bc1fc4cdb4fb2bc5fd7a3b8b"}, + {file = "mypy-1.19.1-cp313-cp313-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:f859fb09d9583a985be9a493d5cfc5515b56b08f7447759a0c5deaf68d80506e"}, + {file = "mypy-1.19.1-cp313-cp313-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:c9a6538e0415310aad77cb94004ca6482330fece18036b5f360b62c45814c4ef"}, + {file = "mypy-1.19.1-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:da4869fc5e7f62a88f3fe0b5c919d1d9f7ea3cef92d3689de2823fd27e40aa75"}, + {file = "mypy-1.19.1-cp313-cp313-win_amd64.whl", hash = "sha256:016f2246209095e8eda7538944daa1d60e1e8134d98983b9fc1e92c1fc0cb8dd"}, + {file = "mypy-1.19.1-cp314-cp314-macosx_10_15_x86_64.whl", hash = "sha256:06e6170bd5836770e8104c8fdd58e5e725cfeb309f0a6c681a811f557e97eac1"}, + {file = "mypy-1.19.1-cp314-cp314-macosx_11_0_arm64.whl", hash = "sha256:804bd67b8054a85447c8954215a906d6eff9cabeabe493fb6334b24f4bfff718"}, + {file = "mypy-1.19.1-cp314-cp314-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:21761006a7f497cb0d4de3d8ef4ca70532256688b0523eee02baf9eec895e27b"}, + {file = "mypy-1.19.1-cp314-cp314-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:28902ee51f12e0f19e1e16fbe2f8f06b6637f482c459dd393efddd0ec7f82045"}, + {file = "mypy-1.19.1-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:481daf36a4c443332e2ae9c137dfee878fcea781a2e3f895d54bd3002a900957"}, + {file = "mypy-1.19.1-cp314-cp314-win_amd64.whl", hash = "sha256:8bb5c6f6d043655e055be9b542aa5f3bdd30e4f3589163e85f93f3640060509f"}, + {file = "mypy-1.19.1-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:7bcfc336a03a1aaa26dfce9fff3e287a3ba99872a157561cbfcebe67c13308e3"}, + {file = "mypy-1.19.1-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:b7951a701c07ea584c4fe327834b92a30825514c868b1f69c30445093fdd9d5a"}, + {file = "mypy-1.19.1-cp39-cp39-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:b13cfdd6c87fc3efb69ea4ec18ef79c74c3f98b4e5498ca9b85ab3b2c2329a67"}, + {file = "mypy-1.19.1-cp39-cp39-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:4f28f99c824ecebcdaa2e55d82953e38ff60ee5ec938476796636b86afa3956e"}, + {file = "mypy-1.19.1-cp39-cp39-musllinux_1_2_x86_64.whl", hash = "sha256:c608937067d2fc5a4dd1a5ce92fd9e1398691b8c5d012d66e1ddd430e9244376"}, + {file = "mypy-1.19.1-cp39-cp39-win_amd64.whl", hash = "sha256:409088884802d511ee52ca067707b90c883426bd95514e8cfda8281dc2effe24"}, + {file = "mypy-1.19.1-py3-none-any.whl", hash = "sha256:f1235f5ea01b7db5468d53ece6aaddf1ad0b88d9e7462b86ef96fe04995d7247"}, + {file = "mypy-1.19.1.tar.gz", hash = "sha256:19d88bb05303fe63f71dd2c6270daca27cb9401c4ca8255fe50d1d920e0eb9ba"}, +] + +[package.dependencies] +librt = {version = ">=0.6.2", markers = "platform_python_implementation != \"PyPy\""} +mypy_extensions = ">=1.0.0" +pathspec = ">=0.9.0" +tomli = {version = ">=1.1.0", markers = "python_version < \"3.11\""} +typing_extensions = ">=4.6.0" + +[package.extras] +dmypy = ["psutil (>=4.0)"] +faster-cache = ["orjson"] +install-types = ["pip"] +mypyc = ["setuptools (>=50)"] +reports = ["lxml"] + +[[package]] +name = "mypy-extensions" +version = "1.0.0" +description = "Type system extensions for programs checked with the mypy type checker." +optional = false +python-versions = ">=3.5" +groups = ["dev"] +files = [ + {file = "mypy_extensions-1.0.0-py3-none-any.whl", hash = "sha256:4392f6c0eb8a5668a69e23d168ffa70f0be9ccfd32b5cc2d26a34ae5b844552d"}, + {file = "mypy_extensions-1.0.0.tar.gz", hash = "sha256:75dbf8955dc00442a438fc4d0666508a9a97b6bd41aa2f0ffe9d2f2725af0782"}, +] + +[[package]] +name = "nodeenv" +version = "1.8.0" +description = "Node.js virtual environment builder" +optional = false +python-versions = ">=2.7,!=3.0.*,!=3.1.*,!=3.2.*,!=3.3.*,!=3.4.*,!=3.5.*,!=3.6.*" +groups = ["dev"] +files = [ + {file = "nodeenv-1.8.0-py2.py3-none-any.whl", hash = "sha256:df865724bb3c3adc86b3876fa209771517b0cfe596beff01a92700e0e8be4cec"}, + {file = "nodeenv-1.8.0.tar.gz", hash = "sha256:d51e0c37e64fbf47d017feac3145cdbb58836d7eee8c6f6d3b6880c5456227d2"}, +] + +[package.dependencies] +setuptools = "*" + +[[package]] +name = "packaging" +version = "24.0" +description = "Core utilities for Python packages" +optional = false +python-versions = ">=3.7" +groups = ["dev"] +files = [ + {file = "packaging-24.0-py3-none-any.whl", hash = "sha256:2ddfb553fdf02fb784c234c7ba6ccc288296ceabec964ad2eae3777778130bc5"}, + {file = "packaging-24.0.tar.gz", hash = "sha256:eb82c5e3e56209074766e6885bb04b8c38a0c015d0a30036ebe7ece34c9989e9"}, +] + +[[package]] +name = "pathspec" +version = "1.0.4" +description = "Utility library for gitignore style pattern matching of file paths." +optional = false +python-versions = ">=3.9" +groups = ["dev"] +files = [ + {file = "pathspec-1.0.4-py3-none-any.whl", hash = "sha256:fb6ae2fd4e7c921a165808a552060e722767cfa526f99ca5156ed2ce45a5c723"}, + {file = "pathspec-1.0.4.tar.gz", hash = "sha256:0210e2ae8a21a9137c0d470578cb0e595af87edaa6ebf12ff176f14a02e0e645"}, +] + +[package.extras] +hyperscan = ["hyperscan (>=0.7)"] +optional = ["typing-extensions (>=4)"] +re2 = ["google-re2 (>=1.1)"] +tests = ["pytest (>=9)", "typing-extensions (>=4.15)"] + +[[package]] +name = "platformdirs" +version = "4.0.0" +description = "A small Python package for determining appropriate platform-specific dirs, e.g. a \"user data dir\"." +optional = false +python-versions = ">=3.7" +groups = ["dev"] +files = [ + {file = "platformdirs-4.0.0-py3-none-any.whl", hash = "sha256:118c954d7e949b35437270383a3f2531e99dd93cf7ce4dc8340d3356d30f173b"}, + {file = "platformdirs-4.0.0.tar.gz", hash = "sha256:cb633b2bcf10c51af60beb0ab06d2f1d69064b43abf4c185ca6b28865f3f9731"}, +] + +[package.extras] +docs = ["furo (>=2023.7.26)", "proselint (>=0.13)", "sphinx (>=7.1.1)", "sphinx-autodoc-typehints (>=1.24)"] +test = ["appdirs (==1.4.4)", "covdefaults (>=2.3)", "pytest (>=7.4)", "pytest-cov (>=4.1)", "pytest-mock (>=3.11.1)"] + +[[package]] +name = "pluggy" +version = "1.2.0" +description = "plugin and hook calling mechanisms for python" +optional = false +python-versions = ">=3.7" +groups = ["dev"] +files = [ + {file = "pluggy-1.2.0-py3-none-any.whl", hash = "sha256:c2fd55a7d7a3863cba1a013e4e2414658b1d07b6bc57b3919e0c63c9abb99849"}, + {file = "pluggy-1.2.0.tar.gz", hash = "sha256:d12f0c4b579b15f5e054301bb226ee85eeeba08ffec228092f8defbaa3a4c4b3"}, +] + +[package.extras] +dev = ["pre-commit", "tox"] +testing = ["pytest", "pytest-benchmark"] + +[[package]] +name = "pre-commit" +version = "2.21.0" +description = "A framework for managing and maintaining multi-language pre-commit hooks." +optional = false +python-versions = ">=3.7" +groups = ["dev"] +files = [ + {file = "pre_commit-2.21.0-py2.py3-none-any.whl", hash = "sha256:e2f91727039fc39a92f58a588a25b87f936de6567eed4f0e673e0507edc75bad"}, + {file = "pre_commit-2.21.0.tar.gz", hash = "sha256:31ef31af7e474a8d8995027fefdfcf509b5c913ff31f2015b4ec4beb26a6f658"}, +] + +[package.dependencies] +cfgv = ">=2.0.0" +identify = ">=1.0.0" +nodeenv = ">=0.11.1" +pyyaml = ">=5.1" +virtualenv = ">=20.10.0" + +[[package]] +name = "pygments" +version = "2.20.0" +description = "Pygments is a syntax highlighting package written in Python." +optional = false +python-versions = ">=3.9" +groups = ["dev"] +files = [ + {file = "pygments-2.20.0-py3-none-any.whl", hash = "sha256:81a9e26dd42fd28a23a2d169d86d7ac03b46e2f8b59ed4698fb4785f946d0176"}, + {file = "pygments-2.20.0.tar.gz", hash = "sha256:6757cd03768053ff99f3039c1a36d6c0aa0b263438fcab17520b30a303a82b5f"}, +] + +[package.extras] +windows-terminal = ["colorama (>=0.4.6)"] + +[[package]] +name = "pytest" +version = "7.4.4" +description = "pytest: simple powerful testing with Python" +optional = false +python-versions = ">=3.7" +groups = ["dev"] +files = [ + {file = "pytest-7.4.4-py3-none-any.whl", hash = "sha256:b090cdf5ed60bf4c45261be03239c2c1c22df034fbffe691abe93cd80cea01d8"}, + {file = "pytest-7.4.4.tar.gz", hash = "sha256:2cf0005922c6ace4a3e2ec8b4080eb0d9753fdc93107415332f50ce9e7994280"}, +] + +[package.dependencies] +colorama = {version = "*", markers = "sys_platform == \"win32\""} +exceptiongroup = {version = ">=1.0.0rc8", markers = "python_version < \"3.11\""} +iniconfig = "*" +packaging = "*" +pluggy = ">=0.12,<2.0" +tomli = {version = ">=1.0.0", markers = "python_version < \"3.11\""} + +[package.extras] +testing = ["argcomplete", "attrs (>=19.2.0)", "hypothesis (>=3.56)", "mock", "nose", "pygments (>=2.7.2)", "requests", "setuptools", "xmlschema"] + +[[package]] +name = "pytest-cov" +version = "4.1.0" +description = "Pytest plugin for measuring coverage." +optional = false +python-versions = ">=3.7" +groups = ["dev"] +files = [ + {file = "pytest-cov-4.1.0.tar.gz", hash = "sha256:3904b13dfbfec47f003b8e77fd5b589cd11904a21ddf1ab38a64f204d6a10ef6"}, + {file = "pytest_cov-4.1.0-py3-none-any.whl", hash = "sha256:6ba70b9e97e69fcc3fb45bfeab2d0a138fb65c4d0d6a41ef33983ad114be8c3a"}, +] + +[package.dependencies] +coverage = {version = ">=5.2.1", extras = ["toml"]} +pytest = ">=4.6" + +[package.extras] +testing = ["fields", "hunter", "process-tests", "pytest-xdist", "six", "virtualenv"] + +[[package]] +name = "pyyaml" +version = "6.0.1" +description = "YAML parser and emitter for Python" +optional = false +python-versions = ">=3.6" +groups = ["dev"] +files = [ + {file = "PyYAML-6.0.1-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:d858aa552c999bc8a8d57426ed01e40bef403cd8ccdd0fc5f6f04a00414cac2a"}, + {file = "PyYAML-6.0.1-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:fd66fc5d0da6d9815ba2cebeb4205f95818ff4b79c3ebe268e75d961704af52f"}, + {file = "PyYAML-6.0.1-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:69b023b2b4daa7548bcfbd4aa3da05b3a74b772db9e23b982788168117739938"}, + {file = "PyYAML-6.0.1-cp310-cp310-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:81e0b275a9ecc9c0c0c07b4b90ba548307583c125f54d5b6946cfee6360c733d"}, + {file = "PyYAML-6.0.1-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:ba336e390cd8e4d1739f42dfe9bb83a3cc2e80f567d8805e11b46f4a943f5515"}, + {file = "PyYAML-6.0.1-cp310-cp310-musllinux_1_1_x86_64.whl", hash = "sha256:326c013efe8048858a6d312ddd31d56e468118ad4cdeda36c719bf5bb6192290"}, + {file = "PyYAML-6.0.1-cp310-cp310-win32.whl", hash = "sha256:bd4af7373a854424dabd882decdc5579653d7868b8fb26dc7d0e99f823aa5924"}, + {file = "PyYAML-6.0.1-cp310-cp310-win_amd64.whl", hash = "sha256:fd1592b3fdf65fff2ad0004b5e363300ef59ced41c2e6b3a99d4089fa8c5435d"}, + {file = "PyYAML-6.0.1-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:6965a7bc3cf88e5a1c3bd2e0b5c22f8d677dc88a455344035f03399034eb3007"}, + {file = "PyYAML-6.0.1-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:f003ed9ad21d6a4713f0a9b5a7a0a79e08dd0f221aff4525a2be4c346ee60aab"}, + {file = "PyYAML-6.0.1-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:42f8152b8dbc4fe7d96729ec2b99c7097d656dc1213a3229ca5383f973a5ed6d"}, + {file = "PyYAML-6.0.1-cp311-cp311-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:062582fca9fabdd2c8b54a3ef1c978d786e0f6b3a1510e0ac93ef59e0ddae2bc"}, + {file = "PyYAML-6.0.1-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:d2b04aac4d386b172d5b9692e2d2da8de7bfb6c387fa4f801fbf6fb2e6ba4673"}, + {file = "PyYAML-6.0.1-cp311-cp311-musllinux_1_1_x86_64.whl", hash = "sha256:e7d73685e87afe9f3b36c799222440d6cf362062f78be1013661b00c5c6f678b"}, + {file = "PyYAML-6.0.1-cp311-cp311-win32.whl", hash = "sha256:1635fd110e8d85d55237ab316b5b011de701ea0f29d07611174a1b42f1444741"}, + {file = "PyYAML-6.0.1-cp311-cp311-win_amd64.whl", hash = "sha256:bf07ee2fef7014951eeb99f56f39c9bb4af143d8aa3c21b1677805985307da34"}, + {file = "PyYAML-6.0.1-cp312-cp312-macosx_10_9_x86_64.whl", hash = "sha256:855fb52b0dc35af121542a76b9a84f8d1cd886ea97c84703eaa6d88e37a2ad28"}, + {file = "PyYAML-6.0.1-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:40df9b996c2b73138957fe23a16a4f0ba614f4c0efce1e9406a184b6d07fa3a9"}, + {file = "PyYAML-6.0.1-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:a08c6f0fe150303c1c6b71ebcd7213c2858041a7e01975da3a99aed1e7a378ef"}, + {file = "PyYAML-6.0.1-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:6c22bec3fbe2524cde73d7ada88f6566758a8f7227bfbf93a408a9d86bcc12a0"}, + {file = "PyYAML-6.0.1-cp312-cp312-musllinux_1_1_x86_64.whl", hash = "sha256:8d4e9c88387b0f5c7d5f281e55304de64cf7f9c0021a3525bd3b1c542da3b0e4"}, + {file = "PyYAML-6.0.1-cp312-cp312-win32.whl", hash = "sha256:d483d2cdf104e7c9fa60c544d92981f12ad66a457afae824d146093b8c294c54"}, + {file = "PyYAML-6.0.1-cp312-cp312-win_amd64.whl", hash = "sha256:0d3304d8c0adc42be59c5f8a4d9e3d7379e6955ad754aa9d6ab7a398b59dd1df"}, + {file = "PyYAML-6.0.1-cp36-cp36m-macosx_10_9_x86_64.whl", hash = "sha256:50550eb667afee136e9a77d6dc71ae76a44df8b3e51e41b77f6de2932bfe0f47"}, + {file = "PyYAML-6.0.1-cp36-cp36m-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:1fe35611261b29bd1de0070f0b2f47cb6ff71fa6595c077e42bd0c419fa27b98"}, + {file = "PyYAML-6.0.1-cp36-cp36m-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:704219a11b772aea0d8ecd7058d0082713c3562b4e271b849ad7dc4a5c90c13c"}, + {file = "PyYAML-6.0.1-cp36-cp36m-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:afd7e57eddb1a54f0f1a974bc4391af8bcce0b444685d936840f125cf046d5bd"}, + {file = "PyYAML-6.0.1-cp36-cp36m-win32.whl", hash = "sha256:fca0e3a251908a499833aa292323f32437106001d436eca0e6e7833256674585"}, + {file = "PyYAML-6.0.1-cp36-cp36m-win_amd64.whl", hash = "sha256:f22ac1c3cac4dbc50079e965eba2c1058622631e526bd9afd45fedd49ba781fa"}, + {file = "PyYAML-6.0.1-cp37-cp37m-macosx_10_9_x86_64.whl", hash = "sha256:b1275ad35a5d18c62a7220633c913e1b42d44b46ee12554e5fd39c70a243d6a3"}, + {file = "PyYAML-6.0.1-cp37-cp37m-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:18aeb1bf9a78867dc38b259769503436b7c72f7a1f1f4c93ff9a17de54319b27"}, + {file = "PyYAML-6.0.1-cp37-cp37m-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:596106435fa6ad000c2991a98fa58eeb8656ef2325d7e158344fb33864ed87e3"}, + {file = "PyYAML-6.0.1-cp37-cp37m-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:baa90d3f661d43131ca170712d903e6295d1f7a0f595074f151c0aed377c9b9c"}, + {file = "PyYAML-6.0.1-cp37-cp37m-win32.whl", hash = "sha256:9046c58c4395dff28dd494285c82ba00b546adfc7ef001486fbf0324bc174fba"}, + {file = "PyYAML-6.0.1-cp37-cp37m-win_amd64.whl", hash = "sha256:4fb147e7a67ef577a588a0e2c17b6db51dda102c71de36f8549b6816a96e1867"}, + {file = "PyYAML-6.0.1-cp38-cp38-macosx_10_9_x86_64.whl", hash = "sha256:1d4c7e777c441b20e32f52bd377e0c409713e8bb1386e1099c2415f26e479595"}, + {file = "PyYAML-6.0.1-cp38-cp38-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:a0cd17c15d3bb3fa06978b4e8958dcdc6e0174ccea823003a106c7d4d7899ac5"}, + {file = "PyYAML-6.0.1-cp38-cp38-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:28c119d996beec18c05208a8bd78cbe4007878c6dd15091efb73a30e90539696"}, + {file = "PyYAML-6.0.1-cp38-cp38-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:7e07cbde391ba96ab58e532ff4803f79c4129397514e1413a7dc761ccd755735"}, + {file = "PyYAML-6.0.1-cp38-cp38-musllinux_1_1_x86_64.whl", hash = "sha256:49a183be227561de579b4a36efbb21b3eab9651dd81b1858589f796549873dd6"}, + {file = "PyYAML-6.0.1-cp38-cp38-win32.whl", hash = "sha256:184c5108a2aca3c5b3d3bf9395d50893a7ab82a38004c8f61c258d4428e80206"}, + {file = "PyYAML-6.0.1-cp38-cp38-win_amd64.whl", hash = "sha256:1e2722cc9fbb45d9b87631ac70924c11d3a401b2d7f410cc0e3bbf249f2dca62"}, + {file = "PyYAML-6.0.1-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:9eb6caa9a297fc2c2fb8862bc5370d0303ddba53ba97e71f08023b6cd73d16a8"}, + {file = "PyYAML-6.0.1-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:c8098ddcc2a85b61647b2590f825f3db38891662cfc2fc776415143f599bb859"}, + {file = "PyYAML-6.0.1-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:5773183b6446b2c99bb77e77595dd486303b4faab2b086e7b17bc6bef28865f6"}, + {file = "PyYAML-6.0.1-cp39-cp39-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:b786eecbdf8499b9ca1d697215862083bd6d2a99965554781d0d8d1ad31e13a0"}, + {file = "PyYAML-6.0.1-cp39-cp39-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:bc1bf2925a1ecd43da378f4db9e4f799775d6367bdb94671027b73b393a7c42c"}, + {file = "PyYAML-6.0.1-cp39-cp39-musllinux_1_1_x86_64.whl", hash = "sha256:04ac92ad1925b2cff1db0cfebffb6ffc43457495c9b3c39d3fcae417d7125dc5"}, + {file = "PyYAML-6.0.1-cp39-cp39-win32.whl", hash = "sha256:faca3bdcf85b2fc05d06ff3fbc1f83e1391b3e724afa3feba7d13eeab355484c"}, + {file = "PyYAML-6.0.1-cp39-cp39-win_amd64.whl", hash = "sha256:510c9deebc5c0225e8c96813043e62b680ba2f9c50a08d3724c7f28a747d1486"}, + {file = "PyYAML-6.0.1.tar.gz", hash = "sha256:bfdf460b1736c775f2ba9f6a92bca30bc2095067b8a9d77876d1fad6cc3b4a43"}, +] + +[[package]] +name = "requests" +version = "2.32.4" +description = "Python HTTP for Humans." +optional = false +python-versions = ">=3.8" +groups = ["dev"] +files = [ + {file = "requests-2.32.4-py3-none-any.whl", hash = "sha256:27babd3cda2a6d50b30443204ee89830707d396671944c998b5975b031ac2b2c"}, + {file = "requests-2.32.4.tar.gz", hash = "sha256:27d0316682c8a29834d3264820024b62a36942083d52caf2f14c0591336d3422"}, +] + +[package.dependencies] +certifi = ">=2017.4.17" +charset_normalizer = ">=2,<4" +idna = ">=2.5,<4" +urllib3 = ">=1.21.1,<3" + +[package.extras] +socks = ["PySocks (>=1.5.6,!=1.5.7)"] +use-chardet-on-py3 = ["chardet (>=3.0.2,<6)"] + +[[package]] +name = "setuptools" +version = "78.1.1" +description = "Easily download, build, install, upgrade, and uninstall Python packages" +optional = false +python-versions = ">=3.9" +groups = ["dev"] +files = [ + {file = "setuptools-78.1.1-py3-none-any.whl", hash = "sha256:c3a9c4211ff4c309edb8b8c4f1cbfa7ae324c4ba9f91ff254e3d305b9fd54561"}, + {file = "setuptools-78.1.1.tar.gz", hash = "sha256:fcc17fd9cd898242f6b4adfaca46137a9edef687f43e6f78469692a5e70d851d"}, +] + +[package.extras] +check = ["pytest-checkdocs (>=2.4)", "pytest-ruff (>=0.2.1) ; sys_platform != \"cygwin\"", "ruff (>=0.8.0) ; sys_platform != \"cygwin\""] +core = ["importlib_metadata (>=6) ; python_version < \"3.10\"", "jaraco.functools (>=4)", "jaraco.text (>=3.7)", "more_itertools", "more_itertools (>=8.8)", "packaging (>=24.2)", "platformdirs (>=4.2.2)", "tomli (>=2.0.1) ; python_version < \"3.11\"", "wheel (>=0.43.0)"] +cover = ["pytest-cov"] +doc = ["furo", "jaraco.packaging (>=9.3)", "jaraco.tidelift (>=1.4)", "pygments-github-lexers (==0.0.5)", "pyproject-hooks (!=1.1)", "rst.linker (>=1.9)", "sphinx (>=3.5)", "sphinx-favicon", "sphinx-inline-tabs", "sphinx-lint", "sphinx-notfound-page (>=1,<2)", "sphinx-reredirects", "sphinxcontrib-towncrier", "towncrier (<24.7)"] +enabler = ["pytest-enabler (>=2.2)"] +test = ["build[virtualenv] (>=1.0.3)", "filelock (>=3.4.0)", "ini2toml[lite] (>=0.14)", "jaraco.develop (>=7.21) ; python_version >= \"3.9\" and sys_platform != \"cygwin\"", "jaraco.envs (>=2.2)", "jaraco.path (>=3.7.2)", "jaraco.test (>=5.5)", "packaging (>=24.2)", "pip (>=19.1)", "pyproject-hooks (!=1.1)", "pytest (>=6,!=8.1.*)", "pytest-home (>=0.5)", "pytest-perf ; sys_platform != \"cygwin\"", "pytest-subprocess", "pytest-timeout", "pytest-xdist (>=3)", "tomli-w (>=1.0.0)", "virtualenv (>=13.0.0)", "wheel (>=0.44.0)"] +type = ["importlib_metadata (>=7.0.2) ; python_version < \"3.10\"", "jaraco.develop (>=7.21) ; sys_platform != \"cygwin\"", "mypy (==1.14.*)", "pytest-mypy"] + +[[package]] +name = "snowballstemmer" +version = "2.2.0" +description = "This package provides 29 stemmers for 28 languages generated from Snowball algorithms." +optional = false +python-versions = "*" +groups = ["dev"] +files = [ + {file = "snowballstemmer-2.2.0-py2.py3-none-any.whl", hash = "sha256:c8e1716e83cc398ae16824e5572ae04e0d9fc2c6b985fb0f900f5f0c96ecba1a"}, + {file = "snowballstemmer-2.2.0.tar.gz", hash = "sha256:09b16deb8547d3412ad7b590689584cd0fe25ec8db3be37788be3810cbf19cb1"}, +] + +[[package]] +name = "soupsieve" +version = "2.8.4" +description = "A modern CSS selector implementation for Beautiful Soup." +optional = false +python-versions = ">=3.9" +groups = ["dev"] +files = [ + {file = "soupsieve-2.8.4-py3-none-any.whl", hash = "sha256:e7e6b0769c8f51ed59acab6e994b00621096cfb1c640a7509295987388fbaf65"}, + {file = "soupsieve-2.8.4.tar.gz", hash = "sha256:e121fd02e975c695e4e9e8774a5ee35d74714b59307868dcc5319ad2d9e3328e"}, +] + +[[package]] +name = "sphinx" +version = "4.5.0" +description = "Python documentation generator" +optional = false +python-versions = ">=3.6" +groups = ["dev"] +files = [ + {file = "Sphinx-4.5.0-py3-none-any.whl", hash = "sha256:ebf612653238bcc8f4359627a9b7ce44ede6fdd75d9d30f68255c7383d3a6226"}, + {file = "Sphinx-4.5.0.tar.gz", hash = "sha256:7bf8ca9637a4ee15af412d1a1d9689fec70523a68ca9bb9127c2f3eeb344e2e6"}, +] + +[package.dependencies] +alabaster = ">=0.7,<0.8" +babel = ">=1.3" +colorama = {version = ">=0.3.5", markers = "sys_platform == \"win32\""} +docutils = ">=0.14,<0.18" +imagesize = "*" +importlib-metadata = {version = ">=4.4", markers = "python_version < \"3.10\""} +Jinja2 = ">=2.3" +packaging = "*" +Pygments = ">=2.0" +requests = ">=2.5.0" +snowballstemmer = ">=1.1" +sphinxcontrib-applehelp = "*" +sphinxcontrib-devhelp = "*" +sphinxcontrib-htmlhelp = ">=2.0.0" +sphinxcontrib-jsmath = "*" +sphinxcontrib-qthelp = "*" +sphinxcontrib-serializinghtml = ">=1.1.5" + +[package.extras] +docs = ["sphinxcontrib-websupport"] +lint = ["docutils-stubs", "flake8 (>=3.5.0)", "isort", "mypy (>=0.931)", "types-requests", "types-typed-ast"] +test = ["cython", "html5lib", "pytest", "pytest-cov", "typed-ast ; python_version < \"3.8\""] + +[[package]] +name = "sphinx-basic-ng" +version = "1.0.0b2" +description = "A modern skeleton for Sphinx themes." +optional = false +python-versions = ">=3.7" +groups = ["dev"] +files = [ + {file = "sphinx_basic_ng-1.0.0b2-py3-none-any.whl", hash = "sha256:eb09aedbabfb650607e9b4b68c9d240b90b1e1be221d6ad71d61c52e29f7932b"}, + {file = "sphinx_basic_ng-1.0.0b2.tar.gz", hash = "sha256:9ec55a47c90c8c002b5960c57492ec3021f5193cb26cebc2dc4ea226848651c9"}, +] + +[package.dependencies] +sphinx = ">=4.0" + +[package.extras] +docs = ["furo", "ipython", "myst-parser", "sphinx-copybutton", "sphinx-inline-tabs"] + +[[package]] +name = "sphinxcontrib-applehelp" +version = "1.0.2" +description = "sphinxcontrib-applehelp is a sphinx extension which outputs Apple help books" +optional = false +python-versions = ">=3.5" +groups = ["dev"] +files = [ + {file = "sphinxcontrib-applehelp-1.0.2.tar.gz", hash = "sha256:a072735ec80e7675e3f432fcae8610ecf509c5f1869d17e2eecff44389cdbc58"}, + {file = "sphinxcontrib_applehelp-1.0.2-py2.py3-none-any.whl", hash = "sha256:806111e5e962be97c29ec4c1e7fe277bfd19e9652fb1a4392105b43e01af885a"}, +] + +[package.extras] +lint = ["docutils-stubs", "flake8", "mypy"] +test = ["pytest"] + +[[package]] +name = "sphinxcontrib-devhelp" +version = "1.0.2" +description = "sphinxcontrib-devhelp is a sphinx extension which outputs Devhelp document." +optional = false +python-versions = ">=3.5" +groups = ["dev"] +files = [ + {file = "sphinxcontrib-devhelp-1.0.2.tar.gz", hash = "sha256:ff7f1afa7b9642e7060379360a67e9c41e8f3121f2ce9164266f61b9f4b338e4"}, + {file = "sphinxcontrib_devhelp-1.0.2-py2.py3-none-any.whl", hash = "sha256:8165223f9a335cc1af7ffe1ed31d2871f325254c0423bc0c4c7cd1c1e4734a2e"}, +] + +[package.extras] +lint = ["docutils-stubs", "flake8", "mypy"] +test = ["pytest"] + +[[package]] +name = "sphinxcontrib-htmlhelp" +version = "2.0.0" +description = "sphinxcontrib-htmlhelp is a sphinx extension which renders HTML help files" +optional = false +python-versions = ">=3.6" +groups = ["dev"] +files = [ + {file = "sphinxcontrib-htmlhelp-2.0.0.tar.gz", hash = "sha256:f5f8bb2d0d629f398bf47d0d69c07bc13b65f75a81ad9e2f71a63d4b7a2f6db2"}, + {file = "sphinxcontrib_htmlhelp-2.0.0-py2.py3-none-any.whl", hash = "sha256:d412243dfb797ae3ec2b59eca0e52dac12e75a241bf0e4eb861e450d06c6ed07"}, +] + +[package.extras] +lint = ["docutils-stubs", "flake8", "mypy"] +test = ["html5lib", "pytest"] + +[[package]] +name = "sphinxcontrib-jsmath" +version = "1.0.1" +description = "A sphinx extension which renders display math in HTML via JavaScript" +optional = false +python-versions = ">=3.5" +groups = ["dev"] +files = [ + {file = "sphinxcontrib-jsmath-1.0.1.tar.gz", hash = "sha256:a9925e4a4587247ed2191a22df5f6970656cb8ca2bd6284309578f2153e0c4b8"}, + {file = "sphinxcontrib_jsmath-1.0.1-py2.py3-none-any.whl", hash = "sha256:2ec2eaebfb78f3f2078e73666b1415417a116cc848b72e5172e596c871103178"}, +] + +[package.extras] +test = ["flake8", "mypy", "pytest"] + +[[package]] +name = "sphinxcontrib-qthelp" +version = "1.0.3" +description = "sphinxcontrib-qthelp is a sphinx extension which outputs QtHelp document." +optional = false +python-versions = ">=3.5" +groups = ["dev"] +files = [ + {file = "sphinxcontrib-qthelp-1.0.3.tar.gz", hash = "sha256:4c33767ee058b70dba89a6fc5c1892c0d57a54be67ddd3e7875a18d14cba5a72"}, + {file = "sphinxcontrib_qthelp-1.0.3-py2.py3-none-any.whl", hash = "sha256:bd9fc24bcb748a8d51fd4ecaade681350aa63009a347a8c14e637895444dfab6"}, +] + +[package.extras] +lint = ["docutils-stubs", "flake8", "mypy"] +test = ["pytest"] + +[[package]] +name = "sphinxcontrib-serializinghtml" +version = "1.1.5" +description = "sphinxcontrib-serializinghtml is a sphinx extension which outputs \"serialized\" HTML files (json and pickle)." +optional = false +python-versions = ">=3.5" +groups = ["dev"] +files = [ + {file = "sphinxcontrib-serializinghtml-1.1.5.tar.gz", hash = "sha256:aa5f6de5dfdf809ef505c4895e51ef5c9eac17d0f287933eb49ec495280b6952"}, + {file = "sphinxcontrib_serializinghtml-1.1.5-py2.py3-none-any.whl", hash = "sha256:352a9a00ae864471d3a7ead8d7d79f5fc0b57e8b3f95e9867eb9eb28999b92fd"}, +] + +[package.extras] +lint = ["docutils-stubs", "flake8", "mypy"] +test = ["pytest"] + +[[package]] +name = "tomli" +version = "2.0.1" +description = "A lil' TOML parser" +optional = false +python-versions = ">=3.7" +groups = ["dev"] +markers = "python_full_version <= \"3.11.0a6\"" +files = [ + {file = "tomli-2.0.1-py3-none-any.whl", hash = "sha256:939de3e7a6161af0c887ef91b7d41a53e7c5a1ca976325f429cb46ea9bc30ecc"}, + {file = "tomli-2.0.1.tar.gz", hash = "sha256:de526c12914f0c550d15924c62d72abc48d6fe7364aa87328337a31007fe8a4f"}, +] + +[[package]] +name = "typing-extensions" +version = "4.15.0" +description = "Backported and Experimental Type Hints for Python 3.9+" +optional = false +python-versions = ">=3.9" +groups = ["dev"] +files = [ + {file = "typing_extensions-4.15.0-py3-none-any.whl", hash = "sha256:f0fa19c6845758ab08074a0cfa8b7aecb71c999ca73d62883bc25cc018c4e548"}, + {file = "typing_extensions-4.15.0.tar.gz", hash = "sha256:0cea48d173cc12fa28ecabc3b837ea3cf6f38c6d1136f85cbaaf598984861466"}, +] + +[[package]] +name = "urllib3" +version = "2.6.3" +description = "HTTP library with thread-safe connection pooling, file post, and more." +optional = false +python-versions = ">=3.9" +groups = ["dev"] +files = [ + {file = "urllib3-2.6.3-py3-none-any.whl", hash = "sha256:bf272323e553dfb2e87d9bfd225ca7b0f467b919d7bbd355436d3fd37cb0acd4"}, + {file = "urllib3-2.6.3.tar.gz", hash = "sha256:1b62b6884944a57dbe321509ab94fd4d3b307075e0c2eae991ac71ee15ad38ed"}, +] + +[package.extras] +brotli = ["brotli (>=1.2.0) ; platform_python_implementation == \"CPython\"", "brotlicffi (>=1.2.0.0) ; platform_python_implementation != \"CPython\""] +h2 = ["h2 (>=4,<5)"] +socks = ["pysocks (>=1.5.6,!=1.5.7,<2.0)"] +zstd = ["backports-zstd (>=1.0.0) ; python_version < \"3.14\""] + +[[package]] +name = "virtualenv" +version = "20.36.1" +description = "Virtual Python Environment builder" +optional = false +python-versions = ">=3.8" +groups = ["dev"] +files = [ + {file = "virtualenv-20.36.1-py3-none-any.whl", hash = "sha256:575a8d6b124ef88f6f51d56d656132389f961062a9177016a50e4f507bbcc19f"}, + {file = "virtualenv-20.36.1.tar.gz", hash = "sha256:8befb5c81842c641f8ee658481e42641c68b5eab3521d8e092d18320902466ba"}, +] + +[package.dependencies] +distlib = ">=0.3.7,<1" +filelock = [ + {version = ">=3.16.1,<4", markers = "python_version < \"3.10\""}, + {version = ">=3.20.1,<4", markers = "python_version >= \"3.10\""}, +] +platformdirs = ">=3.9.1,<5" +typing-extensions = {version = ">=4.13.2", markers = "python_version < \"3.11\""} + +[package.extras] +docs = ["furo (>=2023.7.26)", "proselint (>=0.13)", "sphinx (>=7.1.2,!=7.3)", "sphinx-argparse (>=0.4)", "sphinxcontrib-towncrier (>=0.2.1a0)", "towncrier (>=23.6)"] +test = ["covdefaults (>=2.3)", "coverage (>=7.2.7)", "coverage-enable-subprocess (>=1)", "flaky (>=3.7)", "packaging (>=23.1)", "pytest (>=7.4)", "pytest-env (>=0.8.2)", "pytest-freezer (>=0.4.8) ; platform_python_implementation == \"PyPy\" or platform_python_implementation == \"GraalVM\" or platform_python_implementation == \"CPython\" and sys_platform == \"win32\" and python_version >= \"3.13\"", "pytest-mock (>=3.11.1)", "pytest-randomly (>=3.12)", "pytest-timeout (>=2.1)", "setuptools (>=68)", "time-machine (>=2.10) ; platform_python_implementation == \"CPython\""] + +[[package]] +name = "zipp" +version = "3.19.1" +description = "Backport of pathlib-compatible object wrapper for zip files" +optional = false +python-versions = ">=3.8" +groups = ["dev"] +markers = "python_version == \"3.9\"" +files = [ + {file = "zipp-3.19.1-py3-none-any.whl", hash = "sha256:2828e64edb5386ea6a52e7ba7cdb17bb30a73a858f5eb6eb93d8d36f5ea26091"}, + {file = "zipp-3.19.1.tar.gz", hash = "sha256:35427f6d5594f4acf82d25541438348c26736fa9b3afa2754bcd63cdb99d8e8f"}, +] + +[package.extras] +doc = ["furo", "jaraco.packaging (>=9.3)", "jaraco.tidelift (>=1.4)", "rst.linker (>=1.9)", "sphinx (>=3.5)", "sphinx-lint"] +test = ["big-O", "jaraco.functools", "jaraco.itertools", "jaraco.test", "more-itertools", "pytest (>=6,!=8.1.*)", "pytest-checkdocs (>=2.4)", "pytest-cov", "pytest-enabler (>=2.2)", "pytest-ignore-flaky", "pytest-mypy", "pytest-ruff (>=0.2.1)"] + +[metadata] +lock-version = "2.1" +python-versions = ">=3.9" +content-hash = "0eec9d00c51b390c077534841b531d85c22a7aa138e622b20525166f6a628967" diff --git a/tests/fixtures/real-world-locks/poetry/tomlkit-0.15.1/pyproject.toml b/tests/fixtures/real-world-locks/poetry/tomlkit-0.15.1/pyproject.toml new file mode 100644 index 00000000..c54acdfb --- /dev/null +++ b/tests/fixtures/real-world-locks/poetry/tomlkit-0.15.1/pyproject.toml @@ -0,0 +1,68 @@ +[tool.poetry] +name = "tomlkit" +version = "0.15.1" +description = "Style preserving TOML library" +authors = [ + "Sébastien Eustace ", + "Frost Ming " +] +license = "MIT" + +readme = "README.md" + +homepage = "https://github.com/python-poetry/tomlkit" +repository = "https://github.com/python-poetry/tomlkit" + +include = [ + { path = "tomlkit/py.typed" }, + { path = "tests", format = "sdist" }, + { path = "docs", format = "sdist" }, + { path = "CHANGELOG.md", format = "sdist" }, +] + +[tool.poetry.dependencies] +python = ">=3.9" + +[tool.poetry.group.dev.dependencies] +pytest = "^7.2.0" +pytest-cov = "^4.0.0" +PyYAML = "^6.0" +pre-commit = "^2.20.0" +mypy = "1.19.1" +Sphinx = "^4.3.2" +furo = "^2022.9.29" + +[tool.ruff.lint] +extend-select = [ + "I", # isort + "B", # flake8-bugbear + "C4", # flake8-comprehensions + "PGH", # pygrep-hooks + "RUF", # ruff + "W", # pycodestyle + "YTT", # flake8-2020 +] +extend-ignore = ["B018", "B019", "RUF018"] + +[tool.ruff.lint.mccabe] +max-complexity = 10 + +[tool.ruff.lint.isort] +known-first-party = ["tomlkit"] +known-third-party = ["pytest"] +force-single-line = true +lines-after-imports = 2 +lines-between-types = 1 + +[tool.mypy] +files = "tomlkit, tests" +strict = true +enable_error_code = [ + "ignore-without-code", + "redundant-expr", + "truthy-bool", +] + +[build-system] +requires = ["poetry-core>=1.0.0a9"] +build-backend = "poetry.core.masonry.api" diff --git a/tests/fixtures/real-world-locks/pylock/pipenv-2026.8.0/pylock.toml b/tests/fixtures/real-world-locks/pylock/pipenv-2026.8.0/pylock.toml new file mode 100644 index 00000000..056a2ab7 --- /dev/null +++ b/tests/fixtures/real-world-locks/pylock/pipenv-2026.8.0/pylock.toml @@ -0,0 +1,1774 @@ +lock-version = "1.0" +environments = [] +extras = [] +dependency-groups = ["dev"] +default-groups = ["default"] +created-by = "pipenv" + +[[packages]] +name = "pytz" +version = "2026.1.post1" +index = "https://pypi.org/simple/" +wheels = [ + {name = "pytz-2026.1.post1-py3-none-any.whl", url = "https://pypi.org/simple/pytz/pytz-2026.1.post1-py3-none-any.whl", hashes = {sha256 = "3378dde6a0c3d26719182142c56e60c7f9af7e968076f31aae569d72a0358ee1"}}, + {name = "pytz-2026.1.post1-py3-none-any.whl", url = "https://pypi.org/simple/pytz/pytz-2026.1.post1-py3-none-any.whl", hashes = {sha256 = "f2fd16142fda348286a75e1a524be810bb05d444e5a081f37f7affc635035f7a"}}, +] + + +[[packages]] +name = "alabaster" +version = "1.0.0" +marker = "('dev' in dependency_groups) and (python_version >= '3.10')" +index = "https://pypi.org/simple/" +wheels = [ + {name = "alabaster-1.0.0-py3-none-any.whl", url = "https://pypi.org/simple/alabaster/alabaster-1.0.0-py3-none-any.whl", hashes = {sha256 = "c00dca57bca26fa62a6d7d0a9fcce65f3e026e9bfe33e9c538fd3fbb2144fd9e"}}, + {name = "alabaster-1.0.0-py3-none-any.whl", url = "https://pypi.org/simple/alabaster/alabaster-1.0.0-py3-none-any.whl", hashes = {sha256 = "fc6786402dc3fcb2de3cabd5fe455a2db534b371124f1f21de8731783dec828b"}}, +] + + +[[packages]] +name = "arpeggio" +version = "2.0.3" +marker = "'dev' in dependency_groups" +index = "https://pypi.org/simple/" +wheels = [ + {name = "arpeggio-2.0.3-py3-none-any.whl", url = "https://pypi.org/simple/arpeggio/arpeggio-2.0.3-py3-none-any.whl", hashes = {sha256 = "9374d9c531b62018b787635f37fd81c9a6ee69ef2d28c5db3cd18791b1f7db2f"}}, + {name = "arpeggio-2.0.3-py3-none-any.whl", url = "https://pypi.org/simple/arpeggio/arpeggio-2.0.3-py3-none-any.whl", hashes = {sha256 = "9e85ad35cfc6c938676817c7ae9a1000a7c72a34c71db0c687136c460d12b85e"}}, +] + + +[[packages]] +name = "atomicwrites" +version = "1.4.1" +marker = "('dev' in dependency_groups) and (sys_platform == 'win32')" +index = "https://pypi.org/simple/" +wheels = [ + {name = "atomicwrites-1.4.1-py3-none-any.whl", url = "https://pypi.org/simple/atomicwrites/atomicwrites-1.4.1-py3-none-any.whl", hashes = {sha256 = "81b2c9071a49367a7f770170e5eec8cb66567cfbbc8c73d20ce5ca4a8d71cf11"}}, +] + + +[[packages]] +name = "attrs" +version = "25.4.0" +marker = "('dev' in dependency_groups) and (python_version >= '3.9')" +index = "https://pypi.org/simple/" +wheels = [ + {name = "attrs-25.4.0-py3-none-any.whl", url = "https://pypi.org/simple/attrs/attrs-25.4.0-py3-none-any.whl", hashes = {sha256 = "16d5969b87f0859ef33a48b35d55ac1be6e42ae49d5e853b597db70c35c57e11"}}, + {name = "attrs-25.4.0-py3-none-any.whl", url = "https://pypi.org/simple/attrs/attrs-25.4.0-py3-none-any.whl", hashes = {sha256 = "adcf7e2a1fb3b36ac48d97835bb6d8ade15b8dcce26aba8bf1d14847b57a3373"}}, +] + + +[[packages]] +name = "babel" +version = "2.18.0" +marker = "('dev' in dependency_groups) and (python_version >= '3.8')" +index = "https://pypi.org/simple/" +wheels = [ + {name = "babel-2.18.0-py3-none-any.whl", url = "https://pypi.org/simple/babel/babel-2.18.0-py3-none-any.whl", hashes = {sha256 = "b80b99a14bd085fcacfa15c9165f651fbb3406e66cc603abf11c5750937c992d"}}, + {name = "babel-2.18.0-py3-none-any.whl", url = "https://pypi.org/simple/babel/babel-2.18.0-py3-none-any.whl", hashes = {sha256 = "e2b422b277c2b9a9630c1d7903c2a00d0830c409c59ac8cae9081c92f1aeba35"}}, +] + + +[[packages]] +name = "beautifulsoup4" +version = "4.14.3" +marker = "('dev' in dependency_groups) and (python_full_version >= '3.7.0')" +index = "https://pypi.org/simple/" +wheels = [ + {name = "beautifulsoup4-4.14.3-py3-none-any.whl", url = "https://pypi.org/simple/beautifulsoup4/beautifulsoup4-4.14.3-py3-none-any.whl", hashes = {sha256 = "0918bfe44902e6ad8d57732ba310582e98da931428d231a5ecb9e7c703a735bb"}}, + {name = "beautifulsoup4-4.14.3-py3-none-any.whl", url = "https://pypi.org/simple/beautifulsoup4/beautifulsoup4-4.14.3-py3-none-any.whl", hashes = {sha256 = "6292b1c5186d356bba669ef9f7f051757099565ad9ada5dd630bd9de5fa7fb86"}}, +] + + +[[packages]] +name = "black" +version = "26.3.1" +marker = "('dev' in dependency_groups) and (python_version >= '3.10')" +index = "https://pypi.org/simple/" +wheels = [ + {name = "black-26.3.1-py3-none-any.whl", url = "https://pypi.org/simple/black/black-26.3.1-py3-none-any.whl", hashes = {sha256 = "0126ae5b7c09957da2bdbd91a9ba1207453feada9e9fe51992848658c6c8e01c"}}, + {name = "black-26.3.1-py3-none-any.whl", url = "https://pypi.org/simple/black/black-26.3.1-py3-none-any.whl", hashes = {sha256 = "0f76ff19ec5297dd8e66eb64deda23631e642c9393ab592826fd4bdc97a4bce7"}}, + {name = "black-26.3.1-py3-none-any.whl", url = "https://pypi.org/simple/black/black-26.3.1-py3-none-any.whl", hashes = {sha256 = "28ef38aee69e4b12fda8dba75e21f9b4f979b490c8ac0baa7cb505369ac9e1ff"}}, + {name = "black-26.3.1-py3-none-any.whl", url = "https://pypi.org/simple/black/black-26.3.1-py3-none-any.whl", hashes = {sha256 = "2bd5aa94fc267d38bb21a70d7410a89f1a1d318841855f698746f8e7f51acd1b"}}, + {name = "black-26.3.1-py3-none-any.whl", url = "https://pypi.org/simple/black/black-26.3.1-py3-none-any.whl", hashes = {sha256 = "2c50f5063a9641c7eed7795014ba37b0f5fa227f3d408b968936e24bc0566b07"}}, + {name = "black-26.3.1-py3-none-any.whl", url = "https://pypi.org/simple/black/black-26.3.1-py3-none-any.whl", hashes = {sha256 = "2d6bfaf7fd0993b420bed691f20f9492d53ce9a2bcccea4b797d34e947318a78"}}, + {name = "black-26.3.1-py3-none-any.whl", url = "https://pypi.org/simple/black/black-26.3.1-py3-none-any.whl", hashes = {sha256 = "41cd2012d35b47d589cb8a16faf8a32ef7a336f56356babd9fcf70939ad1897f"}}, + {name = "black-26.3.1-py3-none-any.whl", url = "https://pypi.org/simple/black/black-26.3.1-py3-none-any.whl", hashes = {sha256 = "474c27574d6d7037c1bc875a81d9be0a9a4f9ee95e62800dab3cfaadbf75acd5"}}, + {name = "black-26.3.1-py3-none-any.whl", url = "https://pypi.org/simple/black/black-26.3.1-py3-none-any.whl", hashes = {sha256 = "5602bdb96d52d2d0672f24f6ffe5218795736dd34807fd0fd55ccd6bf206168b"}}, + {name = "black-26.3.1-py3-none-any.whl", url = "https://pypi.org/simple/black/black-26.3.1-py3-none-any.whl", hashes = {sha256 = "5e9d0d86df21f2e1677cc4bd090cd0e446278bcbbe49bf3659c308c3e402843e"}}, + {name = "black-26.3.1-py3-none-any.whl", url = "https://pypi.org/simple/black/black-26.3.1-py3-none-any.whl", hashes = {sha256 = "5ed0ca58586c8d9a487352a96b15272b7fa55d139fc8496b519e78023a8dab0a"}}, + {name = "black-26.3.1-py3-none-any.whl", url = "https://pypi.org/simple/black/black-26.3.1-py3-none-any.whl", hashes = {sha256 = "6c54a4a82e291a1fee5137371ab488866b7c86a3305af4026bdd4dc78642e1ac"}}, + {name = "black-26.3.1-py3-none-any.whl", url = "https://pypi.org/simple/black/black-26.3.1-py3-none-any.whl", hashes = {sha256 = "6e131579c243c98f35bce64a7e08e87fb2d610544754675d4a0e73a070a5aa3a"}}, + {name = "black-26.3.1-py3-none-any.whl", url = "https://pypi.org/simple/black/black-26.3.1-py3-none-any.whl", hashes = {sha256 = "855822d90f884905362f602880ed8b5df1b7e3ee7d0db2502d4388a954cc8c54"}}, + {name = "black-26.3.1-py3-none-any.whl", url = "https://pypi.org/simple/black/black-26.3.1-py3-none-any.whl", hashes = {sha256 = "86a8b5035fce64f5dcd1b794cf8ec4d31fe458cf6ce3986a30deb434df82a1d2"}}, + {name = "black-26.3.1-py3-none-any.whl", url = "https://pypi.org/simple/black/black-26.3.1-py3-none-any.whl", hashes = {sha256 = "8a33d657f3276328ce00e4d37fe70361e1ec7614da5d7b6e78de5426cb56332f"}}, + {name = "black-26.3.1-py3-none-any.whl", url = "https://pypi.org/simple/black/black-26.3.1-py3-none-any.whl", hashes = {sha256 = "92c0ec1f2cc149551a2b7b47efc32c866406b6891b0ee4625e95967c8f4acfb1"}}, + {name = "black-26.3.1-py3-none-any.whl", url = "https://pypi.org/simple/black/black-26.3.1-py3-none-any.whl", hashes = {sha256 = "9a5e9f45e5d5e1c5b5c29b3bd4265dcc90e8b92cf4534520896ed77f791f4da5"}}, + {name = "black-26.3.1-py3-none-any.whl", url = "https://pypi.org/simple/black/black-26.3.1-py3-none-any.whl", hashes = {sha256 = "afc622538b430aa4c8c853f7f63bc582b3b8030fd8c80b70fb5fa5b834e575c2"}}, + {name = "black-26.3.1-py3-none-any.whl", url = "https://pypi.org/simple/black/black-26.3.1-py3-none-any.whl", hashes = {sha256 = "b07fc0dab849d24a80a29cfab8d8a19187d1c4685d8a5e6385a5ce323c1f015f"}}, + {name = "black-26.3.1-py3-none-any.whl", url = "https://pypi.org/simple/black/black-26.3.1-py3-none-any.whl", hashes = {sha256 = "b5e6f89631eb88a7302d416594a32faeee9fb8fb848290da9d0a5f2903519fc1"}}, + {name = "black-26.3.1-py3-none-any.whl", url = "https://pypi.org/simple/black/black-26.3.1-py3-none-any.whl", hashes = {sha256 = "bf9bf162ed91a26f1adba8efda0b573bc6924ec1408a52cc6f82cb73ec2b142c"}}, + {name = "black-26.3.1-py3-none-any.whl", url = "https://pypi.org/simple/black/black-26.3.1-py3-none-any.whl", hashes = {sha256 = "c7e72339f841b5a237ff14f7d3880ddd0fc7f98a1199e8c4327f9a4f478c1839"}}, + {name = "black-26.3.1-py3-none-any.whl", url = "https://pypi.org/simple/black/black-26.3.1-py3-none-any.whl", hashes = {sha256 = "ddb113db38838eb9f043623ba274cfaf7d51d5b0c22ecb30afe58b1bb8322983"}}, + {name = "black-26.3.1-py3-none-any.whl", url = "https://pypi.org/simple/black/black-26.3.1-py3-none-any.whl", hashes = {sha256 = "dfdd51fc3e64ea4f35873d1b3fb25326773d55d2329ff8449139ebaad7357efb"}}, + {name = "black-26.3.1-py3-none-any.whl", url = "https://pypi.org/simple/black/black-26.3.1-py3-none-any.whl", hashes = {sha256 = "f1cd08e99d2f9317292a311dfe578fd2a24b15dbce97792f9c4d752275c1fa56"}}, + {name = "black-26.3.1-py3-none-any.whl", url = "https://pypi.org/simple/black/black-26.3.1-py3-none-any.whl", hashes = {sha256 = "f89f2ab047c76a9c03f78d0d66ca519e389519902fa27e7a91117ef7611c0568"}}, +] + + +[[packages]] +name = "bottle" +version = "0.13.4" +marker = "'dev' in dependency_groups" +index = "https://pypi.org/simple/" +wheels = [ + {name = "bottle-0.13.4-py3-none-any.whl", url = "https://pypi.org/simple/bottle/bottle-0.13.4-py3-none-any.whl", hashes = {sha256 = "045684fbd2764eac9cdeb824861d1551d113e8b683d8d26e296898d3dd99a12e"}}, + {name = "bottle-0.13.4-py3-none-any.whl", url = "https://pypi.org/simple/bottle/bottle-0.13.4-py3-none-any.whl", hashes = {sha256 = "787e78327e12b227938de02248333d788cfe45987edca735f8f88e03472c3f47"}}, +] + + +[[packages]] +name = "build" +version = "1.4.0" +marker = "('dev' in dependency_groups) and (python_version >= '3.9')" +index = "https://pypi.org/simple/" +wheels = [ + {name = "build-1.4.0-py3-none-any.whl", url = "https://pypi.org/simple/build/build-1.4.0-py3-none-any.whl", hashes = {sha256 = "6a07c1b8eb6f2b311b96fcbdbce5dab5fe637ffda0fd83c9cac622e927501596"}}, + {name = "build-1.4.0-py3-none-any.whl", url = "https://pypi.org/simple/build/build-1.4.0-py3-none-any.whl", hashes = {sha256 = "f1b91b925aa322be454f8330c6fb48b465da993d1e7e7e6fa35027ec49f3c936"}}, +] + + +[[packages]] +name = "certifi" +version = "2026.2.25" +marker = "('dev' in dependency_groups) and (python_version >= '3.7')" +index = "https://pypi.org/simple/" +wheels = [ + {name = "certifi-2026.2.25-py3-none-any.whl", url = "https://pypi.org/simple/certifi/certifi-2026.2.25-py3-none-any.whl", hashes = {sha256 = "027692e4402ad994f1c42e52a4997a9763c646b73e4096e4d5d6db8af1d6f0fa"}}, + {name = "certifi-2026.2.25-py3-none-any.whl", url = "https://pypi.org/simple/certifi/certifi-2026.2.25-py3-none-any.whl", hashes = {sha256 = "e887ab5cee78ea814d3472169153c2d12cd43b14bd03329a39a9c6e2e80bfba7"}}, +] + + +[[packages]] +name = "cffi" +version = "2.0.0" +marker = "('dev' in dependency_groups) and (python_version >= '3.9')" +index = "https://pypi.org/simple/" +wheels = [ + {name = "cffi-2.0.0-py3-none-any.whl", url = "https://pypi.org/simple/cffi/cffi-2.0.0-py3-none-any.whl", hashes = {sha256 = "00bdf7acc5f795150faa6957054fbbca2439db2f775ce831222b66f192f03beb"}}, + {name = "cffi-2.0.0-py3-none-any.whl", url = "https://pypi.org/simple/cffi/cffi-2.0.0-py3-none-any.whl", hashes = {sha256 = "07b271772c100085dd28b74fa0cd81c8fb1a3ba18b21e03d7c27f3436a10606b"}}, + {name = "cffi-2.0.0-py3-none-any.whl", url = "https://pypi.org/simple/cffi/cffi-2.0.0-py3-none-any.whl", hashes = {sha256 = "087067fa8953339c723661eda6b54bc98c5625757ea62e95eb4898ad5e776e9f"}}, + {name = "cffi-2.0.0-py3-none-any.whl", url = "https://pypi.org/simple/cffi/cffi-2.0.0-py3-none-any.whl", hashes = {sha256 = "0a1527a803f0a659de1af2e1fd700213caba79377e27e4693648c2923da066f9"}}, + {name = "cffi-2.0.0-py3-none-any.whl", url = "https://pypi.org/simple/cffi/cffi-2.0.0-py3-none-any.whl", hashes = {sha256 = "0cf2d91ecc3fcc0625c2c530fe004f82c110405f101548512cce44322fa8ac44"}}, + {name = "cffi-2.0.0-py3-none-any.whl", url = "https://pypi.org/simple/cffi/cffi-2.0.0-py3-none-any.whl", hashes = {sha256 = "0f6084a0ea23d05d20c3edcda20c3d006f9b6f3fefeac38f59262e10cef47ee2"}}, + {name = "cffi-2.0.0-py3-none-any.whl", url = "https://pypi.org/simple/cffi/cffi-2.0.0-py3-none-any.whl", hashes = {sha256 = "12873ca6cb9b0f0d3a0da705d6086fe911591737a59f28b7936bdfed27c0d47c"}}, + {name = "cffi-2.0.0-py3-none-any.whl", url = "https://pypi.org/simple/cffi/cffi-2.0.0-py3-none-any.whl", hashes = {sha256 = "19f705ada2530c1167abacb171925dd886168931e0a7b78f5bffcae5c6b5be75"}}, + {name = "cffi-2.0.0-py3-none-any.whl", url = "https://pypi.org/simple/cffi/cffi-2.0.0-py3-none-any.whl", hashes = {sha256 = "1cd13c99ce269b3ed80b417dcd591415d3372bcac067009b6e0f59c7d4015e65"}}, + {name = "cffi-2.0.0-py3-none-any.whl", url = "https://pypi.org/simple/cffi/cffi-2.0.0-py3-none-any.whl", hashes = {sha256 = "1e3a615586f05fc4065a8b22b8152f0c1b00cdbc60596d187c2a74f9e3036e4e"}}, + {name = "cffi-2.0.0-py3-none-any.whl", url = "https://pypi.org/simple/cffi/cffi-2.0.0-py3-none-any.whl", hashes = {sha256 = "1f72fb8906754ac8a2cc3f9f5aaa298070652a0ffae577e0ea9bd480dc3c931a"}}, + {name = "cffi-2.0.0-py3-none-any.whl", url = "https://pypi.org/simple/cffi/cffi-2.0.0-py3-none-any.whl", hashes = {sha256 = "1fc9ea04857caf665289b7a75923f2c6ed559b8298a1b8c49e59f7dd95c8481e"}}, + {name = "cffi-2.0.0-py3-none-any.whl", url = "https://pypi.org/simple/cffi/cffi-2.0.0-py3-none-any.whl", hashes = {sha256 = "203a48d1fb583fc7d78a4c6655692963b860a417c0528492a6bc21f1aaefab25"}}, + {name = "cffi-2.0.0-py3-none-any.whl", url = "https://pypi.org/simple/cffi/cffi-2.0.0-py3-none-any.whl", hashes = {sha256 = "2081580ebb843f759b9f617314a24ed5738c51d2aee65d31e02f6f7a2b97707a"}}, + {name = "cffi-2.0.0-py3-none-any.whl", url = "https://pypi.org/simple/cffi/cffi-2.0.0-py3-none-any.whl", hashes = {sha256 = "21d1152871b019407d8ac3985f6775c079416c282e431a4da6afe7aefd2bccbe"}}, + {name = "cffi-2.0.0-py3-none-any.whl", url = "https://pypi.org/simple/cffi/cffi-2.0.0-py3-none-any.whl", hashes = {sha256 = "24b6f81f1983e6df8db3adc38562c83f7d4a0c36162885ec7f7b77c7dcbec97b"}}, + {name = "cffi-2.0.0-py3-none-any.whl", url = "https://pypi.org/simple/cffi/cffi-2.0.0-py3-none-any.whl", hashes = {sha256 = "256f80b80ca3853f90c21b23ee78cd008713787b1b1e93eae9f3d6a7134abd91"}}, + {name = "cffi-2.0.0-py3-none-any.whl", url = "https://pypi.org/simple/cffi/cffi-2.0.0-py3-none-any.whl", hashes = {sha256 = "28a3a209b96630bca57cce802da70c266eb08c6e97e5afd61a75611ee6c64592"}}, + {name = "cffi-2.0.0-py3-none-any.whl", url = "https://pypi.org/simple/cffi/cffi-2.0.0-py3-none-any.whl", hashes = {sha256 = "2c8f814d84194c9ea681642fd164267891702542f028a15fc97d4674b6206187"}}, + {name = "cffi-2.0.0-py3-none-any.whl", url = "https://pypi.org/simple/cffi/cffi-2.0.0-py3-none-any.whl", hashes = {sha256 = "2de9a304e27f7596cd03d16f1b7c72219bd944e99cc52b84d0145aefb07cbd3c"}}, + {name = "cffi-2.0.0-py3-none-any.whl", url = "https://pypi.org/simple/cffi/cffi-2.0.0-py3-none-any.whl", hashes = {sha256 = "38100abb9d1b1435bc4cc340bb4489635dc2f0da7456590877030c9b3d40b0c1"}}, + {name = "cffi-2.0.0-py3-none-any.whl", url = "https://pypi.org/simple/cffi/cffi-2.0.0-py3-none-any.whl", hashes = {sha256 = "3925dd22fa2b7699ed2617149842d2e6adde22b262fcbfada50e3d195e4b3a94"}}, + {name = "cffi-2.0.0-py3-none-any.whl", url = "https://pypi.org/simple/cffi/cffi-2.0.0-py3-none-any.whl", hashes = {sha256 = "3e17ed538242334bf70832644a32a7aae3d83b57567f9fd60a26257e992b79ba"}}, + {name = "cffi-2.0.0-py3-none-any.whl", url = "https://pypi.org/simple/cffi/cffi-2.0.0-py3-none-any.whl", hashes = {sha256 = "3e837e369566884707ddaf85fc1744b47575005c0a229de3327f8f9a20f4efeb"}}, + {name = "cffi-2.0.0-py3-none-any.whl", url = "https://pypi.org/simple/cffi/cffi-2.0.0-py3-none-any.whl", hashes = {sha256 = "3f4d46d8b35698056ec29bca21546e1551a205058ae1a181d871e278b0b28165"}}, + {name = "cffi-2.0.0-py3-none-any.whl", url = "https://pypi.org/simple/cffi/cffi-2.0.0-py3-none-any.whl", hashes = {sha256 = "44d1b5909021139fe36001ae048dbdde8214afa20200eda0f64c068cac5d5529"}}, + {name = "cffi-2.0.0-py3-none-any.whl", url = "https://pypi.org/simple/cffi/cffi-2.0.0-py3-none-any.whl", hashes = {sha256 = "45d5e886156860dc35862657e1494b9bae8dfa63bf56796f2fb56e1679fc0bca"}}, + {name = "cffi-2.0.0-py3-none-any.whl", url = "https://pypi.org/simple/cffi/cffi-2.0.0-py3-none-any.whl", hashes = {sha256 = "4647afc2f90d1ddd33441e5b0e85b16b12ddec4fca55f0d9671fef036ecca27c"}}, + {name = "cffi-2.0.0-py3-none-any.whl", url = "https://pypi.org/simple/cffi/cffi-2.0.0-py3-none-any.whl", hashes = {sha256 = "4671d9dd5ec934cb9a73e7ee9676f9362aba54f7f34910956b84d727b0d73fb6"}}, + {name = "cffi-2.0.0-py3-none-any.whl", url = "https://pypi.org/simple/cffi/cffi-2.0.0-py3-none-any.whl", hashes = {sha256 = "53f77cbe57044e88bbd5ed26ac1d0514d2acf0591dd6bb02a3ae37f76811b80c"}}, + {name = "cffi-2.0.0-py3-none-any.whl", url = "https://pypi.org/simple/cffi/cffi-2.0.0-py3-none-any.whl", hashes = {sha256 = "5eda85d6d1879e692d546a078b44251cdd08dd1cfb98dfb77b670c97cee49ea0"}}, + {name = "cffi-2.0.0-py3-none-any.whl", url = "https://pypi.org/simple/cffi/cffi-2.0.0-py3-none-any.whl", hashes = {sha256 = "5fed36fccc0612a53f1d4d9a816b50a36702c28a2aa880cb8a122b3466638743"}}, + {name = "cffi-2.0.0-py3-none-any.whl", url = "https://pypi.org/simple/cffi/cffi-2.0.0-py3-none-any.whl", hashes = {sha256 = "61d028e90346df14fedc3d1e5441df818d095f3b87d286825dfcbd6459b7ef63"}}, + {name = "cffi-2.0.0-py3-none-any.whl", url = "https://pypi.org/simple/cffi/cffi-2.0.0-py3-none-any.whl", hashes = {sha256 = "66f011380d0e49ed280c789fbd08ff0d40968ee7b665575489afa95c98196ab5"}}, + {name = "cffi-2.0.0-py3-none-any.whl", url = "https://pypi.org/simple/cffi/cffi-2.0.0-py3-none-any.whl", hashes = {sha256 = "6824f87845e3396029f3820c206e459ccc91760e8fa24422f8b0c3d1731cbec5"}}, + {name = "cffi-2.0.0-py3-none-any.whl", url = "https://pypi.org/simple/cffi/cffi-2.0.0-py3-none-any.whl", hashes = {sha256 = "6c6c373cfc5c83a975506110d17457138c8c63016b563cc9ed6e056a82f13ce4"}}, + {name = "cffi-2.0.0-py3-none-any.whl", url = "https://pypi.org/simple/cffi/cffi-2.0.0-py3-none-any.whl", hashes = {sha256 = "6d02d6655b0e54f54c4ef0b94eb6be0607b70853c45ce98bd278dc7de718be5d"}}, + {name = "cffi-2.0.0-py3-none-any.whl", url = "https://pypi.org/simple/cffi/cffi-2.0.0-py3-none-any.whl", hashes = {sha256 = "6d50360be4546678fc1b79ffe7a66265e28667840010348dd69a314145807a1b"}}, + {name = "cffi-2.0.0-py3-none-any.whl", url = "https://pypi.org/simple/cffi/cffi-2.0.0-py3-none-any.whl", hashes = {sha256 = "730cacb21e1bdff3ce90babf007d0a0917cc3e6492f336c2f0134101e0944f93"}}, + {name = "cffi-2.0.0-py3-none-any.whl", url = "https://pypi.org/simple/cffi/cffi-2.0.0-py3-none-any.whl", hashes = {sha256 = "737fe7d37e1a1bffe70bd5754ea763a62a066dc5913ca57e957824b72a85e205"}}, + {name = "cffi-2.0.0-py3-none-any.whl", url = "https://pypi.org/simple/cffi/cffi-2.0.0-py3-none-any.whl", hashes = {sha256 = "74a03b9698e198d47562765773b4a8309919089150a0bb17d829ad7b44b60d27"}}, + {name = "cffi-2.0.0-py3-none-any.whl", url = "https://pypi.org/simple/cffi/cffi-2.0.0-py3-none-any.whl", hashes = {sha256 = "7553fb2090d71822f02c629afe6042c299edf91ba1bf94951165613553984512"}}, + {name = "cffi-2.0.0-py3-none-any.whl", url = "https://pypi.org/simple/cffi/cffi-2.0.0-py3-none-any.whl", hashes = {sha256 = "7a66c7204d8869299919db4d5069a82f1561581af12b11b3c9f48c584eb8743d"}}, + {name = "cffi-2.0.0-py3-none-any.whl", url = "https://pypi.org/simple/cffi/cffi-2.0.0-py3-none-any.whl", hashes = {sha256 = "7cc09976e8b56f8cebd752f7113ad07752461f48a58cbba644139015ac24954c"}}, + {name = "cffi-2.0.0-py3-none-any.whl", url = "https://pypi.org/simple/cffi/cffi-2.0.0-py3-none-any.whl", hashes = {sha256 = "81afed14892743bbe14dacb9e36d9e0e504cd204e0b165062c488942b9718037"}}, + {name = "cffi-2.0.0-py3-none-any.whl", url = "https://pypi.org/simple/cffi/cffi-2.0.0-py3-none-any.whl", hashes = {sha256 = "8941aaadaf67246224cee8c3803777eed332a19d909b47e29c9842ef1e79ac26"}}, + {name = "cffi-2.0.0-py3-none-any.whl", url = "https://pypi.org/simple/cffi/cffi-2.0.0-py3-none-any.whl", hashes = {sha256 = "89472c9762729b5ae1ad974b777416bfda4ac5642423fa93bd57a09204712322"}}, + {name = "cffi-2.0.0-py3-none-any.whl", url = "https://pypi.org/simple/cffi/cffi-2.0.0-py3-none-any.whl", hashes = {sha256 = "8ea985900c5c95ce9db1745f7933eeef5d314f0565b27625d9a10ec9881e1bfb"}}, + {name = "cffi-2.0.0-py3-none-any.whl", url = "https://pypi.org/simple/cffi/cffi-2.0.0-py3-none-any.whl", hashes = {sha256 = "8eca2a813c1cb7ad4fb74d368c2ffbbb4789d377ee5bb8df98373c2cc0dee76c"}}, + {name = "cffi-2.0.0-py3-none-any.whl", url = "https://pypi.org/simple/cffi/cffi-2.0.0-py3-none-any.whl", hashes = {sha256 = "92b68146a71df78564e4ef48af17551a5ddd142e5190cdf2c5624d0c3ff5b2e8"}}, + {name = "cffi-2.0.0-py3-none-any.whl", url = "https://pypi.org/simple/cffi/cffi-2.0.0-py3-none-any.whl", hashes = {sha256 = "9332088d75dc3241c702d852d4671613136d90fa6881da7d770a483fd05248b4"}}, + {name = "cffi-2.0.0-py3-none-any.whl", url = "https://pypi.org/simple/cffi/cffi-2.0.0-py3-none-any.whl", hashes = {sha256 = "94698a9c5f91f9d138526b48fe26a199609544591f859c870d477351dc7b2414"}}, + {name = "cffi-2.0.0-py3-none-any.whl", url = "https://pypi.org/simple/cffi/cffi-2.0.0-py3-none-any.whl", hashes = {sha256 = "9a67fc9e8eb39039280526379fb3a70023d77caec1852002b4da7e8b270c4dd9"}}, + {name = "cffi-2.0.0-py3-none-any.whl", url = "https://pypi.org/simple/cffi/cffi-2.0.0-py3-none-any.whl", hashes = {sha256 = "9de40a7b0323d889cf8d23d1ef214f565ab154443c42737dfe52ff82cf857664"}}, + {name = "cffi-2.0.0-py3-none-any.whl", url = "https://pypi.org/simple/cffi/cffi-2.0.0-py3-none-any.whl", hashes = {sha256 = "a05d0c237b3349096d3981b727493e22147f934b20f6f125a3eba8f994bec4a9"}}, + {name = "cffi-2.0.0-py3-none-any.whl", url = "https://pypi.org/simple/cffi/cffi-2.0.0-py3-none-any.whl", hashes = {sha256 = "afb8db5439b81cf9c9d0c80404b60c3cc9c3add93e114dcae767f1477cb53775"}}, + {name = "cffi-2.0.0-py3-none-any.whl", url = "https://pypi.org/simple/cffi/cffi-2.0.0-py3-none-any.whl", hashes = {sha256 = "b18a3ed7d5b3bd8d9ef7a8cb226502c6bf8308df1525e1cc676c3680e7176739"}}, + {name = "cffi-2.0.0-py3-none-any.whl", url = "https://pypi.org/simple/cffi/cffi-2.0.0-py3-none-any.whl", hashes = {sha256 = "b1e74d11748e7e98e2f426ab176d4ed720a64412b6a15054378afdb71e0f37dc"}}, + {name = "cffi-2.0.0-py3-none-any.whl", url = "https://pypi.org/simple/cffi/cffi-2.0.0-py3-none-any.whl", hashes = {sha256 = "b21e08af67b8a103c71a250401c78d5e0893beff75e28c53c98f4de42f774062"}}, + {name = "cffi-2.0.0-py3-none-any.whl", url = "https://pypi.org/simple/cffi/cffi-2.0.0-py3-none-any.whl", hashes = {sha256 = "b4c854ef3adc177950a8dfc81a86f5115d2abd545751a304c5bcf2c2c7283cfe"}}, + {name = "cffi-2.0.0-py3-none-any.whl", url = "https://pypi.org/simple/cffi/cffi-2.0.0-py3-none-any.whl", hashes = {sha256 = "b882b3df248017dba09d6b16defe9b5c407fe32fc7c65a9c69798e6175601be9"}}, + {name = "cffi-2.0.0-py3-none-any.whl", url = "https://pypi.org/simple/cffi/cffi-2.0.0-py3-none-any.whl", hashes = {sha256 = "baf5215e0ab74c16e2dd324e8ec067ef59e41125d3eade2b863d294fd5035c92"}}, + {name = "cffi-2.0.0-py3-none-any.whl", url = "https://pypi.org/simple/cffi/cffi-2.0.0-py3-none-any.whl", hashes = {sha256 = "c649e3a33450ec82378822b3dad03cc228b8f5963c0c12fc3b1e0ab940f768a5"}}, + {name = "cffi-2.0.0-py3-none-any.whl", url = "https://pypi.org/simple/cffi/cffi-2.0.0-py3-none-any.whl", hashes = {sha256 = "c654de545946e0db659b3400168c9ad31b5d29593291482c43e3564effbcee13"}}, + {name = "cffi-2.0.0-py3-none-any.whl", url = "https://pypi.org/simple/cffi/cffi-2.0.0-py3-none-any.whl", hashes = {sha256 = "c6638687455baf640e37344fe26d37c404db8b80d037c3d29f58fe8d1c3b194d"}}, + {name = "cffi-2.0.0-py3-none-any.whl", url = "https://pypi.org/simple/cffi/cffi-2.0.0-py3-none-any.whl", hashes = {sha256 = "c8d3b5532fc71b7a77c09192b4a5a200ea992702734a2e9279a37f2478236f26"}}, + {name = "cffi-2.0.0-py3-none-any.whl", url = "https://pypi.org/simple/cffi/cffi-2.0.0-py3-none-any.whl", hashes = {sha256 = "cb527a79772e5ef98fb1d700678fe031e353e765d1ca2d409c92263c6d43e09f"}}, + {name = "cffi-2.0.0-py3-none-any.whl", url = "https://pypi.org/simple/cffi/cffi-2.0.0-py3-none-any.whl", hashes = {sha256 = "cf364028c016c03078a23b503f02058f1814320a56ad535686f90565636a9495"}}, + {name = "cffi-2.0.0-py3-none-any.whl", url = "https://pypi.org/simple/cffi/cffi-2.0.0-py3-none-any.whl", hashes = {sha256 = "d48a880098c96020b02d5a1f7d9251308510ce8858940e6fa99ece33f610838b"}}, + {name = "cffi-2.0.0-py3-none-any.whl", url = "https://pypi.org/simple/cffi/cffi-2.0.0-py3-none-any.whl", hashes = {sha256 = "d68b6cef7827e8641e8ef16f4494edda8b36104d79773a334beaa1e3521430f6"}}, + {name = "cffi-2.0.0-py3-none-any.whl", url = "https://pypi.org/simple/cffi/cffi-2.0.0-py3-none-any.whl", hashes = {sha256 = "d9b29c1f0ae438d5ee9acb31cadee00a58c46cc9c0b2f9038c6b0b3470877a8c"}}, + {name = "cffi-2.0.0-py3-none-any.whl", url = "https://pypi.org/simple/cffi/cffi-2.0.0-py3-none-any.whl", hashes = {sha256 = "d9b97165e8aed9272a6bb17c01e3cc5871a594a446ebedc996e2397a1c1ea8ef"}}, + {name = "cffi-2.0.0-py3-none-any.whl", url = "https://pypi.org/simple/cffi/cffi-2.0.0-py3-none-any.whl", hashes = {sha256 = "da68248800ad6320861f129cd9c1bf96ca849a2771a59e0344e88681905916f5"}}, + {name = "cffi-2.0.0-py3-none-any.whl", url = "https://pypi.org/simple/cffi/cffi-2.0.0-py3-none-any.whl", hashes = {sha256 = "da902562c3e9c550df360bfa53c035b2f241fed6d9aef119048073680ace4a18"}}, + {name = "cffi-2.0.0-py3-none-any.whl", url = "https://pypi.org/simple/cffi/cffi-2.0.0-py3-none-any.whl", hashes = {sha256 = "dbd5c7a25a7cb98f5ca55d258b103a2054f859a46ae11aaf23134f9cc0d356ad"}}, + {name = "cffi-2.0.0-py3-none-any.whl", url = "https://pypi.org/simple/cffi/cffi-2.0.0-py3-none-any.whl", hashes = {sha256 = "dd4f05f54a52fb558f1ba9f528228066954fee3ebe629fc1660d874d040ae5a3"}}, + {name = "cffi-2.0.0-py3-none-any.whl", url = "https://pypi.org/simple/cffi/cffi-2.0.0-py3-none-any.whl", hashes = {sha256 = "de8dad4425a6ca6e4e5e297b27b5c824ecc7581910bf9aee86cb6835e6812aa7"}}, + {name = "cffi-2.0.0-py3-none-any.whl", url = "https://pypi.org/simple/cffi/cffi-2.0.0-py3-none-any.whl", hashes = {sha256 = "e11e82b744887154b182fd3e7e8512418446501191994dbf9c9fc1f32cc8efd5"}}, + {name = "cffi-2.0.0-py3-none-any.whl", url = "https://pypi.org/simple/cffi/cffi-2.0.0-py3-none-any.whl", hashes = {sha256 = "e6e73b9e02893c764e7e8d5bb5ce277f1a009cd5243f8228f75f842bf937c534"}}, + {name = "cffi-2.0.0-py3-none-any.whl", url = "https://pypi.org/simple/cffi/cffi-2.0.0-py3-none-any.whl", hashes = {sha256 = "f73b96c41e3b2adedc34a7356e64c8eb96e03a3782b535e043a986276ce12a49"}}, + {name = "cffi-2.0.0-py3-none-any.whl", url = "https://pypi.org/simple/cffi/cffi-2.0.0-py3-none-any.whl", hashes = {sha256 = "f93fd8e5c8c0a4aa1f424d6173f14a892044054871c771f8566e4008eaa359d2"}}, + {name = "cffi-2.0.0-py3-none-any.whl", url = "https://pypi.org/simple/cffi/cffi-2.0.0-py3-none-any.whl", hashes = {sha256 = "fc33c5141b55ed366cfaad382df24fe7dcbc686de5be719b207bb248e3053dc5"}}, + {name = "cffi-2.0.0-py3-none-any.whl", url = "https://pypi.org/simple/cffi/cffi-2.0.0-py3-none-any.whl", hashes = {sha256 = "fc7de24befaeae77ba923797c7c87834c73648a05a4bde34b3b7e5588973a453"}}, + {name = "cffi-2.0.0-py3-none-any.whl", url = "https://pypi.org/simple/cffi/cffi-2.0.0-py3-none-any.whl", hashes = {sha256 = "fe562eb1a64e67dd297ccc4f5addea2501664954f2692b69a76449ec7913ecbf"}}, +] + + +[[packages]] +name = "cfgv" +version = "3.5.0" +marker = "('dev' in dependency_groups) and (python_version >= '3.10')" +index = "https://pypi.org/simple/" +wheels = [ + {name = "cfgv-3.5.0-py3-none-any.whl", url = "https://pypi.org/simple/cfgv/cfgv-3.5.0-py3-none-any.whl", hashes = {sha256 = "a8dc6b26ad22ff227d2634a65cb388215ce6cc96bbcc5cfde7641ae87e8dacc0"}}, + {name = "cfgv-3.5.0-py3-none-any.whl", url = "https://pypi.org/simple/cfgv/cfgv-3.5.0-py3-none-any.whl", hashes = {sha256 = "d5b1034354820651caa73ede66a6294d6e95c1b00acc5e9b098e917404669132"}}, +] + + +[[packages]] +name = "charset-normalizer" +version = "3.4.6" +marker = "('dev' in dependency_groups) and (python_version >= '3.7')" +index = "https://pypi.org/simple/" +wheels = [ + {name = "charset_normalizer-3.4.6-py3-none-any.whl", url = "https://pypi.org/simple/charset-normalizer/charset_normalizer-3.4.6-py3-none-any.whl", hashes = {sha256 = "06a7e86163334edfc5d20fe104db92fcd666e5a5df0977cb5680a506fe26cc8e"}}, + {name = "charset_normalizer-3.4.6-py3-none-any.whl", url = "https://pypi.org/simple/charset-normalizer/charset_normalizer-3.4.6-py3-none-any.whl", hashes = {sha256 = "0c173ce3a681f309f31b87125fecec7a5d1347261ea11ebbb856fa6006b23c8c"}}, + {name = "charset_normalizer-3.4.6-py3-none-any.whl", url = "https://pypi.org/simple/charset-normalizer/charset_normalizer-3.4.6-py3-none-any.whl", hashes = {sha256 = "0e28d62a8fc7a1fa411c43bd65e346f3bce9716dc51b897fbe930c5987b402d5"}}, + {name = "charset_normalizer-3.4.6-py3-none-any.whl", url = "https://pypi.org/simple/charset-normalizer/charset_normalizer-3.4.6-py3-none-any.whl", hashes = {sha256 = "0e901eb1049fdb80f5bd11ed5ea1e498ec423102f7a9b9e4645d5b8204ff2815"}}, + {name = "charset_normalizer-3.4.6-py3-none-any.whl", url = "https://pypi.org/simple/charset-normalizer/charset_normalizer-3.4.6-py3-none-any.whl", hashes = {sha256 = "11afb56037cbc4b1555a34dd69151e8e069bee82e613a73bef6e714ce733585f"}}, + {name = "charset_normalizer-3.4.6-py3-none-any.whl", url = "https://pypi.org/simple/charset-normalizer/charset_normalizer-3.4.6-py3-none-any.whl", hashes = {sha256 = "150b8ce8e830eb7ccb029ec9ca36022f756986aaaa7956aad6d9ec90089338c0"}}, + {name = "charset_normalizer-3.4.6-py3-none-any.whl", url = "https://pypi.org/simple/charset-normalizer/charset_normalizer-3.4.6-py3-none-any.whl", hashes = {sha256 = "172985e4ff804a7ad08eebec0a1640ece87ba5041d565fff23c8f99c1f389484"}}, + {name = "charset_normalizer-3.4.6-py3-none-any.whl", url = "https://pypi.org/simple/charset-normalizer/charset_normalizer-3.4.6-py3-none-any.whl", hashes = {sha256 = "197c1a244a274bb016dd8b79204850144ef77fe81c5b797dc389327adb552407"}}, + {name = "charset_normalizer-3.4.6-py3-none-any.whl", url = "https://pypi.org/simple/charset-normalizer/charset_normalizer-3.4.6-py3-none-any.whl", hashes = {sha256 = "1ae6b62897110aa7c79ea2f5dd38d1abca6db663687c0b1ad9aed6f6bae3d9d6"}}, + {name = "charset_normalizer-3.4.6-py3-none-any.whl", url = "https://pypi.org/simple/charset-normalizer/charset_normalizer-3.4.6-py3-none-any.whl", hashes = {sha256 = "1cf0a70018692f85172348fe06d3a4b63f94ecb055e13a00c644d368eb82e5b8"}}, + {name = "charset_normalizer-3.4.6-py3-none-any.whl", url = "https://pypi.org/simple/charset-normalizer/charset_normalizer-3.4.6-py3-none-any.whl", hashes = {sha256 = "1ed80ff870ca6de33f4d953fda4d55654b9a2b340ff39ab32fa3adbcd718f264"}}, + {name = "charset_normalizer-3.4.6-py3-none-any.whl", url = "https://pypi.org/simple/charset-normalizer/charset_normalizer-3.4.6-py3-none-any.whl", hashes = {sha256 = "22c6f0c2fbc31e76c3b8a86fba1a56eda6166e238c29cdd3d14befdb4a4e4815"}}, + {name = "charset_normalizer-3.4.6-py3-none-any.whl", url = "https://pypi.org/simple/charset-normalizer/charset_normalizer-3.4.6-py3-none-any.whl", hashes = {sha256 = "231d4da14bcd9301310faf492051bee27df11f2bc7549bc0bb41fef11b82daa2"}}, + {name = "charset_normalizer-3.4.6-py3-none-any.whl", url = "https://pypi.org/simple/charset-normalizer/charset_normalizer-3.4.6-py3-none-any.whl", hashes = {sha256 = "259695e2ccc253feb2a016303543d691825e920917e31f894ca1a687982b1de4"}}, + {name = "charset_normalizer-3.4.6-py3-none-any.whl", url = "https://pypi.org/simple/charset-normalizer/charset_normalizer-3.4.6-py3-none-any.whl", hashes = {sha256 = "2a24157fa36980478dd1770b585c0f30d19e18f4fb0c47c13aa568f871718579"}}, + {name = "charset_normalizer-3.4.6-py3-none-any.whl", url = "https://pypi.org/simple/charset-normalizer/charset_normalizer-3.4.6-py3-none-any.whl", hashes = {sha256 = "2b1a63e8224e401cafe7739f77efd3f9e7f5f2026bda4aead8e59afab537784f"}}, + {name = "charset_normalizer-3.4.6-py3-none-any.whl", url = "https://pypi.org/simple/charset-normalizer/charset_normalizer-3.4.6-py3-none-any.whl", hashes = {sha256 = "2bd9d128ef93637a5d7a6af25363cf5dec3fa21cf80e68055aad627f280e8afa"}}, + {name = "charset_normalizer-3.4.6-py3-none-any.whl", url = "https://pypi.org/simple/charset-normalizer/charset_normalizer-3.4.6-py3-none-any.whl", hashes = {sha256 = "2e1d8ca8611099001949d1cdfaefc510cf0f212484fe7c565f735b68c78c3c95"}}, + {name = "charset_normalizer-3.4.6-py3-none-any.whl", url = "https://pypi.org/simple/charset-normalizer/charset_normalizer-3.4.6-py3-none-any.whl", hashes = {sha256 = "2ef7fedc7a6ecbe99969cd09632516738a97eeb8bd7258bf8a0f23114c057dab"}}, + {name = "charset_normalizer-3.4.6-py3-none-any.whl", url = "https://pypi.org/simple/charset-normalizer/charset_normalizer-3.4.6-py3-none-any.whl", hashes = {sha256 = "2f7fdd9b6e6c529d6a2501a2d36b240109e78a8ceaef5687cfcfa2bbe671d297"}}, + {name = "charset_normalizer-3.4.6-py3-none-any.whl", url = "https://pypi.org/simple/charset-normalizer/charset_normalizer-3.4.6-py3-none-any.whl", hashes = {sha256 = "30f445ae60aad5e1f8bdbb3108e39f6fbc09f4ea16c815c66578878325f8f15a"}}, + {name = "charset_normalizer-3.4.6-py3-none-any.whl", url = "https://pypi.org/simple/charset-normalizer/charset_normalizer-3.4.6-py3-none-any.whl", hashes = {sha256 = "31215157227939b4fb3d740cd23fe27be0439afef67b785a1eb78a3ae69cba9e"}}, + {name = "charset_normalizer-3.4.6-py3-none-any.whl", url = "https://pypi.org/simple/charset-normalizer/charset_normalizer-3.4.6-py3-none-any.whl", hashes = {sha256 = "34315ff4fc374b285ad7f4a0bf7dcbfe769e1b104230d40f49f700d4ab6bbd84"}}, + {name = "charset_normalizer-3.4.6-py3-none-any.whl", url = "https://pypi.org/simple/charset-normalizer/charset_normalizer-3.4.6-py3-none-any.whl", hashes = {sha256 = "3516bbb8d42169de9e61b8520cbeeeb716f12f4ecfe3fd30a9919aa16c806ca8"}}, + {name = "charset_normalizer-3.4.6-py3-none-any.whl", url = "https://pypi.org/simple/charset-normalizer/charset_normalizer-3.4.6-py3-none-any.whl", hashes = {sha256 = "3778fd7d7cd04ae8f54651f4a7a0bd6e39a0cf20f801720a4c21d80e9b7ad6b0"}}, + {name = "charset_normalizer-3.4.6-py3-none-any.whl", url = "https://pypi.org/simple/charset-normalizer/charset_normalizer-3.4.6-py3-none-any.whl", hashes = {sha256 = "39f5068d35621da2881271e5c3205125cc456f54e9030d3f723288c873a71bf9"}}, + {name = "charset_normalizer-3.4.6-py3-none-any.whl", url = "https://pypi.org/simple/charset-normalizer/charset_normalizer-3.4.6-py3-none-any.whl", hashes = {sha256 = "404a1e552cf5b675a87f0651f8b79f5f1e6fd100ee88dc612f89aa16abd4486f"}}, + {name = "charset_normalizer-3.4.6-py3-none-any.whl", url = "https://pypi.org/simple/charset-normalizer/charset_normalizer-3.4.6-py3-none-any.whl", hashes = {sha256 = "419a9d91bd238052642a51938af8ac05da5b3343becde08d5cdeab9046df9ee1"}}, + {name = "charset_normalizer-3.4.6-py3-none-any.whl", url = "https://pypi.org/simple/charset-normalizer/charset_normalizer-3.4.6-py3-none-any.whl", hashes = {sha256 = "423fb7e748a08f854a08a222b983f4df1912b1daedce51a72bd24fe8f26a1843"}}, + {name = "charset_normalizer-3.4.6-py3-none-any.whl", url = "https://pypi.org/simple/charset-normalizer/charset_normalizer-3.4.6-py3-none-any.whl", hashes = {sha256 = "4482481cb0572180b6fd976a4d5c72a30263e98564da68b86ec91f0fe35e8565"}}, + {name = "charset_normalizer-3.4.6-py3-none-any.whl", url = "https://pypi.org/simple/charset-normalizer/charset_normalizer-3.4.6-py3-none-any.whl", hashes = {sha256 = "461598cd852bfa5a61b09cae2b1c02e2efcd166ee5516e243d540ac24bfa68a7"}}, + {name = "charset_normalizer-3.4.6-py3-none-any.whl", url = "https://pypi.org/simple/charset-normalizer/charset_normalizer-3.4.6-py3-none-any.whl", hashes = {sha256 = "47955475ac79cc504ef2704b192364e51d0d473ad452caedd0002605f780101c"}}, + {name = "charset_normalizer-3.4.6-py3-none-any.whl", url = "https://pypi.org/simple/charset-normalizer/charset_normalizer-3.4.6-py3-none-any.whl", hashes = {sha256 = "48696db7f18afb80a068821504296eb0787d9ce239b91ca15059d1d3eaacf13b"}}, + {name = "charset_normalizer-3.4.6-py3-none-any.whl", url = "https://pypi.org/simple/charset-normalizer/charset_normalizer-3.4.6-py3-none-any.whl", hashes = {sha256 = "4be9f4830ba8741527693848403e2c457c16e499100963ec711b1c6f2049b7c7"}}, + {name = "charset_normalizer-3.4.6-py3-none-any.whl", url = "https://pypi.org/simple/charset-normalizer/charset_normalizer-3.4.6-py3-none-any.whl", hashes = {sha256 = "4d1d02209e06550bdaef34af58e041ad71b88e624f5d825519da3a3308e22687"}}, + {name = "charset_normalizer-3.4.6-py3-none-any.whl", url = "https://pypi.org/simple/charset-normalizer/charset_normalizer-3.4.6-py3-none-any.whl", hashes = {sha256 = "4f41da960b196ea355357285ad1316a00099f22d0929fe168343b99b254729c9"}}, + {name = "charset_normalizer-3.4.6-py3-none-any.whl", url = "https://pypi.org/simple/charset-normalizer/charset_normalizer-3.4.6-py3-none-any.whl", hashes = {sha256 = "517ad0e93394ac532745129ceabdf2696b609ec9f87863d337140317ebce1c14"}}, + {name = "charset_normalizer-3.4.6-py3-none-any.whl", url = "https://pypi.org/simple/charset-normalizer/charset_normalizer-3.4.6-py3-none-any.whl", hashes = {sha256 = "51fb3c322c81d20567019778cb5a4a6f2dc1c200b886bc0d636238e364848c89"}}, + {name = "charset_normalizer-3.4.6-py3-none-any.whl", url = "https://pypi.org/simple/charset-normalizer/charset_normalizer-3.4.6-py3-none-any.whl", hashes = {sha256 = "5273b9f0b5835ff0350c0828faea623c68bfa65b792720c453e22b25cc72930f"}}, + {name = "charset_normalizer-3.4.6-py3-none-any.whl", url = "https://pypi.org/simple/charset-normalizer/charset_normalizer-3.4.6-py3-none-any.whl", hashes = {sha256 = "530d548084c4a9f7a16ed4a294d459b4f229db50df689bfe92027452452943a0"}}, + {name = "charset_normalizer-3.4.6-py3-none-any.whl", url = "https://pypi.org/simple/charset-normalizer/charset_normalizer-3.4.6-py3-none-any.whl", hashes = {sha256 = "530e8cebeea0d76bdcf93357aa5e41336f48c3dc709ac52da2bb167c5b8271d9"}}, + {name = "charset_normalizer-3.4.6-py3-none-any.whl", url = "https://pypi.org/simple/charset-normalizer/charset_normalizer-3.4.6-py3-none-any.whl", hashes = {sha256 = "54fae94be3d75f3e573c9a1b5402dc593de19377013c9a0e4285e3d402dd3a2a"}}, + {name = "charset_normalizer-3.4.6-py3-none-any.whl", url = "https://pypi.org/simple/charset-normalizer/charset_normalizer-3.4.6-py3-none-any.whl", hashes = {sha256 = "572d7c822caf521f0525ba1bce1a622a0b85cf47ffbdae6c9c19e3b5ac3c4389"}}, + {name = "charset_normalizer-3.4.6-py3-none-any.whl", url = "https://pypi.org/simple/charset-normalizer/charset_normalizer-3.4.6-py3-none-any.whl", hashes = {sha256 = "58c948d0d086229efc484fe2f30c2d382c86720f55cd9bc33591774348ad44e0"}}, + {name = "charset_normalizer-3.4.6-py3-none-any.whl", url = "https://pypi.org/simple/charset-normalizer/charset_normalizer-3.4.6-py3-none-any.whl", hashes = {sha256 = "5d11595abf8dd942a77883a39d81433739b287b6aa71620f15164f8096221b30"}}, + {name = "charset_normalizer-3.4.6-py3-none-any.whl", url = "https://pypi.org/simple/charset-normalizer/charset_normalizer-3.4.6-py3-none-any.whl", hashes = {sha256 = "5f8ddd609f9e1af8c7bd6e2aca279c931aefecd148a14402d4e368f3171769fd"}}, + {name = "charset_normalizer-3.4.6-py3-none-any.whl", url = "https://pypi.org/simple/charset-normalizer/charset_normalizer-3.4.6-py3-none-any.whl", hashes = {sha256 = "5feb91325bbceade6afab43eb3b508c63ee53579fe896c77137ded51c6b6958e"}}, + {name = "charset_normalizer-3.4.6-py3-none-any.whl", url = "https://pypi.org/simple/charset-normalizer/charset_normalizer-3.4.6-py3-none-any.whl", hashes = {sha256 = "60c74963d8350241a79cb8feea80e54d518f72c26db618862a8f53e5023deaf9"}}, + {name = "charset_normalizer-3.4.6-py3-none-any.whl", url = "https://pypi.org/simple/charset-normalizer/charset_normalizer-3.4.6-py3-none-any.whl", hashes = {sha256 = "613f19aa6e082cf96e17e3ffd89383343d0d589abda756b7764cf78361fd41dc"}}, + {name = "charset_normalizer-3.4.6-py3-none-any.whl", url = "https://pypi.org/simple/charset-normalizer/charset_normalizer-3.4.6-py3-none-any.whl", hashes = {sha256 = "659a1e1b500fac8f2779dd9e1570464e012f43e580371470b45277a27baa7532"}}, + {name = "charset_normalizer-3.4.6-py3-none-any.whl", url = "https://pypi.org/simple/charset-normalizer/charset_normalizer-3.4.6-py3-none-any.whl", hashes = {sha256 = "695f5c2823691a25f17bc5d5ffe79fa90972cc34b002ac6c843bb8a1720e950d"}}, + {name = "charset_normalizer-3.4.6-py3-none-any.whl", url = "https://pypi.org/simple/charset-normalizer/charset_normalizer-3.4.6-py3-none-any.whl", hashes = {sha256 = "69dd852c2f0ad631b8b60cfbe25a28c0058a894de5abb566619c205ce0550eae"}}, + {name = "charset_normalizer-3.4.6-py3-none-any.whl", url = "https://pypi.org/simple/charset-normalizer/charset_normalizer-3.4.6-py3-none-any.whl", hashes = {sha256 = "6cceb5473417d28edd20c6c984ab6fee6c6267d38d906823ebfe20b03d607dc2"}}, + {name = "charset_normalizer-3.4.6-py3-none-any.whl", url = "https://pypi.org/simple/charset-normalizer/charset_normalizer-3.4.6-py3-none-any.whl", hashes = {sha256 = "71be7e0e01753a89cf024abf7ecb6bca2c81738ead80d43004d9b5e3f1244e64"}}, + {name = "charset_normalizer-3.4.6-py3-none-any.whl", url = "https://pypi.org/simple/charset-normalizer/charset_normalizer-3.4.6-py3-none-any.whl", hashes = {sha256 = "74119174722c4349af9708993118581686f343adc1c8c9c007d59be90d077f3f"}}, + {name = "charset_normalizer-3.4.6-py3-none-any.whl", url = "https://pypi.org/simple/charset-normalizer/charset_normalizer-3.4.6-py3-none-any.whl", hashes = {sha256 = "74a2e659c7ecbc73562e2a15e05039f1e22c75b7c7618b4b574a3ea9118d1557"}}, + {name = "charset_normalizer-3.4.6-py3-none-any.whl", url = "https://pypi.org/simple/charset-normalizer/charset_normalizer-3.4.6-py3-none-any.whl", hashes = {sha256 = "7504e9b7dc05f99a9bbb4525c67a2c155073b44d720470a148b34166a69c054e"}}, + {name = "charset_normalizer-3.4.6-py3-none-any.whl", url = "https://pypi.org/simple/charset-normalizer/charset_normalizer-3.4.6-py3-none-any.whl", hashes = {sha256 = "79090741d842f564b1b2827c0b82d846405b744d31e84f18d7a7b41c20e473ff"}}, + {name = "charset_normalizer-3.4.6-py3-none-any.whl", url = "https://pypi.org/simple/charset-normalizer/charset_normalizer-3.4.6-py3-none-any.whl", hashes = {sha256 = "7a6967aaf043bceabab5412ed6bd6bd26603dae84d5cb75bf8d9a74a4959d398"}}, + {name = "charset_normalizer-3.4.6-py3-none-any.whl", url = "https://pypi.org/simple/charset-normalizer/charset_normalizer-3.4.6-py3-none-any.whl", hashes = {sha256 = "7bda6eebafd42133efdca535b04ccb338ab29467b3f7bf79569883676fc628db"}}, + {name = "charset_normalizer-3.4.6-py3-none-any.whl", url = "https://pypi.org/simple/charset-normalizer/charset_normalizer-3.4.6-py3-none-any.whl", hashes = {sha256 = "7edbed096e4a4798710ed6bc75dcaa2a21b68b6c356553ac4823c3658d53743a"}}, + {name = "charset_normalizer-3.4.6-py3-none-any.whl", url = "https://pypi.org/simple/charset-normalizer/charset_normalizer-3.4.6-py3-none-any.whl", hashes = {sha256 = "7f9019c9cb613f084481bd6a100b12e1547cf2efe362d873c2e31e4035a6fa43"}}, + {name = "charset_normalizer-3.4.6-py3-none-any.whl", url = "https://pypi.org/simple/charset-normalizer/charset_normalizer-3.4.6-py3-none-any.whl", hashes = {sha256 = "802168e03fba8bbc5ce0d866d589e4b1ca751d06edee69f7f3a19c5a9fe6b597"}}, + {name = "charset_normalizer-3.4.6-py3-none-any.whl", url = "https://pypi.org/simple/charset-normalizer/charset_normalizer-3.4.6-py3-none-any.whl", hashes = {sha256 = "80d0a5615143c0b3225e5e3ef22c8d5d51f3f72ce0ea6fb84c943546c7b25b6c"}}, + {name = "charset_normalizer-3.4.6-py3-none-any.whl", url = "https://pypi.org/simple/charset-normalizer/charset_normalizer-3.4.6-py3-none-any.whl", hashes = {sha256 = "82060f995ab5003a2d6e0f4ad29065b7672b6593c8c63559beefe5b443242c3e"}}, + {name = "charset_normalizer-3.4.6-py3-none-any.whl", url = "https://pypi.org/simple/charset-normalizer/charset_normalizer-3.4.6-py3-none-any.whl", hashes = {sha256 = "836ab36280f21fc1a03c99cd05c6b7af70d2697e374c7af0b61ed271401a72a2"}}, + {name = "charset_normalizer-3.4.6-py3-none-any.whl", url = "https://pypi.org/simple/charset-normalizer/charset_normalizer-3.4.6-py3-none-any.whl", hashes = {sha256 = "8761ac29b6c81574724322a554605608a9960769ea83d2c73e396f3df896ad54"}}, + {name = "charset_normalizer-3.4.6-py3-none-any.whl", url = "https://pypi.org/simple/charset-normalizer/charset_normalizer-3.4.6-py3-none-any.whl", hashes = {sha256 = "87725cfb1a4f1f8c2fc9890ae2f42094120f4b44db9360be5d99a4c6b0e03a9e"}}, + {name = "charset_normalizer-3.4.6-py3-none-any.whl", url = "https://pypi.org/simple/charset-normalizer/charset_normalizer-3.4.6-py3-none-any.whl", hashes = {sha256 = "899d28f422116b08be5118ef350c292b36fc15ec2daeb9ea987c89281c7bb5c4"}}, + {name = "charset_normalizer-3.4.6-py3-none-any.whl", url = "https://pypi.org/simple/charset-normalizer/charset_normalizer-3.4.6-py3-none-any.whl", hashes = {sha256 = "8bc5f0687d796c05b1e28ab0d38a50e6309906ee09375dd3aff6a9c09dd6e8f4"}}, + {name = "charset_normalizer-3.4.6-py3-none-any.whl", url = "https://pypi.org/simple/charset-normalizer/charset_normalizer-3.4.6-py3-none-any.whl", hashes = {sha256 = "8bea55c4eef25b0b19a0337dc4e3f9a15b00d569c77211fa8cde38684f234fb7"}}, + {name = "charset_normalizer-3.4.6-py3-none-any.whl", url = "https://pypi.org/simple/charset-normalizer/charset_normalizer-3.4.6-py3-none-any.whl", hashes = {sha256 = "8e5a94886bedca0f9b78fecd6afb6629142fd2605aa70a125d49f4edc6037ee6"}}, + {name = "charset_normalizer-3.4.6-py3-none-any.whl", url = "https://pypi.org/simple/charset-normalizer/charset_normalizer-3.4.6-py3-none-any.whl", hashes = {sha256 = "90ca27cd8da8118b18a52d5f547859cc1f8354a00cd1e8e5120df3e30d6279e5"}}, + {name = "charset_normalizer-3.4.6-py3-none-any.whl", url = "https://pypi.org/simple/charset-normalizer/charset_normalizer-3.4.6-py3-none-any.whl", hashes = {sha256 = "92734d4d8d187a354a556626c221cd1a892a4e0802ccb2af432a1d85ec012194"}}, + {name = "charset_normalizer-3.4.6-py3-none-any.whl", url = "https://pypi.org/simple/charset-normalizer/charset_normalizer-3.4.6-py3-none-any.whl", hashes = {sha256 = "947cf925bc916d90adba35a64c82aace04fa39b46b52d4630ece166655905a69"}}, + {name = "charset_normalizer-3.4.6-py3-none-any.whl", url = "https://pypi.org/simple/charset-normalizer/charset_normalizer-3.4.6-py3-none-any.whl", hashes = {sha256 = "95b52c68d64c1878818687a473a10547b3292e82b6f6fe483808fb1468e2f52f"}}, + {name = "charset_normalizer-3.4.6-py3-none-any.whl", url = "https://pypi.org/simple/charset-normalizer/charset_normalizer-3.4.6-py3-none-any.whl", hashes = {sha256 = "97d0235baafca5f2b09cf332cc275f021e694e8362c6bb9c96fc9a0eb74fc316"}}, + {name = "charset_normalizer-3.4.6-py3-none-any.whl", url = "https://pypi.org/simple/charset-normalizer/charset_normalizer-3.4.6-py3-none-any.whl", hashes = {sha256 = "9ca4c0b502ab399ef89248a2c84c54954f77a070f28e546a85e91da627d1301e"}}, + {name = "charset_normalizer-3.4.6-py3-none-any.whl", url = "https://pypi.org/simple/charset-normalizer/charset_normalizer-3.4.6-py3-none-any.whl", hashes = {sha256 = "9cc4fc6c196d6a8b76629a70ddfcd4635a6898756e2d9cac5565cf0654605d73"}}, + {name = "charset_normalizer-3.4.6-py3-none-any.whl", url = "https://pypi.org/simple/charset-normalizer/charset_normalizer-3.4.6-py3-none-any.whl", hashes = {sha256 = "9cc6e6d9e571d2f863fa77700701dae73ed5f78881efc8b3f9a4398772ff53e8"}}, + {name = "charset_normalizer-3.4.6-py3-none-any.whl", url = "https://pypi.org/simple/charset-normalizer/charset_normalizer-3.4.6-py3-none-any.whl", hashes = {sha256 = "a056d1ad2633548ca18ffa2f85c202cfb48b68615129143915b8dc72a806a923"}}, + {name = "charset_normalizer-3.4.6-py3-none-any.whl", url = "https://pypi.org/simple/charset-normalizer/charset_normalizer-3.4.6-py3-none-any.whl", hashes = {sha256 = "a26611d9987b230566f24a0a125f17fe0de6a6aff9f25c9f564aaa2721a5fb88"}}, + {name = "charset_normalizer-3.4.6-py3-none-any.whl", url = "https://pypi.org/simple/charset-normalizer/charset_normalizer-3.4.6-py3-none-any.whl", hashes = {sha256 = "a4474d924a47185a06411e0064b803c68be044be2d60e50e8bddcc2649957c1f"}}, + {name = "charset_normalizer-3.4.6-py3-none-any.whl", url = "https://pypi.org/simple/charset-normalizer/charset_normalizer-3.4.6-py3-none-any.whl", hashes = {sha256 = "a4ea868bc28109052790eb2b52a9ab33f3aa7adc02f96673526ff47419490e21"}}, + {name = "charset_normalizer-3.4.6-py3-none-any.whl", url = "https://pypi.org/simple/charset-normalizer/charset_normalizer-3.4.6-py3-none-any.whl", hashes = {sha256 = "a9e68c9d88823b274cf1e72f28cb5dc89c990edf430b0bfd3e2fb0785bfeabf4"}}, + {name = "charset_normalizer-3.4.6-py3-none-any.whl", url = "https://pypi.org/simple/charset-normalizer/charset_normalizer-3.4.6-py3-none-any.whl", hashes = {sha256 = "aa9cccf4a44b9b62d8ba8b4dd06c649ba683e4bf04eea606d2e94cfc2d6ff4d6"}}, + {name = "charset_normalizer-3.4.6-py3-none-any.whl", url = "https://pypi.org/simple/charset-normalizer/charset_normalizer-3.4.6-py3-none-any.whl", hashes = {sha256 = "ab30e5e3e706e3063bc6de96b118688cb10396b70bb9864a430f67df98c61ecc"}}, + {name = "charset_normalizer-3.4.6-py3-none-any.whl", url = "https://pypi.org/simple/charset-normalizer/charset_normalizer-3.4.6-py3-none-any.whl", hashes = {sha256 = "ac2393c73378fea4e52aa56285a3d64be50f1a12395afef9cce47772f60334c2"}}, + {name = "charset_normalizer-3.4.6-py3-none-any.whl", url = "https://pypi.org/simple/charset-normalizer/charset_normalizer-3.4.6-py3-none-any.whl", hashes = {sha256 = "ad8faf8df23f0378c6d527d8b0b15ea4a2e23c89376877c598c4870d1b2c7866"}}, + {name = "charset_normalizer-3.4.6-py3-none-any.whl", url = "https://pypi.org/simple/charset-normalizer/charset_normalizer-3.4.6-py3-none-any.whl", hashes = {sha256 = "b35b200d6a71b9839a46b9b7fff66b6638bb52fc9658aa58796b0326595d3021"}}, + {name = "charset_normalizer-3.4.6-py3-none-any.whl", url = "https://pypi.org/simple/charset-normalizer/charset_normalizer-3.4.6-py3-none-any.whl", hashes = {sha256 = "b3694e3f87f8ac7ce279d4355645b3c878d24d1424581b46282f24b92f5a4ae2"}}, + {name = "charset_normalizer-3.4.6-py3-none-any.whl", url = "https://pypi.org/simple/charset-normalizer/charset_normalizer-3.4.6-py3-none-any.whl", hashes = {sha256 = "b4ff1d35e8c5bd078be89349b6f3a845128e685e751b6ea1169cf2160b344c4d"}}, + {name = "charset_normalizer-3.4.6-py3-none-any.whl", url = "https://pypi.org/simple/charset-normalizer/charset_normalizer-3.4.6-py3-none-any.whl", hashes = {sha256 = "bbc8c8650c6e51041ad1be191742b8b421d05bbd3410f43fa2a00c8db87678e8"}}, + {name = "charset_normalizer-3.4.6-py3-none-any.whl", url = "https://pypi.org/simple/charset-normalizer/charset_normalizer-3.4.6-py3-none-any.whl", hashes = {sha256 = "bc72863f4d9aba2e8fd9085e63548a324ba706d2ea2c83b260da08a59b9482de"}}, + {name = "charset_normalizer-3.4.6-py3-none-any.whl", url = "https://pypi.org/simple/charset-normalizer/charset_normalizer-3.4.6-py3-none-any.whl", hashes = {sha256 = "bf625105bb9eef28a56a943fec8c8a98aeb80e7d7db99bd3c388137e6eb2d237"}}, + {name = "charset_normalizer-3.4.6-py3-none-any.whl", url = "https://pypi.org/simple/charset-normalizer/charset_normalizer-3.4.6-py3-none-any.whl", hashes = {sha256 = "c2274ca724536f173122f36c98ce188fd24ce3dad886ec2b7af859518ce008a4"}}, + {name = "charset_normalizer-3.4.6-py3-none-any.whl", url = "https://pypi.org/simple/charset-normalizer/charset_normalizer-3.4.6-py3-none-any.whl", hashes = {sha256 = "c45a03a4c69820a399f1dda9e1d8fbf3562eda46e7720458180302021b08f778"}}, + {name = "charset_normalizer-3.4.6-py3-none-any.whl", url = "https://pypi.org/simple/charset-normalizer/charset_normalizer-3.4.6-py3-none-any.whl", hashes = {sha256 = "c8ae56368f8cc97c7e40a7ee18e1cedaf8e780cd8bc5ed5ac8b81f238614facb"}}, + {name = "charset_normalizer-3.4.6-py3-none-any.whl", url = "https://pypi.org/simple/charset-normalizer/charset_normalizer-3.4.6-py3-none-any.whl", hashes = {sha256 = "c907cdc8109f6c619e6254212e794d6548373cc40e1ec75e6e3823d9135d29cc"}}, + {name = "charset_normalizer-3.4.6-py3-none-any.whl", url = "https://pypi.org/simple/charset-normalizer/charset_normalizer-3.4.6-py3-none-any.whl", hashes = {sha256 = "ca0276464d148c72defa8bb4390cce01b4a0e425f3b50d1435aa6d7a18107602"}}, + {name = "charset_normalizer-3.4.6-py3-none-any.whl", url = "https://pypi.org/simple/charset-normalizer/charset_normalizer-3.4.6-py3-none-any.whl", hashes = {sha256 = "cd5e2801c89992ed8c0a3f0293ae83c159a60d9a5d685005383ef4caca77f2c4"}}, + {name = "charset_normalizer-3.4.6-py3-none-any.whl", url = "https://pypi.org/simple/charset-normalizer/charset_normalizer-3.4.6-py3-none-any.whl", hashes = {sha256 = "d08ec48f0a1c48d75d0356cea971921848fb620fdeba805b28f937e90691209f"}}, + {name = "charset_normalizer-3.4.6-py3-none-any.whl", url = "https://pypi.org/simple/charset-normalizer/charset_normalizer-3.4.6-py3-none-any.whl", hashes = {sha256 = "d1a2ee9c1499fc8f86f4521f27a973c914b211ffa87322f4ee33bb35392da2c5"}}, + {name = "charset_normalizer-3.4.6-py3-none-any.whl", url = "https://pypi.org/simple/charset-normalizer/charset_normalizer-3.4.6-py3-none-any.whl", hashes = {sha256 = "d5f5d1e9def3405f60e3ca8232d56f35c98fb7bf581efcc60051ebf53cb8b611"}}, + {name = "charset_normalizer-3.4.6-py3-none-any.whl", url = "https://pypi.org/simple/charset-normalizer/charset_normalizer-3.4.6-py3-none-any.whl", hashes = {sha256 = "d60377dce4511655582e300dc1e5a5f24ba0cb229005a1d5c8d0cb72bb758ab8"}}, + {name = "charset_normalizer-3.4.6-py3-none-any.whl", url = "https://pypi.org/simple/charset-normalizer/charset_normalizer-3.4.6-py3-none-any.whl", hashes = {sha256 = "d73beaac5e90173ac3deb9928a74763a6d230f494e4bfb422c217a0ad8e629bf"}}, + {name = "charset_normalizer-3.4.6-py3-none-any.whl", url = "https://pypi.org/simple/charset-normalizer/charset_normalizer-3.4.6-py3-none-any.whl", hashes = {sha256 = "d7de2637729c67d67cf87614b566626057e95c303bc0a55ffe391f5205e7003d"}}, + {name = "charset_normalizer-3.4.6-py3-none-any.whl", url = "https://pypi.org/simple/charset-normalizer/charset_normalizer-3.4.6-py3-none-any.whl", hashes = {sha256 = "dad6e0f2e481fffdcf776d10ebee25e0ef89f16d691f1e5dee4b586375fdc64b"}}, + {name = "charset_normalizer-3.4.6-py3-none-any.whl", url = "https://pypi.org/simple/charset-normalizer/charset_normalizer-3.4.6-py3-none-any.whl", hashes = {sha256 = "dda86aba335c902b6149a02a55b38e96287157e609200811837678214ba2b1db"}}, + {name = "charset_normalizer-3.4.6-py3-none-any.whl", url = "https://pypi.org/simple/charset-normalizer/charset_normalizer-3.4.6-py3-none-any.whl", hashes = {sha256 = "df01808ee470038c3f8dc4f48620df7225c49c2d6639e38f96e6d6ac6e6f7b0e"}}, + {name = "charset_normalizer-3.4.6-py3-none-any.whl", url = "https://pypi.org/simple/charset-normalizer/charset_normalizer-3.4.6-py3-none-any.whl", hashes = {sha256 = "e1f6e2f00a6b8edb562826e4632e26d063ac10307e80f7461f7de3ad8ef3f077"}}, + {name = "charset_normalizer-3.4.6-py3-none-any.whl", url = "https://pypi.org/simple/charset-normalizer/charset_normalizer-3.4.6-py3-none-any.whl", hashes = {sha256 = "e25369dc110d58ddf29b949377a93e0716d72a24f62bad72b2b39f155949c1fd"}}, + {name = "charset_normalizer-3.4.6-py3-none-any.whl", url = "https://pypi.org/simple/charset-normalizer/charset_normalizer-3.4.6-py3-none-any.whl", hashes = {sha256 = "e3c701e954abf6fc03a49f7c579cc80c2c6cc52525340ca3186c41d3f33482ef"}}, + {name = "charset_normalizer-3.4.6-py3-none-any.whl", url = "https://pypi.org/simple/charset-normalizer/charset_normalizer-3.4.6-py3-none-any.whl", hashes = {sha256 = "e5bcc1a1ae744e0bb59641171ae53743760130600da8db48cbb6e4918e186e4e"}}, + {name = "charset_normalizer-3.4.6-py3-none-any.whl", url = "https://pypi.org/simple/charset-normalizer/charset_normalizer-3.4.6-py3-none-any.whl", hashes = {sha256 = "e68c14b04827dd76dcbd1aeea9e604e3e4b78322d8faf2f8132c7138efa340a8"}}, + {name = "charset_normalizer-3.4.6-py3-none-any.whl", url = "https://pypi.org/simple/charset-normalizer/charset_normalizer-3.4.6-py3-none-any.whl", hashes = {sha256 = "e8aeb10fcbe92767f0fa69ad5a72deca50d0dca07fbde97848997d778a50c9fe"}}, + {name = "charset_normalizer-3.4.6-py3-none-any.whl", url = "https://pypi.org/simple/charset-normalizer/charset_normalizer-3.4.6-py3-none-any.whl", hashes = {sha256 = "e985a16ff513596f217cee86c21371b8cd011c0f6f056d0920aa2d926c544058"}}, + {name = "charset_normalizer-3.4.6-py3-none-any.whl", url = "https://pypi.org/simple/charset-normalizer/charset_normalizer-3.4.6-py3-none-any.whl", hashes = {sha256 = "ecbbd45615a6885fe3240eb9db73b9e62518b611850fdf8ab08bd56de7ad2b17"}}, + {name = "charset_normalizer-3.4.6-py3-none-any.whl", url = "https://pypi.org/simple/charset-normalizer/charset_normalizer-3.4.6-py3-none-any.whl", hashes = {sha256 = "ee4ec14bc1680d6b0afab9aea2ef27e26d2024f18b24a2d7155a52b60da7e833"}}, + {name = "charset_normalizer-3.4.6-py3-none-any.whl", url = "https://pypi.org/simple/charset-normalizer/charset_normalizer-3.4.6-py3-none-any.whl", hashes = {sha256 = "ef5960d965e67165d75b7c7ffc60a83ec5abfc5c11b764ec13ea54fbef8b4421"}}, + {name = "charset_normalizer-3.4.6-py3-none-any.whl", url = "https://pypi.org/simple/charset-normalizer/charset_normalizer-3.4.6-py3-none-any.whl", hashes = {sha256 = "f0cdaecd4c953bfae0b6bb64910aaaca5a424ad9c72d85cb88417bb9814f7550"}}, + {name = "charset_normalizer-3.4.6-py3-none-any.whl", url = "https://pypi.org/simple/charset-normalizer/charset_normalizer-3.4.6-py3-none-any.whl", hashes = {sha256 = "f1ce721c8a7dfec21fcbdfe04e8f68174183cf4e8188e0645e92aa23985c57ff"}}, + {name = "charset_normalizer-3.4.6-py3-none-any.whl", url = "https://pypi.org/simple/charset-normalizer/charset_normalizer-3.4.6-py3-none-any.whl", hashes = {sha256 = "f50498891691e0864dc3da965f340fada0771f6142a378083dc4608f4ea513e2"}}, + {name = "charset_normalizer-3.4.6-py3-none-any.whl", url = "https://pypi.org/simple/charset-normalizer/charset_normalizer-3.4.6-py3-none-any.whl", hashes = {sha256 = "f5ea69428fa1b49573eef0cc44a1d43bebd45ad0c611eb7d7eac760c7ae771bc"}}, + {name = "charset_normalizer-3.4.6-py3-none-any.whl", url = "https://pypi.org/simple/charset-normalizer/charset_normalizer-3.4.6-py3-none-any.whl", hashes = {sha256 = "f61aa92e4aad0be58eb6eb4e0c21acf32cf8065f4b2cae5665da756c4ceef982"}}, + {name = "charset_normalizer-3.4.6-py3-none-any.whl", url = "https://pypi.org/simple/charset-normalizer/charset_normalizer-3.4.6-py3-none-any.whl", hashes = {sha256 = "f6e4333fb15c83f7d1482a76d45a0818897b3d33f00efd215528ff7c51b8e35d"}}, + {name = "charset_normalizer-3.4.6-py3-none-any.whl", url = "https://pypi.org/simple/charset-normalizer/charset_normalizer-3.4.6-py3-none-any.whl", hashes = {sha256 = "f820f24b09e3e779fe84c3c456cb4108a7aa639b0d1f02c28046e11bfcd088ed"}}, + {name = "charset_normalizer-3.4.6-py3-none-any.whl", url = "https://pypi.org/simple/charset-normalizer/charset_normalizer-3.4.6-py3-none-any.whl", hashes = {sha256 = "f98059e4fcd3e3e4e2d632b7cf81c2faae96c43c60b569e9c621468082f1d104"}}, + {name = "charset_normalizer-3.4.6-py3-none-any.whl", url = "https://pypi.org/simple/charset-normalizer/charset_normalizer-3.4.6-py3-none-any.whl", hashes = {sha256 = "fcce033e4021347d80ed9c66dcf1e7b1546319834b74445f561d2e2221de5659"}}, +] + + +[[packages]] +name = "click" +version = "8.0.3" +marker = "('dev' in dependency_groups) and (python_version >= '3.6')" +index = "https://pypi.org/simple/" +wheels = [ + {name = "click-8.0.3-py3-none-any.whl", url = "https://pypi.org/simple/click/click-8.0.3-py3-none-any.whl", hashes = {sha256 = "353f466495adaeb40b6b5f592f9f91cb22372351c84caeb068132442a4518ef3"}}, + {name = "click-8.0.3-py3-none-any.whl", url = "https://pypi.org/simple/click/click-8.0.3-py3-none-any.whl", hashes = {sha256 = "410e932b050f5eed773c4cda94de75971c89cdb3155a72a0831139a79e5ecb5b"}}, +] + + + +[[packages]] +name = "coverage" +version = "7.13.5" +marker = "('dev' in dependency_groups) and (python_version >= '3.10')" +index = "https://pypi.org/simple/" +wheels = [ + {name = "coverage-7.13.5-py3-none-any.whl", url = "https://pypi.org/simple/coverage/coverage-7.13.5-py3-none-any.whl", hashes = {sha256 = "012d5319e66e9d5a218834642d6c35d265515a62f01157a45bcc036ecf947256"}}, + {name = "coverage-7.13.5-py3-none-any.whl", url = "https://pypi.org/simple/coverage/coverage-7.13.5-py3-none-any.whl", hashes = {sha256 = "02ca0eed225b2ff301c474aeeeae27d26e2537942aa0f87491d3e147e784a82b"}}, + {name = "coverage-7.13.5-py3-none-any.whl", url = "https://pypi.org/simple/coverage/coverage-7.13.5-py3-none-any.whl", hashes = {sha256 = "03ccc709a17a1de074fb1d11f217342fb0d2b1582ed544f554fc9fc3f07e95f5"}}, + {name = "coverage-7.13.5-py3-none-any.whl", url = "https://pypi.org/simple/coverage/coverage-7.13.5-py3-none-any.whl", hashes = {sha256 = "0428cbef5783ad91fe240f673cc1f76b25e74bbfe1a13115e4aa30d3f538162d"}}, + {name = "coverage-7.13.5-py3-none-any.whl", url = "https://pypi.org/simple/coverage/coverage-7.13.5-py3-none-any.whl", hashes = {sha256 = "04690832cbea4e4663d9149e05dba142546ca05cb1848816760e7f58285c970a"}}, + {name = "coverage-7.13.5-py3-none-any.whl", url = "https://pypi.org/simple/coverage/coverage-7.13.5-py3-none-any.whl", hashes = {sha256 = "0590e44dd2745c696a778f7bab6aa95256de2cbc8b8cff4f7db8ff09813d6969"}}, + {name = "coverage-7.13.5-py3-none-any.whl", url = "https://pypi.org/simple/coverage/coverage-7.13.5-py3-none-any.whl", hashes = {sha256 = "0672854dc733c342fa3e957e0605256d2bf5934feeac328da9e0b5449634a642"}}, + {name = "coverage-7.13.5-py3-none-any.whl", url = "https://pypi.org/simple/coverage/coverage-7.13.5-py3-none-any.whl", hashes = {sha256 = "084b84a8c63e8d6fc7e3931b316a9bcafca1458d753c539db82d31ed20091a87"}}, + {name = "coverage-7.13.5-py3-none-any.whl", url = "https://pypi.org/simple/coverage/coverage-7.13.5-py3-none-any.whl", hashes = {sha256 = "0b67af5492adb31940ee418a5a655c28e48165da5afab8c7fa6fd72a142f8740"}}, + {name = "coverage-7.13.5-py3-none-any.whl", url = "https://pypi.org/simple/coverage/coverage-7.13.5-py3-none-any.whl", hashes = {sha256 = "0cd9ed7a8b181775459296e402ca4fb27db1279740a24e93b3b41942ebe4b215"}}, + {name = "coverage-7.13.5-py3-none-any.whl", url = "https://pypi.org/simple/coverage/coverage-7.13.5-py3-none-any.whl", hashes = {sha256 = "0cef0cdec915d11254a7f549c1170afecce708d30610c6abdded1f74e581666d"}}, + {name = "coverage-7.13.5-py3-none-any.whl", url = "https://pypi.org/simple/coverage/coverage-7.13.5-py3-none-any.whl", hashes = {sha256 = "0e223ce4b4ed47f065bfb123687686512e37629be25cc63728557ae7db261422"}}, + {name = "coverage-7.13.5-py3-none-any.whl", url = "https://pypi.org/simple/coverage/coverage-7.13.5-py3-none-any.whl", hashes = {sha256 = "0e3c426ffc4cd952f54ee9ffbdd10345709ecc78a3ecfd796a57236bfad0b9b8"}}, + {name = "coverage-7.13.5-py3-none-any.whl", url = "https://pypi.org/simple/coverage/coverage-7.13.5-py3-none-any.whl", hashes = {sha256 = "0ecf12ecb326fe2c339d93fc131816f3a7367d223db37817208905c89bded911"}}, + {name = "coverage-7.13.5-py3-none-any.whl", url = "https://pypi.org/simple/coverage/coverage-7.13.5-py3-none-any.whl", hashes = {sha256 = "10a0c37f0b646eaff7cce1874c31d1f1ccb297688d4c747291f4f4c70741cc8b"}}, + {name = "coverage-7.13.5-py3-none-any.whl", url = "https://pypi.org/simple/coverage/coverage-7.13.5-py3-none-any.whl", hashes = {sha256 = "145ede53ccbafb297c1c9287f788d1bc3efd6c900da23bf6931b09eafc931587"}}, + {name = "coverage-7.13.5-py3-none-any.whl", url = "https://pypi.org/simple/coverage/coverage-7.13.5-py3-none-any.whl", hashes = {sha256 = "1b11eef33edeae9d142f9b4358edb76273b3bfd30bc3df9a4f95d0e49caf94e8"}}, + {name = "coverage-7.13.5-py3-none-any.whl", url = "https://pypi.org/simple/coverage/coverage-7.13.5-py3-none-any.whl", hashes = {sha256 = "1b88c69c8ef5d4b6fe7dea66d6636056a0f6a7527c440e890cf9259011f5e606"}}, + {name = "coverage-7.13.5-py3-none-any.whl", url = "https://pypi.org/simple/coverage/coverage-7.13.5-py3-none-any.whl", hashes = {sha256 = "258354455f4e86e3e9d0d17571d522e13b4e1e19bf0f8596bcf9476d61e7d8a9"}}, + {name = "coverage-7.13.5-py3-none-any.whl", url = "https://pypi.org/simple/coverage/coverage-7.13.5-py3-none-any.whl", hashes = {sha256 = "259b69bb83ad9894c4b25be2528139eecba9a82646ebdda2d9db1ba28424a6bf"}}, + {name = "coverage-7.13.5-py3-none-any.whl", url = "https://pypi.org/simple/coverage/coverage-7.13.5-py3-none-any.whl", hashes = {sha256 = "2aa055ae1857258f9e0045be26a6d62bdb47a72448b62d7b55f4820f361a2633"}}, + {name = "coverage-7.13.5-py3-none-any.whl", url = "https://pypi.org/simple/coverage/coverage-7.13.5-py3-none-any.whl", hashes = {sha256 = "2d3807015f138ffea1ed9afeeb8624fd781703f2858b62a8dd8da5a0994c57b6"}}, + {name = "coverage-7.13.5-py3-none-any.whl", url = "https://pypi.org/simple/coverage/coverage-7.13.5-py3-none-any.whl", hashes = {sha256 = "301e3b7dfefecaca37c9f1aa6f0049b7d4ab8dd933742b607765d757aca77d43"}}, + {name = "coverage-7.13.5-py3-none-any.whl", url = "https://pypi.org/simple/coverage/coverage-7.13.5-py3-none-any.whl", hashes = {sha256 = "32ca0c0114c9834a43f045a87dcebd69d108d8ffb666957ea65aa132f50332e2"}}, + {name = "coverage-7.13.5-py3-none-any.whl", url = "https://pypi.org/simple/coverage/coverage-7.13.5-py3-none-any.whl", hashes = {sha256 = "34b02417cf070e173989b3db962f7ed56d2f644307b2cf9d5a0f258e13084a61"}}, + {name = "coverage-7.13.5-py3-none-any.whl", url = "https://pypi.org/simple/coverage/coverage-7.13.5-py3-none-any.whl", hashes = {sha256 = "356e76b46783a98c2a2fe81ec79df4883a1e62895ea952968fb253c114e7f930"}}, + {name = "coverage-7.13.5-py3-none-any.whl", url = "https://pypi.org/simple/coverage/coverage-7.13.5-py3-none-any.whl", hashes = {sha256 = "35a31f2b1578185fbe6aa2e74cea1b1d0bbf4c552774247d9160d29b80ed56cc"}}, + {name = "coverage-7.13.5-py3-none-any.whl", url = "https://pypi.org/simple/coverage/coverage-7.13.5-py3-none-any.whl", hashes = {sha256 = "380e8e9084d8eb38db3a9176a1a4f3c0082c3806fa0dc882d1d87abc3c789247"}}, + {name = "coverage-7.13.5-py3-none-any.whl", url = "https://pypi.org/simple/coverage/coverage-7.13.5-py3-none-any.whl", hashes = {sha256 = "3ad050321264c49c2fa67bb599100456fc51d004b82534f379d16445da40fb75"}}, + {name = "coverage-7.13.5-py3-none-any.whl", url = "https://pypi.org/simple/coverage/coverage-7.13.5-py3-none-any.whl", hashes = {sha256 = "3e1bb5f6c78feeb1be3475789b14a0f0a5b47d505bfc7267126ccbd50289999e"}}, + {name = "coverage-7.13.5-py3-none-any.whl", url = "https://pypi.org/simple/coverage/coverage-7.13.5-py3-none-any.whl", hashes = {sha256 = "3f4818d065964db3c1c66dc0fbdac5ac692ecbc875555e13374fdbe7eedb4376"}}, + {name = "coverage-7.13.5-py3-none-any.whl", url = "https://pypi.org/simple/coverage/coverage-7.13.5-py3-none-any.whl", hashes = {sha256 = "460cf0114c5016fa841214ff5564aa4864f11948da9440bc97e21ad1f4ba1e01"}}, + {name = "coverage-7.13.5-py3-none-any.whl", url = "https://pypi.org/simple/coverage/coverage-7.13.5-py3-none-any.whl", hashes = {sha256 = "48c39bc4a04d983a54a705a6389512883d4a3b9862991b3617d547940e9f52b1"}}, + {name = "coverage-7.13.5-py3-none-any.whl", url = "https://pypi.org/simple/coverage/coverage-7.13.5-py3-none-any.whl", hashes = {sha256 = "4b59148601efcd2bac8c4dbf1f0ad6391693ccf7a74b8205781751637076aee3"}}, + {name = "coverage-7.13.5-py3-none-any.whl", url = "https://pypi.org/simple/coverage/coverage-7.13.5-py3-none-any.whl", hashes = {sha256 = "4d2afbc5cc54d286bfb54541aa50b64cdb07a718227168c87b9e2fb8f25e1743"}}, + {name = "coverage-7.13.5-py3-none-any.whl", url = "https://pypi.org/simple/coverage/coverage-7.13.5-py3-none-any.whl", hashes = {sha256 = "505d7083c8b0c87a8fa8c07370c285847c1f77739b22e299ad75a6af6c32c5c9"}}, + {name = "coverage-7.13.5-py3-none-any.whl", url = "https://pypi.org/simple/coverage/coverage-7.13.5-py3-none-any.whl", hashes = {sha256 = "52f444e86475992506b32d4e5ca55c24fc88d73bcbda0e9745095b28ef4dc0cf"}}, + {name = "coverage-7.13.5-py3-none-any.whl", url = "https://pypi.org/simple/coverage/coverage-7.13.5-py3-none-any.whl", hashes = {sha256 = "5b13955d31d1633cf9376908089b7cebe7d15ddad7aeaabcbe969a595a97e95e"}}, + {name = "coverage-7.13.5-py3-none-any.whl", url = "https://pypi.org/simple/coverage/coverage-7.13.5-py3-none-any.whl", hashes = {sha256 = "5ec4af212df513e399cf11610cc27063f1586419e814755ab362e50a85ea69c1"}}, + {name = "coverage-7.13.5-py3-none-any.whl", url = "https://pypi.org/simple/coverage/coverage-7.13.5-py3-none-any.whl", hashes = {sha256 = "60365289c3741e4db327e7baff2a4aaacf22f788e80fa4683393891b70a89fbd"}}, + {name = "coverage-7.13.5-py3-none-any.whl", url = "https://pypi.org/simple/coverage/coverage-7.13.5-py3-none-any.whl", hashes = {sha256 = "631efb83f01569670a5e866ceb80fe483e7c159fac6f167e6571522636104a0b"}}, + {name = "coverage-7.13.5-py3-none-any.whl", url = "https://pypi.org/simple/coverage/coverage-7.13.5-py3-none-any.whl", hashes = {sha256 = "6697e29b93707167687543480a40f0db8f356e86d9f67ddf2e37e2dfd91a9dab"}}, + {name = "coverage-7.13.5-py3-none-any.whl", url = "https://pypi.org/simple/coverage/coverage-7.13.5-py3-none-any.whl", hashes = {sha256 = "66a80c616f80181f4d643b0f9e709d97bcea413ecd9631e1dedc7401c8e6695d"}}, + {name = "coverage-7.13.5-py3-none-any.whl", url = "https://pypi.org/simple/coverage/coverage-7.13.5-py3-none-any.whl", hashes = {sha256 = "67e9bc5449801fad0e5dff329499fb090ba4c5800b86805c80617b4e29809b2a"}}, + {name = "coverage-7.13.5-py3-none-any.whl", url = "https://pypi.org/simple/coverage/coverage-7.13.5-py3-none-any.whl", hashes = {sha256 = "68a4953be99b17ac3c23b6efbc8a38330d99680c9458927491d18700ef23ded0"}}, + {name = "coverage-7.13.5-py3-none-any.whl", url = "https://pypi.org/simple/coverage/coverage-7.13.5-py3-none-any.whl", hashes = {sha256 = "6c36ddb64ed9d7e496028d1d00dfec3e428e0aabf4006583bb1839958d280510"}}, + {name = "coverage-7.13.5-py3-none-any.whl", url = "https://pypi.org/simple/coverage/coverage-7.13.5-py3-none-any.whl", hashes = {sha256 = "6e3370441f4513c6252bf042b9c36d22491142385049243253c7e48398a15a9f"}}, + {name = "coverage-7.13.5-py3-none-any.whl", url = "https://pypi.org/simple/coverage/coverage-7.13.5-py3-none-any.whl", hashes = {sha256 = "7034b5c56a58ae5e85f23949d52c14aca2cfc6848a31764995b7de88f13a1ea0"}}, + {name = "coverage-7.13.5-py3-none-any.whl", url = "https://pypi.org/simple/coverage/coverage-7.13.5-py3-none-any.whl", hashes = {sha256 = "704de6328e3d612a8f6c07000a878ff38181ec3263d5a11da1db294fa6a9bdf8"}}, + {name = "coverage-7.13.5-py3-none-any.whl", url = "https://pypi.org/simple/coverage/coverage-7.13.5-py3-none-any.whl", hashes = {sha256 = "7132bed4bd7b836200c591410ae7d97bf7ae8be6fc87d160b2bd881df929e7bf"}}, + {name = "coverage-7.13.5-py3-none-any.whl", url = "https://pypi.org/simple/coverage/coverage-7.13.5-py3-none-any.whl", hashes = {sha256 = "7300c8a6d13335b29bb76d7651c66af6bd8658517c43499f110ddc6717bfc209"}}, + {name = "coverage-7.13.5-py3-none-any.whl", url = "https://pypi.org/simple/coverage/coverage-7.13.5-py3-none-any.whl", hashes = {sha256 = "750db93a81e3e5a9831b534be7b1229df848b2e125a604fe6651e48aa070e5f9"}}, + {name = "coverage-7.13.5-py3-none-any.whl", url = "https://pypi.org/simple/coverage/coverage-7.13.5-py3-none-any.whl", hashes = {sha256 = "777c4d1eff1b67876139d24288aaf1817f6c03d6bae9c5cc8d27b83bcfe38fe3"}}, + {name = "coverage-7.13.5-py3-none-any.whl", url = "https://pypi.org/simple/coverage/coverage-7.13.5-py3-none-any.whl", hashes = {sha256 = "78e696e1cc714e57e8b25760b33a8b1026b7048d270140d25dafe1b0a1ee05a3"}}, + {name = "coverage-7.13.5-py3-none-any.whl", url = "https://pypi.org/simple/coverage/coverage-7.13.5-py3-none-any.whl", hashes = {sha256 = "79060214983769c7ba3f0cee10b54c97609dca4d478fa1aa32b914480fd5738d"}}, + {name = "coverage-7.13.5-py3-none-any.whl", url = "https://pypi.org/simple/coverage/coverage-7.13.5-py3-none-any.whl", hashes = {sha256 = "7c8d4bc913dd70b93488d6c496c77f3aff5ea99a07e36a18f865bca55adef8bd"}}, + {name = "coverage-7.13.5-py3-none-any.whl", url = "https://pypi.org/simple/coverage/coverage-7.13.5-py3-none-any.whl", hashes = {sha256 = "7f2c47b36fe7709a6e83bfadf4eefb90bd25fbe4014d715224c4316f808e59a2"}}, + {name = "coverage-7.13.5-py3-none-any.whl", url = "https://pypi.org/simple/coverage/coverage-7.13.5-py3-none-any.whl", hashes = {sha256 = "800bc829053c80d240a687ceeb927a94fd108bbdc68dfbe505d0d75ab578a882"}}, + {name = "coverage-7.13.5-py3-none-any.whl", url = "https://pypi.org/simple/coverage/coverage-7.13.5-py3-none-any.whl", hashes = {sha256 = "843ea8643cf967d1ac7e8ecd4bb00c99135adf4816c0c0593fdcc47b597fcf09"}}, + {name = "coverage-7.13.5-py3-none-any.whl", url = "https://pypi.org/simple/coverage/coverage-7.13.5-py3-none-any.whl", hashes = {sha256 = "8769751c10f339021e2638cd354e13adeac54004d1941119b2c96fe5276d45ea"}}, + {name = "coverage-7.13.5-py3-none-any.whl", url = "https://pypi.org/simple/coverage/coverage-7.13.5-py3-none-any.whl", hashes = {sha256 = "8dd02af98971bdb956363e4827d34425cb3df19ee550ef92855b0acb9c7ce51c"}}, + {name = "coverage-7.13.5-py3-none-any.whl", url = "https://pypi.org/simple/coverage/coverage-7.13.5-py3-none-any.whl", hashes = {sha256 = "8fdf453a942c3e4d99bd80088141c4c6960bb232c409d9c3558e2dbaa3998562"}}, + {name = "coverage-7.13.5-py3-none-any.whl", url = "https://pypi.org/simple/coverage/coverage-7.13.5-py3-none-any.whl", hashes = {sha256 = "941617e518602e2d64942c88ec8499f7fbd49d3f6c4327d3a71d43a1973032f3"}}, + {name = "coverage-7.13.5-py3-none-any.whl", url = "https://pypi.org/simple/coverage/coverage-7.13.5-py3-none-any.whl", hashes = {sha256 = "972a9cd27894afe4bc2b1480107054e062df08e671df7c2f18c205e805ccd806"}}, + {name = "coverage-7.13.5-py3-none-any.whl", url = "https://pypi.org/simple/coverage/coverage-7.13.5-py3-none-any.whl", hashes = {sha256 = "9adb6688e3b53adffefd4a52d72cbd8b02602bfb8f74dcd862337182fd4d1a4e"}}, + {name = "coverage-7.13.5-py3-none-any.whl", url = "https://pypi.org/simple/coverage/coverage-7.13.5-py3-none-any.whl", hashes = {sha256 = "9b74db26dfea4f4e50d48a4602207cd1e78be33182bc9cbf22da94f332f99878"}}, + {name = "coverage-7.13.5-py3-none-any.whl", url = "https://pypi.org/simple/coverage/coverage-7.13.5-py3-none-any.whl", hashes = {sha256 = "9bb2a28101a443669a423b665939381084412b81c3f8c0fcfbac57f4e30b5b8e"}}, + {name = "coverage-7.13.5-py3-none-any.whl", url = "https://pypi.org/simple/coverage/coverage-7.13.5-py3-none-any.whl", hashes = {sha256 = "9d44d7aa963820b1b971dbecd90bfe5fe8f81cff79787eb6cca15750bd2f79b9"}}, + {name = "coverage-7.13.5-py3-none-any.whl", url = "https://pypi.org/simple/coverage/coverage-7.13.5-py3-none-any.whl", hashes = {sha256 = "9dacc2ad679b292709e0f5fc1ac74a6d4d5562e424058962c7bb0c658ad25e45"}}, + {name = "coverage-7.13.5-py3-none-any.whl", url = "https://pypi.org/simple/coverage/coverage-7.13.5-py3-none-any.whl", hashes = {sha256 = "9ddb4f4a5479f2539644be484da179b653273bca1a323947d48ab107b3ed1f29"}}, + {name = "coverage-7.13.5-py3-none-any.whl", url = "https://pypi.org/simple/coverage/coverage-7.13.5-py3-none-any.whl", hashes = {sha256 = "a1a6d79a14e1ec1832cabc833898636ad5f3754a678ef8bb4908515208bf84f4"}}, + {name = "coverage-7.13.5-py3-none-any.whl", url = "https://pypi.org/simple/coverage/coverage-7.13.5-py3-none-any.whl", hashes = {sha256 = "a698e363641b98843c517817db75373c83254781426e94ada3197cabbc2c919c"}}, + {name = "coverage-7.13.5-py3-none-any.whl", url = "https://pypi.org/simple/coverage/coverage-7.13.5-py3-none-any.whl", hashes = {sha256 = "ad14385487393e386e2ea988b09d62dd42c397662ac2dabc3832d71253eee479"}}, + {name = "coverage-7.13.5-py3-none-any.whl", url = "https://pypi.org/simple/coverage/coverage-7.13.5-py3-none-any.whl", hashes = {sha256 = "ad146744ca4fd09b50c482650e3c1b1f4dfa1d4792e0a04a369c7f23336f0400"}}, + {name = "coverage-7.13.5-py3-none-any.whl", url = "https://pypi.org/simple/coverage/coverage-7.13.5-py3-none-any.whl", hashes = {sha256 = "b5db73ba3c41c7008037fa731ad5459fc3944cb7452fc0aa9f822ad3533c583c"}}, + {name = "coverage-7.13.5-py3-none-any.whl", url = "https://pypi.org/simple/coverage/coverage-7.13.5-py3-none-any.whl", hashes = {sha256 = "bd3a2fbc1c6cccb3c5106140d87cc6a8715110373ef42b63cf5aea29df8c217a"}}, + {name = "coverage-7.13.5-py3-none-any.whl", url = "https://pypi.org/simple/coverage/coverage-7.13.5-py3-none-any.whl", hashes = {sha256 = "bdba0a6b8812e8c7df002d908a9a2ea3c36e92611b5708633c50869e6d922fdf"}}, + {name = "coverage-7.13.5-py3-none-any.whl", url = "https://pypi.org/simple/coverage/coverage-7.13.5-py3-none-any.whl", hashes = {sha256 = "be3d4bbad9d4b037791794ddeedd7d64a56f5933a2c1373e18e9e568b9141686"}}, + {name = "coverage-7.13.5-py3-none-any.whl", url = "https://pypi.org/simple/coverage/coverage-7.13.5-py3-none-any.whl", hashes = {sha256 = "bf69236a9a81bdca3bff53796237aab096cdbf8d78a66ad61e992d9dac7eb2de"}}, + {name = "coverage-7.13.5-py3-none-any.whl", url = "https://pypi.org/simple/coverage/coverage-7.13.5-py3-none-any.whl", hashes = {sha256 = "bff95879c33ec8da99fc9b6fe345ddb5be6414b41d6d1ad1c8f188d26f36e028"}}, + {name = "coverage-7.13.5-py3-none-any.whl", url = "https://pypi.org/simple/coverage/coverage-7.13.5-py3-none-any.whl", hashes = {sha256 = "c555b48be1853fe3997c11c4bd521cdd9a9612352de01fa4508f16ec341e6fe0"}}, + {name = "coverage-7.13.5-py3-none-any.whl", url = "https://pypi.org/simple/coverage/coverage-7.13.5-py3-none-any.whl", hashes = {sha256 = "c81f6515c4c40141f83f502b07bbfa5c240ba25bbe73da7b33f1e5b6120ff179"}}, + {name = "coverage-7.13.5-py3-none-any.whl", url = "https://pypi.org/simple/coverage/coverage-7.13.5-py3-none-any.whl", hashes = {sha256 = "c9136ff29c3a91e25b1d1552b5308e53a1e0653a23e53b6366d7c2dcbbaf8a16"}}, + {name = "coverage-7.13.5-py3-none-any.whl", url = "https://pypi.org/simple/coverage/coverage-7.13.5-py3-none-any.whl", hashes = {sha256 = "ce1998c0483007608c8382f4ff50164bfc5bd07a2246dd272aa4043b75e61e85"}}, + {name = "coverage-7.13.5-py3-none-any.whl", url = "https://pypi.org/simple/coverage/coverage-7.13.5-py3-none-any.whl", hashes = {sha256 = "cec2d83125531bd153175354055cdb7a09987af08a9430bd173c937c6d0fba2a"}}, + {name = "coverage-7.13.5-py3-none-any.whl", url = "https://pypi.org/simple/coverage/coverage-7.13.5-py3-none-any.whl", hashes = {sha256 = "cff784eef7f0b8f6cb28804fbddcfa99f89efe4cc35fb5627e3ac58f91ed3ac0"}}, + {name = "coverage-7.13.5-py3-none-any.whl", url = "https://pypi.org/simple/coverage/coverage-7.13.5-py3-none-any.whl", hashes = {sha256 = "d2c87e0c473a10bffe991502eac389220533024c8082ec1ce849f4218dded810"}}, + {name = "coverage-7.13.5-py3-none-any.whl", url = "https://pypi.org/simple/coverage/coverage-7.13.5-py3-none-any.whl", hashes = {sha256 = "d7cfad2d6d81dd298ab6b89fe72c3b7b05ec7544bdda3b707ddaecff8d25c161"}}, + {name = "coverage-7.13.5-py3-none-any.whl", url = "https://pypi.org/simple/coverage/coverage-7.13.5-py3-none-any.whl", hashes = {sha256 = "d8a7a2049c14f413163e2bdabd37e41179b1d1ccb10ffc6ccc4b7a718429c607"}}, + {name = "coverage-7.13.5-py3-none-any.whl", url = "https://pypi.org/simple/coverage/coverage-7.13.5-py3-none-any.whl", hashes = {sha256 = "da305e9937617ee95c2e39d8ff9f040e0487cbf1ac174f777ed5eddd7a7c1f26"}}, + {name = "coverage-7.13.5-py3-none-any.whl", url = "https://pypi.org/simple/coverage/coverage-7.13.5-py3-none-any.whl", hashes = {sha256 = "da86cdcf10d2519e10cabb8ac2de03da1bcb6e4853790b7fbd48523332e3a819"}}, + {name = "coverage-7.13.5-py3-none-any.whl", url = "https://pypi.org/simple/coverage/coverage-7.13.5-py3-none-any.whl", hashes = {sha256 = "dc022073d063b25a402454e5712ef9e007113e3a676b96c5f29b2bda29352f40"}}, + {name = "coverage-7.13.5-py3-none-any.whl", url = "https://pypi.org/simple/coverage/coverage-7.13.5-py3-none-any.whl", hashes = {sha256 = "e0723d2c96324561b9aa76fb982406e11d93cdb388a7a7da2b16e04719cf7ca5"}}, + {name = "coverage-7.13.5-py3-none-any.whl", url = "https://pypi.org/simple/coverage/coverage-7.13.5-py3-none-any.whl", hashes = {sha256 = "e092b9499de38ae0fbfbc603a74660eb6ff3e869e507b50d85a13b6db9863e15"}}, + {name = "coverage-7.13.5-py3-none-any.whl", url = "https://pypi.org/simple/coverage/coverage-7.13.5-py3-none-any.whl", hashes = {sha256 = "e0b216a19534b2427cc201a26c25da4a48633f29a487c61258643e89d28200c0"}}, + {name = "coverage-7.13.5-py3-none-any.whl", url = "https://pypi.org/simple/coverage/coverage-7.13.5-py3-none-any.whl", hashes = {sha256 = "e1c85e0b6c05c592ea6d8768a66a254bfb3874b53774b12d4c89c481eb78cb90"}}, + {name = "coverage-7.13.5-py3-none-any.whl", url = "https://pypi.org/simple/coverage/coverage-7.13.5-py3-none-any.whl", hashes = {sha256 = "e301d30dd7e95ae068671d746ba8c34e945a82682e62918e41b2679acd2051a0"}}, + {name = "coverage-7.13.5-py3-none-any.whl", url = "https://pypi.org/simple/coverage/coverage-7.13.5-py3-none-any.whl", hashes = {sha256 = "e808af52a0513762df4d945ea164a24b37f2f518cbe97e03deaa0ee66139b4d6"}}, + {name = "coverage-7.13.5-py3-none-any.whl", url = "https://pypi.org/simple/coverage/coverage-7.13.5-py3-none-any.whl", hashes = {sha256 = "eb07647a5738b89baab047f14edd18ded523de60f3b30e75c2acc826f79c839a"}}, + {name = "coverage-7.13.5-py3-none-any.whl", url = "https://pypi.org/simple/coverage/coverage-7.13.5-py3-none-any.whl", hashes = {sha256 = "eb7fdf1ef130660e7415e0253a01a7d5a88c9c4d158bcf75cbbd922fd65a5b58"}}, + {name = "coverage-7.13.5-py3-none-any.whl", url = "https://pypi.org/simple/coverage/coverage-7.13.5-py3-none-any.whl", hashes = {sha256 = "ec10e2a42b41c923c2209b846126c6582db5e43a33157e9870ba9fb70dc7854b"}}, + {name = "coverage-7.13.5-py3-none-any.whl", url = "https://pypi.org/simple/coverage/coverage-7.13.5-py3-none-any.whl", hashes = {sha256 = "ee2aa19e03161671ec964004fb74b2257805d9710bf14a5c704558b9d8dbaf17"}}, + {name = "coverage-7.13.5-py3-none-any.whl", url = "https://pypi.org/simple/coverage/coverage-7.13.5-py3-none-any.whl", hashes = {sha256 = "f08fd75c50a760c7eb068ae823777268daaf16a80b918fa58eea888f8e3919f5"}}, + {name = "coverage-7.13.5-py3-none-any.whl", url = "https://pypi.org/simple/coverage/coverage-7.13.5-py3-none-any.whl", hashes = {sha256 = "f4cd16206ad171cbc2470dbea9103cf9a7607d5fe8c242fdf1edf36174020664"}}, + {name = "coverage-7.13.5-py3-none-any.whl", url = "https://pypi.org/simple/coverage/coverage-7.13.5-py3-none-any.whl", hashes = {sha256 = "f70c9ab2595c56f81a89620e22899eea8b212a4041bd728ac6f4a28bf5d3ddd0"}}, + {name = "coverage-7.13.5-py3-none-any.whl", url = "https://pypi.org/simple/coverage/coverage-7.13.5-py3-none-any.whl", hashes = {sha256 = "fbabfaceaeb587e16f7008f7795cd80d20ec548dc7f94fbb0d4ec2e038ce563f"}}, +] + + +[[packages]] +name = "cryptography" +version = "46.0.5" +marker = "('dev' in dependency_groups) and (python_version >= '3.8' and python_full_version not in '3.9.0, 3.9.1')" +index = "https://pypi.org/simple/" +wheels = [ + {name = "cryptography-46.0.5-py3-none-any.whl", url = "https://pypi.org/simple/cryptography/cryptography-46.0.5-py3-none-any.whl", hashes = {sha256 = "02f547fce831f5096c9a567fd41bc12ca8f11df260959ecc7c3202555cc47a72"}}, + {name = "cryptography-46.0.5-py3-none-any.whl", url = "https://pypi.org/simple/cryptography/cryptography-46.0.5-py3-none-any.whl", hashes = {sha256 = "039917b0dc418bb9f6edce8a906572d69e74bd330b0b3fea4f79dab7f8ddd235"}}, + {name = "cryptography-46.0.5-py3-none-any.whl", url = "https://pypi.org/simple/cryptography/cryptography-46.0.5-py3-none-any.whl", hashes = {sha256 = "1abfdb89b41c3be0365328a410baa9df3ff8a9110fb75e7b52e66803ddabc9a9"}}, + {name = "cryptography-46.0.5-py3-none-any.whl", url = "https://pypi.org/simple/cryptography/cryptography-46.0.5-py3-none-any.whl", hashes = {sha256 = "2ae6971afd6246710480e3f15824ed3029a60fc16991db250034efd0b9fb4356"}}, + {name = "cryptography-46.0.5-py3-none-any.whl", url = "https://pypi.org/simple/cryptography/cryptography-46.0.5-py3-none-any.whl", hashes = {sha256 = "2b7a67c9cd56372f3249b39699f2ad479f6991e62ea15800973b956f4b73e257"}}, + {name = "cryptography-46.0.5-py3-none-any.whl", url = "https://pypi.org/simple/cryptography/cryptography-46.0.5-py3-none-any.whl", hashes = {sha256 = "351695ada9ea9618b3500b490ad54c739860883df6c1f555e088eaf25b1bbaad"}}, + {name = "cryptography-46.0.5-py3-none-any.whl", url = "https://pypi.org/simple/cryptography/cryptography-46.0.5-py3-none-any.whl", hashes = {sha256 = "38946c54b16c885c72c4f59846be9743d699eee2b69b6988e0a00a01f46a61a4"}}, + {name = "cryptography-46.0.5-py3-none-any.whl", url = "https://pypi.org/simple/cryptography/cryptography-46.0.5-py3-none-any.whl", hashes = {sha256 = "3b4995dc971c9fb83c25aa44cf45f02ba86f71ee600d81091c2f0cbae116b06c"}}, + {name = "cryptography-46.0.5-py3-none-any.whl", url = "https://pypi.org/simple/cryptography/cryptography-46.0.5-py3-none-any.whl", hashes = {sha256 = "3ce58ba46e1bc2aac4f7d9290223cead56743fa6ab94a5d53292ffaac6a91614"}}, + {name = "cryptography-46.0.5-py3-none-any.whl", url = "https://pypi.org/simple/cryptography/cryptography-46.0.5-py3-none-any.whl", hashes = {sha256 = "3ee190460e2fbe447175cda91b88b84ae8322a104fc27766ad09428754a618ed"}}, + {name = "cryptography-46.0.5-py3-none-any.whl", url = "https://pypi.org/simple/cryptography/cryptography-46.0.5-py3-none-any.whl", hashes = {sha256 = "4108d4c09fbbf2789d0c926eb4152ae1760d5a2d97612b92d508d96c861e4d31"}}, + {name = "cryptography-46.0.5-py3-none-any.whl", url = "https://pypi.org/simple/cryptography/cryptography-46.0.5-py3-none-any.whl", hashes = {sha256 = "420d0e909050490d04359e7fdb5ed7e667ca5c3c402b809ae2563d7e66a92229"}}, + {name = "cryptography-46.0.5-py3-none-any.whl", url = "https://pypi.org/simple/cryptography/cryptography-46.0.5-py3-none-any.whl", hashes = {sha256 = "47fb8a66058b80e509c47118ef8a75d14c455e81ac369050f20ba0d23e77fee0"}}, + {name = "cryptography-46.0.5-py3-none-any.whl", url = "https://pypi.org/simple/cryptography/cryptography-46.0.5-py3-none-any.whl", hashes = {sha256 = "4c3341037c136030cb46e4b1e17b7418ea4cbd9dd207e4a6f3b2b24e0d4ac731"}}, + {name = "cryptography-46.0.5-py3-none-any.whl", url = "https://pypi.org/simple/cryptography/cryptography-46.0.5-py3-none-any.whl", hashes = {sha256 = "4d7e3d356b8cd4ea5aff04f129d5f66ebdc7b6f8eae802b93739ed520c47c79b"}}, + {name = "cryptography-46.0.5-py3-none-any.whl", url = "https://pypi.org/simple/cryptography/cryptography-46.0.5-py3-none-any.whl", hashes = {sha256 = "4d8ae8659ab18c65ced284993c2265910f6c9e650189d4e3f68445ef82a810e4"}}, + {name = "cryptography-46.0.5-py3-none-any.whl", url = "https://pypi.org/simple/cryptography/cryptography-46.0.5-py3-none-any.whl", hashes = {sha256 = "4e817a8920bfbcff8940ecfd60f23d01836408242b30f1a708d93198393a80b4"}}, + {name = "cryptography-46.0.5-py3-none-any.whl", url = "https://pypi.org/simple/cryptography/cryptography-46.0.5-py3-none-any.whl", hashes = {sha256 = "50bfb6925eff619c9c023b967d5b77a54e04256c4281b0e21336a130cd7fc263"}}, + {name = "cryptography-46.0.5-py3-none-any.whl", url = "https://pypi.org/simple/cryptography/cryptography-46.0.5-py3-none-any.whl", hashes = {sha256 = "556e106ee01aa13484ce9b0239bca667be5004efb0aabbed28d353df86445595"}}, + {name = "cryptography-46.0.5-py3-none-any.whl", url = "https://pypi.org/simple/cryptography/cryptography-46.0.5-py3-none-any.whl", hashes = {sha256 = "582f5fcd2afa31622f317f80426a027f30dc792e9c80ffee87b993200ea115f1"}}, + {name = "cryptography-46.0.5-py3-none-any.whl", url = "https://pypi.org/simple/cryptography/cryptography-46.0.5-py3-none-any.whl", hashes = {sha256 = "5be7bf2fb40769e05739dd0046e7b26f9d4670badc7b032d6ce4db64dddc0678"}}, + {name = "cryptography-46.0.5-py3-none-any.whl", url = "https://pypi.org/simple/cryptography/cryptography-46.0.5-py3-none-any.whl", hashes = {sha256 = "60ee7e19e95104d4c03871d7d7dfb3d22ef8a9b9c6778c94e1c8fcc8365afd48"}}, + {name = "cryptography-46.0.5-py3-none-any.whl", url = "https://pypi.org/simple/cryptography/cryptography-46.0.5-py3-none-any.whl", hashes = {sha256 = "61aa400dce22cb001a98014f647dc21cda08f7915ceb95df0c9eaf84b4b6af76"}}, + {name = "cryptography-46.0.5-py3-none-any.whl", url = "https://pypi.org/simple/cryptography/cryptography-46.0.5-py3-none-any.whl", hashes = {sha256 = "68f68d13f2e1cb95163fa3b4db4bf9a159a418f5f6e7242564fc75fcae667fd0"}}, + {name = "cryptography-46.0.5-py3-none-any.whl", url = "https://pypi.org/simple/cryptography/cryptography-46.0.5-py3-none-any.whl", hashes = {sha256 = "7d1f30a86d2757199cb2d56e48cce14deddf1f9c95f1ef1b64ee91ea43fe2e18"}}, + {name = "cryptography-46.0.5-py3-none-any.whl", url = "https://pypi.org/simple/cryptography/cryptography-46.0.5-py3-none-any.whl", hashes = {sha256 = "7d731d4b107030987fd61a7f8ab512b25b53cef8f233a97379ede116f30eb67d"}}, + {name = "cryptography-46.0.5-py3-none-any.whl", url = "https://pypi.org/simple/cryptography/cryptography-46.0.5-py3-none-any.whl", hashes = {sha256 = "803812e111e75d1aa73690d2facc295eaefd4439be1023fefc4995eaea2af90d"}}, + {name = "cryptography-46.0.5-py3-none-any.whl", url = "https://pypi.org/simple/cryptography/cryptography-46.0.5-py3-none-any.whl", hashes = {sha256 = "80a8d7bfdf38f87ca30a5391c0c9ce4ed2926918e017c29ddf643d0ed2778ea1"}}, + {name = "cryptography-46.0.5-py3-none-any.whl", url = "https://pypi.org/simple/cryptography/cryptography-46.0.5-py3-none-any.whl", hashes = {sha256 = "8293f3dea7fc929ef7240796ba231413afa7b68ce38fd21da2995549f5961981"}}, + {name = "cryptography-46.0.5-py3-none-any.whl", url = "https://pypi.org/simple/cryptography/cryptography-46.0.5-py3-none-any.whl", hashes = {sha256 = "8456928655f856c6e1533ff59d5be76578a7157224dbd9ce6872f25055ab9ab7"}}, + {name = "cryptography-46.0.5-py3-none-any.whl", url = "https://pypi.org/simple/cryptography/cryptography-46.0.5-py3-none-any.whl", hashes = {sha256 = "890bcb4abd5a2d3f852196437129eb3667d62630333aacc13dfd470fad3aaa82"}}, + {name = "cryptography-46.0.5-py3-none-any.whl", url = "https://pypi.org/simple/cryptography/cryptography-46.0.5-py3-none-any.whl", hashes = {sha256 = "94a76daa32eb78d61339aff7952ea819b1734b46f73646a07decb40e5b3448e2"}}, + {name = "cryptography-46.0.5-py3-none-any.whl", url = "https://pypi.org/simple/cryptography/cryptography-46.0.5-py3-none-any.whl", hashes = {sha256 = "9f16fbdf4da055efb21c22d81b89f155f02ba420558db21288b3d0035bafd5f4"}}, + {name = "cryptography-46.0.5-py3-none-any.whl", url = "https://pypi.org/simple/cryptography/cryptography-46.0.5-py3-none-any.whl", hashes = {sha256 = "a3d1fae9863299076f05cb8a778c467578262fae09f9dc0ee9b12eb4268ce663"}}, + {name = "cryptography-46.0.5-py3-none-any.whl", url = "https://pypi.org/simple/cryptography/cryptography-46.0.5-py3-none-any.whl", hashes = {sha256 = "a3d507bb6a513ca96ba84443226af944b0f7f47dcc9a399d110cd6146481d24c"}}, + {name = "cryptography-46.0.5-py3-none-any.whl", url = "https://pypi.org/simple/cryptography/cryptography-46.0.5-py3-none-any.whl", hashes = {sha256 = "abace499247268e3757271b2f1e244b36b06f8515cf27c4d49468fc9eb16e93d"}}, + {name = "cryptography-46.0.5-py3-none-any.whl", url = "https://pypi.org/simple/cryptography/cryptography-46.0.5-py3-none-any.whl", hashes = {sha256 = "ba2a27ff02f48193fc4daeadf8ad2590516fa3d0adeeb34336b96f7fa64c1e3a"}}, + {name = "cryptography-46.0.5-py3-none-any.whl", url = "https://pypi.org/simple/cryptography/cryptography-46.0.5-py3-none-any.whl", hashes = {sha256 = "bc84e875994c3b445871ea7181d424588171efec3e185dced958dad9e001950a"}}, + {name = "cryptography-46.0.5-py3-none-any.whl", url = "https://pypi.org/simple/cryptography/cryptography-46.0.5-py3-none-any.whl", hashes = {sha256 = "bfd56bb4b37ed4f330b82402f6f435845a5f5648edf1ad497da51a8452d5d62d"}}, + {name = "cryptography-46.0.5-py3-none-any.whl", url = "https://pypi.org/simple/cryptography/cryptography-46.0.5-py3-none-any.whl", hashes = {sha256 = "c18ff11e86df2e28854939acde2d003f7984f721eba450b56a200ad90eeb0e6b"}}, + {name = "cryptography-46.0.5-py3-none-any.whl", url = "https://pypi.org/simple/cryptography/cryptography-46.0.5-py3-none-any.whl", hashes = {sha256 = "c3bcce8521d785d510b2aad26ae2c966092b7daa8f45dd8f44734a104dc0bc1a"}}, + {name = "cryptography-46.0.5-py3-none-any.whl", url = "https://pypi.org/simple/cryptography/cryptography-46.0.5-py3-none-any.whl", hashes = {sha256 = "c4143987a42a2397f2fc3b4d7e3a7d313fbe684f67ff443999e803dd75a76826"}}, + {name = "cryptography-46.0.5-py3-none-any.whl", url = "https://pypi.org/simple/cryptography/cryptography-46.0.5-py3-none-any.whl", hashes = {sha256 = "c69fd885df7d089548a42d5ec05be26050ebcd2283d89b3d30676eb32ff87dee"}}, + {name = "cryptography-46.0.5-py3-none-any.whl", url = "https://pypi.org/simple/cryptography/cryptography-46.0.5-py3-none-any.whl", hashes = {sha256 = "ced80795227d70549a411a4ab66e8ce307899fad2220ce5ab2f296e687eacde9"}}, + {name = "cryptography-46.0.5-py3-none-any.whl", url = "https://pypi.org/simple/cryptography/cryptography-46.0.5-py3-none-any.whl", hashes = {sha256 = "d66e421495fdb797610a08f43b05269e0a5ea7f5e652a89bfd5a7d3c1dee3648"}}, + {name = "cryptography-46.0.5-py3-none-any.whl", url = "https://pypi.org/simple/cryptography/cryptography-46.0.5-py3-none-any.whl", hashes = {sha256 = "d861ee9e76ace6cf36a6a89b959ec08e7bc2493ee39d07ffe5acb23ef46d27da"}}, + {name = "cryptography-46.0.5-py3-none-any.whl", url = "https://pypi.org/simple/cryptography/cryptography-46.0.5-py3-none-any.whl", hashes = {sha256 = "e9251e3be159d1020c4030bd2e5f84d6a43fe54b6c19c12f51cde9542a2817b2"}}, + {name = "cryptography-46.0.5-py3-none-any.whl", url = "https://pypi.org/simple/cryptography/cryptography-46.0.5-py3-none-any.whl", hashes = {sha256 = "f145bba11b878005c496e93e257c1e88f154d278d2638e6450d17e0f31e558d2"}}, + {name = "cryptography-46.0.5-py3-none-any.whl", url = "https://pypi.org/simple/cryptography/cryptography-46.0.5-py3-none-any.whl", hashes = {sha256 = "fe346b143ff9685e40192a4960938545c699054ba11d4f9029f94751e3f71d87"}}, +] + + +[[packages]] +name = "distlib" +version = "0.4.0" +marker = "'dev' in dependency_groups" +index = "https://pypi.org/simple/" +wheels = [ + {name = "distlib-0.4.0-py3-none-any.whl", url = "https://pypi.org/simple/distlib/distlib-0.4.0-py3-none-any.whl", hashes = {sha256 = "9659f7d87e46584a30b5780e43ac7a2143098441670ff0a49d5f9034c54a6c16"}}, + {name = "distlib-0.4.0-py3-none-any.whl", url = "https://pypi.org/simple/distlib/distlib-0.4.0-py3-none-any.whl", hashes = {sha256 = "feec40075be03a04501a973d81f633735b4b69f98b05450592310c0f401a4e0d"}}, +] + + +[[packages]] +name = "docutils" +version = "0.22.4" +marker = "('dev' in dependency_groups) and (python_version >= '3.9')" +index = "https://pypi.org/simple/" +wheels = [ + {name = "docutils-0.22.4-py3-none-any.whl", url = "https://pypi.org/simple/docutils/docutils-0.22.4-py3-none-any.whl", hashes = {sha256 = "4db53b1fde9abecbb74d91230d32ab626d94f6badfc575d6db9194a49df29968"}}, + {name = "docutils-0.22.4-py3-none-any.whl", url = "https://pypi.org/simple/docutils/docutils-0.22.4-py3-none-any.whl", hashes = {sha256 = "d0013f540772d1420576855455d050a2180186c91c15779301ac2ccb3eeb68de"}}, +] + + +[[packages]] +name = "exceptiongroup" +version = "1.1.0" +marker = "('dev' in dependency_groups) and (python_version >= '3.7')" +index = "https://pypi.org/simple/" +wheels = [ + {name = "exceptiongroup-1.1.0-py3-none-any.whl", url = "https://pypi.org/simple/exceptiongroup/exceptiongroup-1.1.0-py3-none-any.whl", hashes = {sha256 = "327cbda3da756e2de031a3107b81ab7b3770a602c4d16ca618298c526f4bec1e"}}, + {name = "exceptiongroup-1.1.0-py3-none-any.whl", url = "https://pypi.org/simple/exceptiongroup/exceptiongroup-1.1.0-py3-none-any.whl", hashes = {sha256 = "bcb67d800a4497e1b404c2dd44fca47d3b7a5e5433dbab67f96c1a685cdfdf23"}}, +] + + +[[packages]] +name = "execnet" +version = "2.1.2" +marker = "('dev' in dependency_groups) and (python_version >= '3.8')" +index = "https://pypi.org/simple/" +wheels = [ + {name = "execnet-2.1.2-py3-none-any.whl", url = "https://pypi.org/simple/execnet/execnet-2.1.2-py3-none-any.whl", hashes = {sha256 = "63d83bfdd9a23e35b9c6a3261412324f964c2ec8dcd8d3c6916ee9373e0befcd"}}, + {name = "execnet-2.1.2-py3-none-any.whl", url = "https://pypi.org/simple/execnet/execnet-2.1.2-py3-none-any.whl", hashes = {sha256 = "67fba928dd5a544b783f6056f449e5e3931a5c378b128bc18501f7ea79e296ec"}}, +] + + +[[packages]] +name = "filelock" +version = "3.25.2" +marker = "('dev' in dependency_groups) and (python_version >= '3.10')" +index = "https://pypi.org/simple/" +wheels = [ + {name = "filelock-3.25.2-py3-none-any.whl", url = "https://pypi.org/simple/filelock/filelock-3.25.2-py3-none-any.whl", hashes = {sha256 = "b64ece2b38f4ca29dd3e810287aa8c48182bbecd1ae6e9ae126c9b35f1382694"}}, + {name = "filelock-3.25.2-py3-none-any.whl", url = "https://pypi.org/simple/filelock/filelock-3.25.2-py3-none-any.whl", hashes = {sha256 = "ca8afb0da15f229774c9ad1b455ed96e85a81373065fb10446672f64444ddf70"}}, +] + + +[[packages]] +name = "flake8" +version = "3.9.2" +marker = "('dev' in dependency_groups) and (python_version >= '2.7' and python_version not in '3.0, 3.1, 3.2, 3.3, 3.4')" +index = "https://pypi.org/simple/" +wheels = [ + {name = "flake8-3.9.2-py3-none-any.whl", url = "https://pypi.org/simple/flake8/flake8-3.9.2-py3-none-any.whl", hashes = {sha256 = "07528381786f2a6237b061f6e96610a4167b226cb926e2aa2b6b1d78057c576b"}}, + {name = "flake8-3.9.2-py3-none-any.whl", url = "https://pypi.org/simple/flake8/flake8-3.9.2-py3-none-any.whl", hashes = {sha256 = "bf8fd333346d844f616e8d47905ef3a3384edae6b4e9beb0c5101e25e3110907"}}, +] + + +[[packages]] +name = "gunicorn" +version = "23.0.0" +marker = "('dev' in dependency_groups) and (sys_platform == 'linux')" +index = "https://pypi.org/simple/" +wheels = [ + {name = "gunicorn-23.0.0-py3-none-any.whl", url = "https://pypi.org/simple/gunicorn/gunicorn-23.0.0-py3-none-any.whl", hashes = {sha256 = "ec400d38950de4dfd418cff8328b2c8faed0edb0d517d3394e457c317908ca4d"}}, + {name = "gunicorn-23.0.0-py3-none-any.whl", url = "https://pypi.org/simple/gunicorn/gunicorn-23.0.0-py3-none-any.whl", hashes = {sha256 = "f014447a0101dc57e294f6c18ca6b40227a4c90e9bdb586042628030cba004ec"}}, +] + + +[[packages]] +name = "id" +version = "1.6.1" +marker = "('dev' in dependency_groups) and (python_version >= '3.9')" +index = "https://pypi.org/simple/" +wheels = [ + {name = "id-1.6.1-py3-none-any.whl", url = "https://pypi.org/simple/id/id-1.6.1-py3-none-any.whl", hashes = {sha256 = "d0732d624fb46fd4e7bc4e5152f00214450953b9e772c182c1c22964def1a069"}}, + {name = "id-1.6.1-py3-none-any.whl", url = "https://pypi.org/simple/id/id-1.6.1-py3-none-any.whl", hashes = {sha256 = "f5ec41ed2629a508f5d0988eda142e190c9c6da971100612c4de9ad9f9b237ca"}}, +] + + +[[packages]] +name = "identify" +version = "2.6.18" +marker = "('dev' in dependency_groups) and (python_version >= '3.10')" +index = "https://pypi.org/simple/" +wheels = [ + {name = "identify-2.6.18-py3-none-any.whl", url = "https://pypi.org/simple/identify/identify-2.6.18-py3-none-any.whl", hashes = {sha256 = "873ac56a5e3fd63e7438a7ecbc4d91aca692eb3fefa4534db2b7913f3fc352fd"}}, + {name = "identify-2.6.18-py3-none-any.whl", url = "https://pypi.org/simple/identify/identify-2.6.18-py3-none-any.whl", hashes = {sha256 = "8db9d3c8ea9079db92cafb0ebf97abdc09d52e97f4dcf773a2e694048b7cd737"}}, +] + + +[[packages]] +name = "idna" +version = "3.11" +marker = "('dev' in dependency_groups) and (python_version >= '3.8')" +index = "https://pypi.org/simple/" +wheels = [ + {name = "idna-3.11-py3-none-any.whl", url = "https://pypi.org/simple/idna/idna-3.11-py3-none-any.whl", hashes = {sha256 = "771a87f49d9defaf64091e6e6fe9c18d4833f140bd19464795bc32d966ca37ea"}}, + {name = "idna-3.11-py3-none-any.whl", url = "https://pypi.org/simple/idna/idna-3.11-py3-none-any.whl", hashes = {sha256 = "795dafcc9c04ed0c1fb032c2aa73654d8e8c5023a7df64a53f39190ada629902"}}, +] + + +[[packages]] +name = "imagesize" +version = "2.0.0" +marker = "('dev' in dependency_groups) and (python_version >= '3.10' and python_version < '3.15')" +index = "https://pypi.org/simple/" +wheels = [ + {name = "imagesize-2.0.0-py3-none-any.whl", url = "https://pypi.org/simple/imagesize/imagesize-2.0.0-py3-none-any.whl", hashes = {sha256 = "5667c5bbb57ab3f1fa4bc366f4fbc971db3d5ed011fd2715fd8001f782718d96"}}, + {name = "imagesize-2.0.0-py3-none-any.whl", url = "https://pypi.org/simple/imagesize/imagesize-2.0.0-py3-none-any.whl", hashes = {sha256 = "8e8358c4a05c304f1fccf7ff96f036e7243a189e9e42e90851993c558cfe9ee3"}}, +] + + +[[packages]] +name = "importlib-metadata" +version = "8.7.1" +marker = "('dev' in dependency_groups) and (python_version >= '3.9')" +index = "https://pypi.org/simple/" +wheels = [ + {name = "importlib_metadata-8.7.1-py3-none-any.whl", url = "https://pypi.org/simple/importlib-metadata/importlib_metadata-8.7.1-py3-none-any.whl", hashes = {sha256 = "49fef1ae6440c182052f407c8d34a68f72efc36db9ca90dc0113398f2fdde8bb"}}, + {name = "importlib_metadata-8.7.1-py3-none-any.whl", url = "https://pypi.org/simple/importlib-metadata/importlib_metadata-8.7.1-py3-none-any.whl", hashes = {sha256 = "5a1f80bf1daa489495071efbb095d75a634cf28a8bc299581244063b53176151"}}, +] + + +[[packages]] +name = "iniconfig" +version = "2.3.0" +marker = "('dev' in dependency_groups) and (python_version >= '3.10')" +index = "https://pypi.org/simple/" +wheels = [ + {name = "iniconfig-2.3.0-py3-none-any.whl", url = "https://pypi.org/simple/iniconfig/iniconfig-2.3.0-py3-none-any.whl", hashes = {sha256 = "c76315c77db068650d49c5b56314774a7804df16fee4402c1f19d6d15d8c4730"}}, + {name = "iniconfig-2.3.0-py3-none-any.whl", url = "https://pypi.org/simple/iniconfig/iniconfig-2.3.0-py3-none-any.whl", hashes = {sha256 = "f631c04d2c48c52b84d0d0549c99ff3859c98df65b3101406327ecc7d53fbf12"}}, +] + + +[[packages]] +name = "invoke" +version = "2.2.1" +marker = "('dev' in dependency_groups) and (python_version >= '3.6')" +index = "https://pypi.org/simple/" +wheels = [ + {name = "invoke-2.2.1-py3-none-any.whl", url = "https://pypi.org/simple/invoke/invoke-2.2.1-py3-none-any.whl", hashes = {sha256 = "2413bc441b376e5cd3f55bb5d364f973ad8bdd7bf87e53c79de3c11bf3feecc8"}}, + {name = "invoke-2.2.1-py3-none-any.whl", url = "https://pypi.org/simple/invoke/invoke-2.2.1-py3-none-any.whl", hashes = {sha256 = "515bf49b4a48932b79b024590348da22f39c4942dff991ad1fb8b8baea1be707"}}, +] + + +[[packages]] +name = "jaraco.classes" +version = "3.4.0" +marker = "('dev' in dependency_groups) and (python_version >= '3.8')" +index = "https://pypi.org/simple/" +wheels = [ + {name = "jaraco.classes-3.4.0-py3-none-any.whl", url = "https://pypi.org/simple/jaraco.classes/jaraco.classes-3.4.0-py3-none-any.whl", hashes = {sha256 = "47a024b51d0239c0dd8c8540c6c7f484be3b8fcf0b2d85c13825780d3b3f3acd"}}, + {name = "jaraco.classes-3.4.0-py3-none-any.whl", url = "https://pypi.org/simple/jaraco.classes/jaraco.classes-3.4.0-py3-none-any.whl", hashes = {sha256 = "f662826b6bed8cace05e7ff873ce0f9283b5c924470fe664fff1c2f00f581790"}}, +] + + +[[packages]] +name = "jaraco.context" +version = "6.1.1" +marker = "('dev' in dependency_groups) and (python_version >= '3.9')" +index = "https://pypi.org/simple/" +wheels = [ + {name = "jaraco.context-6.1.1-py3-none-any.whl", url = "https://pypi.org/simple/jaraco.context/jaraco.context-6.1.1-py3-none-any.whl", hashes = {sha256 = "0df6a0287258f3e364072c3e40d5411b20cafa30cb28c4839d24319cecf9f808"}}, + {name = "jaraco.context-6.1.1-py3-none-any.whl", url = "https://pypi.org/simple/jaraco.context/jaraco.context-6.1.1-py3-none-any.whl", hashes = {sha256 = "bc046b2dc94f1e5532bd02402684414575cc11f565d929b6563125deb0a6e581"}}, +] + + +[[packages]] +name = "jaraco.functools" +version = "4.4.0" +marker = "('dev' in dependency_groups) and (python_version >= '3.9')" +index = "https://pypi.org/simple/" +wheels = [ + {name = "jaraco.functools-4.4.0-py3-none-any.whl", url = "https://pypi.org/simple/jaraco.functools/jaraco.functools-4.4.0-py3-none-any.whl", hashes = {sha256 = "9eec1e36f45c818d9bf307c8948eb03b2b56cd44087b3cdc989abca1f20b9176"}}, + {name = "jaraco.functools-4.4.0-py3-none-any.whl", url = "https://pypi.org/simple/jaraco.functools/jaraco.functools-4.4.0-py3-none-any.whl", hashes = {sha256 = "da21933b0417b89515562656547a77b4931f98176eb173644c0d35032a33d6bb"}}, +] + + +[[packages]] +name = "jeepney" +version = "0.9.0" +marker = "('dev' in dependency_groups) and (python_version >= '3.7')" +index = "https://pypi.org/simple/" +wheels = [ + {name = "jeepney-0.9.0-py3-none-any.whl", url = "https://pypi.org/simple/jeepney/jeepney-0.9.0-py3-none-any.whl", hashes = {sha256 = "97e5714520c16fc0a45695e5365a2e11b81ea79bba796e26f9f1d178cb182683"}}, + {name = "jeepney-0.9.0-py3-none-any.whl", url = "https://pypi.org/simple/jeepney/jeepney-0.9.0-py3-none-any.whl", hashes = {sha256 = "cf0e9e845622b81e4a28df94c40345400256ec608d0e55bb8a3feaa9163f5732"}}, +] + + +[[packages]] +name = "jinja2" +version = "3.1.6" +marker = "('dev' in dependency_groups) and (python_version >= '3.7')" +index = "https://pypi.org/simple/" +wheels = [ + {name = "jinja2-3.1.6-py3-none-any.whl", url = "https://pypi.org/simple/jinja2/jinja2-3.1.6-py3-none-any.whl", hashes = {sha256 = "0137fb05990d35f1275a587e9aee6d56da821fc83491a0fb838183be43f66d6d"}}, + {name = "jinja2-3.1.6-py3-none-any.whl", url = "https://pypi.org/simple/jinja2/jinja2-3.1.6-py3-none-any.whl", hashes = {sha256 = "85ece4451f492d0c13c5dd7c13a64681a86afae63a5f347908daf103ce6d2f67"}}, +] + + +[[packages]] +name = "keyring" +version = "25.7.0" +marker = "('dev' in dependency_groups) and (python_version >= '3.9')" +index = "https://pypi.org/simple/" +wheels = [ + {name = "keyring-25.7.0-py3-none-any.whl", url = "https://pypi.org/simple/keyring/keyring-25.7.0-py3-none-any.whl", hashes = {sha256 = "be4a0b195f149690c166e850609a477c532ddbfbaed96a404d4e43f8d5e2689f"}}, + {name = "keyring-25.7.0-py3-none-any.whl", url = "https://pypi.org/simple/keyring/keyring-25.7.0-py3-none-any.whl", hashes = {sha256 = "fe01bd85eb3f8fb3dd0405defdeac9a5b4f6f0439edbb3149577f244a2e8245b"}}, +] + + +[[packages]] +name = "legacy-cgi" +version = "2.6.4" +marker = "('dev' in dependency_groups) and (python_version >= '3.13')" +index = "https://pypi.org/simple/" +wheels = [ + {name = "legacy_cgi-2.6.4-py3-none-any.whl", url = "https://pypi.org/simple/legacy-cgi/legacy_cgi-2.6.4-py3-none-any.whl", hashes = {sha256 = "7e235ce58bf1e25d1fc9b2d299015e4e2cd37305eccafec1e6bac3fc04b878cd"}}, + {name = "legacy_cgi-2.6.4-py3-none-any.whl", url = "https://pypi.org/simple/legacy-cgi/legacy_cgi-2.6.4-py3-none-any.whl", hashes = {sha256 = "abb9dfc7835772f7c9317977c63253fd22a7484b5c9bbcdca60a29dcce97c577"}}, +] + + +[[packages]] +name = "linkify-it-py" +version = "2.1.0" +marker = "('dev' in dependency_groups) and (python_version >= '3.10')" +index = "https://pypi.org/simple/" +wheels = [ + {name = "linkify_it_py-2.1.0-py3-none-any.whl", url = "https://pypi.org/simple/linkify-it-py/linkify_it_py-2.1.0-py3-none-any.whl", hashes = {sha256 = "0d252c1594ecba2ecedc444053db5d3a9b7ec1b0dd929c8f1d74dce89f86c05e"}}, + {name = "linkify_it_py-2.1.0-py3-none-any.whl", url = "https://pypi.org/simple/linkify-it-py/linkify_it_py-2.1.0-py3-none-any.whl", hashes = {sha256 = "43360231720999c10e9328dc3691160e27a718e280673d444c38d7d3aaa3b98b"}}, +] + + +[[packages]] +name = "markdown-it-py" +version = "4.0.0" +marker = "('dev' in dependency_groups) and (python_version >= '3.10')" +index = "https://pypi.org/simple/" +wheels = [ + {name = "markdown_it_py-4.0.0-py3-none-any.whl", url = "https://pypi.org/simple/markdown-it-py/markdown_it_py-4.0.0-py3-none-any.whl", hashes = {sha256 = "87327c59b172c5011896038353a81343b6754500a08cd7a4973bb48c6d578147"}}, + {name = "markdown_it_py-4.0.0-py3-none-any.whl", url = "https://pypi.org/simple/markdown-it-py/markdown_it_py-4.0.0-py3-none-any.whl", hashes = {sha256 = "cb0a2b4aa34f932c007117b194e945bd74e0ec24133ceb5bac59009cda1cb9f3"}}, +] + + +[[packages]] +name = "markupsafe" +version = "3.0.3" +marker = "('dev' in dependency_groups) and (python_version >= '3.9')" +index = "https://pypi.org/simple/" +wheels = [ + {name = "markupsafe-3.0.3-py3-none-any.whl", url = "https://pypi.org/simple/markupsafe/markupsafe-3.0.3-py3-none-any.whl", hashes = {sha256 = "0303439a41979d9e74d18ff5e2dd8c43ed6c6001fd40e5bf2e43f7bd9bbc523f"}}, + {name = "markupsafe-3.0.3-py3-none-any.whl", url = "https://pypi.org/simple/markupsafe/markupsafe-3.0.3-py3-none-any.whl", hashes = {sha256 = "068f375c472b3e7acbe2d5318dea141359e6900156b5b2ba06a30b169086b91a"}}, + {name = "markupsafe-3.0.3-py3-none-any.whl", url = "https://pypi.org/simple/markupsafe/markupsafe-3.0.3-py3-none-any.whl", hashes = {sha256 = "0bf2a864d67e76e5c9a34dc26ec616a66b9888e25e7b9460e1c76d3293bd9dbf"}}, + {name = "markupsafe-3.0.3-py3-none-any.whl", url = "https://pypi.org/simple/markupsafe/markupsafe-3.0.3-py3-none-any.whl", hashes = {sha256 = "0db14f5dafddbb6d9208827849fad01f1a2609380add406671a26386cdf15a19"}}, + {name = "markupsafe-3.0.3-py3-none-any.whl", url = "https://pypi.org/simple/markupsafe/markupsafe-3.0.3-py3-none-any.whl", hashes = {sha256 = "0eb9ff8191e8498cca014656ae6b8d61f39da5f95b488805da4bb029cccbfbaf"}}, + {name = "markupsafe-3.0.3-py3-none-any.whl", url = "https://pypi.org/simple/markupsafe/markupsafe-3.0.3-py3-none-any.whl", hashes = {sha256 = "0f4b68347f8c5eab4a13419215bdfd7f8c9b19f2b25520968adfad23eb0ce60c"}}, + {name = "markupsafe-3.0.3-py3-none-any.whl", url = "https://pypi.org/simple/markupsafe/markupsafe-3.0.3-py3-none-any.whl", hashes = {sha256 = "1085e7fbddd3be5f89cc898938f42c0b3c711fdcb37d75221de2666af647c175"}}, + {name = "markupsafe-3.0.3-py3-none-any.whl", url = "https://pypi.org/simple/markupsafe/markupsafe-3.0.3-py3-none-any.whl", hashes = {sha256 = "116bb52f642a37c115f517494ea5feb03889e04df47eeff5b130b1808ce7c219"}}, + {name = "markupsafe-3.0.3-py3-none-any.whl", url = "https://pypi.org/simple/markupsafe/markupsafe-3.0.3-py3-none-any.whl", hashes = {sha256 = "12c63dfb4a98206f045aa9563db46507995f7ef6d83b2f68eda65c307c6829eb"}}, + {name = "markupsafe-3.0.3-py3-none-any.whl", url = "https://pypi.org/simple/markupsafe/markupsafe-3.0.3-py3-none-any.whl", hashes = {sha256 = "133a43e73a802c5562be9bbcd03d090aa5a1fe899db609c29e8c8d815c5f6de6"}}, + {name = "markupsafe-3.0.3-py3-none-any.whl", url = "https://pypi.org/simple/markupsafe/markupsafe-3.0.3-py3-none-any.whl", hashes = {sha256 = "1353ef0c1b138e1907ae78e2f6c63ff67501122006b0f9abad68fda5f4ffc6ab"}}, + {name = "markupsafe-3.0.3-py3-none-any.whl", url = "https://pypi.org/simple/markupsafe/markupsafe-3.0.3-py3-none-any.whl", hashes = {sha256 = "15d939a21d546304880945ca1ecb8a039db6b4dc49b2c5a400387cdae6a62e26"}}, + {name = "markupsafe-3.0.3-py3-none-any.whl", url = "https://pypi.org/simple/markupsafe/markupsafe-3.0.3-py3-none-any.whl", hashes = {sha256 = "177b5253b2834fe3678cb4a5f0059808258584c559193998be2601324fdeafb1"}}, + {name = "markupsafe-3.0.3-py3-none-any.whl", url = "https://pypi.org/simple/markupsafe/markupsafe-3.0.3-py3-none-any.whl", hashes = {sha256 = "1872df69a4de6aead3491198eaf13810b565bdbeec3ae2dc8780f14458ec73ce"}}, + {name = "markupsafe-3.0.3-py3-none-any.whl", url = "https://pypi.org/simple/markupsafe/markupsafe-3.0.3-py3-none-any.whl", hashes = {sha256 = "1b4b79e8ebf6b55351f0d91fe80f893b4743f104bff22e90697db1590e47a218"}}, + {name = "markupsafe-3.0.3-py3-none-any.whl", url = "https://pypi.org/simple/markupsafe/markupsafe-3.0.3-py3-none-any.whl", hashes = {sha256 = "1b52b4fb9df4eb9ae465f8d0c228a00624de2334f216f178a995ccdcf82c4634"}}, + {name = "markupsafe-3.0.3-py3-none-any.whl", url = "https://pypi.org/simple/markupsafe/markupsafe-3.0.3-py3-none-any.whl", hashes = {sha256 = "1ba88449deb3de88bd40044603fafffb7bc2b055d626a330323a9ed736661695"}}, + {name = "markupsafe-3.0.3-py3-none-any.whl", url = "https://pypi.org/simple/markupsafe/markupsafe-3.0.3-py3-none-any.whl", hashes = {sha256 = "1cc7ea17a6824959616c525620e387f6dd30fec8cb44f649e31712db02123dad"}}, + {name = "markupsafe-3.0.3-py3-none-any.whl", url = "https://pypi.org/simple/markupsafe/markupsafe-3.0.3-py3-none-any.whl", hashes = {sha256 = "218551f6df4868a8d527e3062d0fb968682fe92054e89978594c28e642c43a73"}}, + {name = "markupsafe-3.0.3-py3-none-any.whl", url = "https://pypi.org/simple/markupsafe/markupsafe-3.0.3-py3-none-any.whl", hashes = {sha256 = "26a5784ded40c9e318cfc2bdb30fe164bdb8665ded9cd64d500a34fb42067b1c"}}, + {name = "markupsafe-3.0.3-py3-none-any.whl", url = "https://pypi.org/simple/markupsafe/markupsafe-3.0.3-py3-none-any.whl", hashes = {sha256 = "2713baf880df847f2bece4230d4d094280f4e67b1e813eec43b4c0e144a34ffe"}}, + {name = "markupsafe-3.0.3-py3-none-any.whl", url = "https://pypi.org/simple/markupsafe/markupsafe-3.0.3-py3-none-any.whl", hashes = {sha256 = "2a15a08b17dd94c53a1da0438822d70ebcd13f8c3a95abe3a9ef9f11a94830aa"}}, + {name = "markupsafe-3.0.3-py3-none-any.whl", url = "https://pypi.org/simple/markupsafe/markupsafe-3.0.3-py3-none-any.whl", hashes = {sha256 = "2f981d352f04553a7171b8e44369f2af4055f888dfb147d55e42d29e29e74559"}}, + {name = "markupsafe-3.0.3-py3-none-any.whl", url = "https://pypi.org/simple/markupsafe/markupsafe-3.0.3-py3-none-any.whl", hashes = {sha256 = "32001d6a8fc98c8cb5c947787c5d08b0a50663d139f1305bac5885d98d9b40fa"}}, + {name = "markupsafe-3.0.3-py3-none-any.whl", url = "https://pypi.org/simple/markupsafe/markupsafe-3.0.3-py3-none-any.whl", hashes = {sha256 = "3524b778fe5cfb3452a09d31e7b5adefeea8c5be1d43c4f810ba09f2ceb29d37"}}, + {name = "markupsafe-3.0.3-py3-none-any.whl", url = "https://pypi.org/simple/markupsafe/markupsafe-3.0.3-py3-none-any.whl", hashes = {sha256 = "3537e01efc9d4dccdf77221fb1cb3b8e1a38d5428920e0657ce299b20324d758"}}, + {name = "markupsafe-3.0.3-py3-none-any.whl", url = "https://pypi.org/simple/markupsafe/markupsafe-3.0.3-py3-none-any.whl", hashes = {sha256 = "35add3b638a5d900e807944a078b51922212fb3dedb01633a8defc4b01a3c85f"}}, + {name = "markupsafe-3.0.3-py3-none-any.whl", url = "https://pypi.org/simple/markupsafe/markupsafe-3.0.3-py3-none-any.whl", hashes = {sha256 = "38664109c14ffc9e7437e86b4dceb442b0096dfe3541d7864d9cbe1da4cf36c8"}}, + {name = "markupsafe-3.0.3-py3-none-any.whl", url = "https://pypi.org/simple/markupsafe/markupsafe-3.0.3-py3-none-any.whl", hashes = {sha256 = "3a7e8ae81ae39e62a41ec302f972ba6ae23a5c5396c8e60113e9066ef893da0d"}}, + {name = "markupsafe-3.0.3-py3-none-any.whl", url = "https://pypi.org/simple/markupsafe/markupsafe-3.0.3-py3-none-any.whl", hashes = {sha256 = "3b562dd9e9ea93f13d53989d23a7e775fdfd1066c33494ff43f5418bc8c58a5c"}}, + {name = "markupsafe-3.0.3-py3-none-any.whl", url = "https://pypi.org/simple/markupsafe/markupsafe-3.0.3-py3-none-any.whl", hashes = {sha256 = "457a69a9577064c05a97c41f4e65148652db078a3a509039e64d3467b9e7ef97"}}, + {name = "markupsafe-3.0.3-py3-none-any.whl", url = "https://pypi.org/simple/markupsafe/markupsafe-3.0.3-py3-none-any.whl", hashes = {sha256 = "4bd4cd07944443f5a265608cc6aab442e4f74dff8088b0dfc8238647b8f6ae9a"}}, + {name = "markupsafe-3.0.3-py3-none-any.whl", url = "https://pypi.org/simple/markupsafe/markupsafe-3.0.3-py3-none-any.whl", hashes = {sha256 = "4e885a3d1efa2eadc93c894a21770e4bc67899e3543680313b09f139e149ab19"}}, + {name = "markupsafe-3.0.3-py3-none-any.whl", url = "https://pypi.org/simple/markupsafe/markupsafe-3.0.3-py3-none-any.whl", hashes = {sha256 = "4faffd047e07c38848ce017e8725090413cd80cbc23d86e55c587bf979e579c9"}}, + {name = "markupsafe-3.0.3-py3-none-any.whl", url = "https://pypi.org/simple/markupsafe/markupsafe-3.0.3-py3-none-any.whl", hashes = {sha256 = "509fa21c6deb7a7a273d629cf5ec029bc209d1a51178615ddf718f5918992ab9"}}, + {name = "markupsafe-3.0.3-py3-none-any.whl", url = "https://pypi.org/simple/markupsafe/markupsafe-3.0.3-py3-none-any.whl", hashes = {sha256 = "5678211cb9333a6468fb8d8be0305520aa073f50d17f089b5b4b477ea6e67fdc"}}, + {name = "markupsafe-3.0.3-py3-none-any.whl", url = "https://pypi.org/simple/markupsafe/markupsafe-3.0.3-py3-none-any.whl", hashes = {sha256 = "591ae9f2a647529ca990bc681daebdd52c8791ff06c2bfa05b65163e28102ef2"}}, + {name = "markupsafe-3.0.3-py3-none-any.whl", url = "https://pypi.org/simple/markupsafe/markupsafe-3.0.3-py3-none-any.whl", hashes = {sha256 = "5a7d5dc5140555cf21a6fefbdbf8723f06fcd2f63ef108f2854de715e4422cb4"}}, + {name = "markupsafe-3.0.3-py3-none-any.whl", url = "https://pypi.org/simple/markupsafe/markupsafe-3.0.3-py3-none-any.whl", hashes = {sha256 = "69c0b73548bc525c8cb9a251cddf1931d1db4d2258e9599c28c07ef3580ef354"}}, + {name = "markupsafe-3.0.3-py3-none-any.whl", url = "https://pypi.org/simple/markupsafe/markupsafe-3.0.3-py3-none-any.whl", hashes = {sha256 = "6b5420a1d9450023228968e7e6a9ce57f65d148ab56d2313fcd589eee96a7a50"}}, + {name = "markupsafe-3.0.3-py3-none-any.whl", url = "https://pypi.org/simple/markupsafe/markupsafe-3.0.3-py3-none-any.whl", hashes = {sha256 = "722695808f4b6457b320fdc131280796bdceb04ab50fe1795cd540799ebe1698"}}, + {name = "markupsafe-3.0.3-py3-none-any.whl", url = "https://pypi.org/simple/markupsafe/markupsafe-3.0.3-py3-none-any.whl", hashes = {sha256 = "729586769a26dbceff69f7a7dbbf59ab6572b99d94576a5592625d5b411576b9"}}, + {name = "markupsafe-3.0.3-py3-none-any.whl", url = "https://pypi.org/simple/markupsafe/markupsafe-3.0.3-py3-none-any.whl", hashes = {sha256 = "77f0643abe7495da77fb436f50f8dab76dbc6e5fd25d39589a0f1fe6548bfa2b"}}, + {name = "markupsafe-3.0.3-py3-none-any.whl", url = "https://pypi.org/simple/markupsafe/markupsafe-3.0.3-py3-none-any.whl", hashes = {sha256 = "795e7751525cae078558e679d646ae45574b47ed6e7771863fcc079a6171a0fc"}}, + {name = "markupsafe-3.0.3-py3-none-any.whl", url = "https://pypi.org/simple/markupsafe/markupsafe-3.0.3-py3-none-any.whl", hashes = {sha256 = "7be7b61bb172e1ed687f1754f8e7484f1c8019780f6f6b0786e76bb01c2ae115"}}, + {name = "markupsafe-3.0.3-py3-none-any.whl", url = "https://pypi.org/simple/markupsafe/markupsafe-3.0.3-py3-none-any.whl", hashes = {sha256 = "7c3fb7d25180895632e5d3148dbdc29ea38ccb7fd210aa27acbd1201a1902c6e"}}, + {name = "markupsafe-3.0.3-py3-none-any.whl", url = "https://pypi.org/simple/markupsafe/markupsafe-3.0.3-py3-none-any.whl", hashes = {sha256 = "7e68f88e5b8799aa49c85cd116c932a1ac15caaa3f5db09087854d218359e485"}}, + {name = "markupsafe-3.0.3-py3-none-any.whl", url = "https://pypi.org/simple/markupsafe/markupsafe-3.0.3-py3-none-any.whl", hashes = {sha256 = "83891d0e9fb81a825d9a6d61e3f07550ca70a076484292a70fde82c4b807286f"}}, + {name = "markupsafe-3.0.3-py3-none-any.whl", url = "https://pypi.org/simple/markupsafe/markupsafe-3.0.3-py3-none-any.whl", hashes = {sha256 = "8485f406a96febb5140bfeca44a73e3ce5116b2501ac54fe953e488fb1d03b12"}}, + {name = "markupsafe-3.0.3-py3-none-any.whl", url = "https://pypi.org/simple/markupsafe/markupsafe-3.0.3-py3-none-any.whl", hashes = {sha256 = "8709b08f4a89aa7586de0aadc8da56180242ee0ada3999749b183aa23df95025"}}, + {name = "markupsafe-3.0.3-py3-none-any.whl", url = "https://pypi.org/simple/markupsafe/markupsafe-3.0.3-py3-none-any.whl", hashes = {sha256 = "8f71bc33915be5186016f675cd83a1e08523649b0e33efdb898db577ef5bb009"}}, + {name = "markupsafe-3.0.3-py3-none-any.whl", url = "https://pypi.org/simple/markupsafe/markupsafe-3.0.3-py3-none-any.whl", hashes = {sha256 = "915c04ba3851909ce68ccc2b8e2cd691618c4dc4c4232fb7982bca3f41fd8c3d"}}, + {name = "markupsafe-3.0.3-py3-none-any.whl", url = "https://pypi.org/simple/markupsafe/markupsafe-3.0.3-py3-none-any.whl", hashes = {sha256 = "949b8d66bc381ee8b007cd945914c721d9aba8e27f71959d750a46f7c282b20b"}}, + {name = "markupsafe-3.0.3-py3-none-any.whl", url = "https://pypi.org/simple/markupsafe/markupsafe-3.0.3-py3-none-any.whl", hashes = {sha256 = "94c6f0bb423f739146aec64595853541634bde58b2135f27f61c1ffd1cd4d16a"}}, + {name = "markupsafe-3.0.3-py3-none-any.whl", url = "https://pypi.org/simple/markupsafe/markupsafe-3.0.3-py3-none-any.whl", hashes = {sha256 = "9a1abfdc021a164803f4d485104931fb8f8c1efd55bc6b748d2f5774e78b62c5"}}, + {name = "markupsafe-3.0.3-py3-none-any.whl", url = "https://pypi.org/simple/markupsafe/markupsafe-3.0.3-py3-none-any.whl", hashes = {sha256 = "9b79b7a16f7fedff2495d684f2b59b0457c3b493778c9eed31111be64d58279f"}}, + {name = "markupsafe-3.0.3-py3-none-any.whl", url = "https://pypi.org/simple/markupsafe/markupsafe-3.0.3-py3-none-any.whl", hashes = {sha256 = "a320721ab5a1aba0a233739394eb907f8c8da5c98c9181d1161e77a0c8e36f2d"}}, + {name = "markupsafe-3.0.3-py3-none-any.whl", url = "https://pypi.org/simple/markupsafe/markupsafe-3.0.3-py3-none-any.whl", hashes = {sha256 = "a4afe79fb3de0b7097d81da19090f4df4f8d3a2b3adaa8764138aac2e44f3af1"}}, + {name = "markupsafe-3.0.3-py3-none-any.whl", url = "https://pypi.org/simple/markupsafe/markupsafe-3.0.3-py3-none-any.whl", hashes = {sha256 = "ad2cf8aa28b8c020ab2fc8287b0f823d0a7d8630784c31e9ee5edea20f406287"}}, + {name = "markupsafe-3.0.3-py3-none-any.whl", url = "https://pypi.org/simple/markupsafe/markupsafe-3.0.3-py3-none-any.whl", hashes = {sha256 = "b8512a91625c9b3da6f127803b166b629725e68af71f8184ae7e7d54686a56d6"}}, + {name = "markupsafe-3.0.3-py3-none-any.whl", url = "https://pypi.org/simple/markupsafe/markupsafe-3.0.3-py3-none-any.whl", hashes = {sha256 = "bc51efed119bc9cfdf792cdeaa4d67e8f6fcccab66ed4bfdd6bde3e59bfcbb2f"}}, + {name = "markupsafe-3.0.3-py3-none-any.whl", url = "https://pypi.org/simple/markupsafe/markupsafe-3.0.3-py3-none-any.whl", hashes = {sha256 = "bdc919ead48f234740ad807933cdf545180bfbe9342c2bb451556db2ed958581"}}, + {name = "markupsafe-3.0.3-py3-none-any.whl", url = "https://pypi.org/simple/markupsafe/markupsafe-3.0.3-py3-none-any.whl", hashes = {sha256 = "bdd37121970bfd8be76c5fb069c7751683bdf373db1ed6c010162b2a130248ed"}}, + {name = "markupsafe-3.0.3-py3-none-any.whl", url = "https://pypi.org/simple/markupsafe/markupsafe-3.0.3-py3-none-any.whl", hashes = {sha256 = "be8813b57049a7dc738189df53d69395eba14fb99345e0a5994914a3864c8a4b"}}, + {name = "markupsafe-3.0.3-py3-none-any.whl", url = "https://pypi.org/simple/markupsafe/markupsafe-3.0.3-py3-none-any.whl", hashes = {sha256 = "c0c0b3ade1c0b13b936d7970b1d37a57acde9199dc2aecc4c336773e1d86049c"}}, + {name = "markupsafe-3.0.3-py3-none-any.whl", url = "https://pypi.org/simple/markupsafe/markupsafe-3.0.3-py3-none-any.whl", hashes = {sha256 = "c47a551199eb8eb2121d4f0f15ae0f923d31350ab9280078d1e5f12b249e0026"}}, + {name = "markupsafe-3.0.3-py3-none-any.whl", url = "https://pypi.org/simple/markupsafe/markupsafe-3.0.3-py3-none-any.whl", hashes = {sha256 = "c4ffb7ebf07cfe8931028e3e4c85f0357459a3f9f9490886198848f4fa002ec8"}}, + {name = "markupsafe-3.0.3-py3-none-any.whl", url = "https://pypi.org/simple/markupsafe/markupsafe-3.0.3-py3-none-any.whl", hashes = {sha256 = "ccfcd093f13f0f0b7fdd0f198b90053bf7b2f02a3927a30e63f3ccc9df56b676"}}, + {name = "markupsafe-3.0.3-py3-none-any.whl", url = "https://pypi.org/simple/markupsafe/markupsafe-3.0.3-py3-none-any.whl", hashes = {sha256 = "d2ee202e79d8ed691ceebae8e0486bd9a2cd4794cec4824e1c99b6f5009502f6"}}, + {name = "markupsafe-3.0.3-py3-none-any.whl", url = "https://pypi.org/simple/markupsafe/markupsafe-3.0.3-py3-none-any.whl", hashes = {sha256 = "d53197da72cc091b024dd97249dfc7794d6a56530370992a5e1a08983ad9230e"}}, + {name = "markupsafe-3.0.3-py3-none-any.whl", url = "https://pypi.org/simple/markupsafe/markupsafe-3.0.3-py3-none-any.whl", hashes = {sha256 = "d6dd0be5b5b189d31db7cda48b91d7e0a9795f31430b7f271219ab30f1d3ac9d"}}, + {name = "markupsafe-3.0.3-py3-none-any.whl", url = "https://pypi.org/simple/markupsafe/markupsafe-3.0.3-py3-none-any.whl", hashes = {sha256 = "d88b440e37a16e651bda4c7c2b930eb586fd15ca7406cb39e211fcff3bf3017d"}}, + {name = "markupsafe-3.0.3-py3-none-any.whl", url = "https://pypi.org/simple/markupsafe/markupsafe-3.0.3-py3-none-any.whl", hashes = {sha256 = "de8a88e63464af587c950061a5e6a67d3632e36df62b986892331d4620a35c01"}}, + {name = "markupsafe-3.0.3-py3-none-any.whl", url = "https://pypi.org/simple/markupsafe/markupsafe-3.0.3-py3-none-any.whl", hashes = {sha256 = "df2449253ef108a379b8b5d6b43f4b1a8e81a061d6537becd5582fba5f9196d7"}}, + {name = "markupsafe-3.0.3-py3-none-any.whl", url = "https://pypi.org/simple/markupsafe/markupsafe-3.0.3-py3-none-any.whl", hashes = {sha256 = "e1c1493fb6e50ab01d20a22826e57520f1284df32f2d8601fdd90b6304601419"}}, + {name = "markupsafe-3.0.3-py3-none-any.whl", url = "https://pypi.org/simple/markupsafe/markupsafe-3.0.3-py3-none-any.whl", hashes = {sha256 = "e1cf1972137e83c5d4c136c43ced9ac51d0e124706ee1c8aa8532c1287fa8795"}}, + {name = "markupsafe-3.0.3-py3-none-any.whl", url = "https://pypi.org/simple/markupsafe/markupsafe-3.0.3-py3-none-any.whl", hashes = {sha256 = "e2103a929dfa2fcaf9bb4e7c091983a49c9ac3b19c9061b6d5427dd7d14d81a1"}}, + {name = "markupsafe-3.0.3-py3-none-any.whl", url = "https://pypi.org/simple/markupsafe/markupsafe-3.0.3-py3-none-any.whl", hashes = {sha256 = "e56b7d45a839a697b5eb268c82a71bd8c7f6c94d6fd50c3d577fa39a9f1409f5"}}, + {name = "markupsafe-3.0.3-py3-none-any.whl", url = "https://pypi.org/simple/markupsafe/markupsafe-3.0.3-py3-none-any.whl", hashes = {sha256 = "e8afc3f2ccfa24215f8cb28dcf43f0113ac3c37c2f0f0806d8c70e4228c5cf4d"}}, + {name = "markupsafe-3.0.3-py3-none-any.whl", url = "https://pypi.org/simple/markupsafe/markupsafe-3.0.3-py3-none-any.whl", hashes = {sha256 = "e8fc20152abba6b83724d7ff268c249fa196d8259ff481f3b1476383f8f24e42"}}, + {name = "markupsafe-3.0.3-py3-none-any.whl", url = "https://pypi.org/simple/markupsafe/markupsafe-3.0.3-py3-none-any.whl", hashes = {sha256 = "eaa9599de571d72e2daf60164784109f19978b327a3910d3e9de8c97b5b70cfe"}}, + {name = "markupsafe-3.0.3-py3-none-any.whl", url = "https://pypi.org/simple/markupsafe/markupsafe-3.0.3-py3-none-any.whl", hashes = {sha256 = "ec15a59cf5af7be74194f7ab02d0f59a62bdcf1a537677ce67a2537c9b87fcda"}}, + {name = "markupsafe-3.0.3-py3-none-any.whl", url = "https://pypi.org/simple/markupsafe/markupsafe-3.0.3-py3-none-any.whl", hashes = {sha256 = "f190daf01f13c72eac4efd5c430a8de82489d9cff23c364c3ea822545032993e"}}, + {name = "markupsafe-3.0.3-py3-none-any.whl", url = "https://pypi.org/simple/markupsafe/markupsafe-3.0.3-py3-none-any.whl", hashes = {sha256 = "f34c41761022dd093b4b6896d4810782ffbabe30f2d443ff5f083e0cbbb8c737"}}, + {name = "markupsafe-3.0.3-py3-none-any.whl", url = "https://pypi.org/simple/markupsafe/markupsafe-3.0.3-py3-none-any.whl", hashes = {sha256 = "f3e98bb3798ead92273dc0e5fd0f31ade220f59a266ffd8a4f6065e0a3ce0523"}}, + {name = "markupsafe-3.0.3-py3-none-any.whl", url = "https://pypi.org/simple/markupsafe/markupsafe-3.0.3-py3-none-any.whl", hashes = {sha256 = "f42d0984e947b8adf7dd6dde396e720934d12c506ce84eea8476409563607591"}}, + {name = "markupsafe-3.0.3-py3-none-any.whl", url = "https://pypi.org/simple/markupsafe/markupsafe-3.0.3-py3-none-any.whl", hashes = {sha256 = "f71a396b3bf33ecaa1626c255855702aca4d3d9fea5e051b41ac59a9c1c41edc"}}, + {name = "markupsafe-3.0.3-py3-none-any.whl", url = "https://pypi.org/simple/markupsafe/markupsafe-3.0.3-py3-none-any.whl", hashes = {sha256 = "f9e130248f4462aaa8e2552d547f36ddadbeaa573879158d721bbd33dfe4743a"}}, + {name = "markupsafe-3.0.3-py3-none-any.whl", url = "https://pypi.org/simple/markupsafe/markupsafe-3.0.3-py3-none-any.whl", hashes = {sha256 = "fed51ac40f757d41b7c48425901843666a6677e3e8eb0abcff09e4ba6e664f50"}}, +] + + +[[packages]] +name = "mccabe" +version = "0.6.1" +marker = "'dev' in dependency_groups" +index = "https://pypi.org/simple/" +wheels = [ + {name = "mccabe-0.6.1-py3-none-any.whl", url = "https://pypi.org/simple/mccabe/mccabe-0.6.1-py3-none-any.whl", hashes = {sha256 = "ab8a6258860da4b6677da4bd2fe5dc2c659cff31b3ee4f7f5d64e79735b80d42"}}, + {name = "mccabe-0.6.1-py3-none-any.whl", url = "https://pypi.org/simple/mccabe/mccabe-0.6.1-py3-none-any.whl", hashes = {sha256 = "dd8d182285a0fe56bace7f45b5e7d1a6ebcbf524e8f3bd87eb0f125271b8831f"}}, +] + + +[[packages]] +name = "mdit-py-plugins" +version = "0.5.0" +marker = "('dev' in dependency_groups) and (python_version >= '3.10')" +index = "https://pypi.org/simple/" +wheels = [ + {name = "mdit_py_plugins-0.5.0-py3-none-any.whl", url = "https://pypi.org/simple/mdit-py-plugins/mdit_py_plugins-0.5.0-py3-none-any.whl", hashes = {sha256 = "07a08422fc1936a5d26d146759e9155ea466e842f5ab2f7d2266dd084c8dab1f"}}, + {name = "mdit_py_plugins-0.5.0-py3-none-any.whl", url = "https://pypi.org/simple/mdit-py-plugins/mdit_py_plugins-0.5.0-py3-none-any.whl", hashes = {sha256 = "f4918cb50119f50446560513a8e311d574ff6aaed72606ddae6d35716fe809c6"}}, +] + + +[[packages]] +name = "mdurl" +version = "0.1.2" +marker = "('dev' in dependency_groups) and (python_version >= '3.7')" +index = "https://pypi.org/simple/" +wheels = [ + {name = "mdurl-0.1.2-py3-none-any.whl", url = "https://pypi.org/simple/mdurl/mdurl-0.1.2-py3-none-any.whl", hashes = {sha256 = "84008a41e51615a49fc9966191ff91509e3c40b939176e643fd50a5c2196b8f8"}}, + {name = "mdurl-0.1.2-py3-none-any.whl", url = "https://pypi.org/simple/mdurl/mdurl-0.1.2-py3-none-any.whl", hashes = {sha256 = "bb413d29f5eea38f31dd4754dd7377d4465116fb207585f97bf925588687c1ba"}}, +] + + +[[packages]] +name = "mock" +version = "5.2.0" +marker = "('dev' in dependency_groups) and (python_version >= '3.6')" +index = "https://pypi.org/simple/" +wheels = [ + {name = "mock-5.2.0-py3-none-any.whl", url = "https://pypi.org/simple/mock/mock-5.2.0-py3-none-any.whl", hashes = {sha256 = "4e460e818629b4b173f32d08bf30d3af8123afbb8e04bb5707a1fd4799e503f0"}}, + {name = "mock-5.2.0-py3-none-any.whl", url = "https://pypi.org/simple/mock/mock-5.2.0-py3-none-any.whl", hashes = {sha256 = "7ba87f72ca0e915175596069dbbcc7c75af7b5e9b9bc107ad6349ede0819982f"}}, +] + + +[[packages]] +name = "more-itertools" +version = "10.8.0" +marker = "('dev' in dependency_groups) and (python_version >= '3.9')" +index = "https://pypi.org/simple/" +wheels = [ + {name = "more_itertools-10.8.0-py3-none-any.whl", url = "https://pypi.org/simple/more-itertools/more_itertools-10.8.0-py3-none-any.whl", hashes = {sha256 = "52d4362373dcf7c52546bc4af9a86ee7c4579df9a8dc268be0a2f949d376cc9b"}}, + {name = "more_itertools-10.8.0-py3-none-any.whl", url = "https://pypi.org/simple/more-itertools/more_itertools-10.8.0-py3-none-any.whl", hashes = {sha256 = "f638ddf8a1a0d134181275fb5d58b086ead7c6a72429ad725c67503f13ba30bd"}}, +] + + +[[packages]] +name = "mypy-extensions" +version = "1.1.0" +marker = "('dev' in dependency_groups) and (python_version >= '3.8')" +index = "https://pypi.org/simple/" +wheels = [ + {name = "mypy_extensions-1.1.0-py3-none-any.whl", url = "https://pypi.org/simple/mypy-extensions/mypy_extensions-1.1.0-py3-none-any.whl", hashes = {sha256 = "1be4cccdb0f2482337c4743e60421de3a356cd97508abadd57d47403e94f5505"}}, + {name = "mypy_extensions-1.1.0-py3-none-any.whl", url = "https://pypi.org/simple/mypy-extensions/mypy_extensions-1.1.0-py3-none-any.whl", hashes = {sha256 = "52e68efc3284861e772bbcd66823fde5ae21fd2fdb51c62a211403730b916558"}}, +] + + +[[packages]] +name = "myst-parser" +version = "5.0.0" +marker = "('dev' in dependency_groups) and (python_version >= '3.11')" +index = "https://pypi.org/simple/" +wheels = [ + {name = "myst_parser-5.0.0-py3-none-any.whl", url = "https://pypi.org/simple/myst-parser/myst_parser-5.0.0-py3-none-any.whl", hashes = {sha256 = "ab31e516024918296e169139072b81592336f2fef55b8986aa31c9f04b5f7211"}}, + {name = "myst_parser-5.0.0-py3-none-any.whl", url = "https://pypi.org/simple/myst-parser/myst_parser-5.0.0-py3-none-any.whl", hashes = {sha256 = "f6f231452c56e8baa662cc352c548158f6a16fcbd6e3800fc594978002b94f3a"}}, +] + + +[[packages]] +name = "nh3" +version = "0.3.3" +marker = "('dev' in dependency_groups) and (python_version >= '3.8')" +index = "https://pypi.org/simple/" +wheels = [ + {name = "nh3-0.3.3-py3-none-any.whl", url = "https://pypi.org/simple/nh3/nh3-0.3.3-py3-none-any.whl", hashes = {sha256 = "0d5eb734a78ac364af1797fef718340a373f626a9ff6b4fb0b4badf7927e7b81"}}, + {name = "nh3-0.3.3-py3-none-any.whl", url = "https://pypi.org/simple/nh3/nh3-0.3.3-py3-none-any.whl", hashes = {sha256 = "185ed41b88c910b9ca8edc89ca3b4be688a12cb9de129d84befa2f74a0039fee"}}, + {name = "nh3-0.3.3-py3-none-any.whl", url = "https://pypi.org/simple/nh3/nh3-0.3.3-py3-none-any.whl", hashes = {sha256 = "1ef87f8e916321a88b45f2d597f29bd56e560ed4568a50f0f1305afab86b7189"}}, + {name = "nh3-0.3.3-py3-none-any.whl", url = "https://pypi.org/simple/nh3/nh3-0.3.3-py3-none-any.whl", hashes = {sha256 = "21a63ccb18ddad3f784bb775955839b8b80e347e597726f01e43ca1abcc5c808"}}, + {name = "nh3-0.3.3-py3-none-any.whl", url = "https://pypi.org/simple/nh3/nh3-0.3.3-py3-none-any.whl", hashes = {sha256 = "21b058cd20d9f0919421a820a2843fdb5e1749c0bf57a6247ab8f4ba6723c9fc"}}, + {name = "nh3-0.3.3-py3-none-any.whl", url = "https://pypi.org/simple/nh3/nh3-0.3.3-py3-none-any.whl", hashes = {sha256 = "24769a428e9e971e4ccfb24628f83aaa7dc3c8b41b130c8ddc1835fa1c924489"}}, + {name = "nh3-0.3.3-py3-none-any.whl", url = "https://pypi.org/simple/nh3/nh3-0.3.3-py3-none-any.whl", hashes = {sha256 = "2efd17c0355d04d39e6d79122b42662277ac10a17ea48831d90b46e5ef7e4fc0"}}, + {name = "nh3-0.3.3-py3-none-any.whl", url = "https://pypi.org/simple/nh3/nh3-0.3.3-py3-none-any.whl", hashes = {sha256 = "3a62b8ae7c235481715055222e54c682422d0495a5c73326807d4e44c5d14691"}}, + {name = "nh3-0.3.3-py3-none-any.whl", url = "https://pypi.org/simple/nh3/nh3-0.3.3-py3-none-any.whl", hashes = {sha256 = "45fe0d6a607264910daec30360c8a3b5b1500fd832d21b2da608256287bcb92d"}}, + {name = "nh3-0.3.3-py3-none-any.whl", url = "https://pypi.org/simple/nh3/nh3-0.3.3-py3-none-any.whl", hashes = {sha256 = "4c730617bdc15d7092dcc0469dc2826b914c8f874996d105b4bc3842a41c1cd9"}}, + {name = "nh3-0.3.3-py3-none-any.whl", url = "https://pypi.org/simple/nh3/nh3-0.3.3-py3-none-any.whl", hashes = {sha256 = "52e973cb742e95b9ae1b35822ce23992428750f4b46b619fe86eba4205255b30"}}, + {name = "nh3-0.3.3-py3-none-any.whl", url = "https://pypi.org/simple/nh3/nh3-0.3.3-py3-none-any.whl", hashes = {sha256 = "5a4b2c1f3e6f3cbe7048e17f4fefad3f8d3e14cc0fd08fb8599e0d5653f6b181"}}, + {name = "nh3-0.3.3-py3-none-any.whl", url = "https://pypi.org/simple/nh3/nh3-0.3.3-py3-none-any.whl", hashes = {sha256 = "5bc1d4b30ba1ba896669d944b6003630592665974bd11a3dc2f661bde92798a7"}}, + {name = "nh3-0.3.3-py3-none-any.whl", url = "https://pypi.org/simple/nh3/nh3-0.3.3-py3-none-any.whl", hashes = {sha256 = "90126a834c18af03bfd6ff9a027bfa6bbf0e238527bc780a24de6bd7cc1041e2"}}, + {name = "nh3-0.3.3-py3-none-any.whl", url = "https://pypi.org/simple/nh3/nh3-0.3.3-py3-none-any.whl", hashes = {sha256 = "92a958e6f6d0100e025a5686aafd67e3c98eac67495728f8bb64fbeb3e474493"}}, + {name = "nh3-0.3.3-py3-none-any.whl", url = "https://pypi.org/simple/nh3/nh3-0.3.3-py3-none-any.whl", hashes = {sha256 = "9ed40cf8449a59a03aa465114fedce1ff7ac52561688811d047917cc878b19ca"}}, + {name = "nh3-0.3.3-py3-none-any.whl", url = "https://pypi.org/simple/nh3/nh3-0.3.3-py3-none-any.whl", hashes = {sha256 = "a446eae598987f49ee97ac2f18eafcce4e62e7574bd1eb23782e4702e54e217d"}}, + {name = "nh3-0.3.3-py3-none-any.whl", url = "https://pypi.org/simple/nh3/nh3-0.3.3-py3-none-any.whl", hashes = {sha256 = "b50c3770299fb2a7c1113751501e8878d525d15160a4c05194d7fe62b758aad8"}}, + {name = "nh3-0.3.3-py3-none-any.whl", url = "https://pypi.org/simple/nh3/nh3-0.3.3-py3-none-any.whl", hashes = {sha256 = "b7a18ee057761e455d58b9d31445c3e4b2594cff4ddb84d2e331c011ef46f462"}}, + {name = "nh3-0.3.3-py3-none-any.whl", url = "https://pypi.org/simple/nh3/nh3-0.3.3-py3-none-any.whl", hashes = {sha256 = "b838e619f483531483d26d889438e53a880510e832d2aafe73f93b7b1ac2bce2"}}, + {name = "nh3-0.3.3-py3-none-any.whl", url = "https://pypi.org/simple/nh3/nh3-0.3.3-py3-none-any.whl", hashes = {sha256 = "e8ee96156f7dfc6e30ecda650e480c5ae0a7d38f0c6fafc3c1c655e2500421d9"}}, + {name = "nh3-0.3.3-py3-none-any.whl", url = "https://pypi.org/simple/nh3/nh3-0.3.3-py3-none-any.whl", hashes = {sha256 = "e974850b131fdffa75e7ad8e0d9c7a855b96227b093417fdf1bd61656e530f37"}}, + {name = "nh3-0.3.3-py3-none-any.whl", url = "https://pypi.org/simple/nh3/nh3-0.3.3-py3-none-any.whl", hashes = {sha256 = "e98fa3dbfd54e25487e36ba500bc29bca3a4cab4ffba18cfb1a35a2d02624297"}}, + {name = "nh3-0.3.3-py3-none-any.whl", url = "https://pypi.org/simple/nh3/nh3-0.3.3-py3-none-any.whl", hashes = {sha256 = "f433a2dd66545aad4a720ad1b2150edcdca75bfff6f4e6f378ade1ec138d5e77"}}, + {name = "nh3-0.3.3-py3-none-any.whl", url = "https://pypi.org/simple/nh3/nh3-0.3.3-py3-none-any.whl", hashes = {sha256 = "f4400a73c2a62859e769f9d36d1b5a7a5c65c4179d1dddd2f6f3095b2db0cbfc"}}, + {name = "nh3-0.3.3-py3-none-any.whl", url = "https://pypi.org/simple/nh3/nh3-0.3.3-py3-none-any.whl", hashes = {sha256 = "f508ddd4e2433fdcb78c790fc2d24e3a349ba775e5fa904af89891321d4844a3"}}, + {name = "nh3-0.3.3-py3-none-any.whl", url = "https://pypi.org/simple/nh3/nh3-0.3.3-py3-none-any.whl", hashes = {sha256 = "fc305a2264868ec8fa16548296f803d8fd9c1fa66cd28b88b605b1bd06667c0b"}}, +] + + +[[packages]] +name = "nodeenv" +version = "1.10.0" +marker = "('dev' in dependency_groups) and (python_version >= '2.7' and python_version not in '3.0, 3.1, 3.2, 3.3, 3.4, 3.5, 3.6')" +index = "https://pypi.org/simple/" +wheels = [ + {name = "nodeenv-1.10.0-py3-none-any.whl", url = "https://pypi.org/simple/nodeenv/nodeenv-1.10.0-py3-none-any.whl", hashes = {sha256 = "5bb13e3eed2923615535339b3c620e76779af4cb4c6a90deccc9e36b274d3827"}}, + {name = "nodeenv-1.10.0-py3-none-any.whl", url = "https://pypi.org/simple/nodeenv/nodeenv-1.10.0-py3-none-any.whl", hashes = {sha256 = "996c191ad80897d076bdfba80a41994c2b47c68e224c542b48feba42ba00f8bb"}}, +] + + +[[packages]] +name = "packaging" +version = "26.0" +marker = "('dev' in dependency_groups) and (python_version >= '3.8')" +index = "https://pypi.org/simple/" +wheels = [ + {name = "packaging-26.0-py3-none-any.whl", url = "https://pypi.org/simple/packaging/packaging-26.0-py3-none-any.whl", hashes = {sha256 = "00243ae351a257117b6a241061796684b084ed1c516a08c48a3f7e147a9d80b4"}}, + {name = "packaging-26.0-py3-none-any.whl", url = "https://pypi.org/simple/packaging/packaging-26.0-py3-none-any.whl", hashes = {sha256 = "b36f1fef9334a5588b4166f8bcd26a14e521f2b55e6b9de3aaa80d3ff7a37529"}}, +] + + +[[packages]] +name = "parse" +version = "1.21.1" +marker = "'dev' in dependency_groups" +index = "https://pypi.org/simple/" +wheels = [ + {name = "parse-1.21.1-py3-none-any.whl", url = "https://pypi.org/simple/parse/parse-1.21.1-py3-none-any.whl", hashes = {sha256 = "55339ca698019815df3b8e8b550e5933933527e623b0cdf1ca2f404da35ffb47"}}, + {name = "parse-1.21.1-py3-none-any.whl", url = "https://pypi.org/simple/parse/parse-1.21.1-py3-none-any.whl", hashes = {sha256 = "825e1a88e9d9fb481b8d2ca709c6195558b6eaa97c559ad3a9a20aa2d12815a3"}}, +] + + +[[packages]] +name = "parver" +version = "0.5" +marker = "('dev' in dependency_groups) and (python_version >= '3.8')" +index = "https://pypi.org/simple/" +wheels = [ + {name = "parver-0.5-py3-none-any.whl", url = "https://pypi.org/simple/parver/parver-0.5-py3-none-any.whl", hashes = {sha256 = "2281b187276c8e8e3c15634f62287b2fb6fe0efe3010f739a6bd1e45fa2bf2b2"}}, + {name = "parver-0.5-py3-none-any.whl", url = "https://pypi.org/simple/parver/parver-0.5-py3-none-any.whl", hashes = {sha256 = "b9fde1e6bb9ce9f07e08e9c4bea8d8825c5e78e18a0052d02e02bf9517eb4777"}}, +] + + +[[packages]] +name = "pathspec" +version = "1.0.4" +marker = "('dev' in dependency_groups) and (python_version >= '3.9')" +index = "https://pypi.org/simple/" +wheels = [ + {name = "pathspec-1.0.4-py3-none-any.whl", url = "https://pypi.org/simple/pathspec/pathspec-1.0.4-py3-none-any.whl", hashes = {sha256 = "0210e2ae8a21a9137c0d470578cb0e595af87edaa6ebf12ff176f14a02e0e645"}}, + {name = "pathspec-1.0.4-py3-none-any.whl", url = "https://pypi.org/simple/pathspec/pathspec-1.0.4-py3-none-any.whl", hashes = {sha256 = "fb6ae2fd4e7c921a165808a552060e722767cfa526f99ca5156ed2ce45a5c723"}}, +] + + +[[packages]] +name = "pip" +version = "26.0.1" +marker = "('dev' in dependency_groups) and (python_version >= '3.9')" +index = "https://pypi.org/simple/" +wheels = [ + {name = "pip-26.0.1-py3-none-any.whl", url = "https://pypi.org/simple/pip/pip-26.0.1-py3-none-any.whl", hashes = {sha256 = "bdb1b08f4274833d62c1aa29e20907365a2ceb950410df15fc9521bad440122b"}}, + {name = "pip-26.0.1-py3-none-any.whl", url = "https://pypi.org/simple/pip/pip-26.0.1-py3-none-any.whl", hashes = {sha256 = "c4037d8a277c89b320abe636d59f91e6d0922d08a05b60e85e53b296613346d8"}}, +] + + +[[packages]] +name = "platformdirs" +version = "4.9.4" +marker = "('dev' in dependency_groups) and (python_version >= '3.10')" +index = "https://pypi.org/simple/" +wheels = [ + {name = "platformdirs-4.9.4-py3-none-any.whl", url = "https://pypi.org/simple/platformdirs/platformdirs-4.9.4-py3-none-any.whl", hashes = {sha256 = "1ec356301b7dc906d83f371c8f487070e99d3ccf9e501686456394622a01a934"}}, + {name = "platformdirs-4.9.4-py3-none-any.whl", url = "https://pypi.org/simple/platformdirs/platformdirs-4.9.4-py3-none-any.whl", hashes = {sha256 = "68a9a4619a666ea6439f2ff250c12a853cd1cbd5158d258bd824a7df6be2f868"}}, +] + + +[[packages]] +name = "pluggy" +version = "1.6.0" +marker = "('dev' in dependency_groups) and (python_version >= '3.9')" +index = "https://pypi.org/simple/" +wheels = [ + {name = "pluggy-1.6.0-py3-none-any.whl", url = "https://pypi.org/simple/pluggy/pluggy-1.6.0-py3-none-any.whl", hashes = {sha256 = "7dcc130b76258d33b90f61b658791dede3486c3e6bfb003ee5c9bfb396dd22f3"}}, + {name = "pluggy-1.6.0-py3-none-any.whl", url = "https://pypi.org/simple/pluggy/pluggy-1.6.0-py3-none-any.whl", hashes = {sha256 = "e920276dd6813095e9377c0bc5566d94c932c33b27a3e3945d8389c374dd4746"}}, +] + + +[[packages]] +name = "pre-commit" +version = "2.21.0" +marker = "('dev' in dependency_groups) and (python_version >= '3.7')" +index = "https://pypi.org/simple/" +wheels = [ + {name = "pre_commit-2.21.0-py3-none-any.whl", url = "https://pypi.org/simple/pre-commit/pre_commit-2.21.0-py3-none-any.whl", hashes = {sha256 = "31ef31af7e474a8d8995027fefdfcf509b5c913ff31f2015b4ec4beb26a6f658"}}, + {name = "pre_commit-2.21.0-py3-none-any.whl", url = "https://pypi.org/simple/pre-commit/pre_commit-2.21.0-py3-none-any.whl", hashes = {sha256 = "e2f91727039fc39a92f58a588a25b87f936de6567eed4f0e673e0507edc75bad"}}, +] + + +[[packages]] +name = "pycodestyle" +version = "2.7.0" +marker = "('dev' in dependency_groups) and (python_version >= '2.7' and python_version not in '3.0, 3.1, 3.2, 3.3')" +index = "https://pypi.org/simple/" +wheels = [ + {name = "pycodestyle-2.7.0-py3-none-any.whl", url = "https://pypi.org/simple/pycodestyle/pycodestyle-2.7.0-py3-none-any.whl", hashes = {sha256 = "514f76d918fcc0b55c6680472f0a37970994e07bbb80725808c17089be302068"}}, + {name = "pycodestyle-2.7.0-py3-none-any.whl", url = "https://pypi.org/simple/pycodestyle/pycodestyle-2.7.0-py3-none-any.whl", hashes = {sha256 = "c389c1d06bf7904078ca03399a4816f974a1d590090fecea0c63ec26ebaf1cef"}}, +] + + +[[packages]] +name = "pycparser" +version = "3.0" +marker = "('dev' in dependency_groups) and (python_version >= '3.10')" +index = "https://pypi.org/simple/" +wheels = [ + {name = "pycparser-3.0-py3-none-any.whl", url = "https://pypi.org/simple/pycparser/pycparser-3.0-py3-none-any.whl", hashes = {sha256 = "600f49d217304a5902ac3c37e1281c9fe94e4d0489de643a9504c5cdfdfc6b29"}}, + {name = "pycparser-3.0-py3-none-any.whl", url = "https://pypi.org/simple/pycparser/pycparser-3.0-py3-none-any.whl", hashes = {sha256 = "b727414169a36b7d524c1c3e31839a521725078d7b2ff038656844266160a992"}}, +] + + +[[packages]] +name = "pyenchant" +version = "3.3.0" +marker = "('dev' in dependency_groups) and (python_version >= '3.9')" +index = "https://pypi.org/simple/" +wheels = [ + {name = "pyenchant-3.3.0-py3-none-any.whl", url = "https://pypi.org/simple/pyenchant/pyenchant-3.3.0-py3-none-any.whl", hashes = {sha256 = "04a5bd0e022ebe2e8c6d9e498ec3d650602e264ec5486e9c6a1b7f99c9507c49"}}, + {name = "pyenchant-3.3.0-py3-none-any.whl", url = "https://pypi.org/simple/pyenchant/pyenchant-3.3.0-py3-none-any.whl", hashes = {sha256 = "1d55e075645a6edbb3c590fb42f9e02b4d455e4affe28a2227d5cb6d4868e626"}}, + {name = "pyenchant-3.3.0-py3-none-any.whl", url = "https://pypi.org/simple/pyenchant/pyenchant-3.3.0-py3-none-any.whl", hashes = {sha256 = "3da00b1d01314d85aac733bb997415d7a3e875666dc81735ddcf320aa36b7a70"}}, + {name = "pyenchant-3.3.0-py3-none-any.whl", url = "https://pypi.org/simple/pyenchant/pyenchant-3.3.0-py3-none-any.whl", hashes = {sha256 = "825288246b5debc9436f91967650974ef0d5636458502619e322c476f1283891"}}, +] + + +[[packages]] +name = "pyflakes" +version = "2.3.1" +marker = "('dev' in dependency_groups) and (python_version >= '2.7' and python_version not in '3.0, 3.1, 3.2, 3.3')" +index = "https://pypi.org/simple/" +wheels = [ + {name = "pyflakes-2.3.1-py3-none-any.whl", url = "https://pypi.org/simple/pyflakes/pyflakes-2.3.1-py3-none-any.whl", hashes = {sha256 = "7893783d01b8a89811dd72d7dfd4d84ff098e5eed95cfa8905b22bbffe52efc3"}}, + {name = "pyflakes-2.3.1-py3-none-any.whl", url = "https://pypi.org/simple/pyflakes/pyflakes-2.3.1-py3-none-any.whl", hashes = {sha256 = "f5bc8ecabc05bb9d291eb5203d6810b49040f6ff446a756326104746cc00c1db"}}, +] + + +[[packages]] +name = "pygments" +version = "2.19.2" +marker = "('dev' in dependency_groups) and (python_version >= '3.8')" +index = "https://pypi.org/simple/" +wheels = [ + {name = "pygments-2.19.2-py3-none-any.whl", url = "https://pypi.org/simple/pygments/pygments-2.19.2-py3-none-any.whl", hashes = {sha256 = "636cb2477cec7f8952536970bc533bc43743542f70392ae026374600add5b887"}}, + {name = "pygments-2.19.2-py3-none-any.whl", url = "https://pypi.org/simple/pygments/pygments-2.19.2-py3-none-any.whl", hashes = {sha256 = "86540386c03d588bb81d44bc3928634ff26449851e99741617ecb9037ee5ec0b"}}, +] + + +[[packages]] +name = "pypiserver" +version = "2.4.1" +marker = "('dev' in dependency_groups) and (python_version >= '3.10')" +index = "https://pypi.org/simple/" +wheels = [ + {name = "pypiserver-2.4.1-py3-none-any.whl", url = "https://pypi.org/simple/pypiserver/pypiserver-2.4.1-py3-none-any.whl", hashes = {sha256 = "156540f87ecfd6db06ae2c16e25ae5afe4fda6f510bd1c34e46fbb0c491bcd9e"}}, + {name = "pypiserver-2.4.1-py3-none-any.whl", url = "https://pypi.org/simple/pypiserver/pypiserver-2.4.1-py3-none-any.whl", hashes = {sha256 = "45f116d0bff6aafcaed002cfad48a6832e62a82393e3a9b447d5c41a0e310fff"}}, +] + + +[[packages]] +name = "pyproject-hooks" +version = "1.2.0" +marker = "('dev' in dependency_groups) and (python_version >= '3.7')" +index = "https://pypi.org/simple/" +wheels = [ + {name = "pyproject_hooks-1.2.0-py3-none-any.whl", url = "https://pypi.org/simple/pyproject-hooks/pyproject_hooks-1.2.0-py3-none-any.whl", hashes = {sha256 = "1e859bd5c40fae9448642dd871adf459e5e2084186e8d2c2a79a824c970da1f8"}}, + {name = "pyproject_hooks-1.2.0-py3-none-any.whl", url = "https://pypi.org/simple/pyproject-hooks/pyproject_hooks-1.2.0-py3-none-any.whl", hashes = {sha256 = "9e5c6bfa8dcc30091c74b0cf803c81fdd29d94f01992a7707bc97babb1141913"}}, +] + + +[[packages]] +name = "pytest" +version = "9.0.3" +marker = "('dev' in dependency_groups) and (python_version >= '3.10')" +index = "https://pypi.org/simple/" +wheels = [ + {name = "pytest-9.0.3-py3-none-any.whl", url = "https://pypi.org/simple/pytest/pytest-9.0.3-py3-none-any.whl", hashes = {sha256 = "2c5efc453d45394fdd706ade797c0a81091eccd1d6e4bccfcd476e2b8e0ab5d9"}}, + {name = "pytest-9.0.3-py3-none-any.whl", url = "https://pypi.org/simple/pytest/pytest-9.0.3-py3-none-any.whl", hashes = {sha256 = "b86ada508af81d19edeb213c681b1d48246c1a91d304c6c81a427674c17eb91c"}}, +] + + +[[packages]] +name = "pytest-cov" +version = "4.1.0" +marker = "('dev' in dependency_groups) and (python_version >= '3.7')" +index = "https://pypi.org/simple/" +wheels = [ + {name = "pytest_cov-4.1.0-py3-none-any.whl", url = "https://pypi.org/simple/pytest-cov/pytest_cov-4.1.0-py3-none-any.whl", hashes = {sha256 = "3904b13dfbfec47f003b8e77fd5b589cd11904a21ddf1ab38a64f204d6a10ef6"}}, + {name = "pytest_cov-4.1.0-py3-none-any.whl", url = "https://pypi.org/simple/pytest-cov/pytest_cov-4.1.0-py3-none-any.whl", hashes = {sha256 = "6ba70b9e97e69fcc3fb45bfeab2d0a138fb65c4d0d6a41ef33983ad114be8c3a"}}, +] + + +[[packages]] +name = "pytest-rerunfailures" +version = "16.1" +marker = "('dev' in dependency_groups) and (python_version >= '3.10')" +index = "https://pypi.org/simple/" +wheels = [ + {name = "pytest_rerunfailures-16.1-py3-none-any.whl", url = "https://pypi.org/simple/pytest-rerunfailures/pytest_rerunfailures-16.1-py3-none-any.whl", hashes = {sha256 = "5d11b12c0ca9a1665b5054052fcc1084f8deadd9328962745ef6b04e26382e86"}}, + {name = "pytest_rerunfailures-16.1-py3-none-any.whl", url = "https://pypi.org/simple/pytest-rerunfailures/pytest_rerunfailures-16.1-py3-none-any.whl", hashes = {sha256 = "c38b266db8a808953ebd71ac25c381cb1981a78ff9340a14bcb9f1b9bff1899e"}}, +] + + +[[packages]] +name = "pytest-timeout" +version = "2.4.0" +marker = "('dev' in dependency_groups) and (python_version >= '3.7')" +index = "https://pypi.org/simple/" +wheels = [ + {name = "pytest_timeout-2.4.0-py3-none-any.whl", url = "https://pypi.org/simple/pytest-timeout/pytest_timeout-2.4.0-py3-none-any.whl", hashes = {sha256 = "7e68e90b01f9eff71332b25001f85c75495fc4e3a836701876183c4bcfd0540a"}}, + {name = "pytest_timeout-2.4.0-py3-none-any.whl", url = "https://pypi.org/simple/pytest-timeout/pytest_timeout-2.4.0-py3-none-any.whl", hashes = {sha256 = "c42667e5cdadb151aeb5b26d114aff6bdf5a907f176a007a30b940d3d865b5c2"}}, +] + + +[[packages]] +name = "pytest-xdist" +version = "3.8.0" +marker = "('dev' in dependency_groups) and (python_version >= '3.9')" +index = "https://pypi.org/simple/" +wheels = [ + {name = "pytest_xdist-3.8.0-py3-none-any.whl", url = "https://pypi.org/simple/pytest-xdist/pytest_xdist-3.8.0-py3-none-any.whl", hashes = {sha256 = "202ca578cfeb7370784a8c33d6d05bc6e13b4f25b5053c30a152269fd10f0b88"}}, + {name = "pytest_xdist-3.8.0-py3-none-any.whl", url = "https://pypi.org/simple/pytest-xdist/pytest_xdist-3.8.0-py3-none-any.whl", hashes = {sha256 = "7e578125ec9bc6050861aa93f2d59f1d8d085595d6551c2c90b6f4fad8d3a9f1"}}, +] + + +[[packages]] +name = "python-discovery" +version = "1.1.3" +marker = "('dev' in dependency_groups) and (python_version >= '3.8')" +index = "https://pypi.org/simple/" +wheels = [ + {name = "python_discovery-1.1.3-py3-none-any.whl", url = "https://pypi.org/simple/python-discovery/python_discovery-1.1.3-py3-none-any.whl", hashes = {sha256 = "7acca36e818cd88e9b2ba03e045ad7e93e1713e29c6bbfba5d90202310b7baa5"}}, + {name = "python_discovery-1.1.3-py3-none-any.whl", url = "https://pypi.org/simple/python-discovery/python_discovery-1.1.3-py3-none-any.whl", hashes = {sha256 = "90e795f0121bc84572e737c9aa9966311b9fde44ffb88a5953b3ec9b31c6945e"}}, +] + + +[[packages]] +name = "pytokens" +version = "0.4.1" +marker = "('dev' in dependency_groups) and (python_version >= '3.8')" +index = "https://pypi.org/simple/" +wheels = [ + {name = "pytokens-0.4.1-py3-none-any.whl", url = "https://pypi.org/simple/pytokens/pytokens-0.4.1-py3-none-any.whl", hashes = {sha256 = "0fc71786e629cef478cbf29d7ea1923299181d0699dbe7c3c0f4a583811d9fc1"}}, + {name = "pytokens-0.4.1-py3-none-any.whl", url = "https://pypi.org/simple/pytokens/pytokens-0.4.1-py3-none-any.whl", hashes = {sha256 = "11edda0942da80ff58c4408407616a310adecae1ddd22eef8c692fe266fa5009"}}, + {name = "pytokens-0.4.1-py3-none-any.whl", url = "https://pypi.org/simple/pytokens/pytokens-0.4.1-py3-none-any.whl", hashes = {sha256 = "140709331e846b728475786df8aeb27d24f48cbcf7bcd449f8de75cae7a45083"}}, + {name = "pytokens-0.4.1-py3-none-any.whl", url = "https://pypi.org/simple/pytokens/pytokens-0.4.1-py3-none-any.whl", hashes = {sha256 = "24afde1f53d95348b5a0eb19488661147285ca4dd7ed752bbc3e1c6242a304d1"}}, + {name = "pytokens-0.4.1-py3-none-any.whl", url = "https://pypi.org/simple/pytokens/pytokens-0.4.1-py3-none-any.whl", hashes = {sha256 = "26cef14744a8385f35d0e095dc8b3a7583f6c953c2e3d269c7f82484bf5ad2de"}}, + {name = "pytokens-0.4.1-py3-none-any.whl", url = "https://pypi.org/simple/pytokens/pytokens-0.4.1-py3-none-any.whl", hashes = {sha256 = "27b83ad28825978742beef057bfe406ad6ed524b2d28c252c5de7b4a6dd48fa2"}}, + {name = "pytokens-0.4.1-py3-none-any.whl", url = "https://pypi.org/simple/pytokens/pytokens-0.4.1-py3-none-any.whl", hashes = {sha256 = "292052fe80923aae2260c073f822ceba21f3872ced9a68bb7953b348e561179a"}}, + {name = "pytokens-0.4.1-py3-none-any.whl", url = "https://pypi.org/simple/pytokens/pytokens-0.4.1-py3-none-any.whl", hashes = {sha256 = "29d1d8fb1030af4d231789959f21821ab6325e463f0503a61d204343c9b355d1"}}, + {name = "pytokens-0.4.1-py3-none-any.whl", url = "https://pypi.org/simple/pytokens/pytokens-0.4.1-py3-none-any.whl", hashes = {sha256 = "2a44ed93ea23415c54f3face3b65ef2b844d96aeb3455b8a69b3df6beab6acc5"}}, + {name = "pytokens-0.4.1-py3-none-any.whl", url = "https://pypi.org/simple/pytokens/pytokens-0.4.1-py3-none-any.whl", hashes = {sha256 = "30f51edd9bb7f85c748979384165601d028b84f7bd13fe14d3e065304093916a"}}, + {name = "pytokens-0.4.1-py3-none-any.whl", url = "https://pypi.org/simple/pytokens/pytokens-0.4.1-py3-none-any.whl", hashes = {sha256 = "34bcc734bd2f2d5fe3b34e7b3c0116bfb2397f2d9666139988e7a3eb5f7400e3"}}, + {name = "pytokens-0.4.1-py3-none-any.whl", url = "https://pypi.org/simple/pytokens/pytokens-0.4.1-py3-none-any.whl", hashes = {sha256 = "3ad72b851e781478366288743198101e5eb34a414f1d5627cdd585ca3b25f1db"}}, + {name = "pytokens-0.4.1-py3-none-any.whl", url = "https://pypi.org/simple/pytokens/pytokens-0.4.1-py3-none-any.whl", hashes = {sha256 = "3f901fe783e06e48e8cbdc82d631fca8f118333798193e026a50ce1b3757ea68"}}, + {name = "pytokens-0.4.1-py3-none-any.whl", url = "https://pypi.org/simple/pytokens/pytokens-0.4.1-py3-none-any.whl", hashes = {sha256 = "42f144f3aafa5d92bad964d471a581651e28b24434d184871bd02e3a0d956037"}}, + {name = "pytokens-0.4.1-py3-none-any.whl", url = "https://pypi.org/simple/pytokens/pytokens-0.4.1-py3-none-any.whl", hashes = {sha256 = "4a14d5f5fc78ce85e426aa159489e2d5961acf0e47575e08f35584009178e321"}}, + {name = "pytokens-0.4.1-py3-none-any.whl", url = "https://pypi.org/simple/pytokens/pytokens-0.4.1-py3-none-any.whl", hashes = {sha256 = "4a58d057208cb9075c144950d789511220b07636dd2e4708d5645d24de666bdc"}}, + {name = "pytokens-0.4.1-py3-none-any.whl", url = "https://pypi.org/simple/pytokens/pytokens-0.4.1-py3-none-any.whl", hashes = {sha256 = "4e691d7f5186bd2842c14813f79f8884bb03f5995f0575272009982c5ac6c0f7"}}, + {name = "pytokens-0.4.1-py3-none-any.whl", url = "https://pypi.org/simple/pytokens/pytokens-0.4.1-py3-none-any.whl", hashes = {sha256 = "5502408cab1cb18e128570f8d598981c68a50d0cbd7c61312a90507cd3a1276f"}}, + {name = "pytokens-0.4.1-py3-none-any.whl", url = "https://pypi.org/simple/pytokens/pytokens-0.4.1-py3-none-any.whl", hashes = {sha256 = "584c80c24b078eec1e227079d56dc22ff755e0ba8654d8383b2c549107528918"}}, + {name = "pytokens-0.4.1-py3-none-any.whl", url = "https://pypi.org/simple/pytokens/pytokens-0.4.1-py3-none-any.whl", hashes = {sha256 = "5ad948d085ed6c16413eb5fec6b3e02fa00dc29a2534f088d3302c47eb59adf9"}}, + {name = "pytokens-0.4.1-py3-none-any.whl", url = "https://pypi.org/simple/pytokens/pytokens-0.4.1-py3-none-any.whl", hashes = {sha256 = "670d286910b531c7b7e3c0b453fd8156f250adb140146d234a82219459b9640c"}}, + {name = "pytokens-0.4.1-py3-none-any.whl", url = "https://pypi.org/simple/pytokens/pytokens-0.4.1-py3-none-any.whl", hashes = {sha256 = "682fa37ff4d8e95f7df6fe6fe6a431e8ed8e788023c6bcc0f0880a12eab80ad1"}}, + {name = "pytokens-0.4.1-py3-none-any.whl", url = "https://pypi.org/simple/pytokens/pytokens-0.4.1-py3-none-any.whl", hashes = {sha256 = "6d6c4268598f762bc8e91f5dbf2ab2f61f7b95bdc07953b602db879b3c8c18e1"}}, + {name = "pytokens-0.4.1-py3-none-any.whl", url = "https://pypi.org/simple/pytokens/pytokens-0.4.1-py3-none-any.whl", hashes = {sha256 = "79fc6b8699564e1f9b521582c35435f1bd32dd06822322ec44afdeba666d8cb3"}}, + {name = "pytokens-0.4.1-py3-none-any.whl", url = "https://pypi.org/simple/pytokens/pytokens-0.4.1-py3-none-any.whl", hashes = {sha256 = "8bdb9d0ce90cbf99c525e75a2fa415144fd570a1ba987380190e8b786bc6ef9b"}}, + {name = "pytokens-0.4.1-py3-none-any.whl", url = "https://pypi.org/simple/pytokens/pytokens-0.4.1-py3-none-any.whl", hashes = {sha256 = "8fcb9ba3709ff77e77f1c7022ff11d13553f3c30299a9fe246a166903e9091eb"}}, + {name = "pytokens-0.4.1-py3-none-any.whl", url = "https://pypi.org/simple/pytokens/pytokens-0.4.1-py3-none-any.whl", hashes = {sha256 = "941d4343bf27b605e9213b26bfa1c4bf197c9c599a9627eb7305b0defcfe40c1"}}, + {name = "pytokens-0.4.1-py3-none-any.whl", url = "https://pypi.org/simple/pytokens/pytokens-0.4.1-py3-none-any.whl", hashes = {sha256 = "967cf6e3fd4adf7de8fc73cd3043754ae79c36475c1c11d514fc72cf5490094a"}}, + {name = "pytokens-0.4.1-py3-none-any.whl", url = "https://pypi.org/simple/pytokens/pytokens-0.4.1-py3-none-any.whl", hashes = {sha256 = "970b08dd6b86058b6dc07efe9e98414f5102974716232d10f32ff39701e841c4"}}, + {name = "pytokens-0.4.1-py3-none-any.whl", url = "https://pypi.org/simple/pytokens/pytokens-0.4.1-py3-none-any.whl", hashes = {sha256 = "97f50fd18543be72da51dd505e2ed20d2228c74e0464e4262e4899797803d7fa"}}, + {name = "pytokens-0.4.1-py3-none-any.whl", url = "https://pypi.org/simple/pytokens/pytokens-0.4.1-py3-none-any.whl", hashes = {sha256 = "9bd7d7f544d362576be74f9d5901a22f317efc20046efe2034dced238cbbfe78"}}, + {name = "pytokens-0.4.1-py3-none-any.whl", url = "https://pypi.org/simple/pytokens/pytokens-0.4.1-py3-none-any.whl", hashes = {sha256 = "add8bf86b71a5d9fb5b89f023a80b791e04fba57960aa790cc6125f7f1d39dfe"}}, + {name = "pytokens-0.4.1-py3-none-any.whl", url = "https://pypi.org/simple/pytokens/pytokens-0.4.1-py3-none-any.whl", hashes = {sha256 = "b35d7e5ad269804f6697727702da3c517bb8a5228afa450ab0fa787732055fc9"}}, + {name = "pytokens-0.4.1-py3-none-any.whl", url = "https://pypi.org/simple/pytokens/pytokens-0.4.1-py3-none-any.whl", hashes = {sha256 = "b49750419d300e2b5a3813cf229d4e5a4c728dae470bcc89867a9ad6f25a722d"}}, + {name = "pytokens-0.4.1-py3-none-any.whl", url = "https://pypi.org/simple/pytokens/pytokens-0.4.1-py3-none-any.whl", hashes = {sha256 = "d31b97b3de0f61571a124a00ffe9a81fb9939146c122c11060725bd5aea79975"}}, + {name = "pytokens-0.4.1-py3-none-any.whl", url = "https://pypi.org/simple/pytokens/pytokens-0.4.1-py3-none-any.whl", hashes = {sha256 = "d70e77c55ae8380c91c0c18dea05951482e263982911fc7410b1ffd1dadd3440"}}, + {name = "pytokens-0.4.1-py3-none-any.whl", url = "https://pypi.org/simple/pytokens/pytokens-0.4.1-py3-none-any.whl", hashes = {sha256 = "d9907d61f15bf7261d7e775bd5d7ee4d2930e04424bab1972591918497623a16"}}, + {name = "pytokens-0.4.1-py3-none-any.whl", url = "https://pypi.org/simple/pytokens/pytokens-0.4.1-py3-none-any.whl", hashes = {sha256 = "da5baeaf7116dced9c6bb76dc31ba04a2dc3695f3d9f74741d7910122b456edc"}}, + {name = "pytokens-0.4.1-py3-none-any.whl", url = "https://pypi.org/simple/pytokens/pytokens-0.4.1-py3-none-any.whl", hashes = {sha256 = "dc74c035f9bfca0255c1af77ddd2d6ae8419012805453e4b0e7513e17904545d"}}, + {name = "pytokens-0.4.1-py3-none-any.whl", url = "https://pypi.org/simple/pytokens/pytokens-0.4.1-py3-none-any.whl", hashes = {sha256 = "dcafc12c30dbaf1e2af0490978352e0c4041a7cde31f4f81435c2a5e8b9cabb6"}}, + {name = "pytokens-0.4.1-py3-none-any.whl", url = "https://pypi.org/simple/pytokens/pytokens-0.4.1-py3-none-any.whl", hashes = {sha256 = "ee44d0f85b803321710f9239f335aafe16553b39106384cef8e6de40cb4ef2f6"}}, + {name = "pytokens-0.4.1-py3-none-any.whl", url = "https://pypi.org/simple/pytokens/pytokens-0.4.1-py3-none-any.whl", hashes = {sha256 = "f66a6bbe741bd431f6d741e617e0f39ec7257ca1f89089593479347cc4d13324"}}, +] + + +[[packages]] +name = "pyyaml" +version = "6.0.1" +marker = "('dev' in dependency_groups) and (python_version >= '3.6')" +index = "https://pypi.org/simple/" +wheels = [ + {name = "pyyaml-6.0.1-py3-none-any.whl", url = "https://pypi.org/simple/pyyaml/pyyaml-6.0.1-py3-none-any.whl", hashes = {sha256 = "04ac92ad1925b2cff1db0cfebffb6ffc43457495c9b3c39d3fcae417d7125dc5"}}, + {name = "pyyaml-6.0.1-py3-none-any.whl", url = "https://pypi.org/simple/pyyaml/pyyaml-6.0.1-py3-none-any.whl", hashes = {sha256 = "062582fca9fabdd2c8b54a3ef1c978d786e0f6b3a1510e0ac93ef59e0ddae2bc"}}, + {name = "pyyaml-6.0.1-py3-none-any.whl", url = "https://pypi.org/simple/pyyaml/pyyaml-6.0.1-py3-none-any.whl", hashes = {sha256 = "0d3304d8c0adc42be59c5f8a4d9e3d7379e6955ad754aa9d6ab7a398b59dd1df"}}, + {name = "pyyaml-6.0.1-py3-none-any.whl", url = "https://pypi.org/simple/pyyaml/pyyaml-6.0.1-py3-none-any.whl", hashes = {sha256 = "1635fd110e8d85d55237ab316b5b011de701ea0f29d07611174a1b42f1444741"}}, + {name = "pyyaml-6.0.1-py3-none-any.whl", url = "https://pypi.org/simple/pyyaml/pyyaml-6.0.1-py3-none-any.whl", hashes = {sha256 = "184c5108a2aca3c5b3d3bf9395d50893a7ab82a38004c8f61c258d4428e80206"}}, + {name = "pyyaml-6.0.1-py3-none-any.whl", url = "https://pypi.org/simple/pyyaml/pyyaml-6.0.1-py3-none-any.whl", hashes = {sha256 = "18aeb1bf9a78867dc38b259769503436b7c72f7a1f1f4c93ff9a17de54319b27"}}, + {name = "pyyaml-6.0.1-py3-none-any.whl", url = "https://pypi.org/simple/pyyaml/pyyaml-6.0.1-py3-none-any.whl", hashes = {sha256 = "1d4c7e777c441b20e32f52bd377e0c409713e8bb1386e1099c2415f26e479595"}}, + {name = "pyyaml-6.0.1-py3-none-any.whl", url = "https://pypi.org/simple/pyyaml/pyyaml-6.0.1-py3-none-any.whl", hashes = {sha256 = "1e2722cc9fbb45d9b87631ac70924c11d3a401b2d7f410cc0e3bbf249f2dca62"}}, + {name = "pyyaml-6.0.1-py3-none-any.whl", url = "https://pypi.org/simple/pyyaml/pyyaml-6.0.1-py3-none-any.whl", hashes = {sha256 = "1fe35611261b29bd1de0070f0b2f47cb6ff71fa6595c077e42bd0c419fa27b98"}}, + {name = "pyyaml-6.0.1-py3-none-any.whl", url = "https://pypi.org/simple/pyyaml/pyyaml-6.0.1-py3-none-any.whl", hashes = {sha256 = "28c119d996beec18c05208a8bd78cbe4007878c6dd15091efb73a30e90539696"}}, + {name = "pyyaml-6.0.1-py3-none-any.whl", url = "https://pypi.org/simple/pyyaml/pyyaml-6.0.1-py3-none-any.whl", hashes = {sha256 = "326c013efe8048858a6d312ddd31d56e468118ad4cdeda36c719bf5bb6192290"}}, + {name = "pyyaml-6.0.1-py3-none-any.whl", url = "https://pypi.org/simple/pyyaml/pyyaml-6.0.1-py3-none-any.whl", hashes = {sha256 = "40df9b996c2b73138957fe23a16a4f0ba614f4c0efce1e9406a184b6d07fa3a9"}}, + {name = "pyyaml-6.0.1-py3-none-any.whl", url = "https://pypi.org/simple/pyyaml/pyyaml-6.0.1-py3-none-any.whl", hashes = {sha256 = "42f8152b8dbc4fe7d96729ec2b99c7097d656dc1213a3229ca5383f973a5ed6d"}}, + {name = "pyyaml-6.0.1-py3-none-any.whl", url = "https://pypi.org/simple/pyyaml/pyyaml-6.0.1-py3-none-any.whl", hashes = {sha256 = "49a183be227561de579b4a36efbb21b3eab9651dd81b1858589f796549873dd6"}}, + {name = "pyyaml-6.0.1-py3-none-any.whl", url = "https://pypi.org/simple/pyyaml/pyyaml-6.0.1-py3-none-any.whl", hashes = {sha256 = "4fb147e7a67ef577a588a0e2c17b6db51dda102c71de36f8549b6816a96e1867"}}, + {name = "pyyaml-6.0.1-py3-none-any.whl", url = "https://pypi.org/simple/pyyaml/pyyaml-6.0.1-py3-none-any.whl", hashes = {sha256 = "50550eb667afee136e9a77d6dc71ae76a44df8b3e51e41b77f6de2932bfe0f47"}}, + {name = "pyyaml-6.0.1-py3-none-any.whl", url = "https://pypi.org/simple/pyyaml/pyyaml-6.0.1-py3-none-any.whl", hashes = {sha256 = "510c9deebc5c0225e8c96813043e62b680ba2f9c50a08d3724c7f28a747d1486"}}, + {name = "pyyaml-6.0.1-py3-none-any.whl", url = "https://pypi.org/simple/pyyaml/pyyaml-6.0.1-py3-none-any.whl", hashes = {sha256 = "5773183b6446b2c99bb77e77595dd486303b4faab2b086e7b17bc6bef28865f6"}}, + {name = "pyyaml-6.0.1-py3-none-any.whl", url = "https://pypi.org/simple/pyyaml/pyyaml-6.0.1-py3-none-any.whl", hashes = {sha256 = "596106435fa6ad000c2991a98fa58eeb8656ef2325d7e158344fb33864ed87e3"}}, + {name = "pyyaml-6.0.1-py3-none-any.whl", url = "https://pypi.org/simple/pyyaml/pyyaml-6.0.1-py3-none-any.whl", hashes = {sha256 = "6965a7bc3cf88e5a1c3bd2e0b5c22f8d677dc88a455344035f03399034eb3007"}}, + {name = "pyyaml-6.0.1-py3-none-any.whl", url = "https://pypi.org/simple/pyyaml/pyyaml-6.0.1-py3-none-any.whl", hashes = {sha256 = "69b023b2b4daa7548bcfbd4aa3da05b3a74b772db9e23b982788168117739938"}}, + {name = "pyyaml-6.0.1-py3-none-any.whl", url = "https://pypi.org/simple/pyyaml/pyyaml-6.0.1-py3-none-any.whl", hashes = {sha256 = "6c22bec3fbe2524cde73d7ada88f6566758a8f7227bfbf93a408a9d86bcc12a0"}}, + {name = "pyyaml-6.0.1-py3-none-any.whl", url = "https://pypi.org/simple/pyyaml/pyyaml-6.0.1-py3-none-any.whl", hashes = {sha256 = "704219a11b772aea0d8ecd7058d0082713c3562b4e271b849ad7dc4a5c90c13c"}}, + {name = "pyyaml-6.0.1-py3-none-any.whl", url = "https://pypi.org/simple/pyyaml/pyyaml-6.0.1-py3-none-any.whl", hashes = {sha256 = "7e07cbde391ba96ab58e532ff4803f79c4129397514e1413a7dc761ccd755735"}}, + {name = "pyyaml-6.0.1-py3-none-any.whl", url = "https://pypi.org/simple/pyyaml/pyyaml-6.0.1-py3-none-any.whl", hashes = {sha256 = "81e0b275a9ecc9c0c0c07b4b90ba548307583c125f54d5b6946cfee6360c733d"}}, + {name = "pyyaml-6.0.1-py3-none-any.whl", url = "https://pypi.org/simple/pyyaml/pyyaml-6.0.1-py3-none-any.whl", hashes = {sha256 = "855fb52b0dc35af121542a76b9a84f8d1cd886ea97c84703eaa6d88e37a2ad28"}}, + {name = "pyyaml-6.0.1-py3-none-any.whl", url = "https://pypi.org/simple/pyyaml/pyyaml-6.0.1-py3-none-any.whl", hashes = {sha256 = "8d4e9c88387b0f5c7d5f281e55304de64cf7f9c0021a3525bd3b1c542da3b0e4"}}, + {name = "pyyaml-6.0.1-py3-none-any.whl", url = "https://pypi.org/simple/pyyaml/pyyaml-6.0.1-py3-none-any.whl", hashes = {sha256 = "9046c58c4395dff28dd494285c82ba00b546adfc7ef001486fbf0324bc174fba"}}, + {name = "pyyaml-6.0.1-py3-none-any.whl", url = "https://pypi.org/simple/pyyaml/pyyaml-6.0.1-py3-none-any.whl", hashes = {sha256 = "9eb6caa9a297fc2c2fb8862bc5370d0303ddba53ba97e71f08023b6cd73d16a8"}}, + {name = "pyyaml-6.0.1-py3-none-any.whl", url = "https://pypi.org/simple/pyyaml/pyyaml-6.0.1-py3-none-any.whl", hashes = {sha256 = "a08c6f0fe150303c1c6b71ebcd7213c2858041a7e01975da3a99aed1e7a378ef"}}, + {name = "pyyaml-6.0.1-py3-none-any.whl", url = "https://pypi.org/simple/pyyaml/pyyaml-6.0.1-py3-none-any.whl", hashes = {sha256 = "a0cd17c15d3bb3fa06978b4e8958dcdc6e0174ccea823003a106c7d4d7899ac5"}}, + {name = "pyyaml-6.0.1-py3-none-any.whl", url = "https://pypi.org/simple/pyyaml/pyyaml-6.0.1-py3-none-any.whl", hashes = {sha256 = "afd7e57eddb1a54f0f1a974bc4391af8bcce0b444685d936840f125cf046d5bd"}}, + {name = "pyyaml-6.0.1-py3-none-any.whl", url = "https://pypi.org/simple/pyyaml/pyyaml-6.0.1-py3-none-any.whl", hashes = {sha256 = "b1275ad35a5d18c62a7220633c913e1b42d44b46ee12554e5fd39c70a243d6a3"}}, + {name = "pyyaml-6.0.1-py3-none-any.whl", url = "https://pypi.org/simple/pyyaml/pyyaml-6.0.1-py3-none-any.whl", hashes = {sha256 = "b786eecbdf8499b9ca1d697215862083bd6d2a99965554781d0d8d1ad31e13a0"}}, + {name = "pyyaml-6.0.1-py3-none-any.whl", url = "https://pypi.org/simple/pyyaml/pyyaml-6.0.1-py3-none-any.whl", hashes = {sha256 = "ba336e390cd8e4d1739f42dfe9bb83a3cc2e80f567d8805e11b46f4a943f5515"}}, + {name = "pyyaml-6.0.1-py3-none-any.whl", url = "https://pypi.org/simple/pyyaml/pyyaml-6.0.1-py3-none-any.whl", hashes = {sha256 = "baa90d3f661d43131ca170712d903e6295d1f7a0f595074f151c0aed377c9b9c"}}, + {name = "pyyaml-6.0.1-py3-none-any.whl", url = "https://pypi.org/simple/pyyaml/pyyaml-6.0.1-py3-none-any.whl", hashes = {sha256 = "bc1bf2925a1ecd43da378f4db9e4f799775d6367bdb94671027b73b393a7c42c"}}, + {name = "pyyaml-6.0.1-py3-none-any.whl", url = "https://pypi.org/simple/pyyaml/pyyaml-6.0.1-py3-none-any.whl", hashes = {sha256 = "bd4af7373a854424dabd882decdc5579653d7868b8fb26dc7d0e99f823aa5924"}}, + {name = "pyyaml-6.0.1-py3-none-any.whl", url = "https://pypi.org/simple/pyyaml/pyyaml-6.0.1-py3-none-any.whl", hashes = {sha256 = "bf07ee2fef7014951eeb99f56f39c9bb4af143d8aa3c21b1677805985307da34"}}, + {name = "pyyaml-6.0.1-py3-none-any.whl", url = "https://pypi.org/simple/pyyaml/pyyaml-6.0.1-py3-none-any.whl", hashes = {sha256 = "bfdf460b1736c775f2ba9f6a92bca30bc2095067b8a9d77876d1fad6cc3b4a43"}}, + {name = "pyyaml-6.0.1-py3-none-any.whl", url = "https://pypi.org/simple/pyyaml/pyyaml-6.0.1-py3-none-any.whl", hashes = {sha256 = "c8098ddcc2a85b61647b2590f825f3db38891662cfc2fc776415143f599bb859"}}, + {name = "pyyaml-6.0.1-py3-none-any.whl", url = "https://pypi.org/simple/pyyaml/pyyaml-6.0.1-py3-none-any.whl", hashes = {sha256 = "d2b04aac4d386b172d5b9692e2d2da8de7bfb6c387fa4f801fbf6fb2e6ba4673"}}, + {name = "pyyaml-6.0.1-py3-none-any.whl", url = "https://pypi.org/simple/pyyaml/pyyaml-6.0.1-py3-none-any.whl", hashes = {sha256 = "d483d2cdf104e7c9fa60c544d92981f12ad66a457afae824d146093b8c294c54"}}, + {name = "pyyaml-6.0.1-py3-none-any.whl", url = "https://pypi.org/simple/pyyaml/pyyaml-6.0.1-py3-none-any.whl", hashes = {sha256 = "d858aa552c999bc8a8d57426ed01e40bef403cd8ccdd0fc5f6f04a00414cac2a"}}, + {name = "pyyaml-6.0.1-py3-none-any.whl", url = "https://pypi.org/simple/pyyaml/pyyaml-6.0.1-py3-none-any.whl", hashes = {sha256 = "e7d73685e87afe9f3b36c799222440d6cf362062f78be1013661b00c5c6f678b"}}, + {name = "pyyaml-6.0.1-py3-none-any.whl", url = "https://pypi.org/simple/pyyaml/pyyaml-6.0.1-py3-none-any.whl", hashes = {sha256 = "f003ed9ad21d6a4713f0a9b5a7a0a79e08dd0f221aff4525a2be4c346ee60aab"}}, + {name = "pyyaml-6.0.1-py3-none-any.whl", url = "https://pypi.org/simple/pyyaml/pyyaml-6.0.1-py3-none-any.whl", hashes = {sha256 = "f22ac1c3cac4dbc50079e965eba2c1058622631e526bd9afd45fedd49ba781fa"}}, + {name = "pyyaml-6.0.1-py3-none-any.whl", url = "https://pypi.org/simple/pyyaml/pyyaml-6.0.1-py3-none-any.whl", hashes = {sha256 = "faca3bdcf85b2fc05d06ff3fbc1f83e1391b3e724afa3feba7d13eeab355484c"}}, + {name = "pyyaml-6.0.1-py3-none-any.whl", url = "https://pypi.org/simple/pyyaml/pyyaml-6.0.1-py3-none-any.whl", hashes = {sha256 = "fca0e3a251908a499833aa292323f32437106001d436eca0e6e7833256674585"}}, + {name = "pyyaml-6.0.1-py3-none-any.whl", url = "https://pypi.org/simple/pyyaml/pyyaml-6.0.1-py3-none-any.whl", hashes = {sha256 = "fd1592b3fdf65fff2ad0004b5e363300ef59ced41c2e6b3a99d4089fa8c5435d"}}, + {name = "pyyaml-6.0.1-py3-none-any.whl", url = "https://pypi.org/simple/pyyaml/pyyaml-6.0.1-py3-none-any.whl", hashes = {sha256 = "fd66fc5d0da6d9815ba2cebeb4205f95818ff4b79c3ebe268e75d961704af52f"}}, +] + + +[[packages]] +name = "readme-renderer" +version = "44.0" +marker = "('dev' in dependency_groups) and (python_version >= '3.9')" +index = "https://pypi.org/simple/" +wheels = [ + {name = "readme_renderer-44.0-py3-none-any.whl", url = "https://pypi.org/simple/readme-renderer/readme_renderer-44.0-py3-none-any.whl", hashes = {sha256 = "2fbca89b81a08526aadf1357a8c2ae889ec05fb03f5da67f9769c9a592166151"}}, + {name = "readme_renderer-44.0-py3-none-any.whl", url = "https://pypi.org/simple/readme-renderer/readme_renderer-44.0-py3-none-any.whl", hashes = {sha256 = "8712034eabbfa6805cacf1402b4eeb2a73028f72d1166d6f5cb7f9c047c5d1e1"}}, +] + + +[[packages]] +name = "requests" +version = "2.32.5" +marker = "('dev' in dependency_groups) and (python_version >= '3.9')" +index = "https://pypi.org/simple/" +wheels = [ + {name = "requests-2.32.5-py3-none-any.whl", url = "https://pypi.org/simple/requests/requests-2.32.5-py3-none-any.whl", hashes = {sha256 = "2462f94637a34fd532264295e186976db0f5d453d1cdd31473c85a6a161affb6"}}, + {name = "requests-2.32.5-py3-none-any.whl", url = "https://pypi.org/simple/requests/requests-2.32.5-py3-none-any.whl", hashes = {sha256 = "dbba0bac56e100853db0ea71b82b4dfd5fe2bf6d3754a8893c3af500cec7d7cf"}}, +] + + +[[packages]] +name = "requests-toolbelt" +version = "1.0.0" +marker = "('dev' in dependency_groups) and (python_version >= '2.7' and python_version not in '3.0, 3.1, 3.2, 3.3')" +index = "https://pypi.org/simple/" +wheels = [ + {name = "requests_toolbelt-1.0.0-py3-none-any.whl", url = "https://pypi.org/simple/requests-toolbelt/requests_toolbelt-1.0.0-py3-none-any.whl", hashes = {sha256 = "7681a0a3d047012b5bdc0ee37d7f8f07ebe76ab08caeccfc3921ce23c88d5bc6"}}, + {name = "requests_toolbelt-1.0.0-py3-none-any.whl", url = "https://pypi.org/simple/requests-toolbelt/requests_toolbelt-1.0.0-py3-none-any.whl", hashes = {sha256 = "cccfdd665f0a24fcf4726e690f65639d272bb0637b9b92dfd91a5568ccf6bd06"}}, +] + + +[[packages]] +name = "rfc3986" +version = "2.0.0" +marker = "('dev' in dependency_groups) and (python_version >= '3.7')" +index = "https://pypi.org/simple/" +wheels = [ + {name = "rfc3986-2.0.0-py3-none-any.whl", url = "https://pypi.org/simple/rfc3986/rfc3986-2.0.0-py3-none-any.whl", hashes = {sha256 = "50b1502b60e289cb37883f3dfd34532b8873c7de9f49bb546641ce9cbd256ebd"}}, + {name = "rfc3986-2.0.0-py3-none-any.whl", url = "https://pypi.org/simple/rfc3986/rfc3986-2.0.0-py3-none-any.whl", hashes = {sha256 = "97aacf9dbd4bfd829baad6e6309fa6573aaf1be3f6fa735c8ab05e46cecb261c"}}, +] + + +[[packages]] +name = "rich" +version = "14.3.3" +marker = "('dev' in dependency_groups) and (python_full_version >= '3.8.0')" +index = "https://pypi.org/simple/" +wheels = [ + {name = "rich-14.3.3-py3-none-any.whl", url = "https://pypi.org/simple/rich/rich-14.3.3-py3-none-any.whl", hashes = {sha256 = "793431c1f8619afa7d3b52b2cdec859562b950ea0d4b6b505397612db8d5362d"}}, + {name = "rich-14.3.3-py3-none-any.whl", url = "https://pypi.org/simple/rich/rich-14.3.3-py3-none-any.whl", hashes = {sha256 = "b8daa0b9e4eef54dd8cf7c86c03713f53241884e814f4e2f5fb342fe520f639b"}}, +] + + +[[packages]] +name = "roman-numerals" +version = "4.1.0" +marker = "('dev' in dependency_groups) and (python_version >= '3.10')" +index = "https://pypi.org/simple/" +wheels = [ + {name = "roman_numerals-4.1.0-py3-none-any.whl", url = "https://pypi.org/simple/roman-numerals/roman_numerals-4.1.0-py3-none-any.whl", hashes = {sha256 = "1af8b147eb1405d5839e78aeb93131690495fe9da5c91856cb33ad55a7f1e5b2"}}, + {name = "roman_numerals-4.1.0-py3-none-any.whl", url = "https://pypi.org/simple/roman-numerals/roman_numerals-4.1.0-py3-none-any.whl", hashes = {sha256 = "647ba99caddc2cc1e55a51e4360689115551bf4476d90e8162cf8c345fe233c7"}}, +] + + +[[packages]] +name = "secretstorage" +version = "3.5.0" +marker = "('dev' in dependency_groups) and (python_version >= '3.10')" +index = "https://pypi.org/simple/" +wheels = [ + {name = "secretstorage-3.5.0-py3-none-any.whl", url = "https://pypi.org/simple/secretstorage/secretstorage-3.5.0-py3-none-any.whl", hashes = {sha256 = "0ce65888c0725fcb2c5bc0fdb8e5438eece02c523557ea40ce0703c266248137"}}, + {name = "secretstorage-3.5.0-py3-none-any.whl", url = "https://pypi.org/simple/secretstorage/secretstorage-3.5.0-py3-none-any.whl", hashes = {sha256 = "f04b8e4689cbce351744d5537bf6b1329c6fc68f91fa666f60a380edddcd11be"}}, +] + + +[[packages]] +name = "semver" +version = "3.0.4" +marker = "('dev' in dependency_groups) and (python_version >= '3.7')" +index = "https://pypi.org/simple/" +wheels = [ + {name = "semver-3.0.4-py3-none-any.whl", url = "https://pypi.org/simple/semver/semver-3.0.4-py3-none-any.whl", hashes = {sha256 = "9c824d87ba7f7ab4a1890799cec8596f15c1241cb473404ea1cb0c55e4b04746"}}, + {name = "semver-3.0.4-py3-none-any.whl", url = "https://pypi.org/simple/semver/semver-3.0.4-py3-none-any.whl", hashes = {sha256 = "afc7d8c584a5ed0a11033af086e8af226a9c0b206f313e0301f8dd7b6b589602"}}, +] + + +[[packages]] +name = "setuptools" +version = "83.0.0" +marker = "('dev' in dependency_groups) and (python_version >= '3.9')" +index = "https://pypi.org/simple/" +wheels = [ + {name = "setuptools-83.0.0-py3-none-any.whl", url = "https://pypi.org/simple/setuptools/setuptools-83.0.0-py3-none-any.whl", hashes = {sha256 = "025bccbbf0fa05b6192bc64ae1e7b16e001fd6d6d4d5de03c97b1c1ade523bef"}}, + {name = "setuptools-83.0.0-py3-none-any.whl", url = "https://pypi.org/simple/setuptools/setuptools-83.0.0-py3-none-any.whl", hashes = {sha256 = "29b23c360f22f414dc7336bb39178cc7bcbf6021ed2733cde173f09dba19abb3"}}, +] + + +[[packages]] +name = "snowballstemmer" +version = "3.0.1" +marker = "('dev' in dependency_groups) and (python_version not in '3.0, 3.1, 3.2')" +index = "https://pypi.org/simple/" +wheels = [ + {name = "snowballstemmer-3.0.1-py3-none-any.whl", url = "https://pypi.org/simple/snowballstemmer/snowballstemmer-3.0.1-py3-none-any.whl", hashes = {sha256 = "6cd7b3897da8d6c9ffb968a6781fa6532dce9c3618a4b127d920dab764a19064"}}, + {name = "snowballstemmer-3.0.1-py3-none-any.whl", url = "https://pypi.org/simple/snowballstemmer/snowballstemmer-3.0.1-py3-none-any.whl", hashes = {sha256 = "6d5eeeec8e9f84d4d56b847692bacf79bc2c8e90c7f80ca4444ff8b6f2e52895"}}, +] + + +[[packages]] +name = "soupsieve" +version = "2.8.3" +marker = "('dev' in dependency_groups) and (python_version >= '3.9')" +index = "https://pypi.org/simple/" +wheels = [ + {name = "soupsieve-2.8.3-py3-none-any.whl", url = "https://pypi.org/simple/soupsieve/soupsieve-2.8.3-py3-none-any.whl", hashes = {sha256 = "3267f1eeea4251fb42728b6dfb746edc9acaffc4a45b27e19450b676586e8349"}}, + {name = "soupsieve-2.8.3-py3-none-any.whl", url = "https://pypi.org/simple/soupsieve/soupsieve-2.8.3-py3-none-any.whl", hashes = {sha256 = "ed64f2ba4eebeab06cc4962affce381647455978ffc1e36bb79a545b91f45a95"}}, +] + + +[[packages]] +name = "sphinx" +version = "9.1.0" +marker = "('dev' in dependency_groups) and (python_version >= '3.12')" +index = "https://pypi.org/simple/" +wheels = [ + {name = "sphinx-9.1.0-py3-none-any.whl", url = "https://pypi.org/simple/sphinx/sphinx-9.1.0-py3-none-any.whl", hashes = {sha256 = "7741722357dd75f8190766926071fed3bdc211c74dd2d7d4df5404da95930ddb"}}, + {name = "sphinx-9.1.0-py3-none-any.whl", url = "https://pypi.org/simple/sphinx/sphinx-9.1.0-py3-none-any.whl", hashes = {sha256 = "c84fdd4e782504495fe4f2c0b3413d6c2bf388589bb352d439b2a3bb99991978"}}, +] + + +[[packages]] +name = "sphinx-click" +version = "4.4.0" +marker = "('dev' in dependency_groups) and (python_version >= '3.7')" +index = "https://pypi.org/simple/" +wheels = [ + {name = "sphinx_click-4.4.0-py3-none-any.whl", url = "https://pypi.org/simple/sphinx-click/sphinx_click-4.4.0-py3-none-any.whl", hashes = {sha256 = "2821c10a68fc9ee6ce7c92fad26540d8d8c8f45e6d7258f0e4fb7529ae8fab49"}}, + {name = "sphinx_click-4.4.0-py3-none-any.whl", url = "https://pypi.org/simple/sphinx-click/sphinx_click-4.4.0-py3-none-any.whl", hashes = {sha256 = "cc67692bd28f482c7f01531c61b64e9d2f069bfcf3d24cbbb51d4a84a749fa48"}}, +] + + +[[packages]] +name = "sphinxcontrib-applehelp" +version = "2.0.0" +marker = "('dev' in dependency_groups) and (python_version >= '3.9')" +index = "https://pypi.org/simple/" +wheels = [ + {name = "sphinxcontrib_applehelp-2.0.0-py3-none-any.whl", url = "https://pypi.org/simple/sphinxcontrib-applehelp/sphinxcontrib_applehelp-2.0.0-py3-none-any.whl", hashes = {sha256 = "2f29ef331735ce958efa4734873f084941970894c6090408b079c61b2e1c06d1"}}, + {name = "sphinxcontrib_applehelp-2.0.0-py3-none-any.whl", url = "https://pypi.org/simple/sphinxcontrib-applehelp/sphinxcontrib_applehelp-2.0.0-py3-none-any.whl", hashes = {sha256 = "4cd3f0ec4ac5dd9c17ec65e9ab272c9b867ea77425228e68ecf08d6b28ddbdb5"}}, +] + + +[[packages]] +name = "sphinxcontrib-devhelp" +version = "2.0.0" +marker = "('dev' in dependency_groups) and (python_version >= '3.9')" +index = "https://pypi.org/simple/" +wheels = [ + {name = "sphinxcontrib_devhelp-2.0.0-py3-none-any.whl", url = "https://pypi.org/simple/sphinxcontrib-devhelp/sphinxcontrib_devhelp-2.0.0-py3-none-any.whl", hashes = {sha256 = "411f5d96d445d1d73bb5d52133377b4248ec79db5c793ce7dbe59e074b4dd1ad"}}, + {name = "sphinxcontrib_devhelp-2.0.0-py3-none-any.whl", url = "https://pypi.org/simple/sphinxcontrib-devhelp/sphinxcontrib_devhelp-2.0.0-py3-none-any.whl", hashes = {sha256 = "aefb8b83854e4b0998877524d1029fd3e6879210422ee3780459e28a1f03a8a2"}}, +] + + +[[packages]] +name = "sphinxcontrib-htmlhelp" +version = "2.1.0" +marker = "('dev' in dependency_groups) and (python_version >= '3.9')" +index = "https://pypi.org/simple/" +wheels = [ + {name = "sphinxcontrib_htmlhelp-2.1.0-py3-none-any.whl", url = "https://pypi.org/simple/sphinxcontrib-htmlhelp/sphinxcontrib_htmlhelp-2.1.0-py3-none-any.whl", hashes = {sha256 = "166759820b47002d22914d64a075ce08f4c46818e17cfc9470a9786b759b19f8"}}, + {name = "sphinxcontrib_htmlhelp-2.1.0-py3-none-any.whl", url = "https://pypi.org/simple/sphinxcontrib-htmlhelp/sphinxcontrib_htmlhelp-2.1.0-py3-none-any.whl", hashes = {sha256 = "c9e2916ace8aad64cc13a0d233ee22317f2b9025b9cf3295249fa985cc7082e9"}}, +] + + +[[packages]] +name = "sphinxcontrib-jsmath" +version = "1.0.1" +marker = "('dev' in dependency_groups) and (python_version >= '3.5')" +index = "https://pypi.org/simple/" +wheels = [ + {name = "sphinxcontrib_jsmath-1.0.1-py3-none-any.whl", url = "https://pypi.org/simple/sphinxcontrib-jsmath/sphinxcontrib_jsmath-1.0.1-py3-none-any.whl", hashes = {sha256 = "2ec2eaebfb78f3f2078e73666b1415417a116cc848b72e5172e596c871103178"}}, + {name = "sphinxcontrib_jsmath-1.0.1-py3-none-any.whl", url = "https://pypi.org/simple/sphinxcontrib-jsmath/sphinxcontrib_jsmath-1.0.1-py3-none-any.whl", hashes = {sha256 = "a9925e4a4587247ed2191a22df5f6970656cb8ca2bd6284309578f2153e0c4b8"}}, +] + + +[[packages]] +name = "sphinxcontrib-qthelp" +version = "2.0.0" +marker = "('dev' in dependency_groups) and (python_version >= '3.9')" +index = "https://pypi.org/simple/" +wheels = [ + {name = "sphinxcontrib_qthelp-2.0.0-py3-none-any.whl", url = "https://pypi.org/simple/sphinxcontrib-qthelp/sphinxcontrib_qthelp-2.0.0-py3-none-any.whl", hashes = {sha256 = "4fe7d0ac8fc171045be623aba3e2a8f613f8682731f9153bb2e40ece16b9bbab"}}, + {name = "sphinxcontrib_qthelp-2.0.0-py3-none-any.whl", url = "https://pypi.org/simple/sphinxcontrib-qthelp/sphinxcontrib_qthelp-2.0.0-py3-none-any.whl", hashes = {sha256 = "b18a828cdba941ccd6ee8445dbe72ffa3ef8cbe7505d8cd1fa0d42d3f2d5f3eb"}}, +] + + +[[packages]] +name = "sphinxcontrib-serializinghtml" +version = "2.0.0" +marker = "('dev' in dependency_groups) and (python_version >= '3.9')" +index = "https://pypi.org/simple/" +wheels = [ + {name = "sphinxcontrib_serializinghtml-2.0.0-py3-none-any.whl", url = "https://pypi.org/simple/sphinxcontrib-serializinghtml/sphinxcontrib_serializinghtml-2.0.0-py3-none-any.whl", hashes = {sha256 = "6e2cb0eef194e10c27ec0023bfeb25badbbb5868244cf5bc5bdc04e4464bf331"}}, + {name = "sphinxcontrib_serializinghtml-2.0.0-py3-none-any.whl", url = "https://pypi.org/simple/sphinxcontrib-serializinghtml/sphinxcontrib_serializinghtml-2.0.0-py3-none-any.whl", hashes = {sha256 = "e9d912827f872c029017a53f0ef2180b327c3f7fd23c87229f7a8e8b70031d4d"}}, +] + + +[[packages]] +name = "sphinxcontrib-spelling" +version = "7.7.0" +marker = "('dev' in dependency_groups) and (python_version >= '3.6')" +index = "https://pypi.org/simple/" +wheels = [ + {name = "sphinxcontrib_spelling-7.7.0-py3-none-any.whl", url = "https://pypi.org/simple/sphinxcontrib-spelling/sphinxcontrib_spelling-7.7.0-py3-none-any.whl", hashes = {sha256 = "56561c3f6a155b0946914e4de988729859315729dc181b5e4dc8a68fe78de35a"}}, + {name = "sphinxcontrib_spelling-7.7.0-py3-none-any.whl", url = "https://pypi.org/simple/sphinxcontrib-spelling/sphinxcontrib_spelling-7.7.0-py3-none-any.whl", hashes = {sha256 = "95a0defef8ffec6526f9e83b20cc24b08c9179298729d87976891840e3aa3064"}}, +] + + +[[packages]] +name = "stdeb" +version = "0.11.0" +marker = "('dev' in dependency_groups) and (sys_platform == 'linux')" +index = "https://pypi.org/simple/" +wheels = [ + {name = "stdeb-0.11.0-py3-none-any.whl", url = "https://pypi.org/simple/stdeb/stdeb-0.11.0-py3-none-any.whl", hashes = {sha256 = "3f883c522ecb76394514ea4d282eda7671c8bd0db0fd904f9774ba20e02035e6"}}, + {name = "stdeb-0.11.0-py3-none-any.whl", url = "https://pypi.org/simple/stdeb/stdeb-0.11.0-py3-none-any.whl", hashes = {sha256 = "e7084e24f1616ab599d3f390599363b39670905179c261d23cd8ddcc0ecb3b71"}}, +] + + +[[packages]] +name = "tomli" +version = "2.4.0" +marker = "('dev' in dependency_groups) and (python_version >= '3.8')" +index = "https://pypi.org/simple/" +wheels = [ + {name = "tomli-2.4.0-py3-none-any.whl", url = "https://pypi.org/simple/tomli/tomli-2.4.0-py3-none-any.whl", hashes = {sha256 = "0408e3de5ec77cc7f81960c362543cbbd91ef883e3138e81b729fc3eea5b9729"}}, + {name = "tomli-2.4.0-py3-none-any.whl", url = "https://pypi.org/simple/tomli/tomli-2.4.0-py3-none-any.whl", hashes = {sha256 = "0dc56fef0e2c1c470aeac5b6ca8cc7b640bb93e92d9803ddaf9ea03e198f5b0b"}}, + {name = "tomli-2.4.0-py3-none-any.whl", url = "https://pypi.org/simple/tomli/tomli-2.4.0-py3-none-any.whl", hashes = {sha256 = "0e0fe8a0b8312acf3a88077a0802565cb09ee34107813bba1c7cd591fa6cfc8d"}}, + {name = "tomli-2.4.0-py3-none-any.whl", url = "https://pypi.org/simple/tomli/tomli-2.4.0-py3-none-any.whl", hashes = {sha256 = "0f2e3955efea4d1cfbcb87bc321e00dc08d2bcb737fd1d5e398af111d86db5df"}}, + {name = "tomli-2.4.0-py3-none-any.whl", url = "https://pypi.org/simple/tomli/tomli-2.4.0-py3-none-any.whl", hashes = {sha256 = "133e93646ec4300d651839d382d63edff11d8978be23da4cc106f5a18b7d0576"}}, + {name = "tomli-2.4.0-py3-none-any.whl", url = "https://pypi.org/simple/tomli/tomli-2.4.0-py3-none-any.whl", hashes = {sha256 = "1b168f2731796b045128c45982d3a4874057626da0e2ef1fdd722848b741361d"}}, + {name = "tomli-2.4.0-py3-none-any.whl", url = "https://pypi.org/simple/tomli/tomli-2.4.0-py3-none-any.whl", hashes = {sha256 = "1c8a885b370751837c029ef9bc014f27d80840e48bac415f3412e6593bbc18c1"}}, + {name = "tomli-2.4.0-py3-none-any.whl", url = "https://pypi.org/simple/tomli/tomli-2.4.0-py3-none-any.whl", hashes = {sha256 = "1f776e7d669ebceb01dee46484485f43a4048746235e683bcdffacdf1fb4785a"}}, + {name = "tomli-2.4.0-py3-none-any.whl", url = "https://pypi.org/simple/tomli/tomli-2.4.0-py3-none-any.whl", hashes = {sha256 = "1fb2945cbe303b1419e2706e711b7113da57b7db31ee378d08712d678a34e51e"}}, + {name = "tomli-2.4.0-py3-none-any.whl", url = "https://pypi.org/simple/tomli/tomli-2.4.0-py3-none-any.whl", hashes = {sha256 = "20cedb4ee43278bc4f2fee6cb50daec836959aadaf948db5172e776dd3d993fc"}}, + {name = "tomli-2.4.0-py3-none-any.whl", url = "https://pypi.org/simple/tomli/tomli-2.4.0-py3-none-any.whl", hashes = {sha256 = "20ffd184fb1df76a66e34bd1b36b4a4641bd2b82954befa32fe8163e79f1a702"}}, + {name = "tomli-2.4.0-py3-none-any.whl", url = "https://pypi.org/simple/tomli/tomli-2.4.0-py3-none-any.whl", hashes = {sha256 = "26ab906a1eb794cd4e103691daa23d95c6919cc2fa9160000ac02370cc9dd3f6"}}, + {name = "tomli-2.4.0-py3-none-any.whl", url = "https://pypi.org/simple/tomli/tomli-2.4.0-py3-none-any.whl", hashes = {sha256 = "2add28aacc7425117ff6364fe9e06a183bb0251b03f986df0e78e974047571fd"}}, + {name = "tomli-2.4.0-py3-none-any.whl", url = "https://pypi.org/simple/tomli/tomli-2.4.0-py3-none-any.whl", hashes = {sha256 = "2b1e3b80e1d5e52e40e9b924ec43d81570f0e7d09d11081b797bc4692765a3d4"}}, + {name = "tomli-2.4.0-py3-none-any.whl", url = "https://pypi.org/simple/tomli/tomli-2.4.0-py3-none-any.whl", hashes = {sha256 = "31d556d079d72db7c584c0627ff3a24c5d3fb4f730221d3444f3efb1b2514776"}}, + {name = "tomli-2.4.0-py3-none-any.whl", url = "https://pypi.org/simple/tomli/tomli-2.4.0-py3-none-any.whl", hashes = {sha256 = "36b9d05b51e65b254ea6c2585b59d2c4cb91c8a3d91d0ed0f17591a29aaea54a"}}, + {name = "tomli-2.4.0-py3-none-any.whl", url = "https://pypi.org/simple/tomli/tomli-2.4.0-py3-none-any.whl", hashes = {sha256 = "39b0b5d1b6dd03684b3fb276407ebed7090bbec989fa55838c98560c01113b66"}}, + {name = "tomli-2.4.0-py3-none-any.whl", url = "https://pypi.org/simple/tomli/tomli-2.4.0-py3-none-any.whl", hashes = {sha256 = "3cf226acb51d8f1c394c1b310e0e0e61fecdd7adcb78d01e294ac297dd2e7f87"}}, + {name = "tomli-2.4.0-py3-none-any.whl", url = "https://pypi.org/simple/tomli/tomli-2.4.0-py3-none-any.whl", hashes = {sha256 = "3d895d56bd3f82ddd6faaff993c275efc2ff38e52322ea264122d72729dca2b2"}}, + {name = "tomli-2.4.0-py3-none-any.whl", url = "https://pypi.org/simple/tomli/tomli-2.4.0-py3-none-any.whl", hashes = {sha256 = "413540dce94673591859c4c6f794dfeaa845e98bf35d72ed59636f869ef9f86f"}}, + {name = "tomli-2.4.0-py3-none-any.whl", url = "https://pypi.org/simple/tomli/tomli-2.4.0-py3-none-any.whl", hashes = {sha256 = "43e685b9b2341681907759cf3a04e14d7104b3580f808cfde1dfdb60ada85475"}}, + {name = "tomli-2.4.0-py3-none-any.whl", url = "https://pypi.org/simple/tomli/tomli-2.4.0-py3-none-any.whl", hashes = {sha256 = "4cbcb367d44a1f0c2be408758b43e1ffb5308abe0ea222897d6bfc8e8281ef2f"}}, + {name = "tomli-2.4.0-py3-none-any.whl", url = "https://pypi.org/simple/tomli/tomli-2.4.0-py3-none-any.whl", hashes = {sha256 = "551e321c6ba03b55676970b47cb1b73f14a0a4dce6a3e1a9458fd6d921d72e95"}}, + {name = "tomli-2.4.0-py3-none-any.whl", url = "https://pypi.org/simple/tomli/tomli-2.4.0-py3-none-any.whl", hashes = {sha256 = "5572e41282d5268eb09a697c89a7bee84fae66511f87533a6f88bd2f7b652da9"}}, + {name = "tomli-2.4.0-py3-none-any.whl", url = "https://pypi.org/simple/tomli/tomli-2.4.0-py3-none-any.whl", hashes = {sha256 = "5aa48d7c2356055feef06a43611fc401a07337d5b006be13a30f6c58f869e3c3"}}, + {name = "tomli-2.4.0-py3-none-any.whl", url = "https://pypi.org/simple/tomli/tomli-2.4.0-py3-none-any.whl", hashes = {sha256 = "5b5807f3999fb66776dbce568cc9a828544244a8eb84b84b9bafc080c99597b9"}}, + {name = "tomli-2.4.0-py3-none-any.whl", url = "https://pypi.org/simple/tomli/tomli-2.4.0-py3-none-any.whl", hashes = {sha256 = "5e3f639a7a8f10069d0e15408c0b96a2a828cfdec6fca05296ebcdcc28ca7c76"}}, + {name = "tomli-2.4.0-py3-none-any.whl", url = "https://pypi.org/simple/tomli/tomli-2.4.0-py3-none-any.whl", hashes = {sha256 = "685306e2cc7da35be4ee914fd34ab801a6acacb061b6a7abca922aaf9ad368da"}}, + {name = "tomli-2.4.0-py3-none-any.whl", url = "https://pypi.org/simple/tomli/tomli-2.4.0-py3-none-any.whl", hashes = {sha256 = "75c2f8bbddf170e8effc98f5e9084a8751f8174ea6ccf4fca5398436e0320bc8"}}, + {name = "tomli-2.4.0-py3-none-any.whl", url = "https://pypi.org/simple/tomli/tomli-2.4.0-py3-none-any.whl", hashes = {sha256 = "7b438885858efd5be02a9a133caf5812b8776ee0c969fea02c45e8e3f296ba51"}}, + {name = "tomli-2.4.0-py3-none-any.whl", url = "https://pypi.org/simple/tomli/tomli-2.4.0-py3-none-any.whl", hashes = {sha256 = "7d49c66a7d5e56ac959cb6fc583aff0651094ec071ba9ad43df785abc2320d86"}}, + {name = "tomli-2.4.0-py3-none-any.whl", url = "https://pypi.org/simple/tomli/tomli-2.4.0-py3-none-any.whl", hashes = {sha256 = "7d6d9a4aee98fac3eab4952ad1d73aee87359452d1c086b5ceb43ed02ddb16b8"}}, + {name = "tomli-2.4.0-py3-none-any.whl", url = "https://pypi.org/simple/tomli/tomli-2.4.0-py3-none-any.whl", hashes = {sha256 = "84d081fbc252d1b6a982e1870660e7330fb8f90f676f6e78b052ad4e64714bf0"}}, + {name = "tomli-2.4.0-py3-none-any.whl", url = "https://pypi.org/simple/tomli/tomli-2.4.0-py3-none-any.whl", hashes = {sha256 = "8768715ffc41f0008abe25d808c20c3d990f42b6e2e58305d5da280ae7d1fa3b"}}, + {name = "tomli-2.4.0-py3-none-any.whl", url = "https://pypi.org/simple/tomli/tomli-2.4.0-py3-none-any.whl", hashes = {sha256 = "920b1de295e72887bafa3ad9f7a792f811847d57ea6b1215154030cf131f16b1"}}, + {name = "tomli-2.4.0-py3-none-any.whl", url = "https://pypi.org/simple/tomli/tomli-2.4.0-py3-none-any.whl", hashes = {sha256 = "9a08144fa4cba33db5255f9b74f0b89888622109bd2776148f2597447f92a94e"}}, + {name = "tomli-2.4.0-py3-none-any.whl", url = "https://pypi.org/simple/tomli/tomli-2.4.0-py3-none-any.whl", hashes = {sha256 = "a26d7ff68dfdb9f87a016ecfd1e1c2bacbe3108f4e0f8bcd2228ef9a766c787d"}}, + {name = "tomli-2.4.0-py3-none-any.whl", url = "https://pypi.org/simple/tomli/tomli-2.4.0-py3-none-any.whl", hashes = {sha256 = "aa89c3f6c277dd275d8e243ad24f3b5e701491a860d5121f2cdd399fbb31fc9c"}}, + {name = "tomli-2.4.0-py3-none-any.whl", url = "https://pypi.org/simple/tomli/tomli-2.4.0-py3-none-any.whl", hashes = {sha256 = "b5ef256a3fd497d4973c11bf142e9ed78b150d36f5773f1ca6088c230ffc5867"}}, + {name = "tomli-2.4.0-py3-none-any.whl", url = "https://pypi.org/simple/tomli/tomli-2.4.0-py3-none-any.whl", hashes = {sha256 = "b6c78bdf37764092d369722d9946cb65b8767bfa4110f902a1b2542d8d173c8a"}}, + {name = "tomli-2.4.0-py3-none-any.whl", url = "https://pypi.org/simple/tomli/tomli-2.4.0-py3-none-any.whl", hashes = {sha256 = "bbb1b10aa643d973366dc2cb1ad94f99c1726a02343d43cbc011edbfac579e7c"}}, + {name = "tomli-2.4.0-py3-none-any.whl", url = "https://pypi.org/simple/tomli/tomli-2.4.0-py3-none-any.whl", hashes = {sha256 = "c084ad935abe686bd9c898e62a02a19abfc9760b5a79bc29644463eaf2840cb0"}}, + {name = "tomli-2.4.0-py3-none-any.whl", url = "https://pypi.org/simple/tomli/tomli-2.4.0-py3-none-any.whl", hashes = {sha256 = "c73add4bb52a206fd0c0723432db123c0c75c280cbd67174dd9d2db228ebb1b4"}}, + {name = "tomli-2.4.0-py3-none-any.whl", url = "https://pypi.org/simple/tomli/tomli-2.4.0-py3-none-any.whl", hashes = {sha256 = "cae9c19ed12d4e8f3ebf46d1a75090e4c0dc16271c5bce1c833ac168f08fb614"}}, + {name = "tomli-2.4.0-py3-none-any.whl", url = "https://pypi.org/simple/tomli/tomli-2.4.0-py3-none-any.whl", hashes = {sha256 = "d20b797a5c1ad80c516e41bc1fb0443ddb5006e9aaa7bda2d71978346aeb9132"}}, + {name = "tomli-2.4.0-py3-none-any.whl", url = "https://pypi.org/simple/tomli/tomli-2.4.0-py3-none-any.whl", hashes = {sha256 = "d3d1654e11d724760cdb37a3d7691f0be9db5fbdaef59c9f532aabf87006dbaa"}}, + {name = "tomli-2.4.0-py3-none-any.whl", url = "https://pypi.org/simple/tomli/tomli-2.4.0-py3-none-any.whl", hashes = {sha256 = "d878f2a6707cc9d53a1be1414bbb419e629c3d6e67f69230217bb663e76b5087"}}, +] + + +[[packages]] +name = "towncrier" +version = "25.8.0" +marker = "('dev' in dependency_groups) and (python_version >= '3.9')" +index = "https://pypi.org/simple/" +wheels = [ + {name = "towncrier-25.8.0-py3-none-any.whl", url = "https://pypi.org/simple/towncrier/towncrier-25.8.0-py3-none-any.whl", hashes = {sha256 = "b953d133d98f9aeae9084b56a3563fd2519dfc6ec33f61c9cd2c61ff243fb513"}}, + {name = "towncrier-25.8.0-py3-none-any.whl", url = "https://pypi.org/simple/towncrier/towncrier-25.8.0-py3-none-any.whl", hashes = {sha256 = "eef16d29f831ad57abb3ae32a0565739866219f1ebfbdd297d32894eb9940eb1"}}, +] + + +[[packages]] +name = "twine" +version = "6.2.0" +marker = "('dev' in dependency_groups) and (python_version >= '3.9')" +index = "https://pypi.org/simple/" +wheels = [ + {name = "twine-6.2.0-py3-none-any.whl", url = "https://pypi.org/simple/twine/twine-6.2.0-py3-none-any.whl", hashes = {sha256 = "418ebf08ccda9a8caaebe414433b0ba5e25eb5e4a927667122fbe8f829f985d8"}}, + {name = "twine-6.2.0-py3-none-any.whl", url = "https://pypi.org/simple/twine/twine-6.2.0-py3-none-any.whl", hashes = {sha256 = "e5ed0d2fd70c9959770dce51c8f39c8945c574e18173a7b81802dab51b4b75cf"}}, +] + + +[[packages]] +name = "typing-extensions" +version = "4.15.0" +marker = "('dev' in dependency_groups) and (python_version >= '3.9')" +index = "https://pypi.org/simple/" +wheels = [ + {name = "typing_extensions-4.15.0-py3-none-any.whl", url = "https://pypi.org/simple/typing-extensions/typing_extensions-4.15.0-py3-none-any.whl", hashes = {sha256 = "0cea48d173cc12fa28ecabc3b837ea3cf6f38c6d1136f85cbaaf598984861466"}}, + {name = "typing_extensions-4.15.0-py3-none-any.whl", url = "https://pypi.org/simple/typing-extensions/typing_extensions-4.15.0-py3-none-any.whl", hashes = {sha256 = "f0fa19c6845758ab08074a0cfa8b7aecb71c999ca73d62883bc25cc018c4e548"}}, +] + + +[[packages]] +name = "uc-micro-py" +version = "2.0.0" +marker = "('dev' in dependency_groups) and (python_version >= '3.10')" +index = "https://pypi.org/simple/" +wheels = [ + {name = "uc_micro_py-2.0.0-py3-none-any.whl", url = "https://pypi.org/simple/uc-micro-py/uc_micro_py-2.0.0-py3-none-any.whl", hashes = {sha256 = "3603a3859af53e5a39bc7677713c78ea6589ff188d70f4fee165db88e22b242c"}}, + {name = "uc_micro_py-2.0.0-py3-none-any.whl", url = "https://pypi.org/simple/uc-micro-py/uc_micro_py-2.0.0-py3-none-any.whl", hashes = {sha256 = "c53691e495c8db60e16ffc4861a35469b0ba0821fe409a8a7a0a71864d33a811"}}, +] + + +[[packages]] +name = "urllib3" +version = "2.6.3" +marker = "('dev' in dependency_groups) and (python_version >= '3.9')" +index = "https://pypi.org/simple/" +wheels = [ + {name = "urllib3-2.6.3-py3-none-any.whl", url = "https://pypi.org/simple/urllib3/urllib3-2.6.3-py3-none-any.whl", hashes = {sha256 = "1b62b6884944a57dbe321509ab94fd4d3b307075e0c2eae991ac71ee15ad38ed"}}, + {name = "urllib3-2.6.3-py3-none-any.whl", url = "https://pypi.org/simple/urllib3/urllib3-2.6.3-py3-none-any.whl", hashes = {sha256 = "bf272323e553dfb2e87d9bfd225ca7b0f467b919d7bbd355436d3fd37cb0acd4"}}, +] + + +[[packages]] +name = "virtualenv" +version = "21.2.0" +marker = "('dev' in dependency_groups) and (python_version >= '3.8')" +index = "https://pypi.org/simple/" +wheels = [ + {name = "virtualenv-21.2.0-py3-none-any.whl", url = "https://pypi.org/simple/virtualenv/virtualenv-21.2.0-py3-none-any.whl", hashes = {sha256 = "1720dc3a62ef5b443092e3f499228599045d7fea4c79199770499df8becf9098"}}, + {name = "virtualenv-21.2.0-py3-none-any.whl", url = "https://pypi.org/simple/virtualenv/virtualenv-21.2.0-py3-none-any.whl", hashes = {sha256 = "1bd755b504931164a5a496d217c014d098426cddc79363ad66ac78125f9d908f"}}, +] + + +[[packages]] +name = "waitress" +version = "3.0.2" +marker = "('dev' in dependency_groups) and (sys_platform == 'win32')" +index = "https://pypi.org/simple/" +wheels = [ + {name = "waitress-3.0.2-py3-none-any.whl", url = "https://pypi.org/simple/waitress/waitress-3.0.2-py3-none-any.whl", hashes = {sha256 = "682aaaf2af0c44ada4abfb70ded36393f0e307f4ab9456a215ce0020baefc31f"}}, + {name = "waitress-3.0.2-py3-none-any.whl", url = "https://pypi.org/simple/waitress/waitress-3.0.2-py3-none-any.whl", hashes = {sha256 = "c56d67fd6e87c2ee598b76abdd4e96cfad1f24cacdea5078d382b1f9d7b5ed2e"}}, +] + + +[[packages]] +name = "zipp" +version = "3.21.0" +marker = "('dev' in dependency_groups) and (python_version >= '3.9')" +index = "https://pypi.org/simple/" +wheels = [ + {name = "zipp-3.21.0-py3-none-any.whl", url = "https://pypi.org/simple/zipp/zipp-3.21.0-py3-none-any.whl", hashes = {sha256 = "2c9958f6430a2040341a52eb608ed6dd93ef4392e02ffe219417c1b28b5dd1f4"}}, + {name = "zipp-3.21.0-py3-none-any.whl", url = "https://pypi.org/simple/zipp/zipp-3.21.0-py3-none-any.whl", hashes = {sha256 = "ac1bbe05fd2991f160ebce24ffbac5f6d11d83dc90891255885223d42b3cd931"}}, +] + +[tool.pipenv] +generated_from = "Pipfile.lock" +generation_date = "2026-03-30T07:47:16.675639+00:00" diff --git a/tests/fixtures/real-world-locks/pylock/pipenv-2026.8.0/pyproject.toml b/tests/fixtures/real-world-locks/pylock/pipenv-2026.8.0/pyproject.toml new file mode 100644 index 00000000..21920029 --- /dev/null +++ b/tests/fixtures/real-world-locks/pylock/pipenv-2026.8.0/pyproject.toml @@ -0,0 +1,338 @@ +[build-system] +build-backend = "setuptools.build_meta" +requires = [ + "setuptools>=67", +] + +[project] +name = "pipenv" +description = "Python Development Workflow for Humans." +readme = "README.md" +license = { text = "MIT License (MIT)" } +authors = [ + { name = "Pipenv maintainer team", email = "matteius@gmail.com" }, +] +requires-python = ">=3.10" +classifiers = [ + "License :: OSI Approved :: MIT License", + "Programming Language :: Python", + "Programming Language :: Python :: 3 :: Only", + "Programming Language :: Python :: 3.10", + "Programming Language :: Python :: 3.11", + "Programming Language :: Python :: 3.12", + "Programming Language :: Python :: 3.13", + "Programming Language :: Python :: 3.14", + "Programming Language :: Python :: Implementation :: CPython", + "Programming Language :: Python :: Implementation :: PyPy", +] +dynamic = [ + "version", +] +dependencies = [ + "certifi", + "packaging>=22", + "setuptools>=67", + "virtualenv>=20.26.6", +] +optional-dependencies.completion = [ + "argcomplete>=3.1", +] +optional-dependencies.dev = [ + "beautifulsoup4", + "black==26.3.1", + "flake8<4,>=3.3", + "invoke", + "parver", + "sphinx", + "towncrier", +] +optional-dependencies.safety = [ + "safety>=3.0.0", +] +optional-dependencies.tests = [ + "mock", + "pytest>=5", + "pytest-rerunfailures", + "pytest-timeout", + "pytest-xdist", +] +urls.Documentation = "https://pipenv.pypa.io/en/latest/" +urls.Homepage = "https://github.com/pypa/pipenv" +urls.Source = "https://github.com/pypa/pipenv.git" +scripts.pipenv = "pipenv:cli" +scripts.pipenv-resolver = "pipenv.resolver.main:main" + +[tool.setuptools.packages.find] +where = [ "." ] +exclude = [ "tests*", "tests.*", "tasks*", "tasks.*", "docs*", "docs.*" ] + +[tool.setuptools.package-data] +"*" = [ "LICENSE", "NOTICES" ] +"pipenv.patched.pip._vendor.certifi" = [ "*.pem" ] +"pipenv.patched.pip._vendor.requests" = [ "*.pem" ] +"pipenv.patched.pip._vendor.distlib" = [ + "t32.exe", + "t64.exe", + "t64-arm.exe", + "w32.exe", + "w64.exe", + "w64-arm.exe", +] +"pipenv.vendor.ruamel" = [ "yaml" ] + +[tool.setuptools.dynamic] +version = { attr = "pipenv.__version__" } + +## TESTING AND DEVELOPER CONFIGURATION BELOW ## + +[tool.black] +line-length = 90 +include = '\.pyi?$' +exclude = ''' +/( + \.eggs + | \.git + | \.github + | \.hg + | \.mypy_cache + | \.tox + | \.pyre_configuration + | \.venv + | _build + | buck-out + | build + | dist + | pipenv/vendor + | pipenv/patched + | tests/pypi + | tests/test_artifacts + | get-pipenv.py + | pyproject.toml +) +''' + +[tool.ruff] +target-version = "py37" + +line-length = 137 +exclude = [ + "get-pipenv.py", + "pipenv/patched/*", + "pipenv/vendor/*", + "tests/fixtures/*", + "tests/pypi/*", + "tests/test_artifacts/*", +] + +lint.select = [ + "ASYNC", + "B", + "C4", + "C90", + "E", + "F", + "FLY", + "G", + "I", + "ISC", + "PERF", + "PIE", + "PL", + "TID", + "UP", + "W", + "YTT", +] +lint.ignore = [ + "B904", # `raise` without `from` inside `except` + "PIE790", # Unnecessary `pass` statement + "PLC0415", # `import` should be placed at module level + "PLW2901", # `for` loop variable overwritten + "TID252", # Relative imports +] +lint.per-file-ignores = { "pipenv/cli/command.py" = [ + "F811", +], "pipenv/__init__.py" = [ + "E402", + "E501", +], "pipenv/utils/shell.py" = [ + "E402", +], "pipenv/utils/internet.py" = [ + "E401", +], "pipenv/utils/dependencies.py" = [ + "TID252", +], "pipenv/vendor/requirementslib/models/requirements.py" = [ + "PLW0603", +], "pipenv/vendor/requirementslib/models/utils.py" = [ + "B018", +], "pipenv/project.py" = [ + "E501", + "F401", + "I", + "PLC1901", + "S101", +], "pipenv/cli/options.py" = [ + "B003", + "PIE800", + "PLW0603", +], "pipenv/utils/processes.py" = [ + "E741", +], "pipenv/vendor/vistir/misc.py" = [ + "E741", +], "pipenv/vendor/pythonfinder/models/python.py" = [ + "B015", +] } +lint.mccabe.max-complexity = 44 +lint.pylint.allow-magic-value-types = [ "int", "str" ] +lint.pylint.max-args = 18 +lint.pylint.max-branches = 34 +lint.pylint.max-returns = 38 +lint.pylint.max-statements = 155 + +[tool.pyproject-fmt] +# after how many column width split arrays/dicts into multiple lines, 1 will force always +column_width = 120 +# how many spaces use for indentation +indent = 2 +# if false will remove unnecessary trailing ``.0``'s from version specifiers +keep_full_version = true +# maximum Python version to use when generating version specifiers +max_supported_python = "3.14" + +[tool.pytest.ini_options] +addopts = "-ra --no-cov" +plugins = "xdist" +testpaths = [ "tests" ] +# Add vendor and patched in addition to the default list of ignored dirs +# Additionally, ignore tasks, news, test subdirectories +norecursedirs = [ + ".*", + "build", + "dist", + "CVS", + "_darcs", + "{arch}", + "*.egg", + "vendor", + "patched", + "news", + "tasks", + "docs", + "tests/test_artifacts", + "tests/pypi", +] +filterwarnings = [ ] +# These are not all the custom markers, but most of the ones with repeat uses +# `pipenv run pytest --markers` will list all markers including these +markers = [ + "flaky: flaky tests that may need reruns (pytest-rerunfailures)", + "install: tests having to do with `pipenv install`", + "update: tests having to do with `pipenv update`", + "needs_internet: integration tests that require internet to pass", + "basic: basic pipenv tests grouping", + "dev: tests having to do with dev and dev packages", + "system: related or interacting with the os", + "utils: grouping of pipenv utility functions", + "cli: test grouping that relate to command line like `pipenv --flag args`", + "requirements: tests that save and alter pip requirements", + "run: tests that run or execute python through pipenv", + "script: grouping of tests that execute scripts", + "keep_outdated: when an activity is supposed to keep something out of date", + "lock: tests that interact with pipenv lock", + "markers: pipenv environment markers", + "vcs: tests integration with pipenv and vertsion control systems", + "project: tests with the project object", + "sync: related to `pipenv sync`", + "rrule: relating to rrules (as in recurring time)", + "tzoffset: timezone offset", + "gettz: tests with gettz (get timezone) from dateutil lib", + "tzstr: timezone string", + "extras", + "extended", + "ext: extra non-categorized tests", +] + +[tool.coverage.run] +parallel = true +# Initiative G phase 1 (T17): the resolver-module coverage gate +# narrows the scope to the new pure-Python simple-API surface. +# The default ``addopts`` above keeps the regular test workflow +# coverage-free (it's slow on the full suite); coverage runs via +# the dedicated CI step that overrides ``addopts`` to drop +# ``--no-cov`` and adds ``--cov=pipenv.resolver +# --cov-fail-under=90``. +source = [ "pipenv/resolver" ] +# Branch coverage is roughly 2x slower than line coverage; T17 +# ships line coverage only for Phase 1. Revisit in Phase 2 / 3 +# when the resolver-module suite grows. +branch = false + +[tool.coverage.report] +# T17: enforce the per-module coverage floors declared in T11-T16 +# (Candidate 99%, parsers 95%, client 90%, manifest cache 95%, +# parallel fetcher 90%, auth helpers 90%). 90% is the +# *aggregate* floor across the resolver-module surface; per-module +# regressions are caught by ``--cov-fail-under`` overrides in +# targeted tests. +fail_under = 90 +show_missing = true +exclude_lines = [ + "pragma: no cover", + "if TYPE_CHECKING:", + "raise NotImplementedError", +] + +[tool.towncrier] +package = "pipenv" +filename = "CHANGELOG.md" +issue_format = "`#{issue} `_" +directory = "news/" +title_format = "{version} ({project_date})" +template = "news/towncrier_template.rst" + +[[tool.towncrier.type]] +directory = "feature" +name = "Features & Improvements" +showcontent = true + +[[tool.towncrier.type]] +directory = "behavior" +name = "Behavior Changes" +showcontent = true + +[[tool.towncrier.type]] +directory = "bugfix" +name = "Bug Fixes" +showcontent = true + +[[tool.towncrier.type]] +directory = "vendor" +name = "Vendored Libraries" +showcontent = true + +[[tool.towncrier.type]] +directory = "doc" +name = "Improved Documentation" +showcontent = true + +[[tool.towncrier.type]] +directory = "trivial" +name = "Trivial Changes" +showcontent = false + +[[tool.towncrier.type]] +directory = "removal" +name = "Removals and Deprecations" +showcontent = true + +[[tool.towncrier.type]] +directory = "process" +name = "Relates to dev process changes" +showcontent = true + +[tool.mypy] +ignore_missing_imports = true +follow_imports = "skip" +html_report = "mypyhtml" +python_version = "3.7" +mypy_path = "typeshed/pyi:typeshed/imports" diff --git a/tests/fixtures/real-world-locks/pylock/snowflake-cli-3.26.0/LICENSE b/tests/fixtures/real-world-locks/pylock/snowflake-cli-3.26.0/LICENSE new file mode 100644 index 00000000..48773d3d --- /dev/null +++ b/tests/fixtures/real-world-locks/pylock/snowflake-cli-3.26.0/LICENSE @@ -0,0 +1,201 @@ + Apache License + Version 2.0, January 2004 + http://www.apache.org/licenses/ + + TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION + + 1. Definitions. + + "License" shall mean the terms and conditions for use, reproduction, + and distribution as defined by Sections 1 through 9 of this document. + + "Licensor" shall mean the copyright owner or entity authorized by + the copyright owner that is granting the License. + + "Legal Entity" shall mean the union of the acting entity and all + other entities that control, are controlled by, or are under common + control with that entity. For the purposes of this definition, + "control" means (i) the power, direct or indirect, to cause the + direction or management of such entity, whether by contract or + otherwise, or (ii) ownership of fifty percent (50%) or more of the + outstanding shares, or (iii) beneficial ownership of such entity. + + "You" (or "Your") shall mean an individual or Legal Entity + exercising permissions granted by this License. + + "Source" form shall mean the preferred form for making modifications, + including but not limited to software source code, documentation + source, and configuration files. + + "Object" form shall mean any form resulting from mechanical + transformation or translation of a Source form, including but + not limited to compiled object code, generated documentation, + and conversions to other media types. + + "Work" shall mean the work of authorship, whether in Source or + Object form, made available under the License, as indicated by a + copyright notice that is included in or attached to the work + (an example is provided in the Appendix below). + + "Derivative Works" shall mean any work, whether in Source or Object + form, that is based on (or derived from) the Work and for which the + editorial revisions, annotations, elaborations, or other modifications + represent, as a whole, an original work of authorship. For the purposes + of this License, Derivative Works shall not include works that remain + separable from, or merely link (or bind by name) to the interfaces of, + the Work and Derivative Works thereof. + + "Contribution" shall mean any work of authorship, including + the original version of the Work and any modifications or additions + to that Work or Derivative Works thereof, that is intentionally + submitted to Licensor for inclusion in the Work by the copyright owner + or by an individual or Legal Entity authorized to submit on behalf of + the copyright owner. For the purposes of this definition, "submitted" + means any form of electronic, verbal, or written communication sent + to the Licensor or its representatives, including but not limited to + communication on electronic mailing lists, source code control systems, + and issue tracking systems that are managed by, or on behalf of, the + Licensor for the purpose of discussing and improving the Work, but + excluding communication that is conspicuously marked or otherwise + designated in writing by the copyright owner as "Not a Contribution." + + "Contributor" shall mean Licensor and any individual or Legal Entity + on behalf of whom a Contribution has been received by Licensor and + subsequently incorporated within the Work. + + 2. Grant of Copyright License. Subject to the terms and conditions of + this License, each Contributor hereby grants to You a perpetual, + worldwide, non-exclusive, no-charge, royalty-free, irrevocable + copyright license to reproduce, prepare Derivative Works of, + publicly display, publicly perform, sublicense, and distribute the + Work and such Derivative Works in Source or Object form. + + 3. Grant of Patent License. Subject to the terms and conditions of + this License, each Contributor hereby grants to You a perpetual, + worldwide, non-exclusive, no-charge, royalty-free, irrevocable + (except as stated in this section) patent license to make, have made, + use, offer to sell, sell, import, and otherwise transfer the Work, + where such license applies only to those patent claims licensable + by such Contributor that are necessarily infringed by their + Contribution(s) alone or by combination of their Contribution(s) + with the Work to which such Contribution(s) was submitted. If You + institute patent litigation against any entity (including a + cross-claim or counterclaim in a lawsuit) alleging that the Work + or a Contribution incorporated within the Work constitutes direct + or contributory patent infringement, then any patent licenses + granted to You under this License for that Work shall terminate + as of the date such litigation is filed. + + 4. Redistribution. You may reproduce and distribute copies of the + Work or Derivative Works thereof in any medium, with or without + modifications, and in Source or Object form, provided that You + meet the following conditions: + + (a) You must give any other recipients of the Work or + Derivative Works a copy of this License; and + + (b) You must cause any modified files to carry prominent notices + stating that You changed the files; and + + (c) You must retain, in the Source form of any Derivative Works + that You distribute, all copyright, patent, trademark, and + attribution notices from the Source form of the Work, + excluding those notices that do not pertain to any part of + the Derivative Works; and + + (d) If the Work includes a "NOTICE" text file as part of its + distribution, then any Derivative Works that You distribute must + include a readable copy of the attribution notices contained + within such NOTICE file, excluding those notices that do not + pertain to any part of the Derivative Works, in at least one + of the following places: within a NOTICE text file distributed + as part of the Derivative Works; within the Source form or + documentation, if provided along with the Derivative Works; or, + within a display generated by the Derivative Works, if and + wherever such third-party notices normally appear. The contents + of the NOTICE file are for informational purposes only and + do not modify the License. You may add Your own attribution + notices within Derivative Works that You distribute, alongside + or as an addendum to the NOTICE text from the Work, provided + that such additional attribution notices cannot be construed + as modifying the License. + + You may add Your own copyright statement to Your modifications and + may provide additional or different license terms and conditions + for use, reproduction, or distribution of Your modifications, or + for any such Derivative Works as a whole, provided Your use, + reproduction, and distribution of the Work otherwise complies with + the conditions stated in this License. + + 5. Submission of Contributions. Unless You explicitly state otherwise, + any Contribution intentionally submitted for inclusion in the Work + by You to the Licensor shall be under the terms and conditions of + this License, without any additional terms or conditions. + Notwithstanding the above, nothing herein shall supersede or modify + the terms of any separate license agreement you may have executed + with Licensor regarding such Contributions. + + 6. Trademarks. This License does not grant permission to use the trade + names, trademarks, service marks, or product names of the Licensor, + except as required for reasonable and customary use in describing the + origin of the Work and reproducing the content of the NOTICE file. + + 7. Disclaimer of Warranty. Unless required by applicable law or + agreed to in writing, Licensor provides the Work (and each + Contributor provides its Contributions) on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or + implied, including, without limitation, any warranties or conditions + of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A + PARTICULAR PURPOSE. You are solely responsible for determining the + appropriateness of using or redistributing the Work and assume any + risks associated with Your exercise of permissions under this License. + + 8. Limitation of Liability. In no event and under no legal theory, + whether in tort (including negligence), contract, or otherwise, + unless required by applicable law (such as deliberate and grossly + negligent acts) or agreed to in writing, shall any Contributor be + liable to You for damages, including any direct, indirect, special, + incidental, or consequential damages of any character arising as a + result of this License or out of the use or inability to use the + Work (including but not limited to damages for loss of goodwill, + work stoppage, computer failure or malfunction, or any and all + other commercial damages or losses), even if such Contributor + has been advised of the possibility of such damages. + + 9. Accepting Warranty or Additional Liability. While redistributing + the Work or Derivative Works thereof, You may choose to offer, + and charge a fee for, acceptance of support, warranty, indemnity, + or other liability obligations and/or rights consistent with this + License. However, in accepting such obligations, You may act only + on Your own behalf and on Your sole responsibility, not on behalf + of any other Contributor, and only if You agree to indemnify, + defend, and hold each Contributor harmless for any liability + incurred by, or claims asserted against, such Contributor by reason + of your accepting any such warranty or additional liability. + + END OF TERMS AND CONDITIONS + + APPENDIX: How to apply the Apache License to your work. + + To apply the Apache License to your work, attach the following + boilerplate notice, with the fields enclosed by brackets "[]" + replaced with your own identifying information. (Don't include + the brackets!) The text should be enclosed in the appropriate + comment syntax for the file format. We also recommend that a + file or class name and description of purpose be included on the + same "printed page" as the copyright notice for easier + identification within third-party archives. + + Copyright 2024 Snowflake Inc. + + Licensed under the Apache License, Version 2.0 (the "License"); + you may not use this file except in compliance with the License. + You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + + Unless required by applicable law or agreed to in writing, software + distributed under the License is distributed on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + See the License for the specific language governing permissions and + limitations under the License. diff --git a/tests/fixtures/real-world-locks/pylock/snowflake-cli-3.26.0/pylock.toml b/tests/fixtures/real-world-locks/pylock/snowflake-cli-3.26.0/pylock.toml new file mode 100644 index 00000000..eef50cc8 --- /dev/null +++ b/tests/fixtures/real-world-locks/pylock/snowflake-cli-3.26.0/pylock.toml @@ -0,0 +1,839 @@ +# This file was autogenerated by uv via the following command: +# uv pip compile pyproject.toml -o pylock.toml -p 3.10 --no-annotate --universal +lock-version = "1.0" +created-by = "uv" +requires-python = ">=3.10" + +[[packages]] +name = "annotated-types" +version = "0.7.0" +sdist = { url = "https://files.pythonhosted.org/packages/ee/67/531ea369ba64dcff5ec9c3402f9f51bf748cec26dde048a2f973a4eea7f5/annotated_types-0.7.0.tar.gz", upload-time = 2024-05-20T21:33:25Z, size = 16081, hashes = { sha256 = "aff07c09a53a08bc8cfccb9c85b05f1aa9a2a6f23728d790723543408344ce89" } } +wheels = [{ url = "https://files.pythonhosted.org/packages/78/b6/6307fbef88d9b5ee7421e68d78a9f162e0da4900bc5f5793f6d3d0e34fb8/annotated_types-0.7.0-py3-none-any.whl", upload-time = 2024-05-20T21:33:24Z, size = 13643, hashes = { sha256 = "1f02e8b43a8fbbc3f3e0d4f0f4bfc8131bcb4eebe8849b8e5c773f3a1c582a53" } }] + +[[packages]] +name = "asn1crypto" +version = "1.5.1" +sdist = { url = "https://files.pythonhosted.org/packages/de/cf/d547feed25b5244fcb9392e288ff9fdc3280b10260362fc45d37a798a6ee/asn1crypto-1.5.1.tar.gz", upload-time = 2022-03-15T14:46:52Z, size = 121080, hashes = { sha256 = "13ae38502be632115abf8a24cbe5f4da52e3b5231990aff31123c805306ccb9c" } } +wheels = [{ url = "https://files.pythonhosted.org/packages/c9/7f/09065fd9e27da0eda08b4d6897f1c13535066174cc023af248fc2a8d5e5a/asn1crypto-1.5.1-py2.py3-none-any.whl", upload-time = 2022-03-15T14:46:51Z, size = 105045, hashes = { sha256 = "db4e40728b728508912cbb3d44f19ce188f218e9eba635821bb4b68564f8fd67" } }] + +[[packages]] +name = "backports-tarfile" +version = "1.2.0" +marker = "python_full_version < '3.12'" +sdist = { url = "https://files.pythonhosted.org/packages/86/72/cd9b395f25e290e633655a100af28cb253e4393396264a98bd5f5951d50f/backports_tarfile-1.2.0.tar.gz", upload-time = 2024-05-28T17:01:54Z, size = 86406, hashes = { sha256 = "d75e02c268746e1b8144c278978b6e98e85de6ad16f8e4b0844a154557eca991" } } +wheels = [{ url = "https://files.pythonhosted.org/packages/b9/fa/123043af240e49752f1c4bd24da5053b6bd00cad78c2be53c0d1e8b975bc/backports.tarfile-1.2.0-py3-none-any.whl", upload-time = 2024-05-28T17:01:53Z, size = 30181, hashes = { sha256 = "77e284d754527b01fb1e6fa8a1afe577858ebe4e9dad8919e34c862cb399bc34" } }] + +[[packages]] +name = "boto3" +version = "1.39.14" +sdist = { url = "https://files.pythonhosted.org/packages/66/8f/acc7d434730e0c931ece4b46c983bf5afb7ae63abb545b535f0eda538476/boto3-1.39.14.tar.gz", upload-time = 2025-07-25T19:25:26Z, size = 111844, hashes = { sha256 = "fabb16360a93b449d5241006485bcc761c26694e75ac01009f4459f114acc06e" } } +wheels = [{ url = "https://files.pythonhosted.org/packages/93/9a/01ea17a58a27b7f4a7a6f6e2f4d4191b9e92362b77b6d58689f2d7eccb99/boto3-1.39.14-py3-none-any.whl", upload-time = 2025-07-25T19:25:25Z, size = 139898, hashes = { sha256 = "82c6868cad18c3bd4170915e9525f9af5f83e9779c528417f8863629558fc2d0" } }] + +[[packages]] +name = "botocore" +version = "1.39.14" +sdist = { url = "https://files.pythonhosted.org/packages/cc/ca/8994676a67f0a9d39a0844124f196c4dedc2fbca370c839f61246c1fea6d/botocore-1.39.14.tar.gz", upload-time = 2025-07-25T19:25:17Z, size = 14226110, hashes = { sha256 = "7fc44d4ad13b524e5d8a6296785776ef5898ac026ff74df9b35313831d507926" } } +wheels = [{ url = "https://files.pythonhosted.org/packages/08/c0/6b8200686c1f9ea44ab0daab0223b04799d60ccf882b9d7f770fbb40571e/botocore-1.39.14-py3-none-any.whl", upload-time = 2025-07-25T19:25:13Z, size = 13888318, hashes = { sha256 = "4ed551c77194167b7e8063f33059bc2f9b2ead0ed4ee33dc7857273648ed4349" } }] + +[[packages]] +name = "certifi" +version = "2025.7.14" +sdist = { url = "https://files.pythonhosted.org/packages/b3/76/52c535bcebe74590f296d6c77c86dabf761c41980e1347a2422e4aa2ae41/certifi-2025.7.14.tar.gz", upload-time = 2025-07-14T03:29:28Z, size = 163981, hashes = { sha256 = "8ea99dbdfaaf2ba2f9bac77b9249ef62ec5218e7c2b2e903378ed5fccf765995" } } +wheels = [{ url = "https://files.pythonhosted.org/packages/4f/52/34c6cf5bb9285074dc3531c437b3919e825d976fde097a7a73f79e726d03/certifi-2025.7.14-py3-none-any.whl", upload-time = 2025-07-14T03:29:26Z, size = 162722, hashes = { sha256 = "6b31f564a415d79ee77df69d757bb49a5bb53bd9f756cbbe24394ffd6fc1f4b2" } }] + +[[packages]] +name = "cffi" +version = "2.0.0" +marker = "python_full_version >= '3.9' and platform_python_implementation != 'PyPy'" +sdist = { url = "https://files.pythonhosted.org/packages/eb/56/b1ba7935a17738ae8453301356628e8147c79dbb825bcbc73dc7401f9846/cffi-2.0.0.tar.gz", upload-time = 2025-09-08T23:24:04Z, size = 523588, hashes = { sha256 = "44d1b5909021139fe36001ae048dbdde8214afa20200eda0f64c068cac5d5529" } } +wheels = [ + { url = "https://files.pythonhosted.org/packages/93/d7/516d984057745a6cd96575eea814fe1edd6646ee6efd552fb7b0921dec83/cffi-2.0.0-cp310-cp310-macosx_10_13_x86_64.whl", upload-time = 2025-09-08T23:22:08Z, size = 184283, hashes = { sha256 = "0cf2d91ecc3fcc0625c2c530fe004f82c110405f101548512cce44322fa8ac44" } }, + { url = "https://files.pythonhosted.org/packages/9e/84/ad6a0b408daa859246f57c03efd28e5dd1b33c21737c2db84cae8c237aa5/cffi-2.0.0-cp310-cp310-macosx_11_0_arm64.whl", upload-time = 2025-09-08T23:22:10Z, size = 180504, hashes = { sha256 = "f73b96c41e3b2adedc34a7356e64c8eb96e03a3782b535e043a986276ce12a49" } }, + { 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", upload-time = 2025-09-08T23:22:12Z, size = 208811, hashes = { sha256 = "53f77cbe57044e88bbd5ed26ac1d0514d2acf0591dd6bb02a3ae37f76811b80c" } }, + { url = "https://files.pythonhosted.org/packages/4f/27/6933a8b2562d7bd1fb595074cf99cc81fc3789f6a6c05cdabb46284a3188/cffi-2.0.0-cp310-cp310-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", upload-time = 2025-09-08T23:22:13Z, size = 216402, hashes = { sha256 = "3e837e369566884707ddaf85fc1744b47575005c0a229de3327f8f9a20f4efeb" } }, + { url = "https://files.pythonhosted.org/packages/05/eb/b86f2a2645b62adcfff53b0dd97e8dfafb5c8aa864bd0d9a2c2049a0d551/cffi-2.0.0-cp310-cp310-manylinux2014_ppc64le.manylinux_2_17_ppc64le.whl", upload-time = 2025-09-08T23:22:14Z, size = 203217, hashes = { sha256 = "5eda85d6d1879e692d546a078b44251cdd08dd1cfb98dfb77b670c97cee49ea0" } }, + { url = "https://files.pythonhosted.org/packages/9f/e0/6cbe77a53acf5acc7c08cc186c9928864bd7c005f9efd0d126884858a5fe/cffi-2.0.0-cp310-cp310-manylinux2014_s390x.manylinux_2_17_s390x.whl", upload-time = 2025-09-08T23:22:15Z, size = 203079, hashes = { sha256 = "9332088d75dc3241c702d852d4671613136d90fa6881da7d770a483fd05248b4" } }, + { url = "https://files.pythonhosted.org/packages/98/29/9b366e70e243eb3d14a5cb488dfd3a0b6b2f1fb001a203f653b93ccfac88/cffi-2.0.0-cp310-cp310-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", upload-time = 2025-09-08T23:22:17Z, size = 216475, hashes = { sha256 = "fc7de24befaeae77ba923797c7c87834c73648a05a4bde34b3b7e5588973a453" } }, + { url = "https://files.pythonhosted.org/packages/21/7a/13b24e70d2f90a322f2900c5d8e1f14fa7e2a6b3332b7309ba7b2ba51a5a/cffi-2.0.0-cp310-cp310-musllinux_1_2_aarch64.whl", upload-time = 2025-09-08T23:22:19Z, size = 218829, hashes = { sha256 = "cf364028c016c03078a23b503f02058f1814320a56ad535686f90565636a9495" } }, + { url = "https://files.pythonhosted.org/packages/60/99/c9dc110974c59cc981b1f5b66e1d8af8af764e00f0293266824d9c4254bc/cffi-2.0.0-cp310-cp310-musllinux_1_2_i686.whl", upload-time = 2025-09-08T23:22:20Z, size = 211211, hashes = { sha256 = "e11e82b744887154b182fd3e7e8512418446501191994dbf9c9fc1f32cc8efd5" } }, + { url = "https://files.pythonhosted.org/packages/49/72/ff2d12dbf21aca1b32a40ed792ee6b40f6dc3a9cf1644bd7ef6e95e0ac5e/cffi-2.0.0-cp310-cp310-musllinux_1_2_x86_64.whl", upload-time = 2025-09-08T23:22:22Z, size = 218036, hashes = { sha256 = "8ea985900c5c95ce9db1745f7933eeef5d314f0565b27625d9a10ec9881e1bfb" } }, + { url = "https://files.pythonhosted.org/packages/e2/cc/027d7fb82e58c48ea717149b03bcadcbdc293553edb283af792bd4bcbb3f/cffi-2.0.0-cp310-cp310-win32.whl", upload-time = 2025-09-08T23:22:23Z, size = 172184, hashes = { sha256 = "1f72fb8906754ac8a2cc3f9f5aaa298070652a0ffae577e0ea9bd480dc3c931a" } }, + { url = "https://files.pythonhosted.org/packages/33/fa/072dd15ae27fbb4e06b437eb6e944e75b068deb09e2a2826039e49ee2045/cffi-2.0.0-cp310-cp310-win_amd64.whl", upload-time = 2025-09-08T23:22:24Z, size = 182790, hashes = { sha256 = "b18a3ed7d5b3bd8d9ef7a8cb226502c6bf8308df1525e1cc676c3680e7176739" } }, + { url = "https://files.pythonhosted.org/packages/12/4a/3dfd5f7850cbf0d06dc84ba9aa00db766b52ca38d8b86e3a38314d52498c/cffi-2.0.0-cp311-cp311-macosx_10_13_x86_64.whl", upload-time = 2025-09-08T23:22:26Z, size = 184344, hashes = { sha256 = "b4c854ef3adc177950a8dfc81a86f5115d2abd545751a304c5bcf2c2c7283cfe" } }, + { url = "https://files.pythonhosted.org/packages/4f/8b/f0e4c441227ba756aafbe78f117485b25bb26b1c059d01f137fa6d14896b/cffi-2.0.0-cp311-cp311-macosx_11_0_arm64.whl", upload-time = 2025-09-08T23:22:28Z, size = 180560, hashes = { sha256 = "2de9a304e27f7596cd03d16f1b7c72219bd944e99cc52b84d0145aefb07cbd3c" } }, + { 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", upload-time = 2025-09-08T23:22:29Z, size = 209613, hashes = { sha256 = "baf5215e0ab74c16e2dd324e8ec067ef59e41125d3eade2b863d294fd5035c92" } }, + { url = "https://files.pythonhosted.org/packages/b8/56/6033f5e86e8cc9bb629f0077ba71679508bdf54a9a5e112a3c0b91870332/cffi-2.0.0-cp311-cp311-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", upload-time = 2025-09-08T23:22:31Z, size = 216476, hashes = { sha256 = "730cacb21e1bdff3ce90babf007d0a0917cc3e6492f336c2f0134101e0944f93" } }, + { url = "https://files.pythonhosted.org/packages/dc/7f/55fecd70f7ece178db2f26128ec41430d8720f2d12ca97bf8f0a628207d5/cffi-2.0.0-cp311-cp311-manylinux2014_ppc64le.manylinux_2_17_ppc64le.whl", upload-time = 2025-09-08T23:22:32Z, size = 203374, hashes = { sha256 = "6824f87845e3396029f3820c206e459ccc91760e8fa24422f8b0c3d1731cbec5" } }, + { url = "https://files.pythonhosted.org/packages/84/ef/a7b77c8bdc0f77adc3b46888f1ad54be8f3b7821697a7b89126e829e676a/cffi-2.0.0-cp311-cp311-manylinux2014_s390x.manylinux_2_17_s390x.whl", upload-time = 2025-09-08T23:22:34Z, size = 202597, hashes = { sha256 = "9de40a7b0323d889cf8d23d1ef214f565ab154443c42737dfe52ff82cf857664" } }, + { url = "https://files.pythonhosted.org/packages/d7/91/500d892b2bf36529a75b77958edfcd5ad8e2ce4064ce2ecfeab2125d72d1/cffi-2.0.0-cp311-cp311-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", upload-time = 2025-09-08T23:22:35Z, size = 215574, hashes = { sha256 = "8941aaadaf67246224cee8c3803777eed332a19d909b47e29c9842ef1e79ac26" } }, + { url = "https://files.pythonhosted.org/packages/44/64/58f6255b62b101093d5df22dcb752596066c7e89dd725e0afaed242a61be/cffi-2.0.0-cp311-cp311-musllinux_1_2_aarch64.whl", upload-time = 2025-09-08T23:22:36Z, size = 218971, hashes = { sha256 = "a05d0c237b3349096d3981b727493e22147f934b20f6f125a3eba8f994bec4a9" } }, + { url = "https://files.pythonhosted.org/packages/ab/49/fa72cebe2fd8a55fbe14956f9970fe8eb1ac59e5df042f603ef7c8ba0adc/cffi-2.0.0-cp311-cp311-musllinux_1_2_i686.whl", upload-time = 2025-09-08T23:22:38Z, size = 211972, hashes = { sha256 = "94698a9c5f91f9d138526b48fe26a199609544591f859c870d477351dc7b2414" } }, + { url = "https://files.pythonhosted.org/packages/0b/28/dd0967a76aab36731b6ebfe64dec4e981aff7e0608f60c2d46b46982607d/cffi-2.0.0-cp311-cp311-musllinux_1_2_x86_64.whl", upload-time = 2025-09-08T23:22:39Z, size = 217078, hashes = { sha256 = "5fed36fccc0612a53f1d4d9a816b50a36702c28a2aa880cb8a122b3466638743" } }, + { url = "https://files.pythonhosted.org/packages/2b/c0/015b25184413d7ab0a410775fdb4a50fca20f5589b5dab1dbbfa3baad8ce/cffi-2.0.0-cp311-cp311-win32.whl", upload-time = 2025-09-08T23:22:40Z, size = 172076, hashes = { sha256 = "c649e3a33450ec82378822b3dad03cc228b8f5963c0c12fc3b1e0ab940f768a5" } }, + { url = "https://files.pythonhosted.org/packages/ae/8f/dc5531155e7070361eb1b7e4c1a9d896d0cb21c49f807a6c03fd63fc877e/cffi-2.0.0-cp311-cp311-win_amd64.whl", upload-time = 2025-09-08T23:22:42Z, size = 182820, hashes = { sha256 = "66f011380d0e49ed280c789fbd08ff0d40968ee7b665575489afa95c98196ab5" } }, + { url = "https://files.pythonhosted.org/packages/95/5c/1b493356429f9aecfd56bc171285a4c4ac8697f76e9bbbbb105e537853a1/cffi-2.0.0-cp311-cp311-win_arm64.whl", upload-time = 2025-09-08T23:22:43Z, size = 177635, hashes = { sha256 = "c6638687455baf640e37344fe26d37c404db8b80d037c3d29f58fe8d1c3b194d" } }, + { url = "https://files.pythonhosted.org/packages/ea/47/4f61023ea636104d4f16ab488e268b93008c3d0bb76893b1b31db1f96802/cffi-2.0.0-cp312-cp312-macosx_10_13_x86_64.whl", upload-time = 2025-09-08T23:22:44Z, size = 185271, hashes = { sha256 = "6d02d6655b0e54f54c4ef0b94eb6be0607b70853c45ce98bd278dc7de718be5d" } }, + { url = "https://files.pythonhosted.org/packages/df/a2/781b623f57358e360d62cdd7a8c681f074a71d445418a776eef0aadb4ab4/cffi-2.0.0-cp312-cp312-macosx_11_0_arm64.whl", upload-time = 2025-09-08T23:22:45Z, size = 181048, hashes = { sha256 = "8eca2a813c1cb7ad4fb74d368c2ffbbb4789d377ee5bb8df98373c2cc0dee76c" } }, + { 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", upload-time = 2025-09-08T23:22:47Z, size = 212529, hashes = { sha256 = "21d1152871b019407d8ac3985f6775c079416c282e431a4da6afe7aefd2bccbe" } }, + { url = "https://files.pythonhosted.org/packages/d5/72/12b5f8d3865bf0f87cf1404d8c374e7487dcf097a1c91c436e72e6badd83/cffi-2.0.0-cp312-cp312-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", upload-time = 2025-09-08T23:22:48Z, size = 220097, hashes = { sha256 = "b21e08af67b8a103c71a250401c78d5e0893beff75e28c53c98f4de42f774062" } }, + { url = "https://files.pythonhosted.org/packages/c2/95/7a135d52a50dfa7c882ab0ac17e8dc11cec9d55d2c18dda414c051c5e69e/cffi-2.0.0-cp312-cp312-manylinux2014_ppc64le.manylinux_2_17_ppc64le.whl", upload-time = 2025-09-08T23:22:50Z, size = 207983, hashes = { sha256 = "1e3a615586f05fc4065a8b22b8152f0c1b00cdbc60596d187c2a74f9e3036e4e" } }, + { url = "https://files.pythonhosted.org/packages/3a/c8/15cb9ada8895957ea171c62dc78ff3e99159ee7adb13c0123c001a2546c1/cffi-2.0.0-cp312-cp312-manylinux2014_s390x.manylinux_2_17_s390x.whl", upload-time = 2025-09-08T23:22:51Z, size = 206519, hashes = { sha256 = "81afed14892743bbe14dacb9e36d9e0e504cd204e0b165062c488942b9718037" } }, + { url = "https://files.pythonhosted.org/packages/78/2d/7fa73dfa841b5ac06c7b8855cfc18622132e365f5b81d02230333ff26e9e/cffi-2.0.0-cp312-cp312-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", upload-time = 2025-09-08T23:22:52Z, size = 219572, hashes = { sha256 = "3e17ed538242334bf70832644a32a7aae3d83b57567f9fd60a26257e992b79ba" } }, + { url = "https://files.pythonhosted.org/packages/07/e0/267e57e387b4ca276b90f0434ff88b2c2241ad72b16d31836adddfd6031b/cffi-2.0.0-cp312-cp312-musllinux_1_2_aarch64.whl", upload-time = 2025-09-08T23:22:54Z, size = 222963, hashes = { sha256 = "3925dd22fa2b7699ed2617149842d2e6adde22b262fcbfada50e3d195e4b3a94" } }, + { url = "https://files.pythonhosted.org/packages/b6/75/1f2747525e06f53efbd878f4d03bac5b859cbc11c633d0fb81432d98a795/cffi-2.0.0-cp312-cp312-musllinux_1_2_x86_64.whl", upload-time = 2025-09-08T23:22:55Z, size = 221361, hashes = { sha256 = "2c8f814d84194c9ea681642fd164267891702542f028a15fc97d4674b6206187" } }, + { url = "https://files.pythonhosted.org/packages/7b/2b/2b6435f76bfeb6bbf055596976da087377ede68df465419d192acf00c437/cffi-2.0.0-cp312-cp312-win32.whl", upload-time = 2025-09-08T23:22:57Z, size = 172932, hashes = { sha256 = "da902562c3e9c550df360bfa53c035b2f241fed6d9aef119048073680ace4a18" } }, + { url = "https://files.pythonhosted.org/packages/f8/ed/13bd4418627013bec4ed6e54283b1959cf6db888048c7cf4b4c3b5b36002/cffi-2.0.0-cp312-cp312-win_amd64.whl", upload-time = 2025-09-08T23:22:58Z, size = 183557, hashes = { sha256 = "da68248800ad6320861f129cd9c1bf96ca849a2771a59e0344e88681905916f5" } }, + { url = "https://files.pythonhosted.org/packages/95/31/9f7f93ad2f8eff1dbc1c3656d7ca5bfd8fb52c9d786b4dcf19b2d02217fa/cffi-2.0.0-cp312-cp312-win_arm64.whl", upload-time = 2025-09-08T23:22:59Z, size = 177762, hashes = { sha256 = "4671d9dd5ec934cb9a73e7ee9676f9362aba54f7f34910956b84d727b0d73fb6" } }, + { url = "https://files.pythonhosted.org/packages/4b/8d/a0a47a0c9e413a658623d014e91e74a50cdd2c423f7ccfd44086ef767f90/cffi-2.0.0-cp313-cp313-macosx_10_13_x86_64.whl", upload-time = 2025-09-08T23:23:00Z, size = 185230, hashes = { sha256 = "00bdf7acc5f795150faa6957054fbbca2439db2f775ce831222b66f192f03beb" } }, + { url = "https://files.pythonhosted.org/packages/4a/d2/a6c0296814556c68ee32009d9c2ad4f85f2707cdecfd7727951ec228005d/cffi-2.0.0-cp313-cp313-macosx_11_0_arm64.whl", upload-time = 2025-09-08T23:23:02Z, size = 181043, hashes = { sha256 = "45d5e886156860dc35862657e1494b9bae8dfa63bf56796f2fb56e1679fc0bca" } }, + { url = "https://files.pythonhosted.org/packages/b0/1e/d22cc63332bd59b06481ceaac49d6c507598642e2230f201649058a7e704/cffi-2.0.0-cp313-cp313-manylinux1_i686.manylinux2014_i686.manylinux_2_17_i686.manylinux_2_5_i686.whl", upload-time = 2025-09-08T23:23:03Z, size = 212446, hashes = { sha256 = "07b271772c100085dd28b74fa0cd81c8fb1a3ba18b21e03d7c27f3436a10606b" } }, + { url = "https://files.pythonhosted.org/packages/a9/f5/a2c23eb03b61a0b8747f211eb716446c826ad66818ddc7810cc2cc19b3f2/cffi-2.0.0-cp313-cp313-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", upload-time = 2025-09-08T23:23:04Z, size = 220101, hashes = { sha256 = "d48a880098c96020b02d5a1f7d9251308510ce8858940e6fa99ece33f610838b" } }, + { url = "https://files.pythonhosted.org/packages/f2/7f/e6647792fc5850d634695bc0e6ab4111ae88e89981d35ac269956605feba/cffi-2.0.0-cp313-cp313-manylinux2014_ppc64le.manylinux_2_17_ppc64le.whl", upload-time = 2025-09-08T23:23:06Z, size = 207948, hashes = { sha256 = "f93fd8e5c8c0a4aa1f424d6173f14a892044054871c771f8566e4008eaa359d2" } }, + { url = "https://files.pythonhosted.org/packages/cb/1e/a5a1bd6f1fb30f22573f76533de12a00bf274abcdc55c8edab639078abb6/cffi-2.0.0-cp313-cp313-manylinux2014_s390x.manylinux_2_17_s390x.whl", upload-time = 2025-09-08T23:23:07Z, size = 206422, hashes = { sha256 = "dd4f05f54a52fb558f1ba9f528228066954fee3ebe629fc1660d874d040ae5a3" } }, + { url = "https://files.pythonhosted.org/packages/98/df/0a1755e750013a2081e863e7cd37e0cdd02664372c754e5560099eb7aa44/cffi-2.0.0-cp313-cp313-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", upload-time = 2025-09-08T23:23:09Z, size = 219499, hashes = { sha256 = "c8d3b5532fc71b7a77c09192b4a5a200ea992702734a2e9279a37f2478236f26" } }, + { url = "https://files.pythonhosted.org/packages/50/e1/a969e687fcf9ea58e6e2a928ad5e2dd88cc12f6f0ab477e9971f2309b57c/cffi-2.0.0-cp313-cp313-musllinux_1_2_aarch64.whl", upload-time = 2025-09-08T23:23:10Z, size = 222928, hashes = { sha256 = "d9b29c1f0ae438d5ee9acb31cadee00a58c46cc9c0b2f9038c6b0b3470877a8c" } }, + { url = "https://files.pythonhosted.org/packages/36/54/0362578dd2c9e557a28ac77698ed67323ed5b9775ca9d3fe73fe191bb5d8/cffi-2.0.0-cp313-cp313-musllinux_1_2_x86_64.whl", upload-time = 2025-09-08T23:23:12Z, size = 221302, hashes = { sha256 = "6d50360be4546678fc1b79ffe7a66265e28667840010348dd69a314145807a1b" } }, + { url = "https://files.pythonhosted.org/packages/eb/6d/bf9bda840d5f1dfdbf0feca87fbdb64a918a69bca42cfa0ba7b137c48cb8/cffi-2.0.0-cp313-cp313-win32.whl", upload-time = 2025-09-08T23:23:14Z, size = 172909, hashes = { sha256 = "74a03b9698e198d47562765773b4a8309919089150a0bb17d829ad7b44b60d27" } }, + { url = "https://files.pythonhosted.org/packages/37/18/6519e1ee6f5a1e579e04b9ddb6f1676c17368a7aba48299c3759bbc3c8b3/cffi-2.0.0-cp313-cp313-win_amd64.whl", upload-time = 2025-09-08T23:23:15Z, size = 183402, hashes = { sha256 = "19f705ada2530c1167abacb171925dd886168931e0a7b78f5bffcae5c6b5be75" } }, + { url = "https://files.pythonhosted.org/packages/cb/0e/02ceeec9a7d6ee63bb596121c2c8e9b3a9e150936f4fbef6ca1943e6137c/cffi-2.0.0-cp313-cp313-win_arm64.whl", upload-time = 2025-09-08T23:23:16Z, size = 177780, hashes = { sha256 = "256f80b80ca3853f90c21b23ee78cd008713787b1b1e93eae9f3d6a7134abd91" } }, + { url = "https://files.pythonhosted.org/packages/92/c4/3ce07396253a83250ee98564f8d7e9789fab8e58858f35d07a9a2c78de9f/cffi-2.0.0-cp314-cp314-macosx_10_13_x86_64.whl", upload-time = 2025-09-08T23:23:18Z, size = 185320, hashes = { sha256 = "fc33c5141b55ed366cfaad382df24fe7dcbc686de5be719b207bb248e3053dc5" } }, + { url = "https://files.pythonhosted.org/packages/59/dd/27e9fa567a23931c838c6b02d0764611c62290062a6d4e8ff7863daf9730/cffi-2.0.0-cp314-cp314-macosx_11_0_arm64.whl", upload-time = 2025-09-08T23:23:19Z, size = 181487, hashes = { sha256 = "c654de545946e0db659b3400168c9ad31b5d29593291482c43e3564effbcee13" } }, + { url = "https://files.pythonhosted.org/packages/d6/43/0e822876f87ea8a4ef95442c3d766a06a51fc5298823f884ef87aaad168c/cffi-2.0.0-cp314-cp314-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", upload-time = 2025-09-08T23:23:20Z, size = 220049, hashes = { sha256 = "24b6f81f1983e6df8db3adc38562c83f7d4a0c36162885ec7f7b77c7dcbec97b" } }, + { url = "https://files.pythonhosted.org/packages/b4/89/76799151d9c2d2d1ead63c2429da9ea9d7aac304603de0c6e8764e6e8e70/cffi-2.0.0-cp314-cp314-manylinux2014_ppc64le.manylinux_2_17_ppc64le.whl", upload-time = 2025-09-08T23:23:22Z, size = 207793, hashes = { sha256 = "12873ca6cb9b0f0d3a0da705d6086fe911591737a59f28b7936bdfed27c0d47c" } }, + { url = "https://files.pythonhosted.org/packages/bb/dd/3465b14bb9e24ee24cb88c9e3730f6de63111fffe513492bf8c808a3547e/cffi-2.0.0-cp314-cp314-manylinux2014_s390x.manylinux_2_17_s390x.whl", upload-time = 2025-09-08T23:23:23Z, size = 206300, hashes = { sha256 = "d9b97165e8aed9272a6bb17c01e3cc5871a594a446ebedc996e2397a1c1ea8ef" } }, + { url = "https://files.pythonhosted.org/packages/47/d9/d83e293854571c877a92da46fdec39158f8d7e68da75bf73581225d28e90/cffi-2.0.0-cp314-cp314-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", upload-time = 2025-09-08T23:23:24Z, size = 219244, hashes = { sha256 = "afb8db5439b81cf9c9d0c80404b60c3cc9c3add93e114dcae767f1477cb53775" } }, + { url = "https://files.pythonhosted.org/packages/2b/0f/1f177e3683aead2bb00f7679a16451d302c436b5cbf2505f0ea8146ef59e/cffi-2.0.0-cp314-cp314-musllinux_1_2_aarch64.whl", upload-time = 2025-09-08T23:23:26Z, size = 222828, hashes = { sha256 = "737fe7d37e1a1bffe70bd5754ea763a62a066dc5913ca57e957824b72a85e205" } }, + { url = "https://files.pythonhosted.org/packages/c6/0f/cafacebd4b040e3119dcb32fed8bdef8dfe94da653155f9d0b9dc660166e/cffi-2.0.0-cp314-cp314-musllinux_1_2_x86_64.whl", upload-time = 2025-09-08T23:23:27Z, size = 220926, hashes = { sha256 = "38100abb9d1b1435bc4cc340bb4489635dc2f0da7456590877030c9b3d40b0c1" } }, + { url = "https://files.pythonhosted.org/packages/3e/aa/df335faa45b395396fcbc03de2dfcab242cd61a9900e914fe682a59170b1/cffi-2.0.0-cp314-cp314-win32.whl", upload-time = 2025-09-08T23:23:44Z, size = 175328, hashes = { sha256 = "087067fa8953339c723661eda6b54bc98c5625757ea62e95eb4898ad5e776e9f" } }, + { url = "https://files.pythonhosted.org/packages/bb/92/882c2d30831744296ce713f0feb4c1cd30f346ef747b530b5318715cc367/cffi-2.0.0-cp314-cp314-win_amd64.whl", upload-time = 2025-09-08T23:23:45Z, size = 185650, hashes = { sha256 = "203a48d1fb583fc7d78a4c6655692963b860a417c0528492a6bc21f1aaefab25" } }, + { url = "https://files.pythonhosted.org/packages/9f/2c/98ece204b9d35a7366b5b2c6539c350313ca13932143e79dc133ba757104/cffi-2.0.0-cp314-cp314-win_arm64.whl", upload-time = 2025-09-08T23:23:47Z, size = 180687, hashes = { sha256 = "dbd5c7a25a7cb98f5ca55d258b103a2054f859a46ae11aaf23134f9cc0d356ad" } }, + { url = "https://files.pythonhosted.org/packages/3e/61/c768e4d548bfa607abcda77423448df8c471f25dbe64fb2ef6d555eae006/cffi-2.0.0-cp314-cp314t-macosx_10_13_x86_64.whl", upload-time = 2025-09-08T23:23:29Z, size = 188773, hashes = { sha256 = "9a67fc9e8eb39039280526379fb3a70023d77caec1852002b4da7e8b270c4dd9" } }, + { url = "https://files.pythonhosted.org/packages/2c/ea/5f76bce7cf6fcd0ab1a1058b5af899bfbef198bea4d5686da88471ea0336/cffi-2.0.0-cp314-cp314t-macosx_11_0_arm64.whl", upload-time = 2025-09-08T23:23:30Z, size = 185013, hashes = { sha256 = "7a66c7204d8869299919db4d5069a82f1561581af12b11b3c9f48c584eb8743d" } }, + { url = "https://files.pythonhosted.org/packages/be/b4/c56878d0d1755cf9caa54ba71e5d049479c52f9e4afc230f06822162ab2f/cffi-2.0.0-cp314-cp314t-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", upload-time = 2025-09-08T23:23:31Z, size = 221593, hashes = { sha256 = "7cc09976e8b56f8cebd752f7113ad07752461f48a58cbba644139015ac24954c" } }, + { url = "https://files.pythonhosted.org/packages/e0/0d/eb704606dfe8033e7128df5e90fee946bbcb64a04fcdaa97321309004000/cffi-2.0.0-cp314-cp314t-manylinux2014_ppc64le.manylinux_2_17_ppc64le.whl", upload-time = 2025-09-08T23:23:33Z, size = 209354, hashes = { sha256 = "92b68146a71df78564e4ef48af17551a5ddd142e5190cdf2c5624d0c3ff5b2e8" } }, + { url = "https://files.pythonhosted.org/packages/d8/19/3c435d727b368ca475fb8742ab97c9cb13a0de600ce86f62eab7fa3eea60/cffi-2.0.0-cp314-cp314t-manylinux2014_s390x.manylinux_2_17_s390x.whl", upload-time = 2025-09-08T23:23:34Z, size = 208480, hashes = { sha256 = "b1e74d11748e7e98e2f426ab176d4ed720a64412b6a15054378afdb71e0f37dc" } }, + { url = "https://files.pythonhosted.org/packages/d0/44/681604464ed9541673e486521497406fadcc15b5217c3e326b061696899a/cffi-2.0.0-cp314-cp314t-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", upload-time = 2025-09-08T23:23:36Z, size = 221584, hashes = { sha256 = "28a3a209b96630bca57cce802da70c266eb08c6e97e5afd61a75611ee6c64592" } }, + { url = "https://files.pythonhosted.org/packages/25/8e/342a504ff018a2825d395d44d63a767dd8ebc927ebda557fecdaca3ac33a/cffi-2.0.0-cp314-cp314t-musllinux_1_2_aarch64.whl", upload-time = 2025-09-08T23:23:37Z, size = 224443, hashes = { sha256 = "7553fb2090d71822f02c629afe6042c299edf91ba1bf94951165613553984512" } }, + { url = "https://files.pythonhosted.org/packages/e1/5e/b666bacbbc60fbf415ba9988324a132c9a7a0448a9a8f125074671c0f2c3/cffi-2.0.0-cp314-cp314t-musllinux_1_2_x86_64.whl", upload-time = 2025-09-08T23:23:38Z, size = 223437, hashes = { sha256 = "6c6c373cfc5c83a975506110d17457138c8c63016b563cc9ed6e056a82f13ce4" } }, + { url = "https://files.pythonhosted.org/packages/a0/1d/ec1a60bd1a10daa292d3cd6bb0b359a81607154fb8165f3ec95fe003b85c/cffi-2.0.0-cp314-cp314t-win32.whl", upload-time = 2025-09-08T23:23:40Z, size = 180487, hashes = { sha256 = "1fc9ea04857caf665289b7a75923f2c6ed559b8298a1b8c49e59f7dd95c8481e" } }, + { url = "https://files.pythonhosted.org/packages/bf/41/4c1168c74fac325c0c8156f04b6749c8b6a8f405bbf91413ba088359f60d/cffi-2.0.0-cp314-cp314t-win_amd64.whl", upload-time = 2025-09-08T23:23:41Z, size = 191726, hashes = { sha256 = "d68b6cef7827e8641e8ef16f4494edda8b36104d79773a334beaa1e3521430f6" } }, + { url = "https://files.pythonhosted.org/packages/ae/3a/dbeec9d1ee0844c679f6bb5d6ad4e9f198b1224f4e7a32825f47f6192b0c/cffi-2.0.0-cp314-cp314t-win_arm64.whl", upload-time = 2025-09-08T23:23:43Z, size = 184195, hashes = { sha256 = "0a1527a803f0a659de1af2e1fd700213caba79377e27e4693648c2923da066f9" } }, +] + +[[packages]] +name = "charset-normalizer" +version = "3.4.2" +sdist = { url = "https://files.pythonhosted.org/packages/e4/33/89c2ced2b67d1c2a61c19c6751aa8902d46ce3dacb23600a283619f5a12d/charset_normalizer-3.4.2.tar.gz", upload-time = 2025-05-02T08:34:42Z, size = 126367, hashes = { sha256 = "5baececa9ecba31eff645232d59845c07aa030f0c81ee70184a90d35099a0e63" } } +wheels = [ + { url = "https://files.pythonhosted.org/packages/95/28/9901804da60055b406e1a1c5ba7aac1276fb77f1dde635aabfc7fd84b8ab/charset_normalizer-3.4.2-cp310-cp310-macosx_10_9_universal2.whl", upload-time = 2025-05-02T08:31:46Z, size = 201818, hashes = { sha256 = "7c48ed483eb946e6c04ccbe02c6b4d1d48e51944b6db70f697e089c193404941" } }, + { url = "https://files.pythonhosted.org/packages/d9/9b/892a8c8af9110935e5adcbb06d9c6fe741b6bb02608c6513983048ba1a18/charset_normalizer-3.4.2-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", upload-time = 2025-05-02T08:31:48Z, size = 144649, hashes = { sha256 = "b2d318c11350e10662026ad0eb71bb51c7812fc8590825304ae0bdd4ac283acd" } }, + { url = "https://files.pythonhosted.org/packages/7b/a5/4179abd063ff6414223575e008593861d62abfc22455b5d1a44995b7c101/charset_normalizer-3.4.2-cp310-cp310-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", upload-time = 2025-05-02T08:31:50Z, size = 155045, hashes = { sha256 = "9cbfacf36cb0ec2897ce0ebc5d08ca44213af24265bd56eca54bee7923c48fd6" } }, + { url = "https://files.pythonhosted.org/packages/3b/95/bc08c7dfeddd26b4be8c8287b9bb055716f31077c8b0ea1cd09553794665/charset_normalizer-3.4.2-cp310-cp310-manylinux_2_17_s390x.manylinux2014_s390x.whl", upload-time = 2025-05-02T08:31:52Z, size = 147356, hashes = { sha256 = "18dd2e350387c87dabe711b86f83c9c78af772c748904d372ade190b5c7c9d4d" } }, + { url = "https://files.pythonhosted.org/packages/a8/2d/7a5b635aa65284bf3eab7653e8b4151ab420ecbae918d3e359d1947b4d61/charset_normalizer-3.4.2-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", upload-time = 2025-05-02T08:31:56Z, size = 149471, hashes = { sha256 = "8075c35cd58273fee266c58c0c9b670947c19df5fb98e7b66710e04ad4e9ff86" } }, + { url = "https://files.pythonhosted.org/packages/ae/38/51fc6ac74251fd331a8cfdb7ec57beba8c23fd5493f1050f71c87ef77ed0/charset_normalizer-3.4.2-cp310-cp310-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", upload-time = 2025-05-02T08:31:57Z, size = 151317, hashes = { sha256 = "5bf4545e3b962767e5c06fe1738f951f77d27967cb2caa64c28be7c4563e162c" } }, + { url = "https://files.pythonhosted.org/packages/b7/17/edee1e32215ee6e9e46c3e482645b46575a44a2d72c7dfd49e49f60ce6bf/charset_normalizer-3.4.2-cp310-cp310-musllinux_1_2_aarch64.whl", upload-time = 2025-05-02T08:31:59Z, size = 146368, hashes = { sha256 = "7a6ab32f7210554a96cd9e33abe3ddd86732beeafc7a28e9955cdf22ffadbab0" } }, + { url = "https://files.pythonhosted.org/packages/26/2c/ea3e66f2b5f21fd00b2825c94cafb8c326ea6240cd80a91eb09e4a285830/charset_normalizer-3.4.2-cp310-cp310-musllinux_1_2_i686.whl", upload-time = 2025-05-02T08:32:01Z, size = 154491, hashes = { sha256 = "b33de11b92e9f75a2b545d6e9b6f37e398d86c3e9e9653c4864eb7e89c5773ef" } }, + { url = "https://files.pythonhosted.org/packages/52/47/7be7fa972422ad062e909fd62460d45c3ef4c141805b7078dbab15904ff7/charset_normalizer-3.4.2-cp310-cp310-musllinux_1_2_ppc64le.whl", upload-time = 2025-05-02T08:32:03Z, size = 157695, hashes = { sha256 = "8755483f3c00d6c9a77f490c17e6ab0c8729e39e6390328e42521ef175380ae6" } }, + { url = "https://files.pythonhosted.org/packages/2f/42/9f02c194da282b2b340f28e5fb60762de1151387a36842a92b533685c61e/charset_normalizer-3.4.2-cp310-cp310-musllinux_1_2_s390x.whl", upload-time = 2025-05-02T08:32:04Z, size = 154849, hashes = { sha256 = "68a328e5f55ec37c57f19ebb1fdc56a248db2e3e9ad769919a58672958e8f366" } }, + { url = "https://files.pythonhosted.org/packages/67/44/89cacd6628f31fb0b63201a618049be4be2a7435a31b55b5eb1c3674547a/charset_normalizer-3.4.2-cp310-cp310-musllinux_1_2_x86_64.whl", upload-time = 2025-05-02T08:32:06Z, size = 150091, hashes = { sha256 = "21b2899062867b0e1fde9b724f8aecb1af14f2778d69aacd1a5a1853a597a5db" } }, + { url = "https://files.pythonhosted.org/packages/1f/79/4b8da9f712bc079c0f16b6d67b099b0b8d808c2292c937f267d816ec5ecc/charset_normalizer-3.4.2-cp310-cp310-win32.whl", upload-time = 2025-05-02T08:32:08Z, size = 98445, hashes = { sha256 = "e8082b26888e2f8b36a042a58307d5b917ef2b1cacab921ad3323ef91901c71a" } }, + { url = "https://files.pythonhosted.org/packages/7d/d7/96970afb4fb66497a40761cdf7bd4f6fca0fc7bafde3a84f836c1f57a926/charset_normalizer-3.4.2-cp310-cp310-win_amd64.whl", upload-time = 2025-05-02T08:32:10Z, size = 105782, hashes = { sha256 = "f69a27e45c43520f5487f27627059b64aaf160415589230992cec34c5e18a509" } }, + { url = "https://files.pythonhosted.org/packages/05/85/4c40d00dcc6284a1c1ad5de5e0996b06f39d8232f1031cd23c2f5c07ee86/charset_normalizer-3.4.2-cp311-cp311-macosx_10_9_universal2.whl", upload-time = 2025-05-02T08:32:11Z, size = 198794, hashes = { sha256 = "be1e352acbe3c78727a16a455126d9ff83ea2dfdcbc83148d2982305a04714c2" } }, + { url = "https://files.pythonhosted.org/packages/41/d9/7a6c0b9db952598e97e93cbdfcb91bacd89b9b88c7c983250a77c008703c/charset_normalizer-3.4.2-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", upload-time = 2025-05-02T08:32:13Z, size = 142846, hashes = { sha256 = "aa88ca0b1932e93f2d961bf3addbb2db902198dca337d88c89e1559e066e7645" } }, + { url = "https://files.pythonhosted.org/packages/66/82/a37989cda2ace7e37f36c1a8ed16c58cf48965a79c2142713244bf945c89/charset_normalizer-3.4.2-cp311-cp311-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", upload-time = 2025-05-02T08:32:15Z, size = 153350, hashes = { sha256 = "d524ba3f1581b35c03cb42beebab4a13e6cdad7b36246bd22541fa585a56cccd" } }, + { url = "https://files.pythonhosted.org/packages/df/68/a576b31b694d07b53807269d05ec3f6f1093e9545e8607121995ba7a8313/charset_normalizer-3.4.2-cp311-cp311-manylinux_2_17_s390x.manylinux2014_s390x.whl", upload-time = 2025-05-02T08:32:17Z, size = 145657, hashes = { sha256 = "28a1005facc94196e1fb3e82a3d442a9d9110b8434fc1ded7a24a2983c9888d8" } }, + { url = "https://files.pythonhosted.org/packages/92/9b/ad67f03d74554bed3aefd56fe836e1623a50780f7c998d00ca128924a499/charset_normalizer-3.4.2-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", upload-time = 2025-05-02T08:32:18Z, size = 147260, hashes = { sha256 = "fdb20a30fe1175ecabed17cbf7812f7b804b8a315a25f24678bcdf120a90077f" } }, + { url = "https://files.pythonhosted.org/packages/a6/e6/8aebae25e328160b20e31a7e9929b1578bbdc7f42e66f46595a432f8539e/charset_normalizer-3.4.2-cp311-cp311-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", upload-time = 2025-05-02T08:32:20Z, size = 149164, hashes = { sha256 = "0f5d9ed7f254402c9e7d35d2f5972c9bbea9040e99cd2861bd77dc68263277c7" } }, + { url = "https://files.pythonhosted.org/packages/8b/f2/b3c2f07dbcc248805f10e67a0262c93308cfa149a4cd3d1fe01f593e5fd2/charset_normalizer-3.4.2-cp311-cp311-musllinux_1_2_aarch64.whl", upload-time = 2025-05-02T08:32:21Z, size = 144571, hashes = { sha256 = "efd387a49825780ff861998cd959767800d54f8308936b21025326de4b5a42b9" } }, + { url = "https://files.pythonhosted.org/packages/60/5b/c3f3a94bc345bc211622ea59b4bed9ae63c00920e2e8f11824aa5708e8b7/charset_normalizer-3.4.2-cp311-cp311-musllinux_1_2_i686.whl", upload-time = 2025-05-02T08:32:23Z, size = 151952, hashes = { sha256 = "f0aa37f3c979cf2546b73e8222bbfa3dc07a641585340179d768068e3455e544" } }, + { url = "https://files.pythonhosted.org/packages/e2/4d/ff460c8b474122334c2fa394a3f99a04cf11c646da895f81402ae54f5c42/charset_normalizer-3.4.2-cp311-cp311-musllinux_1_2_ppc64le.whl", upload-time = 2025-05-02T08:32:24Z, size = 155959, hashes = { sha256 = "e70e990b2137b29dc5564715de1e12701815dacc1d056308e2b17e9095372a82" } }, + { url = "https://files.pythonhosted.org/packages/a2/2b/b964c6a2fda88611a1fe3d4c400d39c66a42d6c169c924818c848f922415/charset_normalizer-3.4.2-cp311-cp311-musllinux_1_2_s390x.whl", upload-time = 2025-05-02T08:32:26Z, size = 153030, hashes = { sha256 = "0c8c57f84ccfc871a48a47321cfa49ae1df56cd1d965a09abe84066f6853b9c0" } }, + { url = "https://files.pythonhosted.org/packages/59/2e/d3b9811db26a5ebf444bc0fa4f4be5aa6d76fc6e1c0fd537b16c14e849b6/charset_normalizer-3.4.2-cp311-cp311-musllinux_1_2_x86_64.whl", upload-time = 2025-05-02T08:32:28Z, size = 148015, hashes = { sha256 = "6b66f92b17849b85cad91259efc341dce9c1af48e2173bf38a85c6329f1033e5" } }, + { url = "https://files.pythonhosted.org/packages/90/07/c5fd7c11eafd561bb51220d600a788f1c8d77c5eef37ee49454cc5c35575/charset_normalizer-3.4.2-cp311-cp311-win32.whl", upload-time = 2025-05-02T08:32:30Z, size = 98106, hashes = { sha256 = "daac4765328a919a805fa5e2720f3e94767abd632ae410a9062dff5412bae65a" } }, + { url = "https://files.pythonhosted.org/packages/a8/05/5e33dbef7e2f773d672b6d79f10ec633d4a71cd96db6673625838a4fd532/charset_normalizer-3.4.2-cp311-cp311-win_amd64.whl", upload-time = 2025-05-02T08:32:32Z, size = 105402, hashes = { sha256 = "e53efc7c7cee4c1e70661e2e112ca46a575f90ed9ae3fef200f2a25e954f4b28" } }, + { url = "https://files.pythonhosted.org/packages/d7/a4/37f4d6035c89cac7930395a35cc0f1b872e652eaafb76a6075943754f095/charset_normalizer-3.4.2-cp312-cp312-macosx_10_13_universal2.whl", upload-time = 2025-05-02T08:32:33Z, size = 199936, hashes = { sha256 = "0c29de6a1a95f24b9a1aa7aefd27d2487263f00dfd55a77719b530788f75cff7" } }, + { url = "https://files.pythonhosted.org/packages/ee/8a/1a5e33b73e0d9287274f899d967907cd0bf9c343e651755d9307e0dbf2b3/charset_normalizer-3.4.2-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", upload-time = 2025-05-02T08:32:35Z, size = 143790, hashes = { sha256 = "cddf7bd982eaa998934a91f69d182aec997c6c468898efe6679af88283b498d3" } }, + { url = "https://files.pythonhosted.org/packages/66/52/59521f1d8e6ab1482164fa21409c5ef44da3e9f653c13ba71becdd98dec3/charset_normalizer-3.4.2-cp312-cp312-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", upload-time = 2025-05-02T08:32:37Z, size = 153924, hashes = { sha256 = "fcbe676a55d7445b22c10967bceaaf0ee69407fbe0ece4d032b6eb8d4565982a" } }, + { url = "https://files.pythonhosted.org/packages/86/2d/fb55fdf41964ec782febbf33cb64be480a6b8f16ded2dbe8db27a405c09f/charset_normalizer-3.4.2-cp312-cp312-manylinux_2_17_s390x.manylinux2014_s390x.whl", upload-time = 2025-05-02T08:32:38Z, size = 146626, hashes = { sha256 = "d41c4d287cfc69060fa91cae9683eacffad989f1a10811995fa309df656ec214" } }, + { url = "https://files.pythonhosted.org/packages/8c/73/6ede2ec59bce19b3edf4209d70004253ec5f4e319f9a2e3f2f15601ed5f7/charset_normalizer-3.4.2-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", upload-time = 2025-05-02T08:32:40Z, size = 148567, hashes = { sha256 = "4e594135de17ab3866138f496755f302b72157d115086d100c3f19370839dd3a" } }, + { url = "https://files.pythonhosted.org/packages/09/14/957d03c6dc343c04904530b6bef4e5efae5ec7d7990a7cbb868e4595ee30/charset_normalizer-3.4.2-cp312-cp312-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", upload-time = 2025-05-02T08:32:41Z, size = 150957, hashes = { sha256 = "cf713fe9a71ef6fd5adf7a79670135081cd4431c2943864757f0fa3a65b1fafd" } }, + { url = "https://files.pythonhosted.org/packages/0d/c8/8174d0e5c10ccebdcb1b53cc959591c4c722a3ad92461a273e86b9f5a302/charset_normalizer-3.4.2-cp312-cp312-musllinux_1_2_aarch64.whl", upload-time = 2025-05-02T08:32:43Z, size = 145408, hashes = { sha256 = "a370b3e078e418187da8c3674eddb9d983ec09445c99a3a263c2011993522981" } }, + { url = "https://files.pythonhosted.org/packages/58/aa/8904b84bc8084ac19dc52feb4f5952c6df03ffb460a887b42615ee1382e8/charset_normalizer-3.4.2-cp312-cp312-musllinux_1_2_i686.whl", upload-time = 2025-05-02T08:32:46Z, size = 153399, hashes = { sha256 = "a955b438e62efdf7e0b7b52a64dc5c3396e2634baa62471768a64bc2adb73d5c" } }, + { url = "https://files.pythonhosted.org/packages/c2/26/89ee1f0e264d201cb65cf054aca6038c03b1a0c6b4ae998070392a3ce605/charset_normalizer-3.4.2-cp312-cp312-musllinux_1_2_ppc64le.whl", upload-time = 2025-05-02T08:32:48Z, size = 156815, hashes = { sha256 = "7222ffd5e4de8e57e03ce2cef95a4c43c98fcb72ad86909abdfc2c17d227fc1b" } }, + { url = "https://files.pythonhosted.org/packages/fd/07/68e95b4b345bad3dbbd3a8681737b4338ff2c9df29856a6d6d23ac4c73cb/charset_normalizer-3.4.2-cp312-cp312-musllinux_1_2_s390x.whl", upload-time = 2025-05-02T08:32:49Z, size = 154537, hashes = { sha256 = "bee093bf902e1d8fc0ac143c88902c3dfc8941f7ea1d6a8dd2bcb786d33db03d" } }, + { url = "https://files.pythonhosted.org/packages/77/1a/5eefc0ce04affb98af07bc05f3bac9094513c0e23b0562d64af46a06aae4/charset_normalizer-3.4.2-cp312-cp312-musllinux_1_2_x86_64.whl", upload-time = 2025-05-02T08:32:51Z, size = 149565, hashes = { sha256 = "dedb8adb91d11846ee08bec4c8236c8549ac721c245678282dcb06b221aab59f" } }, + { url = "https://files.pythonhosted.org/packages/37/a0/2410e5e6032a174c95e0806b1a6585eb21e12f445ebe239fac441995226a/charset_normalizer-3.4.2-cp312-cp312-win32.whl", upload-time = 2025-05-02T08:32:53Z, size = 98357, hashes = { sha256 = "db4c7bf0e07fc3b7d89ac2a5880a6a8062056801b83ff56d8464b70f65482b6c" } }, + { url = "https://files.pythonhosted.org/packages/6c/4f/c02d5c493967af3eda9c771ad4d2bbc8df6f99ddbeb37ceea6e8716a32bc/charset_normalizer-3.4.2-cp312-cp312-win_amd64.whl", upload-time = 2025-05-02T08:32:54Z, size = 105776, hashes = { sha256 = "5a9979887252a82fefd3d3ed2a8e3b937a7a809f65dcb1e068b090e165bbe99e" } }, + { url = "https://files.pythonhosted.org/packages/ea/12/a93df3366ed32db1d907d7593a94f1fe6293903e3e92967bebd6950ed12c/charset_normalizer-3.4.2-cp313-cp313-macosx_10_13_universal2.whl", upload-time = 2025-05-02T08:32:56Z, size = 199622, hashes = { sha256 = "926ca93accd5d36ccdabd803392ddc3e03e6d4cd1cf17deff3b989ab8e9dbcf0" } }, + { url = "https://files.pythonhosted.org/packages/04/93/bf204e6f344c39d9937d3c13c8cd5bbfc266472e51fc8c07cb7f64fcd2de/charset_normalizer-3.4.2-cp313-cp313-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", upload-time = 2025-05-02T08:32:58Z, size = 143435, hashes = { sha256 = "eba9904b0f38a143592d9fc0e19e2df0fa2e41c3c3745554761c5f6447eedabf" } }, + { url = "https://files.pythonhosted.org/packages/22/2a/ea8a2095b0bafa6c5b5a55ffdc2f924455233ee7b91c69b7edfcc9e02284/charset_normalizer-3.4.2-cp313-cp313-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", upload-time = 2025-05-02T08:33:00Z, size = 153653, hashes = { sha256 = "3fddb7e2c84ac87ac3a947cb4e66d143ca5863ef48e4a5ecb83bd48619e4634e" } }, + { url = "https://files.pythonhosted.org/packages/b6/57/1b090ff183d13cef485dfbe272e2fe57622a76694061353c59da52c9a659/charset_normalizer-3.4.2-cp313-cp313-manylinux_2_17_s390x.manylinux2014_s390x.whl", upload-time = 2025-05-02T08:33:02Z, size = 146231, hashes = { sha256 = "98f862da73774290f251b9df8d11161b6cf25b599a66baf087c1ffe340e9bfd1" } }, + { url = "https://files.pythonhosted.org/packages/e2/28/ffc026b26f441fc67bd21ab7f03b313ab3fe46714a14b516f931abe1a2d8/charset_normalizer-3.4.2-cp313-cp313-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", upload-time = 2025-05-02T08:33:04Z, size = 148243, hashes = { sha256 = "6c9379d65defcab82d07b2a9dfbfc2e95bc8fe0ebb1b176a3190230a3ef0e07c" } }, + { url = "https://files.pythonhosted.org/packages/c0/0f/9abe9bd191629c33e69e47c6ef45ef99773320e9ad8e9cb08b8ab4a8d4cb/charset_normalizer-3.4.2-cp313-cp313-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", upload-time = 2025-05-02T08:33:06Z, size = 150442, hashes = { sha256 = "e635b87f01ebc977342e2697d05b56632f5f879a4f15955dfe8cef2448b51691" } }, + { url = "https://files.pythonhosted.org/packages/67/7c/a123bbcedca91d5916c056407f89a7f5e8fdfce12ba825d7d6b9954a1a3c/charset_normalizer-3.4.2-cp313-cp313-musllinux_1_2_aarch64.whl", upload-time = 2025-05-02T08:33:08Z, size = 145147, hashes = { sha256 = "1c95a1e2902a8b722868587c0e1184ad5c55631de5afc0eb96bc4b0d738092c0" } }, + { url = "https://files.pythonhosted.org/packages/ec/fe/1ac556fa4899d967b83e9893788e86b6af4d83e4726511eaaad035e36595/charset_normalizer-3.4.2-cp313-cp313-musllinux_1_2_i686.whl", upload-time = 2025-05-02T08:33:09Z, size = 153057, hashes = { sha256 = "ef8de666d6179b009dce7bcb2ad4c4a779f113f12caf8dc77f0162c29d20490b" } }, + { url = "https://files.pythonhosted.org/packages/2b/ff/acfc0b0a70b19e3e54febdd5301a98b72fa07635e56f24f60502e954c461/charset_normalizer-3.4.2-cp313-cp313-musllinux_1_2_ppc64le.whl", upload-time = 2025-05-02T08:33:11Z, size = 156454, hashes = { sha256 = "32fc0341d72e0f73f80acb0a2c94216bd704f4f0bce10aedea38f30502b271ff" } }, + { url = "https://files.pythonhosted.org/packages/92/08/95b458ce9c740d0645feb0e96cea1f5ec946ea9c580a94adfe0b617f3573/charset_normalizer-3.4.2-cp313-cp313-musllinux_1_2_s390x.whl", upload-time = 2025-05-02T08:33:13Z, size = 154174, hashes = { sha256 = "289200a18fa698949d2b39c671c2cc7a24d44096784e76614899a7ccf2574b7b" } }, + { url = "https://files.pythonhosted.org/packages/78/be/8392efc43487ac051eee6c36d5fbd63032d78f7728cb37aebcc98191f1ff/charset_normalizer-3.4.2-cp313-cp313-musllinux_1_2_x86_64.whl", upload-time = 2025-05-02T08:33:15Z, size = 149166, hashes = { sha256 = "4a476b06fbcf359ad25d34a057b7219281286ae2477cc5ff5e3f70a246971148" } }, + { url = "https://files.pythonhosted.org/packages/44/96/392abd49b094d30b91d9fbda6a69519e95802250b777841cf3bda8fe136c/charset_normalizer-3.4.2-cp313-cp313-win32.whl", upload-time = 2025-05-02T08:33:17Z, size = 98064, hashes = { sha256 = "aaeeb6a479c7667fbe1099af9617c83aaca22182d6cf8c53966491a0f1b7ffb7" } }, + { url = "https://files.pythonhosted.org/packages/e9/b0/0200da600134e001d91851ddc797809e2fe0ea72de90e09bec5a2fbdaccb/charset_normalizer-3.4.2-cp313-cp313-win_amd64.whl", upload-time = 2025-05-02T08:33:18Z, size = 105641, hashes = { sha256 = "aa6af9e7d59f9c12b33ae4e9450619cf2488e2bbe9b44030905877f0b2324980" } }, + { url = "https://files.pythonhosted.org/packages/20/94/c5790835a017658cbfabd07f3bfb549140c3ac458cfc196323996b10095a/charset_normalizer-3.4.2-py3-none-any.whl", upload-time = 2025-05-02T08:34:40Z, size = 52626, hashes = { sha256 = "7f56930ab0abd1c45cd15be65cc741c28b1c9a34876ce8c17a2fa107810c0af0" } }, +] + +[[packages]] +name = "click" +version = "8.1.8" +sdist = { url = "https://files.pythonhosted.org/packages/b9/2e/0090cbf739cee7d23781ad4b89a9894a41538e4fcf4c31dcdd705b78eb8b/click-8.1.8.tar.gz", upload-time = 2024-12-21T18:38:44Z, size = 226593, hashes = { sha256 = "ed53c9d8990d83c2a27deae68e4ee337473f6330c040a31d4225c9574d16096a" } } +wheels = [{ url = "https://files.pythonhosted.org/packages/7e/d4/7ebdbd03970677812aac39c869717059dbb71a4cfc033ca6e5221787892c/click-8.1.8-py3-none-any.whl", upload-time = 2024-12-21T18:38:41Z, size = 98188, hashes = { sha256 = "63c132bbbed01578a06712a2d1f497bb62d9c1c0d329b7903a866228027263b2" } }] + +[[packages]] +name = "cloudpickle" +version = "3.0.0" +sdist = { url = "https://files.pythonhosted.org/packages/c8/72/42a6570fc61b1f8913529728ad314c7cf5961540728dcad22c33fb2db6b6/cloudpickle-3.0.0.tar.gz", upload-time = 2023-10-16T12:51:26Z, size = 21231, hashes = { sha256 = "996d9a482c6fb4f33c1a35335cf8afd065d2a56e973270364840712d9131a882" } } +wheels = [{ url = "https://files.pythonhosted.org/packages/96/43/dae06432d0c4b1dc9e9149ad37b4ca8384cf6eb7700cd9215b177b914f0a/cloudpickle-3.0.0-py3-none-any.whl", upload-time = 2023-10-16T12:51:24Z, size = 20088, hashes = { sha256 = "246ee7d0c295602a036e86369c77fecda4ab17b506496730f2f576d9016fd9c7" } }] + +[[packages]] +name = "colorama" +version = "0.4.6" +marker = "sys_platform == 'win32'" +sdist = { url = "https://files.pythonhosted.org/packages/d8/53/6f443c9a4a8358a93a6792e2acffb9d9d5cb0a5cfd8802644b7b1c9a02e4/colorama-0.4.6.tar.gz", upload-time = 2022-10-25T02:36:22Z, size = 27697, hashes = { sha256 = "08695f5cb7ed6e0531a20572697297273c47b8cae5a63ffc6d6ed5c201be6e44" } } +wheels = [{ url = "https://files.pythonhosted.org/packages/d1/d6/3965ed04c63042e047cb6a3e6ed1a63a35087b6a609aa3a15ed8ac56c221/colorama-0.4.6-py2.py3-none-any.whl", upload-time = 2022-10-25T02:36:20Z, size = 25335, hashes = { sha256 = "4f1d9991f5acc0ca119f9d443620b77f9d6b33703e51011c16baf57afb285fc6" } }] + +[[packages]] +name = "cryptography" +version = "46.0.6" +sdist = { url = "https://files.pythonhosted.org/packages/a4/ba/04b1bd4218cbc58dc90ce967106d51582371b898690f3ae0402876cc4f34/cryptography-46.0.6.tar.gz", upload-time = 2026-03-25T23:34:53Z, size = 750542, hashes = { sha256 = "27550628a518c5c6c903d84f637fbecf287f6cb9ced3804838a1295dc1fd0759" } } +wheels = [ + { url = "https://files.pythonhosted.org/packages/47/23/9285e15e3bc57325b0a72e592921983a701efc1ee8f91c06c5f0235d86d9/cryptography-46.0.6-cp311-abi3-macosx_10_9_universal2.whl", upload-time = 2026-03-25T23:33:22Z, size = 7176401, hashes = { sha256 = "64235194bad039a10bb6d2d930ab3323baaec67e2ce36215fd0952fad0930ca8" } }, + { url = "https://files.pythonhosted.org/packages/60/f8/e61f8f13950ab6195b31913b42d39f0f9afc7d93f76710f299b5ec286ae6/cryptography-46.0.6-cp311-abi3-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", upload-time = 2026-03-25T23:33:23Z, size = 4275275, hashes = { sha256 = "26031f1e5ca62fcb9d1fcb34b2b60b390d1aacaa15dc8b895a9ed00968b97b30" } }, + { url = "https://files.pythonhosted.org/packages/19/69/732a736d12c2631e140be2348b4ad3d226302df63ef64d30dfdb8db7ad1c/cryptography-46.0.6-cp311-abi3-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", upload-time = 2026-03-25T23:33:25Z, size = 4425320, hashes = { sha256 = "9a693028b9cbe51b5a1136232ee8f2bc242e4e19d456ded3fa7c86e43c713b4a" } }, + { url = "https://files.pythonhosted.org/packages/d4/12/123be7292674abf76b21ac1fc0e1af50661f0e5b8f0ec8285faac18eb99e/cryptography-46.0.6-cp311-abi3-manylinux_2_28_aarch64.whl", upload-time = 2026-03-25T23:33:27Z, size = 4278082, hashes = { sha256 = "67177e8a9f421aa2d3a170c3e56eca4e0128883cf52a071a7cbf53297f18b175" } }, + { url = "https://files.pythonhosted.org/packages/5b/ba/d5e27f8d68c24951b0a484924a84c7cdaed7502bac9f18601cd357f8b1d2/cryptography-46.0.6-cp311-abi3-manylinux_2_28_ppc64le.whl", upload-time = 2026-03-25T23:33:29Z, size = 4926514, hashes = { sha256 = "d9528b535a6c4f8ff37847144b8986a9a143585f0540fbcb1a98115b543aa463" } }, + { url = "https://files.pythonhosted.org/packages/34/71/1ea5a7352ae516d5512d17babe7e1b87d9db5150b21f794b1377eac1edc0/cryptography-46.0.6-cp311-abi3-manylinux_2_28_x86_64.whl", upload-time = 2026-03-25T23:33:30Z, size = 4457766, hashes = { sha256 = "22259338084d6ae497a19bae5d4c66b7ca1387d3264d1c2c0e72d9e9b6a77b97" } }, + { url = "https://files.pythonhosted.org/packages/01/59/562be1e653accee4fdad92c7a2e88fced26b3fdfce144047519bbebc299e/cryptography-46.0.6-cp311-abi3-manylinux_2_31_armv7l.whl", upload-time = 2026-03-25T23:33:33Z, size = 3986535, hashes = { sha256 = "760997a4b950ff00d418398ad73fbc91aa2894b5c1db7ccb45b4f68b42a63b3c" } }, + { url = "https://files.pythonhosted.org/packages/d6/8b/b1ebfeb788bf4624d36e45ed2662b8bd43a05ff62157093c1539c1288a18/cryptography-46.0.6-cp311-abi3-manylinux_2_34_aarch64.whl", upload-time = 2026-03-25T23:33:34Z, size = 4277618, hashes = { sha256 = "3dfa6567f2e9e4c5dceb8ccb5a708158a2a871052fa75c8b78cb0977063f1507" } }, + { url = "https://files.pythonhosted.org/packages/dd/52/a005f8eabdb28df57c20f84c44d397a755782d6ff6d455f05baa2785bd91/cryptography-46.0.6-cp311-abi3-manylinux_2_34_ppc64le.whl", upload-time = 2026-03-25T23:33:37Z, size = 4890802, hashes = { sha256 = "cdcd3edcbc5d55757e5f5f3d330dd00007ae463a7e7aa5bf132d1f22a4b62b19" } }, + { url = "https://files.pythonhosted.org/packages/ec/4d/8e7d7245c79c617d08724e2efa397737715ca0ec830ecb3c91e547302555/cryptography-46.0.6-cp311-abi3-manylinux_2_34_x86_64.whl", upload-time = 2026-03-25T23:33:38Z, size = 4457425, hashes = { sha256 = "d4e4aadb7fc1f88687f47ca20bb7227981b03afaae69287029da08096853b738" } }, + { url = "https://files.pythonhosted.org/packages/1d/5c/f6c3596a1430cec6f949085f0e1a970638d76f81c3ea56d93d564d04c340/cryptography-46.0.6-cp311-abi3-musllinux_1_2_aarch64.whl", upload-time = 2026-03-25T23:33:40Z, size = 4405530, hashes = { sha256 = "2b417edbe8877cda9022dde3a008e2deb50be9c407eef034aeeb3a8b11d9db3c" } }, + { url = "https://files.pythonhosted.org/packages/7e/c9/9f9cea13ee2dbde070424e0c4f621c091a91ffcc504ffea5e74f0e1daeff/cryptography-46.0.6-cp311-abi3-musllinux_1_2_x86_64.whl", upload-time = 2026-03-25T23:33:42Z, size = 4667896, hashes = { sha256 = "380343e0653b1c9d7e1f55b52aaa2dbb2fdf2730088d48c43ca1c7c0abb7cc2f" } }, + { url = "https://files.pythonhosted.org/packages/ad/b5/1895bc0821226f129bc74d00eccfc6a5969e2028f8617c09790bf89c185e/cryptography-46.0.6-cp311-abi3-win32.whl", upload-time = 2026-03-25T23:33:45Z, size = 3026348, hashes = { sha256 = "bcb87663e1f7b075e48c3be3ecb5f0b46c8fc50b50a97cf264e7f60242dca3f2" } }, + { url = "https://files.pythonhosted.org/packages/c3/f8/c9bcbf0d3e6ad288b9d9aa0b1dee04b063d19e8c4f871855a03ab3a297ab/cryptography-46.0.6-cp311-abi3-win_amd64.whl", upload-time = 2026-03-25T23:33:46Z, size = 3483896, hashes = { sha256 = "6739d56300662c468fddb0e5e291f9b4d084bead381667b9e654c7dd81705124" } }, + { url = "https://files.pythonhosted.org/packages/01/41/3a578f7fd5c70611c0aacba52cd13cb364a5dee895a5c1d467208a9380b0/cryptography-46.0.6-cp314-cp314t-macosx_10_9_universal2.whl", upload-time = 2026-03-25T23:33:48Z, size = 7117147, hashes = { sha256 = "2ef9e69886cbb137c2aef9772c2e7138dc581fad4fcbcf13cc181eb5a3ab6275" } }, + { url = "https://files.pythonhosted.org/packages/fa/87/887f35a6fca9dde90cad08e0de0c89263a8e59b2d2ff904fd9fcd8025b6f/cryptography-46.0.6-cp314-cp314t-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", upload-time = 2026-03-25T23:33:49Z, size = 4266221, hashes = { sha256 = "7f417f034f91dcec1cb6c5c35b07cdbb2ef262557f701b4ecd803ee8cefed4f4" } }, + { url = "https://files.pythonhosted.org/packages/aa/a8/0a90c4f0b0871e0e3d1ed126aed101328a8a57fd9fd17f00fb67e82a51ca/cryptography-46.0.6-cp314-cp314t-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", upload-time = 2026-03-25T23:33:52Z, size = 4408952, hashes = { sha256 = "d24c13369e856b94892a89ddf70b332e0b70ad4a5c43cf3e9cb71d6d7ffa1f7b" } }, + { url = "https://files.pythonhosted.org/packages/16/0b/b239701eb946523e4e9f329336e4ff32b1247e109cbab32d1a7b61da8ed7/cryptography-46.0.6-cp314-cp314t-manylinux_2_28_aarch64.whl", upload-time = 2026-03-25T23:33:54Z, size = 4270141, hashes = { sha256 = "aad75154a7ac9039936d50cf431719a2f8d4ed3d3c277ac03f3339ded1a5e707" } }, + { url = "https://files.pythonhosted.org/packages/0f/a8/976acdd4f0f30df7b25605f4b9d3d89295351665c2091d18224f7ad5cdbf/cryptography-46.0.6-cp314-cp314t-manylinux_2_28_ppc64le.whl", upload-time = 2026-03-25T23:33:55Z, size = 4904178, hashes = { sha256 = "3c21d92ed15e9cfc6eb64c1f5a0326db22ca9c2566ca46d845119b45b4400361" } }, + { url = "https://files.pythonhosted.org/packages/b1/1b/bf0e01a88efd0e59679b69f42d4afd5bced8700bb5e80617b2d63a3741af/cryptography-46.0.6-cp314-cp314t-manylinux_2_28_x86_64.whl", upload-time = 2026-03-25T23:33:57Z, size = 4441812, hashes = { sha256 = "4668298aef7cddeaf5c6ecc244c2302a2b8e40f384255505c22875eebb47888b" } }, + { url = "https://files.pythonhosted.org/packages/bb/8b/11df86de2ea389c65aa1806f331cae145f2ed18011f30234cc10ca253de8/cryptography-46.0.6-cp314-cp314t-manylinux_2_31_armv7l.whl", upload-time = 2026-03-25T23:33:59Z, size = 3963923, hashes = { sha256 = "8ce35b77aaf02f3b59c90b2c8a05c73bac12cea5b4e8f3fbece1f5fddea5f0ca" } }, + { url = "https://files.pythonhosted.org/packages/91/e0/207fb177c3a9ef6a8108f234208c3e9e76a6aa8cf20d51932916bd43bda0/cryptography-46.0.6-cp314-cp314t-manylinux_2_34_aarch64.whl", upload-time = 2026-03-25T23:34:00Z, size = 4269695, hashes = { sha256 = "c89eb37fae9216985d8734c1afd172ba4927f5a05cfd9bf0e4863c6d5465b013" } }, + { url = "https://files.pythonhosted.org/packages/21/5e/19f3260ed1e95bced52ace7501fabcd266df67077eeb382b79c81729d2d3/cryptography-46.0.6-cp314-cp314t-manylinux_2_34_ppc64le.whl", upload-time = 2026-03-25T23:34:02Z, size = 4869785, hashes = { sha256 = "ed418c37d095aeddf5336898a132fba01091f0ac5844e3e8018506f014b6d2c4" } }, + { url = "https://files.pythonhosted.org/packages/10/38/cd7864d79aa1d92ef6f1a584281433419b955ad5a5ba8d1eb6c872165bcb/cryptography-46.0.6-cp314-cp314t-manylinux_2_34_x86_64.whl", upload-time = 2026-03-25T23:34:04Z, size = 4441404, hashes = { sha256 = "69cf0056d6947edc6e6760e5f17afe4bea06b56a9ac8a06de9d2bd6b532d4f3a" } }, + { url = "https://files.pythonhosted.org/packages/09/0a/4fe7a8d25fed74419f91835cf5829ade6408fd1963c9eae9c4bce390ecbb/cryptography-46.0.6-cp314-cp314t-musllinux_1_2_aarch64.whl", upload-time = 2026-03-25T23:34:06Z, size = 4397549, hashes = { sha256 = "8e7304c4f4e9490e11efe56af6713983460ee0780f16c63f219984dab3af9d2d" } }, + { url = "https://files.pythonhosted.org/packages/5f/a0/7d738944eac6513cd60a8da98b65951f4a3b279b93479a7e8926d9cd730b/cryptography-46.0.6-cp314-cp314t-musllinux_1_2_x86_64.whl", upload-time = 2026-03-25T23:34:07Z, size = 4651874, hashes = { sha256 = "b928a3ca837c77a10e81a814a693f2295200adb3352395fad024559b7be7a736" } }, + { url = "https://files.pythonhosted.org/packages/cb/f1/c2326781ca05208845efca38bf714f76939ae446cd492d7613808badedf1/cryptography-46.0.6-cp314-cp314t-win32.whl", upload-time = 2026-03-25T23:34:09Z, size = 3001511, hashes = { sha256 = "97c8115b27e19e592a05c45d0dd89c57f81f841cc9880e353e0d3bf25b2139ed" } }, + { url = "https://files.pythonhosted.org/packages/c9/57/fe4a23eb549ac9d903bd4698ffda13383808ef0876cc912bcb2838799ece/cryptography-46.0.6-cp314-cp314t-win_amd64.whl", upload-time = 2026-03-25T23:34:11Z, size = 3471692, hashes = { sha256 = "c797e2517cb7880f8297e2c0f43bb910e91381339336f75d2c1c2cbf811b70b4" } }, + { url = "https://files.pythonhosted.org/packages/c4/cc/f330e982852403da79008552de9906804568ae9230da8432f7496ce02b71/cryptography-46.0.6-cp38-abi3-macosx_10_9_universal2.whl", upload-time = 2026-03-25T23:34:13Z, size = 7162776, hashes = { sha256 = "12cae594e9473bca1a7aceb90536060643128bb274fcea0fc459ab90f7d1ae7a" } }, + { url = "https://files.pythonhosted.org/packages/49/b3/dc27efd8dcc4bff583b3f01d4a3943cd8b5821777a58b3a6a5f054d61b79/cryptography-46.0.6-cp38-abi3-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", upload-time = 2026-03-25T23:34:15Z, size = 4270529, hashes = { sha256 = "639301950939d844a9e1c4464d7e07f902fe9a7f6b215bb0d4f28584729935d8" } }, + { url = "https://files.pythonhosted.org/packages/e6/05/e8d0e6eb4f0d83365b3cb0e00eb3c484f7348db0266652ccd84632a3d58d/cryptography-46.0.6-cp38-abi3-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", upload-time = 2026-03-25T23:34:16Z, size = 4414827, hashes = { sha256 = "ed3775295fb91f70b4027aeba878d79b3e55c0b3e97eaa4de71f8f23a9f2eb77" } }, + { url = "https://files.pythonhosted.org/packages/2f/97/daba0f5d2dc6d855e2dcb70733c812558a7977a55dd4a6722756628c44d1/cryptography-46.0.6-cp38-abi3-manylinux_2_28_aarch64.whl", upload-time = 2026-03-25T23:34:18Z, size = 4271265, hashes = { sha256 = "8927ccfbe967c7df312ade694f987e7e9e22b2425976ddbf28271d7e58845290" } }, + { url = "https://files.pythonhosted.org/packages/89/06/fe1fce39a37ac452e58d04b43b0855261dac320a2ebf8f5260dd55b201a9/cryptography-46.0.6-cp38-abi3-manylinux_2_28_ppc64le.whl", upload-time = 2026-03-25T23:34:20Z, size = 4916800, hashes = { sha256 = "b12c6b1e1651e42ab5de8b1e00dc3b6354fdfd778e7fa60541ddacc27cd21410" } }, + { url = "https://files.pythonhosted.org/packages/ff/8a/b14f3101fe9c3592603339eb5d94046c3ce5f7fc76d6512a2d40efd9724e/cryptography-46.0.6-cp38-abi3-manylinux_2_28_x86_64.whl", upload-time = 2026-03-25T23:34:22Z, size = 4448771, hashes = { sha256 = "063b67749f338ca9c5a0b7fe438a52c25f9526b851e24e6c9310e7195aad3b4d" } }, + { url = "https://files.pythonhosted.org/packages/01/b3/0796998056a66d1973fd52ee89dc1bb3b6581960a91ad4ac705f182d398f/cryptography-46.0.6-cp38-abi3-manylinux_2_31_armv7l.whl", upload-time = 2026-03-25T23:34:24Z, size = 3978333, hashes = { sha256 = "02fad249cb0e090b574e30b276a3da6a149e04ee2f049725b1f69e7b8351ec70" } }, + { url = "https://files.pythonhosted.org/packages/c5/3d/db200af5a4ffd08918cd55c08399dc6c9c50b0bc72c00a3246e099d3a849/cryptography-46.0.6-cp38-abi3-manylinux_2_34_aarch64.whl", upload-time = 2026-03-25T23:34:25Z, size = 4271069, hashes = { sha256 = "7e6142674f2a9291463e5e150090b95a8519b2fb6e6aaec8917dd8d094ce750d" } }, + { url = "https://files.pythonhosted.org/packages/d7/18/61acfd5b414309d74ee838be321c636fe71815436f53c9f0334bf19064fa/cryptography-46.0.6-cp38-abi3-manylinux_2_34_ppc64le.whl", upload-time = 2026-03-25T23:34:27Z, size = 4878358, hashes = { sha256 = "456b3215172aeefb9284550b162801d62f5f264a081049a3e94307fe20792cfa" } }, + { url = "https://files.pythonhosted.org/packages/8b/65/5bf43286d566f8171917cae23ac6add941654ccf085d739195a4eacf1674/cryptography-46.0.6-cp38-abi3-manylinux_2_34_x86_64.whl", upload-time = 2026-03-25T23:34:29Z, size = 4448061, hashes = { sha256 = "341359d6c9e68834e204ceaf25936dffeafea3829ab80e9503860dcc4f4dac58" } }, + { url = "https://files.pythonhosted.org/packages/e0/25/7e49c0fa7205cf3597e525d156a6bce5b5c9de1fd7e8cb01120e459f205a/cryptography-46.0.6-cp38-abi3-musllinux_1_2_aarch64.whl", upload-time = 2026-03-25T23:34:32Z, size = 4399103, hashes = { sha256 = "9a9c42a2723999a710445bc0d974e345c32adfd8d2fac6d8a251fa829ad31cfb" } }, + { url = "https://files.pythonhosted.org/packages/44/46/466269e833f1c4718d6cd496ffe20c56c9c8d013486ff66b4f69c302a68d/cryptography-46.0.6-cp38-abi3-musllinux_1_2_x86_64.whl", upload-time = 2026-03-25T23:34:33Z, size = 4659255, hashes = { sha256 = "6617f67b1606dfd9fe4dbfa354a9508d4a6d37afe30306fe6c101b7ce3274b72" } }, + { url = "https://files.pythonhosted.org/packages/0a/09/ddc5f630cc32287d2c953fc5d32705e63ec73e37308e5120955316f53827/cryptography-46.0.6-cp38-abi3-win32.whl", upload-time = 2026-03-25T23:34:35Z, size = 3010660, hashes = { sha256 = "7f6690b6c55e9c5332c0b59b9c8a3fb232ebf059094c17f9019a51e9827df91c" } }, + { url = "https://files.pythonhosted.org/packages/1b/82/ca4893968aeb2709aacfb57a30dec6fa2ab25b10fa9f064b8882ce33f599/cryptography-46.0.6-cp38-abi3-win_amd64.whl", upload-time = 2026-03-25T23:34:37Z, size = 3471160, hashes = { sha256 = "79e865c642cfc5c0b3eb12af83c35c5aeff4fa5c672dc28c43721c2c9fdd2f0f" } }, + { url = "https://files.pythonhosted.org/packages/2e/84/7ccff00ced5bac74b775ce0beb7d1be4e8637536b522b5df9b73ada42da2/cryptography-46.0.6-pp311-pypy311_pp73-macosx_11_0_arm64.whl", upload-time = 2026-03-25T23:34:38Z, size = 3475444, hashes = { sha256 = "2ea0f37e9a9cf0df2952893ad145fd9627d326a59daec9b0802480fa3bcd2ead" } }, + { url = "https://files.pythonhosted.org/packages/bc/1f/4c926f50df7749f000f20eede0c896769509895e2648db5da0ed55db711d/cryptography-46.0.6-pp311-pypy311_pp73-manylinux_2_28_aarch64.whl", upload-time = 2026-03-25T23:34:40Z, size = 4218227, hashes = { sha256 = "a3e84d5ec9ba01f8fd03802b2147ba77f0c8f2617b2aff254cedd551844209c8" } }, + { url = "https://files.pythonhosted.org/packages/c6/65/707be3ffbd5f786028665c3223e86e11c4cda86023adbc56bd72b1b6bab5/cryptography-46.0.6-pp311-pypy311_pp73-manylinux_2_28_x86_64.whl", upload-time = 2026-03-25T23:34:42Z, size = 4381399, hashes = { sha256 = "12f0fa16cc247b13c43d56d7b35287ff1569b5b1f4c5e87e92cc4fcc00cd10c0" } }, + { url = "https://files.pythonhosted.org/packages/f3/6d/73557ed0ef7d73d04d9aba745d2c8e95218213687ee5e76b7d236a5030fc/cryptography-46.0.6-pp311-pypy311_pp73-manylinux_2_34_aarch64.whl", upload-time = 2026-03-25T23:34:44Z, size = 4217595, hashes = { sha256 = "50575a76e2951fe7dbd1f56d181f8c5ceeeb075e9ff88e7ad997d2f42af06e7b" } }, + { url = "https://files.pythonhosted.org/packages/9e/c5/e1594c4eec66a567c3ac4400008108a415808be2ce13dcb9a9045c92f1a0/cryptography-46.0.6-pp311-pypy311_pp73-manylinux_2_34_x86_64.whl", upload-time = 2026-03-25T23:34:46Z, size = 4380912, hashes = { sha256 = "90e5f0a7b3be5f40c3a0a0eafb32c681d8d2c181fc2a1bdabe9b3f611d9f6b1a" } }, + { url = "https://files.pythonhosted.org/packages/1a/89/843b53614b47f97fe1abc13f9a86efa5ec9e275292c457af1d4a60dc80e0/cryptography-46.0.6-pp311-pypy311_pp73-win_amd64.whl", upload-time = 2026-03-25T23:34:48Z, size = 3409955, hashes = { sha256 = "6728c49e3b2c180ef26f8e9f0a883a2c585638db64cf265b49c9ba10652d430e" } }, +] + +[[packages]] +name = "filelock" +version = "3.18.0" +sdist = { url = "https://files.pythonhosted.org/packages/0a/10/c23352565a6544bdc5353e0b15fc1c563352101f30e24bf500207a54df9a/filelock-3.18.0.tar.gz", upload-time = 2025-03-14T07:11:40Z, size = 18075, hashes = { sha256 = "adbc88eabb99d2fec8c9c1b229b171f18afa655400173ddc653d5d01501fb9f2" } } +wheels = [{ url = "https://files.pythonhosted.org/packages/4d/36/2a115987e2d8c300a974597416d9de88f2444426de9571f4b59b2cca3acc/filelock-3.18.0-py3-none-any.whl", upload-time = 2025-03-14T07:11:39Z, size = 16215, hashes = { sha256 = "c401f4f8377c4464e6db25fff06205fd89bdd83b65eb0488ed1b160f780e21de" } }] + +[[packages]] +name = "gitdb" +version = "4.0.12" +sdist = { url = "https://files.pythonhosted.org/packages/72/94/63b0fc47eb32792c7ba1fe1b694daec9a63620db1e313033d18140c2320a/gitdb-4.0.12.tar.gz", upload-time = 2025-01-02T07:20:46Z, size = 394684, hashes = { sha256 = "5ef71f855d191a3326fcfbc0d5da835f26b13fbcba60c32c21091c349ffdb571" } } +wheels = [{ url = "https://files.pythonhosted.org/packages/a0/61/5c78b91c3143ed5c14207f463aecfc8f9dbb5092fb2869baf37c273b2705/gitdb-4.0.12-py3-none-any.whl", upload-time = 2025-01-02T07:20:43Z, size = 62794, hashes = { sha256 = "67073e15955400952c6565cc3e707c554a4eea2e428946f7a4c162fab9bd9bcf" } }] + +[[packages]] +name = "gitpython" +version = "3.1.58" +sdist = { url = "https://files.pythonhosted.org/packages/26/d6/5f358ff283325580c2003a6d953aea18cfe10ae87b46f5ebc80fa3a386dc/gitpython-3.1.58.tar.gz", upload-time = 2026-08-04T15:05:49Z, size = 228498, hashes = { sha256 = "621416df10ef3fd0e19fabf9172ddeed0fa704d353d04f194eec56a625a95b22" } } +wheels = [{ url = "https://files.pythonhosted.org/packages/ec/0c/9d8752098bc442f0726e64aa6135940b3a96809915d1aa4206c1bb97881d/gitpython-3.1.58-py3-none-any.whl", upload-time = 2026-08-04T15:05:48Z, size = 220183, hashes = { sha256 = "d331e722577f0fd7fc1f857419b3ecc07af66282b933d2a4d95f84a042fdd50f" } }] + +[[packages]] +name = "id" +version = "1.5.0" +sdist = { url = "https://files.pythonhosted.org/packages/22/11/102da08f88412d875fa2f1a9a469ff7ad4c874b0ca6fed0048fe385bdb3d/id-1.5.0.tar.gz", upload-time = 2024-12-04T19:53:05Z, size = 15237, hashes = { sha256 = "292cb8a49eacbbdbce97244f47a97b4c62540169c976552e497fd57df0734c1d" } } +wheels = [{ url = "https://files.pythonhosted.org/packages/9f/cb/18326d2d89ad3b0dd143da971e77afd1e6ca6674f1b1c3df4b6bec6279fc/id-1.5.0-py3-none-any.whl", upload-time = 2024-12-04T19:53:03Z, size = 13611, hashes = { sha256 = "f1434e1cef91f2cbb8a4ec64663d5a23b9ed43ef44c4c957d02583d61714c658" } }] + +[[packages]] +name = "idna" +version = "3.10" +sdist = { url = "https://files.pythonhosted.org/packages/f1/70/7703c29685631f5a7590aa73f1f1d3fa9a380e654b86af429e0934a32f7d/idna-3.10.tar.gz", upload-time = 2024-09-15T18:07:39Z, size = 190490, hashes = { sha256 = "12f65c9b470abda6dc35cf8e63cc574b1c52b11df2c86030af0ac09b01b13ea9" } } +wheels = [{ url = "https://files.pythonhosted.org/packages/76/c6/c88e154df9c4e1a2a66ccf0005a88dfb2650c1dffb6f5ce603dfbd452ce3/idna-3.10-py3-none-any.whl", upload-time = 2024-09-15T18:07:37Z, size = 70442, hashes = { sha256 = "946d195a0d259cbba61165e88e65941f16e9b36ea6ddb97f00452bae8b1287d3" } }] + +[[packages]] +name = "importlib-metadata" +version = "8.7.0" +marker = "python_full_version < '3.12'" +sdist = { url = "https://files.pythonhosted.org/packages/76/66/650a33bd90f786193e4de4b3ad86ea60b53c89b669a5c7be931fac31cdb0/importlib_metadata-8.7.0.tar.gz", upload-time = 2025-04-27T15:29:01Z, size = 56641, hashes = { sha256 = "d13b81ad223b890aa16c5471f2ac3056cf76c5f10f82d6f9292f0b415f389000" } } +wheels = [{ url = "https://files.pythonhosted.org/packages/20/b0/36bd937216ec521246249be3bf9855081de4c5e06a0c9b4219dbeda50373/importlib_metadata-8.7.0-py3-none-any.whl", upload-time = 2025-04-27T15:29:00Z, size = 27656, hashes = { sha256 = "e5dd1551894c77868a30651cef00984d50e1002d06942a7101d34870c5f02afd" } }] + +[[packages]] +name = "jaraco-classes" +version = "3.4.0" +sdist = { url = "https://files.pythonhosted.org/packages/06/c0/ed4a27bc5571b99e3cff68f8a9fa5b56ff7df1c2251cc715a652ddd26402/jaraco.classes-3.4.0.tar.gz", upload-time = 2024-03-31T07:27:36Z, size = 11780, hashes = { sha256 = "47a024b51d0239c0dd8c8540c6c7f484be3b8fcf0b2d85c13825780d3b3f3acd" } } +wheels = [{ url = "https://files.pythonhosted.org/packages/7f/66/b15ce62552d84bbfcec9a4873ab79d993a1dd4edb922cbfccae192bd5b5f/jaraco.classes-3.4.0-py3-none-any.whl", upload-time = 2024-03-31T07:27:34Z, size = 6777, hashes = { sha256 = "f662826b6bed8cace05e7ff873ce0f9283b5c924470fe664fff1c2f00f581790" } }] + +[[packages]] +name = "jaraco-context" +version = "6.0.1" +sdist = { url = "https://files.pythonhosted.org/packages/df/ad/f3777b81bf0b6e7bc7514a1656d3e637b2e8e15fab2ce3235730b3e7a4e6/jaraco_context-6.0.1.tar.gz", upload-time = 2024-08-20T03:39:27Z, size = 13912, hashes = { sha256 = "9bae4ea555cf0b14938dc0aee7c9f32ed303aa20a3b73e7dc80111628792d1b3" } } +wheels = [{ url = "https://files.pythonhosted.org/packages/ff/db/0c52c4cf5e4bd9f5d7135ec7669a3a767af21b3a308e1ed3674881e52b62/jaraco.context-6.0.1-py3-none-any.whl", upload-time = 2024-08-20T03:39:25Z, size = 6825, hashes = { sha256 = "f797fc481b490edb305122c9181830a3a5b76d84ef6d1aef2fb9b47ab956f9e4" } }] + +[[packages]] +name = "jaraco-functools" +version = "4.2.1" +sdist = { url = "https://files.pythonhosted.org/packages/49/1c/831faaaa0f090b711c355c6d8b2abf277c72133aab472b6932b03322294c/jaraco_functools-4.2.1.tar.gz", upload-time = 2025-06-21T19:22:03Z, size = 19661, hashes = { sha256 = "be634abfccabce56fa3053f8c7ebe37b682683a4ee7793670ced17bab0087353" } } +wheels = [{ url = "https://files.pythonhosted.org/packages/f3/fd/179a20f832824514df39a90bb0e5372b314fea99f217f5ab942b10a8a4e8/jaraco_functools-4.2.1-py3-none-any.whl", upload-time = 2025-06-21T19:22:02Z, size = 10349, hashes = { sha256 = "590486285803805f4b1f99c60ca9e94ed348d4added84b74c7a12885561e524e" } }] + +[[packages]] +name = "jeepney" +version = "0.9.0" +marker = "sys_platform == 'linux'" +sdist = { url = "https://files.pythonhosted.org/packages/7b/6f/357efd7602486741aa73ffc0617fb310a29b588ed0fd69c2399acbb85b0c/jeepney-0.9.0.tar.gz", upload-time = 2025-02-27T18:51:01Z, size = 106758, hashes = { sha256 = "cf0e9e845622b81e4a28df94c40345400256ec608d0e55bb8a3feaa9163f5732" } } +wheels = [{ url = "https://files.pythonhosted.org/packages/b2/a3/e137168c9c44d18eff0376253da9f1e9234d0239e0ee230d2fee6cea8e55/jeepney-0.9.0-py3-none-any.whl", upload-time = 2025-02-27T18:51:00Z, size = 49010, hashes = { sha256 = "97e5714520c16fc0a45695e5365a2e11b81ea79bba796e26f9f1d178cb182683" } }] + +[[packages]] +name = "jinja2" +version = "3.1.6" +sdist = { url = "https://files.pythonhosted.org/packages/df/bf/f7da0350254c0ed7c72f3e33cef02e048281fec7ecec5f032d4aac52226b/jinja2-3.1.6.tar.gz", upload-time = 2025-03-05T20:05:02Z, size = 245115, hashes = { sha256 = "0137fb05990d35f1275a587e9aee6d56da821fc83491a0fb838183be43f66d6d" } } +wheels = [{ url = "https://files.pythonhosted.org/packages/62/a1/3d680cbfd5f4b8f15abc1d571870c5fc3e594bb582bc3b64ea099db13e56/jinja2-3.1.6-py3-none-any.whl", upload-time = 2025-03-05T20:05:00Z, size = 134899, hashes = { sha256 = "85ece4451f492d0c13c5dd7c13a64681a86afae63a5f347908daf103ce6d2f67" } }] + +[[packages]] +name = "jmespath" +version = "1.0.1" +sdist = { url = "https://files.pythonhosted.org/packages/00/2a/e867e8531cf3e36b41201936b7fa7ba7b5702dbef42922193f05c8976cd6/jmespath-1.0.1.tar.gz", upload-time = 2022-06-17T18:00:12Z, size = 25843, hashes = { sha256 = "90261b206d6defd58fdd5e85f478bf633a2901798906be2ad389150c5c60edbe" } } +wheels = [{ url = "https://files.pythonhosted.org/packages/31/b4/b9b800c45527aadd64d5b442f9b932b00648617eb5d63d2c7a6587b7cafc/jmespath-1.0.1-py3-none-any.whl", upload-time = 2022-06-17T18:00:10Z, size = 20256, hashes = { sha256 = "02e2e4cc71b5bcab88332eebf907519190dd9e6e82107fa7f83b1003a6252980" } }] + +[[packages]] +name = "keyring" +version = "25.6.0" +sdist = { url = "https://files.pythonhosted.org/packages/70/09/d904a6e96f76ff214be59e7aa6ef7190008f52a0ab6689760a98de0bf37d/keyring-25.6.0.tar.gz", upload-time = 2024-12-25T15:26:45Z, size = 62750, hashes = { sha256 = "0b39998aa941431eb3d9b0d4b2460bc773b9df6fed7621c2dfb291a7e0187a66" } } +wheels = [{ url = "https://files.pythonhosted.org/packages/d3/32/da7f44bcb1105d3e88a0b74ebdca50c59121d2ddf71c9e34ba47df7f3a56/keyring-25.6.0-py3-none-any.whl", upload-time = 2024-12-25T15:26:44Z, size = 39085, hashes = { sha256 = "552a3f7af126ece7ed5c89753650eec89c7eaae8617d0aa4d9ad2b75111266bd" } }] + +[[packages]] +name = "markdown-it-py" +version = "3.0.0" +sdist = { url = "https://files.pythonhosted.org/packages/38/71/3b932df36c1a044d397a1f92d1cf91ee0a503d91e470cbd670aa66b07ed0/markdown-it-py-3.0.0.tar.gz", upload-time = 2023-06-03T06:41:14Z, size = 74596, hashes = { sha256 = "e3f60a94fa066dc52ec76661e37c851cb232d92f9886b15cb560aaada2df8feb" } } +wheels = [{ url = "https://files.pythonhosted.org/packages/42/d7/1ec15b46af6af88f19b8e5ffea08fa375d433c998b8a7639e76935c14f1f/markdown_it_py-3.0.0-py3-none-any.whl", upload-time = 2023-06-03T06:41:11Z, size = 87528, hashes = { sha256 = "355216845c60bd96232cd8d8c40e8f9765cc86f46880e43a8fd22dc1a1a8cab1" } }] + +[[packages]] +name = "markupsafe" +version = "3.0.2" +sdist = { url = "https://files.pythonhosted.org/packages/b2/97/5d42485e71dfc078108a86d6de8fa46db44a1a9295e89c5d6d4a06e23a62/markupsafe-3.0.2.tar.gz", upload-time = 2024-10-18T15:21:54Z, size = 20537, hashes = { sha256 = "ee55d3edf80167e48ea11a923c7386f4669df67d7994554387f84e7d8b0a2bf0" } } +wheels = [ + { url = "https://files.pythonhosted.org/packages/04/90/d08277ce111dd22f77149fd1a5d4653eeb3b3eaacbdfcbae5afb2600eebd/MarkupSafe-3.0.2-cp310-cp310-macosx_10_9_universal2.whl", upload-time = 2024-10-18T15:20:51Z, size = 14357, hashes = { sha256 = "7e94c425039cde14257288fd61dcfb01963e658efbc0ff54f5306b06054700f8" } }, + { url = "https://files.pythonhosted.org/packages/04/e1/6e2194baeae0bca1fae6629dc0cbbb968d4d941469cbab11a3872edff374/MarkupSafe-3.0.2-cp310-cp310-macosx_11_0_arm64.whl", upload-time = 2024-10-18T15:20:52Z, size = 12393, hashes = { sha256 = "9e2d922824181480953426608b81967de705c3cef4d1af983af849d7bd619158" } }, + { url = "https://files.pythonhosted.org/packages/1d/69/35fa85a8ece0a437493dc61ce0bb6d459dcba482c34197e3efc829aa357f/MarkupSafe-3.0.2-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", upload-time = 2024-10-18T15:20:53Z, size = 21732, hashes = { sha256 = "38a9ef736c01fccdd6600705b09dc574584b89bea478200c5fbf112a6b0d5579" } }, + { url = "https://files.pythonhosted.org/packages/22/35/137da042dfb4720b638d2937c38a9c2df83fe32d20e8c8f3185dbfef05f7/MarkupSafe-3.0.2-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", upload-time = 2024-10-18T15:20:55Z, size = 20866, hashes = { sha256 = "bbcb445fa71794da8f178f0f6d66789a28d7319071af7a496d4d507ed566270d" } }, + { url = "https://files.pythonhosted.org/packages/29/28/6d029a903727a1b62edb51863232152fd335d602def598dade38996887f0/MarkupSafe-3.0.2-cp310-cp310-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", upload-time = 2024-10-18T15:20:55Z, size = 20964, hashes = { sha256 = "57cb5a3cf367aeb1d316576250f65edec5bb3be939e9247ae594b4bcbc317dfb" } }, + { url = "https://files.pythonhosted.org/packages/cc/cd/07438f95f83e8bc028279909d9c9bd39e24149b0d60053a97b2bc4f8aa51/MarkupSafe-3.0.2-cp310-cp310-musllinux_1_2_aarch64.whl", upload-time = 2024-10-18T15:20:57Z, size = 21977, hashes = { sha256 = "3809ede931876f5b2ec92eef964286840ed3540dadf803dd570c3b7e13141a3b" } }, + { url = "https://files.pythonhosted.org/packages/29/01/84b57395b4cc062f9c4c55ce0df7d3108ca32397299d9df00fedd9117d3d/MarkupSafe-3.0.2-cp310-cp310-musllinux_1_2_i686.whl", upload-time = 2024-10-18T15:20:58Z, size = 21366, hashes = { sha256 = "e07c3764494e3776c602c1e78e298937c3315ccc9043ead7e685b7f2b8d47b3c" } }, + { url = "https://files.pythonhosted.org/packages/bd/6e/61ebf08d8940553afff20d1fb1ba7294b6f8d279df9fd0c0db911b4bbcfd/MarkupSafe-3.0.2-cp310-cp310-musllinux_1_2_x86_64.whl", upload-time = 2024-10-18T15:20:59Z, size = 21091, hashes = { sha256 = "b424c77b206d63d500bcb69fa55ed8d0e6a3774056bdc4839fc9298a7edca171" } }, + { url = "https://files.pythonhosted.org/packages/11/23/ffbf53694e8c94ebd1e7e491de185124277964344733c45481f32ede2499/MarkupSafe-3.0.2-cp310-cp310-win32.whl", upload-time = 2024-10-18T15:21:00Z, size = 15065, hashes = { sha256 = "fcabf5ff6eea076f859677f5f0b6b5c1a51e70a376b0579e0eadef8db48c6b50" } }, + { url = "https://files.pythonhosted.org/packages/44/06/e7175d06dd6e9172d4a69a72592cb3f7a996a9c396eee29082826449bbc3/MarkupSafe-3.0.2-cp310-cp310-win_amd64.whl", upload-time = 2024-10-18T15:21:01Z, size = 15514, hashes = { sha256 = "6af100e168aa82a50e186c82875a5893c5597a0c1ccdb0d8b40240b1f28b969a" } }, + { url = "https://files.pythonhosted.org/packages/6b/28/bbf83e3f76936960b850435576dd5e67034e200469571be53f69174a2dfd/MarkupSafe-3.0.2-cp311-cp311-macosx_10_9_universal2.whl", upload-time = 2024-10-18T15:21:02Z, size = 14353, hashes = { sha256 = "9025b4018f3a1314059769c7bf15441064b2207cb3f065e6ea1e7359cb46db9d" } }, + { url = "https://files.pythonhosted.org/packages/6c/30/316d194b093cde57d448a4c3209f22e3046c5bb2fb0820b118292b334be7/MarkupSafe-3.0.2-cp311-cp311-macosx_11_0_arm64.whl", upload-time = 2024-10-18T15:21:02Z, size = 12392, hashes = { sha256 = "93335ca3812df2f366e80509ae119189886b0f3c2b81325d39efdb84a1e2ae93" } }, + { url = "https://files.pythonhosted.org/packages/f2/96/9cdafba8445d3a53cae530aaf83c38ec64c4d5427d975c974084af5bc5d2/MarkupSafe-3.0.2-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", upload-time = 2024-10-18T15:21:03Z, size = 23984, hashes = { sha256 = "2cb8438c3cbb25e220c2ab33bb226559e7afb3baec11c4f218ffa7308603c832" } }, + { url = "https://files.pythonhosted.org/packages/f1/a4/aefb044a2cd8d7334c8a47d3fb2c9f328ac48cb349468cc31c20b539305f/MarkupSafe-3.0.2-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", upload-time = 2024-10-18T15:21:06Z, size = 23120, hashes = { sha256 = "a123e330ef0853c6e822384873bef7507557d8e4a082961e1defa947aa59ba84" } }, + { url = "https://files.pythonhosted.org/packages/8d/21/5e4851379f88f3fad1de30361db501300d4f07bcad047d3cb0449fc51f8c/MarkupSafe-3.0.2-cp311-cp311-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", upload-time = 2024-10-18T15:21:07Z, size = 23032, hashes = { sha256 = "1e084f686b92e5b83186b07e8a17fc09e38fff551f3602b249881fec658d3eca" } }, + { url = "https://files.pythonhosted.org/packages/00/7b/e92c64e079b2d0d7ddf69899c98842f3f9a60a1ae72657c89ce2655c999d/MarkupSafe-3.0.2-cp311-cp311-musllinux_1_2_aarch64.whl", upload-time = 2024-10-18T15:21:08Z, size = 24057, hashes = { sha256 = "d8213e09c917a951de9d09ecee036d5c7d36cb6cb7dbaece4c71a60d79fb9798" } }, + { url = "https://files.pythonhosted.org/packages/f9/ac/46f960ca323037caa0a10662ef97d0a4728e890334fc156b9f9e52bcc4ca/MarkupSafe-3.0.2-cp311-cp311-musllinux_1_2_i686.whl", upload-time = 2024-10-18T15:21:09Z, size = 23359, hashes = { sha256 = "5b02fb34468b6aaa40dfc198d813a641e3a63b98c2b05a16b9f80b7ec314185e" } }, + { url = "https://files.pythonhosted.org/packages/69/84/83439e16197337b8b14b6a5b9c2105fff81d42c2a7c5b58ac7b62ee2c3b1/MarkupSafe-3.0.2-cp311-cp311-musllinux_1_2_x86_64.whl", upload-time = 2024-10-18T15:21:10Z, size = 23306, hashes = { sha256 = "0bff5e0ae4ef2e1ae4fdf2dfd5b76c75e5c2fa4132d05fc1b0dabcd20c7e28c4" } }, + { url = "https://files.pythonhosted.org/packages/9a/34/a15aa69f01e2181ed8d2b685c0d2f6655d5cca2c4db0ddea775e631918cd/MarkupSafe-3.0.2-cp311-cp311-win32.whl", upload-time = 2024-10-18T15:21:11Z, size = 15094, hashes = { sha256 = "6c89876f41da747c8d3677a2b540fb32ef5715f97b66eeb0c6b66f5e3ef6f59d" } }, + { url = "https://files.pythonhosted.org/packages/da/b8/3a3bd761922d416f3dc5d00bfbed11f66b1ab89a0c2b6e887240a30b0f6b/MarkupSafe-3.0.2-cp311-cp311-win_amd64.whl", upload-time = 2024-10-18T15:21:12Z, size = 15521, hashes = { sha256 = "70a87b411535ccad5ef2f1df5136506a10775d267e197e4cf531ced10537bd6b" } }, + { url = "https://files.pythonhosted.org/packages/22/09/d1f21434c97fc42f09d290cbb6350d44eb12f09cc62c9476effdb33a18aa/MarkupSafe-3.0.2-cp312-cp312-macosx_10_13_universal2.whl", upload-time = 2024-10-18T15:21:13Z, size = 14274, hashes = { sha256 = "9778bd8ab0a994ebf6f84c2b949e65736d5575320a17ae8984a77fab08db94cf" } }, + { url = "https://files.pythonhosted.org/packages/6b/b0/18f76bba336fa5aecf79d45dcd6c806c280ec44538b3c13671d49099fdd0/MarkupSafe-3.0.2-cp312-cp312-macosx_11_0_arm64.whl", upload-time = 2024-10-18T15:21:14Z, size = 12348, hashes = { sha256 = "846ade7b71e3536c4e56b386c2a47adf5741d2d8b94ec9dc3e92e5e1ee1e2225" } }, + { url = "https://files.pythonhosted.org/packages/e0/25/dd5c0f6ac1311e9b40f4af06c78efde0f3b5cbf02502f8ef9501294c425b/MarkupSafe-3.0.2-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", upload-time = 2024-10-18T15:21:15Z, size = 24149, hashes = { sha256 = "1c99d261bd2d5f6b59325c92c73df481e05e57f19837bdca8413b9eac4bd8028" } }, + { url = "https://files.pythonhosted.org/packages/f3/f0/89e7aadfb3749d0f52234a0c8c7867877876e0a20b60e2188e9850794c17/MarkupSafe-3.0.2-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", upload-time = 2024-10-18T15:21:17Z, size = 23118, hashes = { sha256 = "e17c96c14e19278594aa4841ec148115f9c7615a47382ecb6b82bd8fea3ab0c8" } }, + { url = "https://files.pythonhosted.org/packages/d5/da/f2eeb64c723f5e3777bc081da884b414671982008c47dcc1873d81f625b6/MarkupSafe-3.0.2-cp312-cp312-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", upload-time = 2024-10-18T15:21:18Z, size = 22993, hashes = { sha256 = "88416bd1e65dcea10bc7569faacb2c20ce071dd1f87539ca2ab364bf6231393c" } }, + { url = "https://files.pythonhosted.org/packages/da/0e/1f32af846df486dce7c227fe0f2398dc7e2e51d4a370508281f3c1c5cddc/MarkupSafe-3.0.2-cp312-cp312-musllinux_1_2_aarch64.whl", upload-time = 2024-10-18T15:21:18Z, size = 24178, hashes = { sha256 = "2181e67807fc2fa785d0592dc2d6206c019b9502410671cc905d132a92866557" } }, + { url = "https://files.pythonhosted.org/packages/c4/f6/bb3ca0532de8086cbff5f06d137064c8410d10779c4c127e0e47d17c0b71/MarkupSafe-3.0.2-cp312-cp312-musllinux_1_2_i686.whl", upload-time = 2024-10-18T15:21:19Z, size = 23319, hashes = { sha256 = "52305740fe773d09cffb16f8ed0427942901f00adedac82ec8b67752f58a1b22" } }, + { url = "https://files.pythonhosted.org/packages/a2/82/8be4c96ffee03c5b4a034e60a31294daf481e12c7c43ab8e34a1453ee48b/MarkupSafe-3.0.2-cp312-cp312-musllinux_1_2_x86_64.whl", upload-time = 2024-10-18T15:21:20Z, size = 23352, hashes = { sha256 = "ad10d3ded218f1039f11a75f8091880239651b52e9bb592ca27de44eed242a48" } }, + { url = "https://files.pythonhosted.org/packages/51/ae/97827349d3fcffee7e184bdf7f41cd6b88d9919c80f0263ba7acd1bbcb18/MarkupSafe-3.0.2-cp312-cp312-win32.whl", upload-time = 2024-10-18T15:21:22Z, size = 15097, hashes = { sha256 = "0f4ca02bea9a23221c0182836703cbf8930c5e9454bacce27e767509fa286a30" } }, + { url = "https://files.pythonhosted.org/packages/c1/80/a61f99dc3a936413c3ee4e1eecac96c0da5ed07ad56fd975f1a9da5bc630/MarkupSafe-3.0.2-cp312-cp312-win_amd64.whl", upload-time = 2024-10-18T15:21:23Z, size = 15601, hashes = { sha256 = "8e06879fc22a25ca47312fbe7c8264eb0b662f6db27cb2d3bbbc74b1df4b9b87" } }, + { url = "https://files.pythonhosted.org/packages/83/0e/67eb10a7ecc77a0c2bbe2b0235765b98d164d81600746914bebada795e97/MarkupSafe-3.0.2-cp313-cp313-macosx_10_13_universal2.whl", upload-time = 2024-10-18T15:21:24Z, size = 14274, hashes = { sha256 = "ba9527cdd4c926ed0760bc301f6728ef34d841f405abf9d4f959c478421e4efd" } }, + { url = "https://files.pythonhosted.org/packages/2b/6d/9409f3684d3335375d04e5f05744dfe7e9f120062c9857df4ab490a1031a/MarkupSafe-3.0.2-cp313-cp313-macosx_11_0_arm64.whl", upload-time = 2024-10-18T15:21:25Z, size = 12352, hashes = { sha256 = "f8b3d067f2e40fe93e1ccdd6b2e1d16c43140e76f02fb1319a05cf2b79d99430" } }, + { url = "https://files.pythonhosted.org/packages/d2/f5/6eadfcd3885ea85fe2a7c128315cc1bb7241e1987443d78c8fe712d03091/MarkupSafe-3.0.2-cp313-cp313-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", upload-time = 2024-10-18T15:21:26Z, size = 24122, hashes = { sha256 = "569511d3b58c8791ab4c2e1285575265991e6d8f8700c7be0e88f86cb0672094" } }, + { url = "https://files.pythonhosted.org/packages/0c/91/96cf928db8236f1bfab6ce15ad070dfdd02ed88261c2afafd4b43575e9e9/MarkupSafe-3.0.2-cp313-cp313-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", upload-time = 2024-10-18T15:21:27Z, size = 23085, hashes = { sha256 = "15ab75ef81add55874e7ab7055e9c397312385bd9ced94920f2802310c930396" } }, + { url = "https://files.pythonhosted.org/packages/c2/cf/c9d56af24d56ea04daae7ac0940232d31d5a8354f2b457c6d856b2057d69/MarkupSafe-3.0.2-cp313-cp313-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", upload-time = 2024-10-18T15:21:27Z, size = 22978, hashes = { sha256 = "f3818cb119498c0678015754eba762e0d61e5b52d34c8b13d770f0719f7b1d79" } }, + { url = "https://files.pythonhosted.org/packages/2a/9f/8619835cd6a711d6272d62abb78c033bda638fdc54c4e7f4272cf1c0962b/MarkupSafe-3.0.2-cp313-cp313-musllinux_1_2_aarch64.whl", upload-time = 2024-10-18T15:21:28Z, size = 24208, hashes = { sha256 = "cdb82a876c47801bb54a690c5ae105a46b392ac6099881cdfb9f6e95e4014c6a" } }, + { url = "https://files.pythonhosted.org/packages/f9/bf/176950a1792b2cd2102b8ffeb5133e1ed984547b75db47c25a67d3359f77/MarkupSafe-3.0.2-cp313-cp313-musllinux_1_2_i686.whl", upload-time = 2024-10-18T15:21:29Z, size = 23357, hashes = { sha256 = "cabc348d87e913db6ab4aa100f01b08f481097838bdddf7c7a84b7575b7309ca" } }, + { url = "https://files.pythonhosted.org/packages/ce/4f/9a02c1d335caabe5c4efb90e1b6e8ee944aa245c1aaaab8e8a618987d816/MarkupSafe-3.0.2-cp313-cp313-musllinux_1_2_x86_64.whl", upload-time = 2024-10-18T15:21:30Z, size = 23344, hashes = { sha256 = "444dcda765c8a838eaae23112db52f1efaf750daddb2d9ca300bcae1039adc5c" } }, + { url = "https://files.pythonhosted.org/packages/ee/55/c271b57db36f748f0e04a759ace9f8f759ccf22b4960c270c78a394f58be/MarkupSafe-3.0.2-cp313-cp313-win32.whl", upload-time = 2024-10-18T15:21:31Z, size = 15101, hashes = { sha256 = "bcf3e58998965654fdaff38e58584d8937aa3096ab5354d493c77d1fdd66d7a1" } }, + { url = "https://files.pythonhosted.org/packages/29/88/07df22d2dd4df40aba9f3e402e6dc1b8ee86297dddbad4872bd5e7b0094f/MarkupSafe-3.0.2-cp313-cp313-win_amd64.whl", upload-time = 2024-10-18T15:21:32Z, size = 15603, hashes = { sha256 = "e6a2a455bd412959b57a172ce6328d2dd1f01cb2135efda2e4576e8a23fa3b0f" } }, + { url = "https://files.pythonhosted.org/packages/62/6a/8b89d24db2d32d433dffcd6a8779159da109842434f1dd2f6e71f32f738c/MarkupSafe-3.0.2-cp313-cp313t-macosx_10_13_universal2.whl", upload-time = 2024-10-18T15:21:33Z, size = 14510, hashes = { sha256 = "b5a6b3ada725cea8a5e634536b1b01c30bcdcd7f9c6fff4151548d5bf6b3a36c" } }, + { url = "https://files.pythonhosted.org/packages/7a/06/a10f955f70a2e5a9bf78d11a161029d278eeacbd35ef806c3fd17b13060d/MarkupSafe-3.0.2-cp313-cp313t-macosx_11_0_arm64.whl", upload-time = 2024-10-18T15:21:34Z, size = 12486, hashes = { sha256 = "a904af0a6162c73e3edcb969eeeb53a63ceeb5d8cf642fade7d39e7963a22ddb" } }, + { url = "https://files.pythonhosted.org/packages/34/cf/65d4a571869a1a9078198ca28f39fba5fbb910f952f9dbc5220afff9f5e6/MarkupSafe-3.0.2-cp313-cp313t-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", upload-time = 2024-10-18T15:21:35Z, size = 25480, hashes = { sha256 = "4aa4e5faecf353ed117801a068ebab7b7e09ffb6e1d5e412dc852e0da018126c" } }, + { url = "https://files.pythonhosted.org/packages/0c/e3/90e9651924c430b885468b56b3d597cabf6d72be4b24a0acd1fa0e12af67/MarkupSafe-3.0.2-cp313-cp313t-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", upload-time = 2024-10-18T15:21:36Z, size = 23914, hashes = { sha256 = "c0ef13eaeee5b615fb07c9a7dadb38eac06a0608b41570d8ade51c56539e509d" } }, + { url = "https://files.pythonhosted.org/packages/66/8c/6c7cf61f95d63bb866db39085150df1f2a5bd3335298f14a66b48e92659c/MarkupSafe-3.0.2-cp313-cp313t-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", upload-time = 2024-10-18T15:21:37Z, size = 23796, hashes = { sha256 = "d16a81a06776313e817c951135cf7340a3e91e8c1ff2fac444cfd75fffa04afe" } }, + { url = "https://files.pythonhosted.org/packages/bb/35/cbe9238ec3f47ac9a7c8b3df7a808e7cb50fe149dc7039f5f454b3fba218/MarkupSafe-3.0.2-cp313-cp313t-musllinux_1_2_aarch64.whl", upload-time = 2024-10-18T15:21:37Z, size = 25473, hashes = { sha256 = "6381026f158fdb7c72a168278597a5e3a5222e83ea18f543112b2662a9b699c5" } }, + { url = "https://files.pythonhosted.org/packages/e6/32/7621a4382488aa283cc05e8984a9c219abad3bca087be9ec77e89939ded9/MarkupSafe-3.0.2-cp313-cp313t-musllinux_1_2_i686.whl", upload-time = 2024-10-18T15:21:39Z, size = 24114, hashes = { sha256 = "3d79d162e7be8f996986c064d1c7c817f6df3a77fe3d6859f6f9e7be4b8c213a" } }, + { url = "https://files.pythonhosted.org/packages/0d/80/0985960e4b89922cb5a0bac0ed39c5b96cbc1a536a99f30e8c220a996ed9/MarkupSafe-3.0.2-cp313-cp313t-musllinux_1_2_x86_64.whl", upload-time = 2024-10-18T15:21:40Z, size = 24098, hashes = { sha256 = "131a3c7689c85f5ad20f9f6fb1b866f402c445b220c19fe4308c0b147ccd2ad9" } }, + { url = "https://files.pythonhosted.org/packages/82/78/fedb03c7d5380df2427038ec8d973587e90561b2d90cd472ce9254cf348b/MarkupSafe-3.0.2-cp313-cp313t-win32.whl", upload-time = 2024-10-18T15:21:41Z, size = 15208, hashes = { sha256 = "ba8062ed2cf21c07a9e295d5b8a2a5ce678b913b45fdf68c32d95d6c1291e0b6" } }, + { url = "https://files.pythonhosted.org/packages/4f/65/6079a46068dfceaeabb5dcad6d674f5f5c61a6fa5673746f42a9f4c233b3/MarkupSafe-3.0.2-cp313-cp313t-win_amd64.whl", upload-time = 2024-10-18T15:21:42Z, size = 15739, hashes = { sha256 = "e444a31f8db13eb18ada366ab3cf45fd4b31e4db1236a4448f68778c1d1a5a2f" } }, +] + +[[packages]] +name = "mdurl" +version = "0.1.2" +sdist = { url = "https://files.pythonhosted.org/packages/d6/54/cfe61301667036ec958cb99bd3efefba235e65cdeb9c84d24a8293ba1d90/mdurl-0.1.2.tar.gz", upload-time = 2022-08-14T12:40:10Z, size = 8729, hashes = { sha256 = "bb413d29f5eea38f31dd4754dd7377d4465116fb207585f97bf925588687c1ba" } } +wheels = [{ url = "https://files.pythonhosted.org/packages/b3/38/89ba8ad64ae25be8de66a6d463314cf1eb366222074cfda9ee839c56a4b4/mdurl-0.1.2-py3-none-any.whl", upload-time = 2022-08-14T12:40:09Z, size = 9979, hashes = { sha256 = "84008a41e51615a49fc9966191ff91509e3c40b939176e643fd50a5c2196b8f8" } }] + +[[packages]] +name = "more-itertools" +version = "10.7.0" +sdist = { url = "https://files.pythonhosted.org/packages/ce/a0/834b0cebabbfc7e311f30b46c8188790a37f89fc8d756660346fe5abfd09/more_itertools-10.7.0.tar.gz", upload-time = 2025-04-22T14:17:41Z, size = 127671, hashes = { sha256 = "9fddd5403be01a94b204faadcff459ec3568cf110265d3c54323e1e866ad29d3" } } +wheels = [{ url = "https://files.pythonhosted.org/packages/2b/9f/7ba6f94fc1e9ac3d2b853fdff3035fb2fa5afbed898c4a72b8a020610594/more_itertools-10.7.0-py3-none-any.whl", upload-time = 2025-04-22T14:17:40Z, size = 65278, hashes = { sha256 = "d43980384673cb07d2f7d2d918c616b30c659c089ee23953f601d6609c67510e" } }] + +[[packages]] +name = "packaging" +version = "25.0" +sdist = { url = "https://files.pythonhosted.org/packages/a1/d4/1fc4078c65507b51b96ca8f8c3ba19e6a61c8253c72794544580a7b6c24d/packaging-25.0.tar.gz", upload-time = 2025-04-19T11:48:59Z, size = 165727, hashes = { sha256 = "d443872c98d677bf60f6a1f2f8c1cb748e8fe762d2bf9d3148b5599295b0fc4f" } } +wheels = [{ url = "https://files.pythonhosted.org/packages/20/12/38679034af332785aac8774540895e234f4d07f7545804097de4b666afd8/packaging-25.0-py3-none-any.whl", upload-time = 2025-04-19T11:48:57Z, size = 66469, hashes = { sha256 = "29572ef2b1f17581046b3a2227d5c611fb25ec70ca1ba8554b24b0e69331a484" } }] + +[[packages]] +name = "pip" +version = "26.2.1" +sdist = { url = "https://files.pythonhosted.org/packages/ae/15/4500e320e6b101ec3b719ae85b697d9940b6cda672bc555bd6016fc60c6f/pip-26.2.1.tar.gz", upload-time = 2026-08-04T22:51:14Z, size = 1848877, hashes = { sha256 = "f6ad667e89a1fe78046c8f13232b247200f5258d7828f3f7883d660878e0813f" } } +wheels = [{ url = "https://files.pythonhosted.org/packages/f3/6e/1736e5b4ae2b778ef2f81c47d797de9f891d4d8acb047a24ca37a60294dd/pip-26.2.1-py3-none-any.whl", upload-time = 2026-08-04T22:51:12Z, size = 1816632, hashes = { sha256 = "71138adf1f4ca900cdb7d289c21b7494329f2332b6d85f0e1c42108c0384ed3e" } }] + +[[packages]] +name = "platformdirs" +version = "4.3.8" +sdist = { url = "https://files.pythonhosted.org/packages/fe/8b/3c73abc9c759ecd3f1f7ceff6685840859e8070c4d947c93fae71f6a0bf2/platformdirs-4.3.8.tar.gz", upload-time = 2025-05-07T22:47:42Z, size = 21362, hashes = { sha256 = "3d512d96e16bcb959a814c9f348431070822a6496326a4be0911c40b5a74c2bc" } } +wheels = [{ url = "https://files.pythonhosted.org/packages/fe/39/979e8e21520d4e47a0bbe349e2713c0aac6f3d853d0e5b34d76206c439aa/platformdirs-4.3.8-py3-none-any.whl", upload-time = 2025-05-07T22:47:40Z, size = 18567, hashes = { sha256 = "ff7059bb7eb1179e2685604f4aaf157cfd9535242bd23742eadc3c13542139b4" } }] + +[[packages]] +name = "pluggy" +version = "1.6.0" +sdist = { url = "https://files.pythonhosted.org/packages/f9/e2/3e91f31a7d2b083fe6ef3fa267035b518369d9511ffab804f839851d2779/pluggy-1.6.0.tar.gz", upload-time = 2025-05-15T12:30:07Z, size = 69412, hashes = { sha256 = "7dcc130b76258d33b90f61b658791dede3486c3e6bfb003ee5c9bfb396dd22f3" } } +wheels = [{ url = "https://files.pythonhosted.org/packages/54/20/4d324d65cc6d9205fabedc306948156824eb9f0ee1633355a8f7ec5c66bf/pluggy-1.6.0-py3-none-any.whl", upload-time = 2025-05-15T12:30:06Z, size = 20538, hashes = { sha256 = "e920276dd6813095e9377c0bc5566d94c932c33b27a3e3945d8389c374dd4746" } }] + +[[packages]] +name = "prompt-toolkit" +version = "3.0.51" +sdist = { url = "https://files.pythonhosted.org/packages/bb/6e/9d084c929dfe9e3bfe0c6a47e31f78a25c54627d64a66e884a8bf5474f1c/prompt_toolkit-3.0.51.tar.gz", upload-time = 2025-04-15T09:18:47Z, size = 428940, hashes = { sha256 = "931a162e3b27fc90c86f1b48bb1fb2c528c2761475e57c9c06de13311c7b54ed" } } +wheels = [{ url = "https://files.pythonhosted.org/packages/ce/4f/5249960887b1fbe561d9ff265496d170b55a735b76724f10ef19f9e40716/prompt_toolkit-3.0.51-py3-none-any.whl", upload-time = 2025-04-15T09:18:44Z, size = 387810, hashes = { sha256 = "52742911fde84e2d423e2f9a4cf1de7d7ac4e51958f648d9540e0fb8db077b07" } }] + +[[packages]] +name = "protobuf" +version = "5.29.6" +sdist = { url = "https://files.pythonhosted.org/packages/7e/57/394a763c103e0edf87f0938dafcd918d53b4c011dfc5c8ae80f3b0452dbb/protobuf-5.29.6.tar.gz", upload-time = 2026-02-04T22:54:40Z, size = 425623, hashes = { sha256 = "da9ee6a5424b6b30fd5e45c5ea663aef540ca95f9ad99d1e887e819cdf9b8723" } } +wheels = [ + { url = "https://files.pythonhosted.org/packages/d4/88/9ee58ff7863c479d6f8346686d4636dd4c415b0cbeed7a6a7d0617639c2a/protobuf-5.29.6-cp310-abi3-win32.whl", upload-time = 2026-02-04T22:54:25Z, size = 423357, hashes = { sha256 = "62e8a3114992c7c647bce37dcc93647575fc52d50e48de30c6fcb28a6a291eb1" } }, + { url = "https://files.pythonhosted.org/packages/1c/66/2dc736a4d576847134fb6d80bd995c569b13cdc7b815d669050bf0ce2d2c/protobuf-5.29.6-cp310-abi3-win_amd64.whl", upload-time = 2026-02-04T22:54:28Z, size = 435175, hashes = { sha256 = "7e6ad413275be172f67fdee0f43484b6de5a904cc1c3ea9804cb6fe2ff366eda" } }, + { url = "https://files.pythonhosted.org/packages/06/db/49b05966fd208ae3f44dcd33837b6243b4915c57561d730a43f881f24dea/protobuf-5.29.6-cp38-abi3-macosx_10_9_universal2.whl", upload-time = 2026-02-04T22:54:30Z, size = 418619, hashes = { sha256 = "b5a169e664b4057183a34bdc424540e86eea47560f3c123a0d64de4e137f9269" } }, + { url = "https://files.pythonhosted.org/packages/b7/d7/48cbf6b0c3c39761e47a99cb483405f0fde2be22cf00d71ef316ce52b458/protobuf-5.29.6-cp38-abi3-manylinux2014_aarch64.whl", upload-time = 2026-02-04T22:54:31Z, size = 320284, hashes = { sha256 = "a8866b2cff111f0f863c1b3b9e7572dc7eaea23a7fae27f6fc613304046483e6" } }, + { url = "https://files.pythonhosted.org/packages/e3/dd/cadd6ec43069247d91f6345fa7a0d2858bef6af366dbd7ba8f05d2c77d3b/protobuf-5.29.6-cp38-abi3-manylinux2014_x86_64.whl", upload-time = 2026-02-04T22:54:32Z, size = 320478, hashes = { sha256 = "e3387f44798ac1106af0233c04fb8abf543772ff241169946f698b3a9a3d3ab9" } }, + { url = "https://files.pythonhosted.org/packages/5a/cb/e3065b447186cb70aa65acc70c86baf482d82bf75625bf5a2c4f6919c6a3/protobuf-5.29.6-py3-none-any.whl", upload-time = 2026-02-04T22:54:39Z, size = 173126, hashes = { sha256 = "6b9edb641441b2da9fa8f428760fc136a49cf97a52076010cf22a2ff73438a86" } }, +] + +[[packages]] +name = "pycparser" +version = "2.22" +marker = "python_full_version >= '3.9' and implementation_name != 'PyPy' and platform_python_implementation != 'PyPy'" +sdist = { url = "https://files.pythonhosted.org/packages/1d/b2/31537cf4b1ca988837256c910a668b553fceb8f069bedc4b1c826024b52c/pycparser-2.22.tar.gz", upload-time = 2024-03-30T13:22:22Z, size = 172736, hashes = { sha256 = "491c8be9c040f5390f5bf44a5b07752bd07f56edf992381b05c701439eec10f6" } } +wheels = [{ url = "https://files.pythonhosted.org/packages/13/a3/a812df4e2dd5696d1f351d58b8fe16a405b234ad2886a0dab9183fb78109/pycparser-2.22-py3-none-any.whl", upload-time = 2024-03-30T13:22:20Z, size = 117552, hashes = { sha256 = "c3702b6d3dd8c7abc1afa565d7e63d53a1d0bd86cdc24edd75470f4de499cfcc" } }] + +[[packages]] +name = "pydantic" +version = "2.12.5" +sdist = { url = "https://files.pythonhosted.org/packages/69/44/36f1a6e523abc58ae5f928898e4aca2e0ea509b5aa6f6f392a5d882be928/pydantic-2.12.5.tar.gz", upload-time = 2025-11-26T15:11:46Z, size = 821591, hashes = { sha256 = "4d351024c75c0f085a9febbb665ce8c0c6ec5d30e903bdb6394b7ede26aebb49" } } +wheels = [{ url = "https://files.pythonhosted.org/packages/5a/87/b70ad306ebb6f9b585f114d0ac2137d792b48be34d732d60e597c2f8465a/pydantic-2.12.5-py3-none-any.whl", upload-time = 2025-11-26T15:11:44Z, size = 463580, hashes = { sha256 = "e561593fccf61e8a20fc46dfc2dfe075b8be7d0188df33f221ad1f0139180f9d" } }] + +[[packages]] +name = "pydantic-core" +version = "2.41.5" +sdist = { url = "https://files.pythonhosted.org/packages/71/70/23b021c950c2addd24ec408e9ab05d59b035b39d97cdc1130e1bce647bb6/pydantic_core-2.41.5.tar.gz", upload-time = 2025-11-04T13:43:49Z, size = 460952, hashes = { sha256 = "08daa51ea16ad373ffd5e7606252cc32f07bc72b28284b6bc9c6df804816476e" } } +wheels = [ + { url = "https://files.pythonhosted.org/packages/c6/90/32c9941e728d564b411d574d8ee0cf09b12ec978cb22b294995bae5549a5/pydantic_core-2.41.5-cp310-cp310-macosx_10_12_x86_64.whl", upload-time = 2025-11-04T13:39:04Z, size = 2107298, hashes = { sha256 = "77b63866ca88d804225eaa4af3e664c5faf3568cea95360d21f4725ab6e07146" } }, + { url = "https://files.pythonhosted.org/packages/fb/a8/61c96a77fe28993d9a6fb0f4127e05430a267b235a124545d79fea46dd65/pydantic_core-2.41.5-cp310-cp310-macosx_11_0_arm64.whl", upload-time = 2025-11-04T13:39:06Z, size = 1901475, hashes = { sha256 = "dfa8a0c812ac681395907e71e1274819dec685fec28273a28905df579ef137e2" } }, + { url = "https://files.pythonhosted.org/packages/5d/b6/338abf60225acc18cdc08b4faef592d0310923d19a87fba1faf05af5346e/pydantic_core-2.41.5-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", upload-time = 2025-11-04T13:39:10Z, size = 1918815, hashes = { sha256 = "5921a4d3ca3aee735d9fd163808f5e8dd6c6972101e4adbda9a4667908849b97" } }, + { url = "https://files.pythonhosted.org/packages/d1/1c/2ed0433e682983d8e8cba9c8d8ef274d4791ec6a6f24c58935b90e780e0a/pydantic_core-2.41.5-cp310-cp310-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", upload-time = 2025-11-04T13:39:12Z, size = 2065567, hashes = { sha256 = "e25c479382d26a2a41b7ebea1043564a937db462816ea07afa8a44c0866d52f9" } }, + { url = "https://files.pythonhosted.org/packages/b3/24/cf84974ee7d6eae06b9e63289b7b8f6549d416b5c199ca2d7ce13bbcf619/pydantic_core-2.41.5-cp310-cp310-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", upload-time = 2025-11-04T13:39:13Z, size = 2230442, hashes = { sha256 = "f547144f2966e1e16ae626d8ce72b4cfa0caedc7fa28052001c94fb2fcaa1c52" } }, + { url = "https://files.pythonhosted.org/packages/fd/21/4e287865504b3edc0136c89c9c09431be326168b1eb7841911cbc877a995/pydantic_core-2.41.5-cp310-cp310-manylinux_2_17_s390x.manylinux2014_s390x.whl", upload-time = 2025-11-04T13:39:15Z, size = 2350956, hashes = { sha256 = "6f52298fbd394f9ed112d56f3d11aabd0d5bd27beb3084cc3d8ad069483b8941" } }, + { url = "https://files.pythonhosted.org/packages/a8/76/7727ef2ffa4b62fcab916686a68a0426b9b790139720e1934e8ba797e238/pydantic_core-2.41.5-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", upload-time = 2025-11-04T13:39:17Z, size = 2068253, hashes = { sha256 = "100baa204bb412b74fe285fb0f3a385256dad1d1879f0a5cb1499ed2e83d132a" } }, + { url = "https://files.pythonhosted.org/packages/d5/8c/a4abfc79604bcb4c748e18975c44f94f756f08fb04218d5cb87eb0d3a63e/pydantic_core-2.41.5-cp310-cp310-manylinux_2_5_i686.manylinux1_i686.whl", upload-time = 2025-11-04T13:39:19Z, size = 2177050, hashes = { sha256 = "05a2c8852530ad2812cb7914dc61a1125dc4e06252ee98e5638a12da6cc6fb6c" } }, + { url = "https://files.pythonhosted.org/packages/67/b1/de2e9a9a79b480f9cb0b6e8b6ba4c50b18d4e89852426364c66aa82bb7b3/pydantic_core-2.41.5-cp310-cp310-musllinux_1_1_aarch64.whl", upload-time = 2025-11-04T13:39:21Z, size = 2147178, hashes = { sha256 = "29452c56df2ed968d18d7e21f4ab0ac55e71dc59524872f6fc57dcf4a3249ed2" } }, + { url = "https://files.pythonhosted.org/packages/16/c1/dfb33f837a47b20417500efaa0378adc6635b3c79e8369ff7a03c494b4ac/pydantic_core-2.41.5-cp310-cp310-musllinux_1_1_armv7l.whl", upload-time = 2025-11-04T13:39:22Z, size = 2341833, hashes = { sha256 = "d5160812ea7a8a2ffbe233d8da666880cad0cbaf5d4de74ae15c313213d62556" } }, + { url = "https://files.pythonhosted.org/packages/47/36/00f398642a0f4b815a9a558c4f1dca1b4020a7d49562807d7bc9ff279a6c/pydantic_core-2.41.5-cp310-cp310-musllinux_1_1_x86_64.whl", upload-time = 2025-11-04T13:39:25Z, size = 2321156, hashes = { sha256 = "df3959765b553b9440adfd3c795617c352154e497a4eaf3752555cfb5da8fc49" } }, + { url = "https://files.pythonhosted.org/packages/7e/70/cad3acd89fde2010807354d978725ae111ddf6d0ea46d1ea1775b5c1bd0c/pydantic_core-2.41.5-cp310-cp310-win32.whl", upload-time = 2025-11-04T13:39:27Z, size = 1989378, hashes = { sha256 = "1f8d33a7f4d5a7889e60dc39856d76d09333d8a6ed0f5f1190635cbec70ec4ba" } }, + { url = "https://files.pythonhosted.org/packages/76/92/d338652464c6c367e5608e4488201702cd1cbb0f33f7b6a85a60fe5f3720/pydantic_core-2.41.5-cp310-cp310-win_amd64.whl", upload-time = 2025-11-04T13:39:29Z, size = 2013622, hashes = { sha256 = "62de39db01b8d593e45871af2af9e497295db8d73b085f6bfd0b18c83c70a8f9" } }, + { url = "https://files.pythonhosted.org/packages/e8/72/74a989dd9f2084b3d9530b0915fdda64ac48831c30dbf7c72a41a5232db8/pydantic_core-2.41.5-cp311-cp311-macosx_10_12_x86_64.whl", upload-time = 2025-11-04T13:39:31Z, size = 2105873, hashes = { sha256 = "a3a52f6156e73e7ccb0f8cced536adccb7042be67cb45f9562e12b319c119da6" } }, + { url = "https://files.pythonhosted.org/packages/12/44/37e403fd9455708b3b942949e1d7febc02167662bf1a7da5b78ee1ea2842/pydantic_core-2.41.5-cp311-cp311-macosx_11_0_arm64.whl", upload-time = 2025-11-04T13:39:32Z, size = 1899826, hashes = { sha256 = "7f3bf998340c6d4b0c9a2f02d6a400e51f123b59565d74dc60d252ce888c260b" } }, + { url = "https://files.pythonhosted.org/packages/33/7f/1d5cab3ccf44c1935a359d51a8a2a9e1a654b744b5e7f80d41b88d501eec/pydantic_core-2.41.5-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", upload-time = 2025-11-04T13:39:34Z, size = 1917869, hashes = { sha256 = "378bec5c66998815d224c9ca994f1e14c0c21cb95d2f52b6021cc0b2a58f2a5a" } }, + { url = "https://files.pythonhosted.org/packages/6e/6a/30d94a9674a7fe4f4744052ed6c5e083424510be1e93da5bc47569d11810/pydantic_core-2.41.5-cp311-cp311-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", upload-time = 2025-11-04T13:39:36Z, size = 2063890, hashes = { sha256 = "e7b576130c69225432866fe2f4a469a85a54ade141d96fd396dffcf607b558f8" } }, + { url = "https://files.pythonhosted.org/packages/50/be/76e5d46203fcb2750e542f32e6c371ffa9b8ad17364cf94bb0818dbfb50c/pydantic_core-2.41.5-cp311-cp311-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", upload-time = 2025-11-04T13:39:37Z, size = 2229740, hashes = { sha256 = "6cb58b9c66f7e4179a2d5e0f849c48eff5c1fca560994d6eb6543abf955a149e" } }, + { url = "https://files.pythonhosted.org/packages/d3/ee/fed784df0144793489f87db310a6bbf8118d7b630ed07aa180d6067e653a/pydantic_core-2.41.5-cp311-cp311-manylinux_2_17_s390x.manylinux2014_s390x.whl", upload-time = 2025-11-04T13:39:40Z, size = 2350021, hashes = { sha256 = "88942d3a3dff3afc8288c21e565e476fc278902ae4d6d134f1eeda118cc830b1" } }, + { url = "https://files.pythonhosted.org/packages/c8/be/8fed28dd0a180dca19e72c233cbf58efa36df055e5b9d90d64fd1740b828/pydantic_core-2.41.5-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", upload-time = 2025-11-04T13:39:42Z, size = 2066378, hashes = { sha256 = "f31d95a179f8d64d90f6831d71fa93290893a33148d890ba15de25642c5d075b" } }, + { url = "https://files.pythonhosted.org/packages/b0/3b/698cf8ae1d536a010e05121b4958b1257f0b5522085e335360e53a6b1c8b/pydantic_core-2.41.5-cp311-cp311-manylinux_2_5_i686.manylinux1_i686.whl", upload-time = 2025-11-04T13:39:44Z, size = 2175761, hashes = { sha256 = "c1df3d34aced70add6f867a8cf413e299177e0c22660cc767218373d0779487b" } }, + { url = "https://files.pythonhosted.org/packages/b8/ba/15d537423939553116dea94ce02f9c31be0fa9d0b806d427e0308ec17145/pydantic_core-2.41.5-cp311-cp311-musllinux_1_1_aarch64.whl", upload-time = 2025-11-04T13:39:46Z, size = 2146303, hashes = { sha256 = "4009935984bd36bd2c774e13f9a09563ce8de4abaa7226f5108262fa3e637284" } }, + { url = "https://files.pythonhosted.org/packages/58/7f/0de669bf37d206723795f9c90c82966726a2ab06c336deba4735b55af431/pydantic_core-2.41.5-cp311-cp311-musllinux_1_1_armv7l.whl", upload-time = 2025-11-04T13:39:48Z, size = 2340355, hashes = { sha256 = "34a64bc3441dc1213096a20fe27e8e128bd3ff89921706e83c0b1ac971276594" } }, + { url = "https://files.pythonhosted.org/packages/e5/de/e7482c435b83d7e3c3ee5ee4451f6e8973cff0eb6007d2872ce6383f6398/pydantic_core-2.41.5-cp311-cp311-musllinux_1_1_x86_64.whl", upload-time = 2025-11-04T13:39:49Z, size = 2319875, hashes = { sha256 = "c9e19dd6e28fdcaa5a1de679aec4141f691023916427ef9bae8584f9c2fb3b0e" } }, + { url = "https://files.pythonhosted.org/packages/fe/e6/8c9e81bb6dd7560e33b9053351c29f30c8194b72f2d6932888581f503482/pydantic_core-2.41.5-cp311-cp311-win32.whl", upload-time = 2025-11-04T13:39:51Z, size = 1987549, hashes = { sha256 = "2c010c6ded393148374c0f6f0bf89d206bf3217f201faa0635dcd56bd1520f6b" } }, + { url = "https://files.pythonhosted.org/packages/11/66/f14d1d978ea94d1bc21fc98fcf570f9542fe55bfcc40269d4e1a21c19bf7/pydantic_core-2.41.5-cp311-cp311-win_amd64.whl", upload-time = 2025-11-04T13:39:53Z, size = 2011305, hashes = { sha256 = "76ee27c6e9c7f16f47db7a94157112a2f3a00e958bc626e2f4ee8bec5c328fbe" } }, + { url = "https://files.pythonhosted.org/packages/56/d8/0e271434e8efd03186c5386671328154ee349ff0354d83c74f5caaf096ed/pydantic_core-2.41.5-cp311-cp311-win_arm64.whl", upload-time = 2025-11-04T13:39:56Z, size = 1972902, hashes = { sha256 = "4bc36bbc0b7584de96561184ad7f012478987882ebf9f9c389b23f432ea3d90f" } }, + { url = "https://files.pythonhosted.org/packages/5f/5d/5f6c63eebb5afee93bcaae4ce9a898f3373ca23df3ccaef086d0233a35a7/pydantic_core-2.41.5-cp312-cp312-macosx_10_12_x86_64.whl", upload-time = 2025-11-04T13:39:58Z, size = 2110990, hashes = { sha256 = "f41a7489d32336dbf2199c8c0a215390a751c5b014c2c1c5366e817202e9cdf7" } }, + { url = "https://files.pythonhosted.org/packages/aa/32/9c2e8ccb57c01111e0fd091f236c7b371c1bccea0fa85247ac55b1e2b6b6/pydantic_core-2.41.5-cp312-cp312-macosx_11_0_arm64.whl", upload-time = 2025-11-04T13:39:59Z, size = 1896003, hashes = { sha256 = "070259a8818988b9a84a449a2a7337c7f430a22acc0859c6b110aa7212a6d9c0" } }, + { url = "https://files.pythonhosted.org/packages/68/b8/a01b53cb0e59139fbc9e4fda3e9724ede8de279097179be4ff31f1abb65a/pydantic_core-2.41.5-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", upload-time = 2025-11-04T13:40:02Z, size = 1919200, hashes = { sha256 = "e96cea19e34778f8d59fe40775a7a574d95816eb150850a85a7a4c8f4b94ac69" } }, + { url = "https://files.pythonhosted.org/packages/38/de/8c36b5198a29bdaade07b5985e80a233a5ac27137846f3bc2d3b40a47360/pydantic_core-2.41.5-cp312-cp312-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", upload-time = 2025-11-04T13:40:04Z, size = 2052578, hashes = { sha256 = "ed2e99c456e3fadd05c991f8f437ef902e00eedf34320ba2b0842bd1c3ca3a75" } }, + { url = "https://files.pythonhosted.org/packages/00/b5/0e8e4b5b081eac6cb3dbb7e60a65907549a1ce035a724368c330112adfdd/pydantic_core-2.41.5-cp312-cp312-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", upload-time = 2025-11-04T13:40:06Z, size = 2208504, hashes = { sha256 = "65840751b72fbfd82c3c640cff9284545342a4f1eb1586ad0636955b261b0b05" } }, + { url = "https://files.pythonhosted.org/packages/77/56/87a61aad59c7c5b9dc8caad5a41a5545cba3810c3e828708b3d7404f6cef/pydantic_core-2.41.5-cp312-cp312-manylinux_2_17_s390x.manylinux2014_s390x.whl", upload-time = 2025-11-04T13:40:07Z, size = 2335816, hashes = { sha256 = "e536c98a7626a98feb2d3eaf75944ef6f3dbee447e1f841eae16f2f0a72d8ddc" } }, + { url = "https://files.pythonhosted.org/packages/0d/76/941cc9f73529988688a665a5c0ecff1112b3d95ab48f81db5f7606f522d3/pydantic_core-2.41.5-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", upload-time = 2025-11-04T13:40:09Z, size = 2075366, hashes = { sha256 = "eceb81a8d74f9267ef4081e246ffd6d129da5d87e37a77c9bde550cb04870c1c" } }, + { url = "https://files.pythonhosted.org/packages/d3/43/ebef01f69baa07a482844faaa0a591bad1ef129253ffd0cdaa9d8a7f72d3/pydantic_core-2.41.5-cp312-cp312-manylinux_2_5_i686.manylinux1_i686.whl", upload-time = 2025-11-04T13:40:12Z, size = 2171698, hashes = { sha256 = "d38548150c39b74aeeb0ce8ee1d8e82696f4a4e16ddc6de7b1d8823f7de4b9b5" } }, + { url = "https://files.pythonhosted.org/packages/b1/87/41f3202e4193e3bacfc2c065fab7706ebe81af46a83d3e27605029c1f5a6/pydantic_core-2.41.5-cp312-cp312-musllinux_1_1_aarch64.whl", upload-time = 2025-11-04T13:40:13Z, size = 2132603, hashes = { sha256 = "c23e27686783f60290e36827f9c626e63154b82b116d7fe9adba1fda36da706c" } }, + { url = "https://files.pythonhosted.org/packages/49/7d/4c00df99cb12070b6bccdef4a195255e6020a550d572768d92cc54dba91a/pydantic_core-2.41.5-cp312-cp312-musllinux_1_1_armv7l.whl", upload-time = 2025-11-04T13:40:15Z, size = 2329591, hashes = { sha256 = "482c982f814460eabe1d3bb0adfdc583387bd4691ef00b90575ca0d2b6fe2294" } }, + { url = "https://files.pythonhosted.org/packages/cc/6a/ebf4b1d65d458f3cda6a7335d141305dfa19bdc61140a884d165a8a1bbc7/pydantic_core-2.41.5-cp312-cp312-musllinux_1_1_x86_64.whl", upload-time = 2025-11-04T13:40:17Z, size = 2319068, hashes = { sha256 = "bfea2a5f0b4d8d43adf9d7b8bf019fb46fdd10a2e5cde477fbcb9d1fa08c68e1" } }, + { url = "https://files.pythonhosted.org/packages/49/3b/774f2b5cd4192d5ab75870ce4381fd89cf218af999515baf07e7206753f0/pydantic_core-2.41.5-cp312-cp312-win32.whl", upload-time = 2025-11-04T13:40:19Z, size = 1985908, hashes = { sha256 = "b74557b16e390ec12dca509bce9264c3bbd128f8a2c376eaa68003d7f327276d" } }, + { url = "https://files.pythonhosted.org/packages/86/45/00173a033c801cacf67c190fef088789394feaf88a98a7035b0e40d53dc9/pydantic_core-2.41.5-cp312-cp312-win_amd64.whl", upload-time = 2025-11-04T13:40:21Z, size = 2020145, hashes = { sha256 = "1962293292865bca8e54702b08a4f26da73adc83dd1fcf26fbc875b35d81c815" } }, + { url = "https://files.pythonhosted.org/packages/f9/22/91fbc821fa6d261b376a3f73809f907cec5ca6025642c463d3488aad22fb/pydantic_core-2.41.5-cp312-cp312-win_arm64.whl", upload-time = 2025-11-04T13:40:23Z, size = 1976179, hashes = { sha256 = "1746d4a3d9a794cacae06a5eaaccb4b8643a131d45fbc9af23e353dc0a5ba5c3" } }, + { url = "https://files.pythonhosted.org/packages/87/06/8806241ff1f70d9939f9af039c6c35f2360cf16e93c2ca76f184e76b1564/pydantic_core-2.41.5-cp313-cp313-macosx_10_12_x86_64.whl", upload-time = 2025-11-04T13:40:25Z, size = 2120403, hashes = { sha256 = "941103c9be18ac8daf7b7adca8228f8ed6bb7a1849020f643b3a14d15b1924d9" } }, + { url = "https://files.pythonhosted.org/packages/94/02/abfa0e0bda67faa65fef1c84971c7e45928e108fe24333c81f3bfe35d5f5/pydantic_core-2.41.5-cp313-cp313-macosx_11_0_arm64.whl", upload-time = 2025-11-04T13:40:27Z, size = 1896206, hashes = { sha256 = "112e305c3314f40c93998e567879e887a3160bb8689ef3d2c04b6cc62c33ac34" } }, + { url = "https://files.pythonhosted.org/packages/15/df/a4c740c0943e93e6500f9eb23f4ca7ec9bf71b19e608ae5b579678c8d02f/pydantic_core-2.41.5-cp313-cp313-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", upload-time = 2025-11-04T13:40:29Z, size = 1919307, hashes = { sha256 = "0cbaad15cb0c90aa221d43c00e77bb33c93e8d36e0bf74760cd00e732d10a6a0" } }, + { url = "https://files.pythonhosted.org/packages/9a/e3/6324802931ae1d123528988e0e86587c2072ac2e5394b4bc2bc34b61ff6e/pydantic_core-2.41.5-cp313-cp313-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", upload-time = 2025-11-04T13:40:33Z, size = 2063258, hashes = { sha256 = "03ca43e12fab6023fc79d28ca6b39b05f794ad08ec2feccc59a339b02f2b3d33" } }, + { url = "https://files.pythonhosted.org/packages/c9/d4/2230d7151d4957dd79c3044ea26346c148c98fbf0ee6ebd41056f2d62ab5/pydantic_core-2.41.5-cp313-cp313-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", upload-time = 2025-11-04T13:40:35Z, size = 2214917, hashes = { sha256 = "dc799088c08fa04e43144b164feb0c13f9a0bc40503f8df3e9fde58a3c0c101e" } }, + { url = "https://files.pythonhosted.org/packages/e6/9f/eaac5df17a3672fef0081b6c1bb0b82b33ee89aa5cec0d7b05f52fd4a1fa/pydantic_core-2.41.5-cp313-cp313-manylinux_2_17_s390x.manylinux2014_s390x.whl", upload-time = 2025-11-04T13:40:37Z, size = 2332186, hashes = { sha256 = "97aeba56665b4c3235a0e52b2c2f5ae9cd071b8a8310ad27bddb3f7fb30e9aa2" } }, + { url = "https://files.pythonhosted.org/packages/cf/4e/35a80cae583a37cf15604b44240e45c05e04e86f9cfd766623149297e971/pydantic_core-2.41.5-cp313-cp313-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", upload-time = 2025-11-04T13:40:40Z, size = 2073164, hashes = { sha256 = "406bf18d345822d6c21366031003612b9c77b3e29ffdb0f612367352aab7d586" } }, + { url = "https://files.pythonhosted.org/packages/bf/e3/f6e262673c6140dd3305d144d032f7bd5f7497d3871c1428521f19f9efa2/pydantic_core-2.41.5-cp313-cp313-manylinux_2_5_i686.manylinux1_i686.whl", upload-time = 2025-11-04T13:40:42Z, size = 2179146, hashes = { sha256 = "b93590ae81f7010dbe380cdeab6f515902ebcbefe0b9327cc4804d74e93ae69d" } }, + { url = "https://files.pythonhosted.org/packages/75/c7/20bd7fc05f0c6ea2056a4565c6f36f8968c0924f19b7d97bbfea55780e73/pydantic_core-2.41.5-cp313-cp313-musllinux_1_1_aarch64.whl", upload-time = 2025-11-04T13:40:44Z, size = 2137788, hashes = { sha256 = "01a3d0ab748ee531f4ea6c3e48ad9dac84ddba4b0d82291f87248f2f9de8d740" } }, + { url = "https://files.pythonhosted.org/packages/3a/8d/34318ef985c45196e004bc46c6eab2eda437e744c124ef0dbe1ff2c9d06b/pydantic_core-2.41.5-cp313-cp313-musllinux_1_1_armv7l.whl", upload-time = 2025-11-04T13:40:46Z, size = 2340133, hashes = { sha256 = "6561e94ba9dacc9c61bce40e2d6bdc3bfaa0259d3ff36ace3b1e6901936d2e3e" } }, + { url = "https://files.pythonhosted.org/packages/9c/59/013626bf8c78a5a5d9350d12e7697d3d4de951a75565496abd40ccd46bee/pydantic_core-2.41.5-cp313-cp313-musllinux_1_1_x86_64.whl", upload-time = 2025-11-04T13:40:48Z, size = 2324852, hashes = { sha256 = "915c3d10f81bec3a74fbd4faebe8391013ba61e5a1a8d48c4455b923bdda7858" } }, + { url = "https://files.pythonhosted.org/packages/1a/d9/c248c103856f807ef70c18a4f986693a46a8ffe1602e5d361485da502d20/pydantic_core-2.41.5-cp313-cp313-win32.whl", upload-time = 2025-11-04T13:40:50Z, size = 1994679, hashes = { sha256 = "650ae77860b45cfa6e2cdafc42618ceafab3a2d9a3811fcfbd3bbf8ac3c40d36" } }, + { url = "https://files.pythonhosted.org/packages/9e/8b/341991b158ddab181cff136acd2552c9f35bd30380422a639c0671e99a91/pydantic_core-2.41.5-cp313-cp313-win_amd64.whl", upload-time = 2025-11-04T13:40:52Z, size = 2019766, hashes = { sha256 = "79ec52ec461e99e13791ec6508c722742ad745571f234ea6255bed38c6480f11" } }, + { url = "https://files.pythonhosted.org/packages/73/7d/f2f9db34af103bea3e09735bb40b021788a5e834c81eedb541991badf8f5/pydantic_core-2.41.5-cp313-cp313-win_arm64.whl", upload-time = 2025-11-04T13:40:54Z, size = 1981005, hashes = { sha256 = "3f84d5c1b4ab906093bdc1ff10484838aca54ef08de4afa9de0f5f14d69639cd" } }, + { url = "https://files.pythonhosted.org/packages/ea/28/46b7c5c9635ae96ea0fbb779e271a38129df2550f763937659ee6c5dbc65/pydantic_core-2.41.5-cp314-cp314-macosx_10_12_x86_64.whl", upload-time = 2025-11-04T13:40:56Z, size = 2119622, hashes = { sha256 = "3f37a19d7ebcdd20b96485056ba9e8b304e27d9904d233d7b1015db320e51f0a" } }, + { url = "https://files.pythonhosted.org/packages/74/1a/145646e5687e8d9a1e8d09acb278c8535ebe9e972e1f162ed338a622f193/pydantic_core-2.41.5-cp314-cp314-macosx_11_0_arm64.whl", upload-time = 2025-11-04T13:40:58Z, size = 1891725, hashes = { sha256 = "1d1d9764366c73f996edd17abb6d9d7649a7eb690006ab6adbda117717099b14" } }, + { url = "https://files.pythonhosted.org/packages/23/04/e89c29e267b8060b40dca97bfc64a19b2a3cf99018167ea1677d96368273/pydantic_core-2.41.5-cp314-cp314-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", upload-time = 2025-11-04T13:41:00Z, size = 1915040, hashes = { sha256 = "25e1c2af0fce638d5f1988b686f3b3ea8cd7de5f244ca147c777769e798a9cd1" } }, + { url = "https://files.pythonhosted.org/packages/84/a3/15a82ac7bd97992a82257f777b3583d3e84bdb06ba6858f745daa2ec8a85/pydantic_core-2.41.5-cp314-cp314-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", upload-time = 2025-11-04T13:41:03Z, size = 2063691, hashes = { sha256 = "506d766a8727beef16b7adaeb8ee6217c64fc813646b424d0804d67c16eddb66" } }, + { url = "https://files.pythonhosted.org/packages/74/9b/0046701313c6ef08c0c1cf0e028c67c770a4e1275ca73131563c5f2a310a/pydantic_core-2.41.5-cp314-cp314-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", upload-time = 2025-11-04T13:41:05Z, size = 2213897, hashes = { sha256 = "4819fa52133c9aa3c387b3328f25c1facc356491e6135b459f1de698ff64d869" } }, + { url = "https://files.pythonhosted.org/packages/8a/cd/6bac76ecd1b27e75a95ca3a9a559c643b3afcd2dd62086d4b7a32a18b169/pydantic_core-2.41.5-cp314-cp314-manylinux_2_17_s390x.manylinux2014_s390x.whl", upload-time = 2025-11-04T13:41:07Z, size = 2333302, hashes = { sha256 = "2b761d210c9ea91feda40d25b4efe82a1707da2ef62901466a42492c028553a2" } }, + { url = "https://files.pythonhosted.org/packages/4c/d2/ef2074dc020dd6e109611a8be4449b98cd25e1b9b8a303c2f0fca2f2bcf7/pydantic_core-2.41.5-cp314-cp314-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", upload-time = 2025-11-04T13:41:09Z, size = 2064877, hashes = { sha256 = "22f0fb8c1c583a3b6f24df2470833b40207e907b90c928cc8d3594b76f874375" } }, + { url = "https://files.pythonhosted.org/packages/18/66/e9db17a9a763d72f03de903883c057b2592c09509ccfe468187f2a2eef29/pydantic_core-2.41.5-cp314-cp314-manylinux_2_5_i686.manylinux1_i686.whl", upload-time = 2025-11-04T13:41:12Z, size = 2180680, hashes = { sha256 = "2782c870e99878c634505236d81e5443092fba820f0373997ff75f90f68cd553" } }, + { url = "https://files.pythonhosted.org/packages/d3/9e/3ce66cebb929f3ced22be85d4c2399b8e85b622db77dad36b73c5387f8f8/pydantic_core-2.41.5-cp314-cp314-musllinux_1_1_aarch64.whl", upload-time = 2025-11-04T13:41:14Z, size = 2138960, hashes = { sha256 = "0177272f88ab8312479336e1d777f6b124537d47f2123f89cb37e0accea97f90" } }, + { url = "https://files.pythonhosted.org/packages/a6/62/205a998f4327d2079326b01abee48e502ea739d174f0a89295c481a2272e/pydantic_core-2.41.5-cp314-cp314-musllinux_1_1_armv7l.whl", upload-time = 2025-11-04T13:41:16Z, size = 2339102, hashes = { sha256 = "63510af5e38f8955b8ee5687740d6ebf7c2a0886d15a6d65c32814613681bc07" } }, + { url = "https://files.pythonhosted.org/packages/3c/0d/f05e79471e889d74d3d88f5bd20d0ed189ad94c2423d81ff8d0000aab4ff/pydantic_core-2.41.5-cp314-cp314-musllinux_1_1_x86_64.whl", upload-time = 2025-11-04T13:41:18Z, size = 2326039, hashes = { sha256 = "e56ba91f47764cc14f1daacd723e3e82d1a89d783f0f5afe9c364b8bb491ccdb" } }, + { url = "https://files.pythonhosted.org/packages/ec/e1/e08a6208bb100da7e0c4b288eed624a703f4d129bde2da475721a80cab32/pydantic_core-2.41.5-cp314-cp314-win32.whl", upload-time = 2025-11-04T13:41:21Z, size = 1995126, hashes = { sha256 = "aec5cf2fd867b4ff45b9959f8b20ea3993fc93e63c7363fe6851424c8a7e7c23" } }, + { url = "https://files.pythonhosted.org/packages/48/5d/56ba7b24e9557f99c9237e29f5c09913c81eeb2f3217e40e922353668092/pydantic_core-2.41.5-cp314-cp314-win_amd64.whl", upload-time = 2025-11-04T13:41:24Z, size = 2015489, hashes = { sha256 = "8e7c86f27c585ef37c35e56a96363ab8de4e549a95512445b85c96d3e2f7c1bf" } }, + { url = "https://files.pythonhosted.org/packages/4e/bb/f7a190991ec9e3e0ba22e4993d8755bbc4a32925c0b5b42775c03e8148f9/pydantic_core-2.41.5-cp314-cp314-win_arm64.whl", upload-time = 2025-11-04T13:41:26Z, size = 1977288, hashes = { sha256 = "e672ba74fbc2dc8eea59fb6d4aed6845e6905fc2a8afe93175d94a83ba2a01a0" } }, + { url = "https://files.pythonhosted.org/packages/92/ed/77542d0c51538e32e15afe7899d79efce4b81eee631d99850edc2f5e9349/pydantic_core-2.41.5-cp314-cp314t-macosx_10_12_x86_64.whl", upload-time = 2025-11-04T13:41:28Z, size = 2120255, hashes = { sha256 = "8566def80554c3faa0e65ac30ab0932b9e3a5cd7f8323764303d468e5c37595a" } }, + { url = "https://files.pythonhosted.org/packages/bb/3d/6913dde84d5be21e284439676168b28d8bbba5600d838b9dca99de0fad71/pydantic_core-2.41.5-cp314-cp314t-macosx_11_0_arm64.whl", upload-time = 2025-11-04T13:41:31Z, size = 1863760, hashes = { sha256 = "b80aa5095cd3109962a298ce14110ae16b8c1aece8b72f9dafe81cf597ad80b3" } }, + { url = "https://files.pythonhosted.org/packages/5a/f0/e5e6b99d4191da102f2b0eb9687aaa7f5bea5d9964071a84effc3e40f997/pydantic_core-2.41.5-cp314-cp314t-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", upload-time = 2025-11-04T13:41:33Z, size = 1878092, hashes = { sha256 = "3006c3dd9ba34b0c094c544c6006cc79e87d8612999f1a5d43b769b89181f23c" } }, + { url = "https://files.pythonhosted.org/packages/71/48/36fb760642d568925953bcc8116455513d6e34c4beaa37544118c36aba6d/pydantic_core-2.41.5-cp314-cp314t-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", upload-time = 2025-11-04T13:41:35Z, size = 2053385, hashes = { sha256 = "72f6c8b11857a856bcfa48c86f5368439f74453563f951e473514579d44aa612" } }, + { url = "https://files.pythonhosted.org/packages/20/25/92dc684dd8eb75a234bc1c764b4210cf2646479d54b47bf46061657292a8/pydantic_core-2.41.5-cp314-cp314t-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", upload-time = 2025-11-04T13:41:37Z, size = 2218832, hashes = { sha256 = "5cb1b2f9742240e4bb26b652a5aeb840aa4b417c7748b6f8387927bc6e45e40d" } }, + { url = "https://files.pythonhosted.org/packages/e2/09/f53e0b05023d3e30357d82eb35835d0f6340ca344720a4599cd663dca599/pydantic_core-2.41.5-cp314-cp314t-manylinux_2_17_s390x.manylinux2014_s390x.whl", upload-time = 2025-11-04T13:41:40Z, size = 2327585, hashes = { sha256 = "bd3d54f38609ff308209bd43acea66061494157703364ae40c951f83ba99a1a9" } }, + { url = "https://files.pythonhosted.org/packages/aa/4e/2ae1aa85d6af35a39b236b1b1641de73f5a6ac4d5a7509f77b814885760c/pydantic_core-2.41.5-cp314-cp314t-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", upload-time = 2025-11-04T13:41:42Z, size = 2041078, hashes = { sha256 = "2ff4321e56e879ee8d2a879501c8e469414d948f4aba74a2d4593184eb326660" } }, + { url = "https://files.pythonhosted.org/packages/cd/13/2e215f17f0ef326fc72afe94776edb77525142c693767fc347ed6288728d/pydantic_core-2.41.5-cp314-cp314t-manylinux_2_5_i686.manylinux1_i686.whl", upload-time = 2025-11-04T13:41:45Z, size = 2173914, hashes = { sha256 = "d0d2568a8c11bf8225044aa94409e21da0cb09dcdafe9ecd10250b2baad531a9" } }, + { url = "https://files.pythonhosted.org/packages/02/7a/f999a6dcbcd0e5660bc348a3991c8915ce6599f4f2c6ac22f01d7a10816c/pydantic_core-2.41.5-cp314-cp314t-musllinux_1_1_aarch64.whl", upload-time = 2025-11-04T13:41:47Z, size = 2129560, hashes = { sha256 = "a39455728aabd58ceabb03c90e12f71fd30fa69615760a075b9fec596456ccc3" } }, + { url = "https://files.pythonhosted.org/packages/3a/b1/6c990ac65e3b4c079a4fb9f5b05f5b013afa0f4ed6780a3dd236d2cbdc64/pydantic_core-2.41.5-cp314-cp314t-musllinux_1_1_armv7l.whl", upload-time = 2025-11-04T13:41:49Z, size = 2329244, hashes = { sha256 = "239edca560d05757817c13dc17c50766136d21f7cd0fac50295499ae24f90fdf" } }, + { url = "https://files.pythonhosted.org/packages/d9/02/3c562f3a51afd4d88fff8dffb1771b30cfdfd79befd9883ee094f5b6c0d8/pydantic_core-2.41.5-cp314-cp314t-musllinux_1_1_x86_64.whl", upload-time = 2025-11-04T13:41:54Z, size = 2331955, hashes = { sha256 = "2a5e06546e19f24c6a96a129142a75cee553cc018ffee48a460059b1185f4470" } }, + { url = "https://files.pythonhosted.org/packages/5c/96/5fb7d8c3c17bc8c62fdb031c47d77a1af698f1d7a406b0f79aaa1338f9ad/pydantic_core-2.41.5-cp314-cp314t-win32.whl", upload-time = 2025-11-04T13:41:56Z, size = 1988906, hashes = { sha256 = "b4ececa40ac28afa90871c2cc2b9ffd2ff0bf749380fbdf57d165fd23da353aa" } }, + { url = "https://files.pythonhosted.org/packages/22/ed/182129d83032702912c2e2d8bbe33c036f342cc735737064668585dac28f/pydantic_core-2.41.5-cp314-cp314t-win_amd64.whl", upload-time = 2025-11-04T13:41:58Z, size = 1981607, hashes = { sha256 = "80aa89cad80b32a912a65332f64a4450ed00966111b6615ca6816153d3585a8c" } }, + { url = "https://files.pythonhosted.org/packages/9f/ed/068e41660b832bb0b1aa5b58011dea2a3fe0ba7861ff38c4d4904c1c1a99/pydantic_core-2.41.5-cp314-cp314t-win_arm64.whl", upload-time = 2025-11-04T13:42:01Z, size = 1974769, hashes = { sha256 = "35b44f37a3199f771c3eaa53051bc8a70cd7b54f333531c59e29fd4db5d15008" } }, + { url = "https://files.pythonhosted.org/packages/11/72/90fda5ee3b97e51c494938a4a44c3a35a9c96c19bba12372fb9c634d6f57/pydantic_core-2.41.5-graalpy311-graalpy242_311_native-macosx_10_12_x86_64.whl", upload-time = 2025-11-04T13:42:39Z, size = 2115441, hashes = { sha256 = "b96d5f26b05d03cc60f11a7761a5ded1741da411e7fe0909e27a5e6a0cb7b034" } }, + { url = "https://files.pythonhosted.org/packages/1f/53/8942f884fa33f50794f119012dc6a1a02ac43a56407adaac20463df8e98f/pydantic_core-2.41.5-graalpy311-graalpy242_311_native-macosx_11_0_arm64.whl", upload-time = 2025-11-04T13:42:42Z, size = 1930291, hashes = { sha256 = "634e8609e89ceecea15e2d61bc9ac3718caaaa71963717bf3c8f38bfde64242c" } }, + { url = "https://files.pythonhosted.org/packages/79/c8/ecb9ed9cd942bce09fc888ee960b52654fbdbede4ba6c2d6e0d3b1d8b49c/pydantic_core-2.41.5-graalpy311-graalpy242_311_native-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", upload-time = 2025-11-04T13:42:44Z, size = 1948632, hashes = { sha256 = "93e8740d7503eb008aa2df04d3b9735f845d43ae845e6dcd2be0b55a2da43cd2" } }, + { url = "https://files.pythonhosted.org/packages/2e/1b/687711069de7efa6af934e74f601e2a4307365e8fdc404703afc453eab26/pydantic_core-2.41.5-graalpy311-graalpy242_311_native-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", upload-time = 2025-11-04T13:42:47Z, size = 2138905, hashes = { sha256 = "f15489ba13d61f670dcc96772e733aad1a6f9c429cc27574c6cdaed82d0146ad" } }, + { url = "https://files.pythonhosted.org/packages/09/32/59b0c7e63e277fa7911c2fc70ccfb45ce4b98991e7ef37110663437005af/pydantic_core-2.41.5-graalpy312-graalpy250_312_native-macosx_10_12_x86_64.whl", upload-time = 2025-11-04T13:42:49Z, size = 2110495, hashes = { sha256 = "7da7087d756b19037bc2c06edc6c170eeef3c3bafcb8f532ff17d64dc427adfd" } }, + { url = "https://files.pythonhosted.org/packages/aa/81/05e400037eaf55ad400bcd318c05bb345b57e708887f07ddb2d20e3f0e98/pydantic_core-2.41.5-graalpy312-graalpy250_312_native-macosx_11_0_arm64.whl", upload-time = 2025-11-04T13:42:52Z, size = 1915388, hashes = { sha256 = "aabf5777b5c8ca26f7824cb4a120a740c9588ed58df9b2d196ce92fba42ff8dc" } }, + { url = "https://files.pythonhosted.org/packages/6e/0d/e3549b2399f71d56476b77dbf3cf8937cec5cd70536bdc0e374a421d0599/pydantic_core-2.41.5-graalpy312-graalpy250_312_native-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", upload-time = 2025-11-04T13:42:56Z, size = 1942879, hashes = { sha256 = "c007fe8a43d43b3969e8469004e9845944f1a80e6acd47c150856bb87f230c56" } }, + { url = "https://files.pythonhosted.org/packages/f7/07/34573da085946b6a313d7c42f82f16e8920bfd730665de2d11c0c37a74b5/pydantic_core-2.41.5-graalpy312-graalpy250_312_native-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", upload-time = 2025-11-04T13:42:59Z, size = 2139017, hashes = { sha256 = "76d0819de158cd855d1cbb8fcafdf6f5cf1eb8e470abe056d5d161106e38062b" } }, + { url = "https://files.pythonhosted.org/packages/e6/b0/1a2aa41e3b5a4ba11420aba2d091b2d17959c8d1519ece3627c371951e73/pydantic_core-2.41.5-pp310-pypy310_pp73-macosx_10_12_x86_64.whl", upload-time = 2025-11-04T13:43:02Z, size = 2103351, hashes = { sha256 = "b5819cd790dbf0c5eb9f82c73c16b39a65dd6dd4d1439dcdea7816ec9adddab8" } }, + { url = "https://files.pythonhosted.org/packages/a4/ee/31b1f0020baaf6d091c87900ae05c6aeae101fa4e188e1613c80e4f1ea31/pydantic_core-2.41.5-pp310-pypy310_pp73-macosx_11_0_arm64.whl", upload-time = 2025-11-04T13:43:05Z, size = 1925363, hashes = { sha256 = "5a4e67afbc95fa5c34cf27d9089bca7fcab4e51e57278d710320a70b956d1b9a" } }, + { url = "https://files.pythonhosted.org/packages/e1/89/ab8e86208467e467a80deaca4e434adac37b10a9d134cd2f99b28a01e483/pydantic_core-2.41.5-pp310-pypy310_pp73-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", upload-time = 2025-11-04T13:43:08Z, size = 2135615, hashes = { sha256 = "ece5c59f0ce7d001e017643d8d24da587ea1f74f6993467d85ae8a5ef9d4f42b" } }, + { url = "https://files.pythonhosted.org/packages/99/0a/99a53d06dd0348b2008f2f30884b34719c323f16c3be4e6cc1203b74a91d/pydantic_core-2.41.5-pp310-pypy310_pp73-manylinux_2_5_i686.manylinux1_i686.whl", upload-time = 2025-11-04T13:43:12Z, size = 2175369, hashes = { sha256 = "16f80f7abe3351f8ea6858914ddc8c77e02578544a0ebc15b4c2e1a0e813b0b2" } }, + { url = "https://files.pythonhosted.org/packages/6d/94/30ca3b73c6d485b9bb0bc66e611cff4a7138ff9736b7e66bcf0852151636/pydantic_core-2.41.5-pp310-pypy310_pp73-musllinux_1_1_aarch64.whl", upload-time = 2025-11-04T13:43:15Z, size = 2144218, hashes = { sha256 = "33cb885e759a705b426baada1fe68cbb0a2e68e34c5d0d0289a364cf01709093" } }, + { url = "https://files.pythonhosted.org/packages/87/57/31b4f8e12680b739a91f472b5671294236b82586889ef764b5fbc6669238/pydantic_core-2.41.5-pp310-pypy310_pp73-musllinux_1_1_armv7l.whl", upload-time = 2025-11-04T13:43:18Z, size = 2329951, hashes = { sha256 = "c8d8b4eb992936023be7dee581270af5c6e0697a8559895f527f5b7105ecd36a" } }, + { url = "https://files.pythonhosted.org/packages/7d/73/3c2c8edef77b8f7310e6fb012dbc4b8551386ed575b9eb6fb2506e28a7eb/pydantic_core-2.41.5-pp310-pypy310_pp73-musllinux_1_1_x86_64.whl", upload-time = 2025-11-04T13:43:20Z, size = 2318428, hashes = { sha256 = "242a206cd0318f95cd21bdacff3fcc3aab23e79bba5cac3db5a841c9ef9c6963" } }, + { url = "https://files.pythonhosted.org/packages/2f/02/8559b1f26ee0d502c74f9cca5c0d2fd97e967e083e006bbbb4e97f3a043a/pydantic_core-2.41.5-pp310-pypy310_pp73-win_amd64.whl", upload-time = 2025-11-04T13:43:23Z, size = 2147009, hashes = { sha256 = "d3a978c4f57a597908b7e697229d996d77a6d3c94901e9edee593adada95ce1a" } }, + { url = "https://files.pythonhosted.org/packages/5f/9b/1b3f0e9f9305839d7e84912f9e8bfbd191ed1b1ef48083609f0dabde978c/pydantic_core-2.41.5-pp311-pypy311_pp73-macosx_10_12_x86_64.whl", upload-time = 2025-11-04T13:43:25Z, size = 2101980, hashes = { sha256 = "b2379fa7ed44ddecb5bfe4e48577d752db9fc10be00a6b7446e9663ba143de26" } }, + { url = "https://files.pythonhosted.org/packages/a4/ed/d71fefcb4263df0da6a85b5d8a7508360f2f2e9b3bf5814be9c8bccdccc1/pydantic_core-2.41.5-pp311-pypy311_pp73-macosx_11_0_arm64.whl", upload-time = 2025-11-04T13:43:28Z, size = 1923865, hashes = { sha256 = "266fb4cbf5e3cbd0b53669a6d1b039c45e3ce651fd5442eff4d07c2cc8d66808" } }, + { url = "https://files.pythonhosted.org/packages/ce/3a/626b38db460d675f873e4444b4bb030453bbe7b4ba55df821d026a0493c4/pydantic_core-2.41.5-pp311-pypy311_pp73-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", upload-time = 2025-11-04T13:43:31Z, size = 2134256, hashes = { sha256 = "58133647260ea01e4d0500089a8c4f07bd7aa6ce109682b1426394988d8aaacc" } }, + { url = "https://files.pythonhosted.org/packages/83/d9/8412d7f06f616bbc053d30cb4e5f76786af3221462ad5eee1f202021eb4e/pydantic_core-2.41.5-pp311-pypy311_pp73-manylinux_2_5_i686.manylinux1_i686.whl", upload-time = 2025-11-04T13:43:34Z, size = 2174762, hashes = { sha256 = "287dad91cfb551c363dc62899a80e9e14da1f0e2b6ebde82c806612ca2a13ef1" } }, + { url = "https://files.pythonhosted.org/packages/55/4c/162d906b8e3ba3a99354e20faa1b49a85206c47de97a639510a0e673f5da/pydantic_core-2.41.5-pp311-pypy311_pp73-musllinux_1_1_aarch64.whl", upload-time = 2025-11-04T13:43:37Z, size = 2143141, hashes = { sha256 = "03b77d184b9eb40240ae9fd676ca364ce1085f203e1b1256f8ab9984dca80a84" } }, + { url = "https://files.pythonhosted.org/packages/1f/f2/f11dd73284122713f5f89fc940f370d035fa8e1e078d446b3313955157fe/pydantic_core-2.41.5-pp311-pypy311_pp73-musllinux_1_1_armv7l.whl", upload-time = 2025-11-04T13:43:40Z, size = 2330317, hashes = { sha256 = "a668ce24de96165bb239160b3d854943128f4334822900534f2fe947930e5770" } }, + { url = "https://files.pythonhosted.org/packages/88/9d/b06ca6acfe4abb296110fb1273a4d848a0bfb2ff65f3ee92127b3244e16b/pydantic_core-2.41.5-pp311-pypy311_pp73-musllinux_1_1_x86_64.whl", upload-time = 2025-11-04T13:43:43Z, size = 2316992, hashes = { sha256 = "f14f8f046c14563f8eb3f45f499cc658ab8d10072961e07225e507adb700e93f" } }, + { url = "https://files.pythonhosted.org/packages/36/c7/cfc8e811f061c841d7990b0201912c3556bfeb99cdcb7ed24adc8d6f8704/pydantic_core-2.41.5-pp311-pypy311_pp73-win_amd64.whl", upload-time = 2025-11-04T13:43:46Z, size = 2145302, hashes = { sha256 = "56121965f7a4dc965bff783d70b907ddf3d57f6eba29b6d2e5dabfaf07799c51" } }, +] + +[[packages]] +name = "pygments" +version = "2.19.2" +sdist = { url = "https://files.pythonhosted.org/packages/b0/77/a5b8c569bf593b0140bde72ea885a803b82086995367bf2037de0159d924/pygments-2.19.2.tar.gz", upload-time = 2025-06-21T13:39:12Z, size = 4968631, hashes = { sha256 = "636cb2477cec7f8952536970bc533bc43743542f70392ae026374600add5b887" } } +wheels = [{ url = "https://files.pythonhosted.org/packages/c7/21/705964c7812476f378728bdf590ca4b771ec72385c533964653c68e86bdc/pygments-2.19.2-py3-none-any.whl", upload-time = 2025-06-21T13:39:07Z, size = 1225217, hashes = { sha256 = "86540386c03d588bb81d44bc3928634ff26449851e99741617ecb9037ee5ec0b" } }] + +[[packages]] +name = "pyjwt" +version = "2.10.1" +sdist = { url = "https://files.pythonhosted.org/packages/e7/46/bd74733ff231675599650d3e47f361794b22ef3e3770998dda30d3b63726/pyjwt-2.10.1.tar.gz", upload-time = 2024-11-28T03:43:29Z, size = 87785, hashes = { sha256 = "3cc5772eb20009233caf06e9d8a0577824723b44e6648ee0a2aedb6cf9381953" } } +wheels = [{ url = "https://files.pythonhosted.org/packages/61/ad/689f02752eeec26aed679477e80e632ef1b682313be70793d798c1d5fc8f/PyJWT-2.10.1-py3-none-any.whl", upload-time = 2024-11-28T03:43:27Z, size = 22997, hashes = { sha256 = "dcdd193e30abefd5debf142f9adfcdd2b58004e644f25406ffaebd50bd98dacb" } }] + +[[packages]] +name = "pyopenssl" +version = "26.0.0" +sdist = { url = "https://files.pythonhosted.org/packages/8e/11/a62e1d33b373da2b2c2cd9eb508147871c80f12b1cacde3c5d314922afdd/pyopenssl-26.0.0.tar.gz", upload-time = 2026-03-15T14:28:26Z, size = 185534, hashes = { sha256 = "f293934e52936f2e3413b89c6ce36df66a0b34ae1ea3a053b8c5020ff2f513fc" } } +wheels = [{ url = "https://files.pythonhosted.org/packages/fb/7d/d4f7d908fa8415571771b30669251d57c3cf313b36a856e6d7548ae01619/pyopenssl-26.0.0-py3-none-any.whl", upload-time = 2026-03-15T14:28:24Z, size = 57969, hashes = { sha256 = "df94d28498848b98cc1c0ffb8ef1e71e40210d3b0a8064c9d29571ed2904bf81" } }] + +[[packages]] +name = "python-dateutil" +version = "2.9.0.post0" +sdist = { url = "https://files.pythonhosted.org/packages/66/c0/0c8b6ad9f17a802ee498c46e004a0eb49bc148f2fd230864601a86dcf6db/python-dateutil-2.9.0.post0.tar.gz", upload-time = 2024-03-01T18:36:20Z, size = 342432, hashes = { sha256 = "37dd54208da7e1cd875388217d5e00ebd4179249f90fb72437e91a35459a0ad3" } } +wheels = [{ url = "https://files.pythonhosted.org/packages/ec/57/56b9bcc3c9c6a792fcbaf139543cee77261f3651ca9da0c93f5c1221264b/python_dateutil-2.9.0.post0-py2.py3-none-any.whl", upload-time = 2024-03-01T18:36:18Z, size = 229892, hashes = { sha256 = "a8b2bc7bffae282281c8140a97d3aa9c14da0b136dfe83f850eea9a5f7470427" } }] + +[[packages]] +name = "python-dotenv" +version = "1.2.2" +sdist = { url = "https://files.pythonhosted.org/packages/82/ed/0301aeeac3e5353ef3d94b6ec08bbcabd04a72018415dcb29e588514bba8/python_dotenv-1.2.2.tar.gz", upload-time = 2026-03-01T16:00:26Z, size = 50135, hashes = { sha256 = "2c371a91fbd7ba082c2c1dc1f8bf89ca22564a087c2c287cd9b662adde799cf3" } } +wheels = [{ url = "https://files.pythonhosted.org/packages/0b/d7/1959b9648791274998a9c3526f6d0ec8fd2233e4d4acce81bbae76b44b2a/python_dotenv-1.2.2-py3-none-any.whl", upload-time = 2026-03-01T16:00:25Z, size = 22101, hashes = { sha256 = "1d8214789a24de455a8b8bd8ae6fe3c6b69a5e3d64aa8a8e5d68e694bbcb285a" } }] + +[[packages]] +name = "pytz" +version = "2025.2" +sdist = { url = "https://files.pythonhosted.org/packages/f8/bf/abbd3cdfb8fbc7fb3d4d38d320f2441b1e7cbe29be4f23797b4a2b5d8aac/pytz-2025.2.tar.gz", upload-time = 2025-03-25T02:25:00Z, size = 320884, hashes = { sha256 = "360b9e3dbb49a209c21ad61809c7fb453643e048b38924c765813546746e81c3" } } +wheels = [{ url = "https://files.pythonhosted.org/packages/81/c4/34e93fe5f5429d7570ec1fa436f1986fb1f00c3e0f43a589fe2bbcd22c3f/pytz-2025.2-py2.py3-none-any.whl", upload-time = 2025-03-25T02:24:58Z, size = 509225, hashes = { sha256 = "5ddf76296dd8c44c26eb8f4b6f35488f3ccbf6fbbd7adee0b7262d43f0ec2f00" } }] + +[[packages]] +name = "pywin32-ctypes" +version = "0.2.3" +marker = "sys_platform == 'win32'" +sdist = { url = "https://files.pythonhosted.org/packages/85/9f/01a1a99704853cb63f253eea009390c88e7131c67e66a0a02099a8c917cb/pywin32-ctypes-0.2.3.tar.gz", upload-time = 2024-08-14T10:15:34Z, size = 29471, hashes = { sha256 = "d162dc04946d704503b2edc4d55f3dba5c1d539ead017afa00142c38b9885755" } } +wheels = [{ url = "https://files.pythonhosted.org/packages/de/3d/8161f7711c017e01ac9f008dfddd9410dff3674334c233bde66e7ba65bbf/pywin32_ctypes-0.2.3-py3-none-any.whl", upload-time = 2024-08-14T10:15:33Z, size = 30756, hashes = { sha256 = "8a1513379d709975552d202d942d9837758905c8d01eb82b8bcc30918929e7b8" } }] + +[[packages]] +name = "pyyaml" +version = "6.0.2" +sdist = { url = "https://files.pythonhosted.org/packages/54/ed/79a089b6be93607fa5cdaedf301d7dfb23af5f25c398d5ead2525b063e17/pyyaml-6.0.2.tar.gz", upload-time = 2024-08-06T20:33:50Z, size = 130631, hashes = { sha256 = "d584d9ec91ad65861cc08d42e834324ef890a082e591037abe114850ff7bbc3e" } } +wheels = [ + { url = "https://files.pythonhosted.org/packages/9b/95/a3fac87cb7158e231b5a6012e438c647e1a87f09f8e0d123acec8ab8bf71/PyYAML-6.0.2-cp310-cp310-macosx_10_9_x86_64.whl", upload-time = 2024-08-06T20:31:40Z, size = 184199, hashes = { sha256 = "0a9a2848a5b7feac301353437eb7d5957887edbf81d56e903999a75a3d743086" } }, + { url = "https://files.pythonhosted.org/packages/c7/7a/68bd47624dab8fd4afbfd3c48e3b79efe09098ae941de5b58abcbadff5cb/PyYAML-6.0.2-cp310-cp310-macosx_11_0_arm64.whl", upload-time = 2024-08-06T20:31:42Z, size = 171758, hashes = { sha256 = "29717114e51c84ddfba879543fb232a6ed60086602313ca38cce623c1d62cfbf" } }, + { url = "https://files.pythonhosted.org/packages/49/ee/14c54df452143b9ee9f0f29074d7ca5516a36edb0b4cc40c3f280131656f/PyYAML-6.0.2-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", upload-time = 2024-08-06T20:31:44Z, size = 718463, hashes = { sha256 = "8824b5a04a04a047e72eea5cec3bc266db09e35de6bdfe34c9436ac5ee27d237" } }, + { url = "https://files.pythonhosted.org/packages/4d/61/de363a97476e766574650d742205be468921a7b532aa2499fcd886b62530/PyYAML-6.0.2-cp310-cp310-manylinux_2_17_s390x.manylinux2014_s390x.whl", upload-time = 2024-08-06T20:31:50Z, size = 719280, hashes = { sha256 = "7c36280e6fb8385e520936c3cb3b8042851904eba0e58d277dca80a5cfed590b" } }, + { url = "https://files.pythonhosted.org/packages/6b/4e/1523cb902fd98355e2e9ea5e5eb237cbc5f3ad5f3075fa65087aa0ecb669/PyYAML-6.0.2-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", upload-time = 2024-08-06T20:31:52Z, size = 751239, hashes = { sha256 = "ec031d5d2feb36d1d1a24380e4db6d43695f3748343d99434e6f5f9156aaa2ed" } }, + { url = "https://files.pythonhosted.org/packages/b7/33/5504b3a9a4464893c32f118a9cc045190a91637b119a9c881da1cf6b7a72/PyYAML-6.0.2-cp310-cp310-musllinux_1_1_aarch64.whl", upload-time = 2024-08-06T20:31:53Z, size = 695802, hashes = { sha256 = "936d68689298c36b53b29f23c6dbb74de12b4ac12ca6cfe0e047bedceea56180" } }, + { url = "https://files.pythonhosted.org/packages/5c/20/8347dcabd41ef3a3cdc4f7b7a2aff3d06598c8779faa189cdbf878b626a4/PyYAML-6.0.2-cp310-cp310-musllinux_1_1_x86_64.whl", upload-time = 2024-08-06T20:31:55Z, size = 720527, hashes = { sha256 = "23502f431948090f597378482b4812b0caae32c22213aecf3b55325e049a6c68" } }, + { url = "https://files.pythonhosted.org/packages/be/aa/5afe99233fb360d0ff37377145a949ae258aaab831bde4792b32650a4378/PyYAML-6.0.2-cp310-cp310-win32.whl", upload-time = 2024-08-06T20:31:56Z, size = 144052, hashes = { sha256 = "2e99c6826ffa974fe6e27cdb5ed0021786b03fc98e5ee3c5bfe1fd5015f42b99" } }, + { url = "https://files.pythonhosted.org/packages/b5/84/0fa4b06f6d6c958d207620fc60005e241ecedceee58931bb20138e1e5776/PyYAML-6.0.2-cp310-cp310-win_amd64.whl", upload-time = 2024-08-06T20:31:58Z, size = 161774, hashes = { sha256 = "a4d3091415f010369ae4ed1fc6b79def9416358877534caf6a0fdd2146c87a3e" } }, + { url = "https://files.pythonhosted.org/packages/f8/aa/7af4e81f7acba21a4c6be026da38fd2b872ca46226673c89a758ebdc4fd2/PyYAML-6.0.2-cp311-cp311-macosx_10_9_x86_64.whl", upload-time = 2024-08-06T20:32:03Z, size = 184612, hashes = { sha256 = "cc1c1159b3d456576af7a3e4d1ba7e6924cb39de8f67111c735f6fc832082774" } }, + { url = "https://files.pythonhosted.org/packages/8b/62/b9faa998fd185f65c1371643678e4d58254add437edb764a08c5a98fb986/PyYAML-6.0.2-cp311-cp311-macosx_11_0_arm64.whl", upload-time = 2024-08-06T20:32:04Z, size = 172040, hashes = { sha256 = "1e2120ef853f59c7419231f3bf4e7021f1b936f6ebd222406c3b60212205d2ee" } }, + { url = "https://files.pythonhosted.org/packages/ad/0c/c804f5f922a9a6563bab712d8dcc70251e8af811fce4524d57c2c0fd49a4/PyYAML-6.0.2-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", upload-time = 2024-08-06T20:32:06Z, size = 736829, hashes = { sha256 = "5d225db5a45f21e78dd9358e58a98702a0302f2659a3c6cd320564b75b86f47c" } }, + { url = "https://files.pythonhosted.org/packages/51/16/6af8d6a6b210c8e54f1406a6b9481febf9c64a3109c541567e35a49aa2e7/PyYAML-6.0.2-cp311-cp311-manylinux_2_17_s390x.manylinux2014_s390x.whl", upload-time = 2024-08-06T20:32:08Z, size = 764167, hashes = { sha256 = "5ac9328ec4831237bec75defaf839f7d4564be1e6b25ac710bd1a96321cc8317" } }, + { url = "https://files.pythonhosted.org/packages/75/e4/2c27590dfc9992f73aabbeb9241ae20220bd9452df27483b6e56d3975cc5/PyYAML-6.0.2-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", upload-time = 2024-08-06T20:32:14Z, size = 762952, hashes = { sha256 = "3ad2a3decf9aaba3d29c8f537ac4b243e36bef957511b4766cb0057d32b0be85" } }, + { url = "https://files.pythonhosted.org/packages/9b/97/ecc1abf4a823f5ac61941a9c00fe501b02ac3ab0e373c3857f7d4b83e2b6/PyYAML-6.0.2-cp311-cp311-musllinux_1_1_aarch64.whl", upload-time = 2024-08-06T20:32:16Z, size = 735301, hashes = { sha256 = "ff3824dc5261f50c9b0dfb3be22b4567a6f938ccce4587b38952d85fd9e9afe4" } }, + { url = "https://files.pythonhosted.org/packages/45/73/0f49dacd6e82c9430e46f4a027baa4ca205e8b0a9dce1397f44edc23559d/PyYAML-6.0.2-cp311-cp311-musllinux_1_1_x86_64.whl", upload-time = 2024-08-06T20:32:18Z, size = 756638, hashes = { sha256 = "797b4f722ffa07cc8d62053e4cff1486fa6dc094105d13fea7b1de7d8bf71c9e" } }, + { url = "https://files.pythonhosted.org/packages/22/5f/956f0f9fc65223a58fbc14459bf34b4cc48dec52e00535c79b8db361aabd/PyYAML-6.0.2-cp311-cp311-win32.whl", upload-time = 2024-08-06T20:32:19Z, size = 143850, hashes = { sha256 = "11d8f3dd2b9c1207dcaf2ee0bbbfd5991f571186ec9cc78427ba5bd32afae4b5" } }, + { url = "https://files.pythonhosted.org/packages/ed/23/8da0bbe2ab9dcdd11f4f4557ccaf95c10b9811b13ecced089d43ce59c3c8/PyYAML-6.0.2-cp311-cp311-win_amd64.whl", upload-time = 2024-08-06T20:32:21Z, size = 161980, hashes = { sha256 = "e10ce637b18caea04431ce14fabcf5c64a1c61ec9c56b071a4b7ca131ca52d44" } }, + { url = "https://files.pythonhosted.org/packages/86/0c/c581167fc46d6d6d7ddcfb8c843a4de25bdd27e4466938109ca68492292c/PyYAML-6.0.2-cp312-cp312-macosx_10_9_x86_64.whl", upload-time = 2024-08-06T20:32:25Z, size = 183873, hashes = { sha256 = "c70c95198c015b85feafc136515252a261a84561b7b1d51e3384e0655ddf25ab" } }, + { url = "https://files.pythonhosted.org/packages/a8/0c/38374f5bb272c051e2a69281d71cba6fdb983413e6758b84482905e29a5d/PyYAML-6.0.2-cp312-cp312-macosx_11_0_arm64.whl", upload-time = 2024-08-06T20:32:26Z, size = 173302, hashes = { sha256 = "ce826d6ef20b1bc864f0a68340c8b3287705cae2f8b4b1d932177dcc76721725" } }, + { url = "https://files.pythonhosted.org/packages/c3/93/9916574aa8c00aa06bbac729972eb1071d002b8e158bd0e83a3b9a20a1f7/PyYAML-6.0.2-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", upload-time = 2024-08-06T20:32:28Z, size = 739154, hashes = { sha256 = "1f71ea527786de97d1a0cc0eacd1defc0985dcf6b3f17bb77dcfc8c34bec4dc5" } }, + { url = "https://files.pythonhosted.org/packages/95/0f/b8938f1cbd09739c6da569d172531567dbcc9789e0029aa070856f123984/PyYAML-6.0.2-cp312-cp312-manylinux_2_17_s390x.manylinux2014_s390x.whl", upload-time = 2024-08-06T20:32:30Z, size = 766223, hashes = { sha256 = "9b22676e8097e9e22e36d6b7bda33190d0d400f345f23d4065d48f4ca7ae0425" } }, + { url = "https://files.pythonhosted.org/packages/b9/2b/614b4752f2e127db5cc206abc23a8c19678e92b23c3db30fc86ab731d3bd/PyYAML-6.0.2-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", upload-time = 2024-08-06T20:32:31Z, size = 767542, hashes = { sha256 = "80bab7bfc629882493af4aa31a4cfa43a4c57c83813253626916b8c7ada83476" } }, + { url = "https://files.pythonhosted.org/packages/d4/00/dd137d5bcc7efea1836d6264f049359861cf548469d18da90cd8216cf05f/PyYAML-6.0.2-cp312-cp312-musllinux_1_1_aarch64.whl", upload-time = 2024-08-06T20:32:37Z, size = 731164, hashes = { sha256 = "0833f8694549e586547b576dcfaba4a6b55b9e96098b36cdc7ebefe667dfed48" } }, + { url = "https://files.pythonhosted.org/packages/c9/1f/4f998c900485e5c0ef43838363ba4a9723ac0ad73a9dc42068b12aaba4e4/PyYAML-6.0.2-cp312-cp312-musllinux_1_1_x86_64.whl", upload-time = 2024-08-06T20:32:38Z, size = 756611, hashes = { sha256 = "8b9c7197f7cb2738065c481a0461e50ad02f18c78cd75775628afb4d7137fb3b" } }, + { url = "https://files.pythonhosted.org/packages/df/d1/f5a275fdb252768b7a11ec63585bc38d0e87c9e05668a139fea92b80634c/PyYAML-6.0.2-cp312-cp312-win32.whl", upload-time = 2024-08-06T20:32:40Z, size = 140591, hashes = { sha256 = "ef6107725bd54b262d6dedcc2af448a266975032bc85ef0172c5f059da6325b4" } }, + { url = "https://files.pythonhosted.org/packages/0c/e8/4f648c598b17c3d06e8753d7d13d57542b30d56e6c2dedf9c331ae56312e/PyYAML-6.0.2-cp312-cp312-win_amd64.whl", upload-time = 2024-08-06T20:32:41Z, size = 156338, hashes = { sha256 = "7e7401d0de89a9a855c839bc697c079a4af81cf878373abd7dc625847d25cbd8" } }, + { url = "https://files.pythonhosted.org/packages/ef/e3/3af305b830494fa85d95f6d95ef7fa73f2ee1cc8ef5b495c7c3269fb835f/PyYAML-6.0.2-cp313-cp313-macosx_10_13_x86_64.whl", upload-time = 2024-08-06T20:32:43Z, size = 181309, hashes = { sha256 = "efdca5630322a10774e8e98e1af481aad470dd62c3170801852d752aa7a783ba" } }, + { url = "https://files.pythonhosted.org/packages/45/9f/3b1c20a0b7a3200524eb0076cc027a970d320bd3a6592873c85c92a08731/PyYAML-6.0.2-cp313-cp313-macosx_11_0_arm64.whl", upload-time = 2024-08-06T20:32:44Z, size = 171679, hashes = { sha256 = "50187695423ffe49e2deacb8cd10510bc361faac997de9efef88badc3bb9e2d1" } }, + { url = "https://files.pythonhosted.org/packages/7c/9a/337322f27005c33bcb656c655fa78325b730324c78620e8328ae28b64d0c/PyYAML-6.0.2-cp313-cp313-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", upload-time = 2024-08-06T20:32:46Z, size = 733428, hashes = { sha256 = "0ffe8360bab4910ef1b9e87fb812d8bc0a308b0d0eef8c8f44e0254ab3b07133" } }, + { url = "https://files.pythonhosted.org/packages/a3/69/864fbe19e6c18ea3cc196cbe5d392175b4cf3d5d0ac1403ec3f2d237ebb5/PyYAML-6.0.2-cp313-cp313-manylinux_2_17_s390x.manylinux2014_s390x.whl", upload-time = 2024-08-06T20:32:51Z, size = 763361, hashes = { sha256 = "17e311b6c678207928d649faa7cb0d7b4c26a0ba73d41e99c4fff6b6c3276484" } }, + { url = "https://files.pythonhosted.org/packages/04/24/b7721e4845c2f162d26f50521b825fb061bc0a5afcf9a386840f23ea19fa/PyYAML-6.0.2-cp313-cp313-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", upload-time = 2024-08-06T20:32:53Z, size = 759523, hashes = { sha256 = "70b189594dbe54f75ab3a1acec5f1e3faa7e8cf2f1e08d9b561cb41b845f69d5" } }, + { url = "https://files.pythonhosted.org/packages/2b/b2/e3234f59ba06559c6ff63c4e10baea10e5e7df868092bf9ab40e5b9c56b6/PyYAML-6.0.2-cp313-cp313-musllinux_1_1_aarch64.whl", upload-time = 2024-08-06T20:32:54Z, size = 726660, hashes = { sha256 = "41e4e3953a79407c794916fa277a82531dd93aad34e29c2a514c2c0c5fe971cc" } }, + { url = "https://files.pythonhosted.org/packages/fe/0f/25911a9f080464c59fab9027482f822b86bf0608957a5fcc6eaac85aa515/PyYAML-6.0.2-cp313-cp313-musllinux_1_1_x86_64.whl", upload-time = 2024-08-06T20:32:56Z, size = 751597, hashes = { sha256 = "68ccc6023a3400877818152ad9a1033e3db8625d899c72eacb5a668902e4d652" } }, + { url = "https://files.pythonhosted.org/packages/14/0d/e2c3b43bbce3cf6bd97c840b46088a3031085179e596d4929729d8d68270/PyYAML-6.0.2-cp313-cp313-win32.whl", upload-time = 2024-08-06T20:33:03Z, size = 140527, hashes = { sha256 = "bc2fa7c6b47d6bc618dd7fb02ef6fdedb1090ec036abab80d4681424b84c1183" } }, + { url = "https://files.pythonhosted.org/packages/fa/de/02b54f42487e3d3c6efb3f89428677074ca7bf43aae402517bc7cca949f3/PyYAML-6.0.2-cp313-cp313-win_amd64.whl", upload-time = 2024-08-06T20:33:04Z, size = 156446, hashes = { sha256 = "8388ee1976c416731879ac16da0aff3f63b286ffdd57cdeb95f3f2e085687563" } }, +] + +[[packages]] +name = "requests" +version = "2.33.0" +sdist = { url = "https://files.pythonhosted.org/packages/34/64/8860370b167a9721e8956ae116825caff829224fbca0ca6e7bf8ddef8430/requests-2.33.0.tar.gz", upload-time = 2026-03-25T15:10:41Z, size = 134232, hashes = { sha256 = "c7ebc5e8b0f21837386ad0e1c8fe8b829fa5f544d8df3b2253bff14ef29d7652" } } +wheels = [{ url = "https://files.pythonhosted.org/packages/56/5d/c814546c2333ceea4ba42262d8c4d55763003e767fa169adc693bd524478/requests-2.33.0-py3-none-any.whl", upload-time = 2026-03-25T15:10:40Z, size = 65017, hashes = { sha256 = "3324635456fa185245e24865e810cecec7b4caf933d7eb133dcde67d48cee69b" } }] + +[[packages]] +name = "requirements-parser" +version = "0.13.0" +sdist = { url = "https://files.pythonhosted.org/packages/95/96/fb6dbfebb524d5601d359a47c78fe7ba1eef90fc4096404aa60c9a906fbb/requirements_parser-0.13.0.tar.gz", upload-time = 2025-05-21T13:42:05Z, size = 22630, hashes = { sha256 = "0843119ca2cb2331de4eb31b10d70462e39ace698fd660a915c247d2301a4418" } } +wheels = [{ url = "https://files.pythonhosted.org/packages/bd/60/50fbb6ffb35f733654466f1a90d162bcbea358adc3b0871339254fbc37b2/requirements_parser-0.13.0-py3-none-any.whl", upload-time = 2025-05-21T13:42:04Z, size = 14782, hashes = { sha256 = "2b3173faecf19ec5501971b7222d38f04cb45bb9d87d0ad629ca71e2e62ded14" } }] + +[[packages]] +name = "rich" +version = "14.0.0" +sdist = { url = "https://files.pythonhosted.org/packages/a1/53/830aa4c3066a8ab0ae9a9955976fb770fe9c6102117c8ec4ab3ea62d89e8/rich-14.0.0.tar.gz", upload-time = 2025-03-30T14:15:14Z, size = 224078, hashes = { sha256 = "82f1bc23a6a21ebca4ae0c45af9bdbc492ed20231dcb63f297d6d1021a9d5725" } } +wheels = [{ url = "https://files.pythonhosted.org/packages/0d/9b/63f4c7ebc259242c89b3acafdb37b41d1185c07ff0011164674e9076b491/rich-14.0.0-py3-none-any.whl", upload-time = 2025-03-30T14:15:12Z, size = 243229, hashes = { sha256 = "1c9491e1951aac09caffd42f448ee3d04e58923ffe14993f6e83068dc395d7e0" } }] + +[[packages]] +name = "s3transfer" +version = "0.13.1" +sdist = { url = "https://files.pythonhosted.org/packages/6d/05/d52bf1e65044b4e5e27d4e63e8d1579dbdec54fce685908ae09bc3720030/s3transfer-0.13.1.tar.gz", upload-time = 2025-07-18T19:22:42Z, size = 150589, hashes = { sha256 = "c3fdba22ba1bd367922f27ec8032d6a1cf5f10c934fb5d68cf60fd5a23d936cf" } } +wheels = [{ url = "https://files.pythonhosted.org/packages/6d/4f/d073e09df851cfa251ef7840007d04db3293a0482ce607d2b993926089be/s3transfer-0.13.1-py3-none-any.whl", upload-time = 2025-07-18T19:22:40Z, size = 85308, hashes = { sha256 = "a981aa7429be23fe6dfc13e80e4020057cbab622b08c0315288758d67cabc724" } }] + +[[packages]] +name = "secretstorage" +version = "3.3.3" +marker = "sys_platform == 'linux'" +sdist = { url = "https://files.pythonhosted.org/packages/53/a4/f48c9d79cb507ed1373477dbceaba7401fd8a23af63b837fa61f1dcd3691/SecretStorage-3.3.3.tar.gz", upload-time = 2022-08-13T16:22:46Z, size = 19739, hashes = { sha256 = "2403533ef369eca6d2ba81718576c5e0f564d5cca1b58f73a8b23e7d4eeebd77" } } +wheels = [{ url = "https://files.pythonhosted.org/packages/54/24/b4293291fa1dd830f353d2cb163295742fa87f179fcc8a20a306a81978b7/SecretStorage-3.3.3-py3-none-any.whl", upload-time = 2022-08-13T16:22:44Z, size = 15221, hashes = { sha256 = "f356e6628222568e3af06f2eba8df495efa13b3b63081dafd4f7d9a7b7bc9f99" } }] + +[[packages]] +name = "setuptools" +version = "80.8.0" +sdist = { url = "https://files.pythonhosted.org/packages/8d/d2/ec1acaaff45caed5c2dedb33b67055ba9d4e96b091094df90762e60135fe/setuptools-80.8.0.tar.gz", upload-time = 2025-05-20T14:02:53Z, size = 1319720, hashes = { sha256 = "49f7af965996f26d43c8ae34539c8d99c5042fbff34302ea151eaa9c207cd257" } } +wheels = [{ url = "https://files.pythonhosted.org/packages/58/29/93c53c098d301132196c3238c312825324740851d77a8500a2462c0fd888/setuptools-80.8.0-py3-none-any.whl", upload-time = 2025-05-20T14:02:51Z, size = 1201470, hashes = { sha256 = "95a60484590d24103af13b686121328cc2736bee85de8936383111e421b9edc0" } }] + +[[packages]] +name = "shellingham" +version = "1.5.4" +sdist = { url = "https://files.pythonhosted.org/packages/58/15/8b3609fd3830ef7b27b655beb4b4e9c62313a4e8da8c676e142cc210d58e/shellingham-1.5.4.tar.gz", upload-time = 2023-10-24T04:13:40Z, size = 10310, hashes = { sha256 = "8dbca0739d487e5bd35ab3ca4b36e11c4078f3a234bfce294b0a0291363404de" } } +wheels = [{ url = "https://files.pythonhosted.org/packages/e0/f9/0595336914c5619e5f28a1fb793285925a8cd4b432c9da0a987836c7f822/shellingham-1.5.4-py2.py3-none-any.whl", upload-time = 2023-10-24T04:13:38Z, size = 9755, hashes = { sha256 = "7ecfff8f2fd72616f7481040475a65b2bf8af90a56c89140852d1120324e8686" } }] + +[[packages]] +name = "six" +version = "1.17.0" +sdist = { url = "https://files.pythonhosted.org/packages/94/e7/b2c673351809dca68a0e064b6af791aa332cf192da575fd474ed7d6f16a2/six-1.17.0.tar.gz", upload-time = 2024-12-04T17:35:28Z, size = 34031, hashes = { sha256 = "ff70335d468e7eb6ec65b95b99d3a2836546063f63acc5171de367e834932a81" } } +wheels = [{ url = "https://files.pythonhosted.org/packages/b7/ce/149a00dd41f10bc29e5921b496af8b574d8413afcd5e30dfa0ed46c2cc5e/six-1.17.0-py2.py3-none-any.whl", upload-time = 2024-12-04T17:35:26Z, size = 11050, hashes = { sha256 = "4721f391ed90541fddacab5acf947aa0d3dc7d27b2e1e8eda2be8970586c3274" } }] + +[[packages]] +name = "smmap" +version = "5.0.2" +sdist = { url = "https://files.pythonhosted.org/packages/44/cd/a040c4b3119bbe532e5b0732286f805445375489fceaec1f48306068ee3b/smmap-5.0.2.tar.gz", upload-time = 2025-01-02T07:14:40Z, size = 22329, hashes = { sha256 = "26ea65a03958fa0c8a1c7e8c7a58fdc77221b8910f6be2131affade476898ad5" } } +wheels = [{ url = "https://files.pythonhosted.org/packages/04/be/d09147ad1ec7934636ad912901c5fd7667e1c858e19d355237db0d0cd5e4/smmap-5.0.2-py3-none-any.whl", upload-time = 2025-01-02T07:14:38Z, size = 24303, hashes = { sha256 = "b30115f0def7d7531d22a0fb6502488d879e75b260a9db4d0819cfb25403af5e" } }] + +[[packages]] +name = "snowflake-connector-python" +version = "4.7.1" +sdist = { url = "https://files.pythonhosted.org/packages/61/36/78aab852031f30b559d745dc9bae95bd1028c647a2c16ce5af7f98a072aa/snowflake_connector_python-4.7.1.tar.gz", upload-time = 2026-07-15T16:25:19Z, size = 947196, hashes = { sha256 = "fad8e1fb0c49eb1d93dad785ce738dac17473c6826f8cd36ee8d6f9675bceae1" } } +wheels = [ + { url = "https://files.pythonhosted.org/packages/1e/64/6f4f0e3f336727aac455195a9dbb60966e1876f670a58ecd6cab839e2174/snowflake_connector_python-4.7.1-cp310-cp310-macosx_14_0_arm64.whl", upload-time = 2026-07-15T16:25:21Z, size = 1176798, hashes = { sha256 = "ff5bb51c1c21cbbb5c90b9785cc3df9d649c64db26553668b1a1ea8a461a0d5b" } }, + { url = "https://files.pythonhosted.org/packages/5e/52/48ab58056da9daab1ca9a6922ddfa4ea965634fb9803b08c271e5b495d19/snowflake_connector_python-4.7.1-cp310-cp310-macosx_14_0_x86_64.whl", upload-time = 2026-07-15T16:25:22Z, size = 1188930, hashes = { sha256 = "0e23a5aaa1e9eaa9ba88b8867247d55c24e40f3be6367e3aa4f8633b37f56317" } }, + { url = "https://files.pythonhosted.org/packages/97/6e/0f5a5a6eba6914188a4b3eca6fef18f7d41efb50e7f99d28222414788d3c/snowflake_connector_python-4.7.1-cp310-cp310-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", upload-time = 2026-07-15T16:25:02Z, size = 2828348, hashes = { sha256 = "571eeeb5f3b034671c125186ecb02a2e54ebee1fad1b3af7f091773dc11f693d" } }, + { url = "https://files.pythonhosted.org/packages/86/e6/58240046024f1d9471585bd844e964867ad923c4b2e95fe168adbb32db45/snowflake_connector_python-4.7.1-cp310-cp310-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", upload-time = 2026-07-15T16:25:04Z, size = 2859089, hashes = { sha256 = "167ba97d5b615b507fc8234266c8736ea97f91b7324db7a305a28ec3505c0edf" } }, + { url = "https://files.pythonhosted.org/packages/4a/69/61bfa65620636635eb896bc75222234782243499bf1800c1095d55620c81/snowflake_connector_python-4.7.1-cp310-cp310-win_amd64.whl", upload-time = 2026-07-15T16:25:37Z, size = 5396818, hashes = { sha256 = "d5821dc73d305b0804aa415a4fafee018e611cc8cf339de36a5c8c44fb57beaa" } }, + { url = "https://files.pythonhosted.org/packages/80/d4/490c982c2d0f55c2570cbc61625bda8cafe11a6498b533788245696414f4/snowflake_connector_python-4.7.1-cp311-cp311-macosx_14_0_arm64.whl", upload-time = 2026-07-15T16:25:23Z, size = 1176486, hashes = { sha256 = "4b79e818d83306babff9d0803e697a008e8ada961deff55e3c5da0a9c3505d9a" } }, + { url = "https://files.pythonhosted.org/packages/58/21/f4c28c940ee435b29a6e1d227330461c134b3d3384393ef13b15a97c688c/snowflake_connector_python-4.7.1-cp311-cp311-macosx_14_0_x86_64.whl", upload-time = 2026-07-15T16:25:25Z, size = 1188641, hashes = { sha256 = "f5a066d2c1db940740c49bafe1c8983395bc574bc31465263898c9bd050c5c52" } }, + { url = "https://files.pythonhosted.org/packages/98/00/4ab3d2fccdbaa8bcaf76565cb67df7414dce84abc89300649fad522aa36d/snowflake_connector_python-4.7.1-cp311-cp311-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", upload-time = 2026-07-15T16:25:06Z, size = 2841694, hashes = { sha256 = "4f2c6f40544739b43f2da262dd67a32d4ac7ca2b09cc6b7181f02b24c60d5754" } }, + { url = "https://files.pythonhosted.org/packages/e5/c7/092bea1c5f3b65f0dd8b317017a0c0a6ddd6722064aaf51cb88fb288e221/snowflake_connector_python-4.7.1-cp311-cp311-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", upload-time = 2026-07-15T16:25:08Z, size = 2870751, hashes = { sha256 = "e582f6ac2c5fa53170ae28303d361ea6cc64e695ba528f1b6ec8529476324327" } }, + { url = "https://files.pythonhosted.org/packages/9d/f6/6be011782ff028fc1f85173db10ffb5d8dd869face5385323525352335ff/snowflake_connector_python-4.7.1-cp311-cp311-win_amd64.whl", upload-time = 2026-07-15T16:25:39Z, size = 5396794, hashes = { sha256 = "22afd6de9fec8ef2cc23b231af9fc0f352351cd53da0cc11c2495272eff04ccf" } }, + { url = "https://files.pythonhosted.org/packages/59/9f/c6d26c377c5b2251d680410d9f8ef755375f1dc686fcfac185aaee877d8b/snowflake_connector_python-4.7.1-cp312-cp312-macosx_14_0_arm64.whl", upload-time = 2026-07-15T16:25:27Z, size = 1175873, hashes = { sha256 = "b03ef22742fee88d387f2ec3969dfb032417a2119bd366414b2c5ac9acb38a45" } }, + { url = "https://files.pythonhosted.org/packages/00/fd/9081cc54c1876ce432955d7ccbdfdf690089e7052a38f4ef4a03fcd3e1bb/snowflake_connector_python-4.7.1-cp312-cp312-macosx_14_0_x86_64.whl", upload-time = 2026-07-15T16:25:28Z, size = 1188966, hashes = { sha256 = "1ed85264edb186a39e641f1b744a1a1973e10d9d15a96a46a1e78ac67442c7b6" } }, + { url = "https://files.pythonhosted.org/packages/e3/cc/4d746edc16b72d256376168df0308c17df4f1945fdf7f09a6af238f91058/snowflake_connector_python-4.7.1-cp312-cp312-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", upload-time = 2026-07-15T16:25:09Z, size = 2888618, hashes = { sha256 = "f4e3b1222bc53b56ba4aa88224afe1ad894ee6f7389b2c0105b829d13d559aa7" } }, + { url = "https://files.pythonhosted.org/packages/e4/59/ab32d9c83b3392a3124a2880cb24a292f69770a0c50702ccf87a26efa7f0/snowflake_connector_python-4.7.1-cp312-cp312-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", upload-time = 2026-07-15T16:25:11Z, size = 2921449, hashes = { sha256 = "5b7b27df86fc13afccabdb525973c0e9a730f8c25f07a4a55f95bc2ad88f8d35" } }, + { url = "https://files.pythonhosted.org/packages/85/2d/1fe3e09b8a24a144be3e7e4690443c06a75f2a7dd17fcff57db95c986385/snowflake_connector_python-4.7.1-cp312-cp312-win_amd64.whl", upload-time = 2026-07-15T16:25:40Z, size = 5395979, hashes = { sha256 = "92b935a4f73651ea1306b8c3c58003e0e5a72fa3a86453087161296168c31ed3" } }, + { url = "https://files.pythonhosted.org/packages/8a/c9/bb7999c73c0d6b4a0af84099f9812c85c89f38c196e95855beb1cd675eeb/snowflake_connector_python-4.7.1-cp313-cp313-macosx_14_0_arm64.whl", upload-time = 2026-07-15T16:25:30Z, size = 1174896, hashes = { sha256 = "5011a5eb55dd80fed4198081743f75a8a56412b4623cb9cf61ce2813f600cbd9" } }, + { url = "https://files.pythonhosted.org/packages/10/7f/a376d21db923f15e560843582c1669747e14c8281a84eb15339f314cd85a/snowflake_connector_python-4.7.1-cp313-cp313-macosx_14_0_x86_64.whl", upload-time = 2026-07-15T16:25:32Z, size = 1187518, hashes = { sha256 = "f2482d1b059fcaa45b7edf8ea97b0f35179ab781fb1ce4716809d83d9f5f4d4c" } }, + { url = "https://files.pythonhosted.org/packages/03/1f/b58abba9d48ca3d824a5a855a6b98665bc7aa7ac75a53ec09c3f14dad8fc/snowflake_connector_python-4.7.1-cp313-cp313-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", upload-time = 2026-07-15T16:25:12Z, size = 2858741, hashes = { sha256 = "8a5e2bd3701176521577eea8c5b384178b404bbcd73d634f86b888929c15fb8a" } }, + { url = "https://files.pythonhosted.org/packages/2c/96/6b80dbb95450155780c152f64dbd59352ed38cc268e7e707ce049e449f5f/snowflake_connector_python-4.7.1-cp313-cp313-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", upload-time = 2026-07-15T16:25:14Z, size = 2894377, hashes = { sha256 = "7c67e735d38403109f6bc1a17022a4c950966e1c989f5e2376b9c28c5ece0fb5" } }, + { url = "https://files.pythonhosted.org/packages/87/1b/706fead3bfbd7e879f428151352d38dc35dd3e611304f40bc16423bbb3cc/snowflake_connector_python-4.7.1-cp313-cp313-win_amd64.whl", upload-time = 2026-07-15T16:25:42Z, size = 5396043, hashes = { sha256 = "81a8f1ae86222e7b8561f41f688462687a435f5afc8aa34ae42f69c0c0f16a57" } }, + { url = "https://files.pythonhosted.org/packages/f1/cd/1a274a7c3648bd80205763146bcf999773fe6066eb39f49bf15399b2cbe6/snowflake_connector_python-4.7.1-cp314-cp314-macosx_14_0_arm64.whl", upload-time = 2026-07-15T16:25:33Z, size = 1175354, hashes = { sha256 = "052fd457fa79616d074f5b8f2e106f9f2ca36645526e4d0abddf7e3685d1bdb7" } }, + { url = "https://files.pythonhosted.org/packages/e7/a9/3af345429d114ea44f3e7658266cf76545e2a7fea57786553ae0fa008aa7/snowflake_connector_python-4.7.1-cp314-cp314-macosx_14_0_x86_64.whl", upload-time = 2026-07-15T16:25:35Z, size = 1187525, hashes = { sha256 = "171ef3515c44e804dd86ab2b288179037fec40b3f45e1aa28fba095b764f9a6a" } }, + { url = "https://files.pythonhosted.org/packages/03/4f/91bb2e52c2724520719876683b00bf200926fe1b379911fdda98d19290de/snowflake_connector_python-4.7.1-cp314-cp314-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", upload-time = 2026-07-15T16:25:15Z, size = 2859174, hashes = { sha256 = "051e49157d955e5ba76577345169dd1869288ecef903dab17ec6180e1009b117" } }, + { url = "https://files.pythonhosted.org/packages/99/63/182cab4e40430ab89eda244d59569e5c9f94eb69a6c106b9acc7cee96924/snowflake_connector_python-4.7.1-cp314-cp314-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", upload-time = 2026-07-15T16:25:17Z, size = 2889887, hashes = { sha256 = "2b3e53626c600b59c181ce18e2b253487fb7e2b0d30ac0d3cfbd3ddffd13743b" } }, + { url = "https://files.pythonhosted.org/packages/95/12/f5e4a07dba58c36ab2d899e46c06145719698b8970bc0089e5708a83957b/snowflake_connector_python-4.7.1-cp314-cp314-win_amd64.whl", upload-time = 2026-07-15T16:25:44Z, size = 5454130, hashes = { sha256 = "7703f33059daa6e30d824e5d88bd525a7feccd38412b888c6e35cf0847cc34b0" } }, +] + +[[packages]] +name = "snowflake-core" +version = "1.10.0" +sdist = { url = "https://files.pythonhosted.org/packages/70/6e/35682a0f5c427386a5520dffff09cd70dd2738c1c7bc4d58a30397deb84d/snowflake_core-1.10.0.tar.gz", upload-time = 2025-12-08T11:49:10Z, size = 952413, hashes = { sha256 = "242d9635aea06dc2a5e5494ceb6077c76da93bd102ef6e5b477d6820c7d209d9" } } +wheels = [{ url = "https://files.pythonhosted.org/packages/6f/96/fe627f6c8bcb4778e3a58265d06ea37b71407f4721ae4cb14911e39382b8/snowflake_core-1.10.0-py3-none-any.whl", upload-time = 2025-12-08T11:49:12Z, size = 1598709, hashes = { sha256 = "b1bf11a7fd787b338b06acbd210d21a6c491084504d4219fb7f59bbd35e115f2" } }] + +[[packages]] +name = "snowflake-snowpark-python" +version = "1.53.0" +sdist = { url = "https://files.pythonhosted.org/packages/67/98/38189e919c54fc6f09a43e778d850ebf2116ced491595971ae5f9ca01b0c/snowflake_snowpark_python-1.53.0.tar.gz", upload-time = 2026-07-09T16:15:19Z, size = 1783158, hashes = { sha256 = "6eab04d5703fac72982f3666140c006d6fb9964d46a04fd953766410af8fc62e" } } +wheels = [{ url = "https://files.pythonhosted.org/packages/e9/43/ae0aa755bfddf902223386ed55e290113763632d6cc0bc3932fc20c6902f/snowflake_snowpark_python-1.53.0-py3-none-any.whl", upload-time = 2026-07-09T16:15:18Z, size = 1871191, hashes = { sha256 = "5b1048fe624534be181ffd152f05ac6e621455674359532de6ab6e478a287b9b" } }] + +[[packages]] +name = "sortedcontainers" +version = "2.4.0" +sdist = { url = "https://files.pythonhosted.org/packages/e8/c4/ba2f8066cceb6f23394729afe52f3bf7adec04bf9ed2c820b39e19299111/sortedcontainers-2.4.0.tar.gz", upload-time = 2021-05-16T22:03:42Z, size = 30594, hashes = { sha256 = "25caa5a06cc30b6b83d11423433f65d1f9d76c4c6a0c90e3379eaa43b9bfdb88" } } +wheels = [{ url = "https://files.pythonhosted.org/packages/32/46/9cb0e58b2deb7f82b84065f37f3bffeb12413f947f9388e4cac22c4621ce/sortedcontainers-2.4.0-py2.py3-none-any.whl", upload-time = 2021-05-16T22:03:41Z, size = 29575, hashes = { sha256 = "a163dcaede0f1c021485e957a39245190e74249897e2ae4b2aa38595db237ee0" } }] + +[[packages]] +name = "tomlkit" +version = "0.13.3" +sdist = { url = "https://files.pythonhosted.org/packages/cc/18/0bbf3884e9eaa38819ebe46a7bd25dcd56b67434402b66a58c4b8e552575/tomlkit-0.13.3.tar.gz", upload-time = 2025-06-05T07:13:44Z, size = 185207, hashes = { sha256 = "430cf247ee57df2b94ee3fbe588e71d362a941ebb545dec29b53961d61add2a1" } } +wheels = [{ url = "https://files.pythonhosted.org/packages/bd/75/8539d011f6be8e29f339c42e633aae3cb73bffa95dd0f9adec09b9c58e85/tomlkit-0.13.3-py3-none-any.whl", upload-time = 2025-06-05T07:13:43Z, size = 38901, hashes = { sha256 = "c89c649d79ee40629a9fda55f8ace8c6a1b42deb912b2a8fd8d942ddadb606b0" } }] + +[[packages]] +name = "typer" +version = "0.17.3" +sdist = { url = "https://files.pythonhosted.org/packages/dd/82/f4bfed3bc18c6ebd6f828320811bbe4098f92a31adf4040bee59c4ae02ea/typer-0.17.3.tar.gz", upload-time = 2025-08-30T12:35:24Z, size = 103517, hashes = { sha256 = "0c600503d472bcf98d29914d4dcd67f80c24cc245395e2e00ba3603c9332e8ba" } } +wheels = [{ url = "https://files.pythonhosted.org/packages/ca/e8/b3d537470e8404659a6335e7af868e90657efb73916ef31ddf3d8b9cb237/typer-0.17.3-py3-none-any.whl", upload-time = 2025-08-30T12:35:22Z, size = 46494, hashes = { sha256 = "643919a79182ab7ac7581056d93c6a2b865b026adf2872c4d02c72758e6f095b" } }] + +[[packages]] +name = "typing-extensions" +version = "4.14.1" +sdist = { url = "https://files.pythonhosted.org/packages/98/5a/da40306b885cc8c09109dc2e1abd358d5684b1425678151cdaed4731c822/typing_extensions-4.14.1.tar.gz", upload-time = 2025-07-04T13:28:34Z, size = 107673, hashes = { sha256 = "38b39f4aeeab64884ce9f74c94263ef78f3c22467c8724005483154c26648d36" } } +wheels = [{ url = "https://files.pythonhosted.org/packages/b5/00/d631e67a838026495268c2f6884f3711a15a9a2a96cd244fdaea53b823fb/typing_extensions-4.14.1-py3-none-any.whl", upload-time = 2025-07-04T13:28:32Z, size = 43906, hashes = { sha256 = "d1e1e3b58374dc93031d6eda2420a48ea44a36c2b4766a4fdeb3710755731d76" } }] + +[[packages]] +name = "typing-inspection" +version = "0.4.2" +sdist = { url = "https://files.pythonhosted.org/packages/55/e3/70399cb7dd41c10ac53367ae42139cf4b1ca5f36bb3dc6c9d33acdb43655/typing_inspection-0.4.2.tar.gz", upload-time = 2025-10-01T02:14:41Z, size = 75949, hashes = { sha256 = "ba561c48a67c5958007083d386c3295464928b01faa735ab8547c5692e87f464" } } +wheels = [{ url = "https://files.pythonhosted.org/packages/dc/9b/47798a6c91d8bdb567fe2698fe81e0c6b7cb7ef4d13da4114b41d239f65d/typing_inspection-0.4.2-py3-none-any.whl", upload-time = 2025-10-01T02:14:40Z, size = 14611, hashes = { sha256 = "4ed1cacbdc298c220f1bd249ed5287caa16f34d44ef4e9c3d0cbad5b521545e7" } }] + +[[packages]] +name = "tzdata" +version = "2025.2" +marker = "sys_platform == 'win32'" +sdist = { url = "https://files.pythonhosted.org/packages/95/32/1a225d6164441be760d75c2c42e2780dc0873fe382da3e98a2e1e48361e5/tzdata-2025.2.tar.gz", upload-time = 2025-03-23T13:54:43Z, size = 196380, hashes = { sha256 = "b60a638fcc0daffadf82fe0f57e53d06bdec2f36c4df66280ae79bce6bd6f2b9" } } +wheels = [{ url = "https://files.pythonhosted.org/packages/5c/23/c7abc0ca0a1526a0774eca151daeb8de62ec457e77262b66b359c3c7679e/tzdata-2025.2-py2.py3-none-any.whl", upload-time = 2025-03-23T13:54:41Z, size = 347839, hashes = { sha256 = "1a403fada01ff9221ca8044d701868fa132215d84beb92242d9acd2147f667a8" } }] + +[[packages]] +name = "tzlocal" +version = "5.3.1" +sdist = { url = "https://files.pythonhosted.org/packages/8b/2e/c14812d3d4d9cd1773c6be938f89e5735a1f11a9f184ac3639b93cef35d5/tzlocal-5.3.1.tar.gz", upload-time = 2025-03-05T21:17:41Z, size = 30761, hashes = { sha256 = "cceffc7edecefea1f595541dbd6e990cb1ea3d19bf01b2809f362a03dd7921fd" } } +wheels = [{ url = "https://files.pythonhosted.org/packages/c2/14/e2a54fabd4f08cd7af1c07030603c3356b74da07f7cc056e600436edfa17/tzlocal-5.3.1-py3-none-any.whl", upload-time = 2025-03-05T21:17:39Z, size = 18026, hashes = { sha256 = "eb1a66c3ef5847adf7a834f1be0800581b683b5608e74f86ecbcef8ab91bb85d" } }] + +[[packages]] +name = "urllib3" +version = "2.6.3" +sdist = { url = "https://files.pythonhosted.org/packages/c7/24/5f1b3bdffd70275f6661c76461e25f024d5a38a46f04aaca912426a2b1d3/urllib3-2.6.3.tar.gz", upload-time = 2026-01-07T16:24:43Z, size = 435556, hashes = { sha256 = "1b62b6884944a57dbe321509ab94fd4d3b307075e0c2eae991ac71ee15ad38ed" } } +wheels = [{ url = "https://files.pythonhosted.org/packages/39/08/aaaad47bc4e9dc8c725e68f9d04865dbcb2052843ff09c97b08904852d84/urllib3-2.6.3-py3-none-any.whl", upload-time = 2026-01-07T16:24:42Z, size = 131584, hashes = { sha256 = "bf272323e553dfb2e87d9bfd225ca7b0f467b919d7bbd355436d3fd37cb0acd4" } }] + +[[packages]] +name = "wcwidth" +version = "0.2.13" +sdist = { url = "https://files.pythonhosted.org/packages/6c/63/53559446a878410fc5a5974feb13d31d78d752eb18aeba59c7fef1af7598/wcwidth-0.2.13.tar.gz", upload-time = 2024-01-06T02:10:57Z, size = 101301, hashes = { sha256 = "72ea0c06399eb286d978fdedb6923a9eb47e1c486ce63e9b4e64fc18303972b5" } } +wheels = [{ url = "https://files.pythonhosted.org/packages/fd/84/fd2ba7aafacbad3c4201d395674fc6348826569da3c0937e75505ead3528/wcwidth-0.2.13-py2.py3-none-any.whl", upload-time = 2024-01-06T02:10:55Z, size = 34166, hashes = { sha256 = "3da69048e4540d84af32131829ff948f1e022c1c6bdb8d6102117aac784f6859" } }] + +[[packages]] +name = "websocket-client" +version = "1.9.0" +sdist = { url = "https://files.pythonhosted.org/packages/2c/41/aa4bf9664e4cda14c3b39865b12251e8e7d239f4cd0e3cc1b6c2ccde25c1/websocket_client-1.9.0.tar.gz", upload-time = 2025-10-07T21:16:36Z, size = 70576, hashes = { sha256 = "9e813624b6eb619999a97dc7958469217c3176312b3a16a4bd1bc7e08a46ec98" } } +wheels = [{ url = "https://files.pythonhosted.org/packages/34/db/b10e48aa8fff7407e67470363eac595018441cf32d5e1001567a7aeba5d2/websocket_client-1.9.0-py3-none-any.whl", upload-time = 2025-10-07T21:16:34Z, size = 82616, hashes = { sha256 = "af248a825037ef591efbf6ed20cc5faa03d3b47b9e5a2230a529eeee1c1fc3ef" } }] + +[[packages]] +name = "wheel" +version = "0.45.1" +sdist = { url = "https://files.pythonhosted.org/packages/8a/98/2d9906746cdc6a6ef809ae6338005b3f21bb568bea3165cfc6a243fdc25c/wheel-0.45.1.tar.gz", upload-time = 2024-11-23T00:18:23Z, size = 107545, hashes = { sha256 = "661e1abd9198507b1409a20c02106d9670b2576e916d58f520316666abca6729" } } +wheels = [{ url = "https://files.pythonhosted.org/packages/0b/2c/87f3254fd8ffd29e4c02732eee68a83a1d3c346ae39bc6822dcbcb697f2b/wheel-0.45.1-py3-none-any.whl", upload-time = 2024-11-23T00:18:21Z, size = 72494, hashes = { sha256 = "708e7481cc80179af0e556bbf0cc00b8444c7321e2700b8d8580231d13017248" } }] + +[[packages]] +name = "zipp" +version = "3.23.0" +marker = "python_full_version < '3.12'" +sdist = { url = "https://files.pythonhosted.org/packages/e3/02/0f2892c661036d50ede074e376733dca2ae7c6eb617489437771209d4180/zipp-3.23.0.tar.gz", upload-time = 2025-06-08T17:06:39Z, size = 25547, hashes = { sha256 = "a07157588a12518c9d4034df3fbbee09c814741a33ff63c05fa29d26a2404166" } } +wheels = [{ url = "https://files.pythonhosted.org/packages/2e/54/647ade08bf0db230bfea292f893923872fd20be6ac6f53b2b936ba839d75/zipp-3.23.0-py3-none-any.whl", upload-time = 2025-06-08T17:06:38Z, size = 10276, hashes = { sha256 = "071652d6115ed432f5ce1d34c336c0adfd6a884660d1e9712a256d3d3bd4b14e" } }] diff --git a/tests/fixtures/real-world-locks/pylock/snowflake-cli-3.26.0/pyproject.toml b/tests/fixtures/real-world-locks/pylock/snowflake-cli-3.26.0/pyproject.toml new file mode 100644 index 00000000..36760c76 --- /dev/null +++ b/tests/fixtures/real-world-locks/pylock/snowflake-cli-3.26.0/pyproject.toml @@ -0,0 +1,249 @@ +# Copyright (c) 2024 Snowflake Inc. +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +[build-system] +requires = ["hatchling"] +build-backend = "hatchling.build" + +[project] +name = "snowflake-cli" +authors = [{ name = "Snowflake Inc." }] +license = { file = "LICENSE" } +dynamic = ["version"] +requires-python = ">=3.10" +description = "Snowflake CLI" +readme = "README.md" +dependencies = [ + # Actual project dependencies, from which we generate [project.dependencies] section serving as a lockfile for PyPi + "click==8.1.8", + "GitPython==3.1.58", + "PyYAML==6.0.2", + "id==1.5.0", + "jinja2==3.1.6", + "packaging==25.0", + "pip==26.2.1", + "pluggy==1.6.0", + "prompt-toolkit==3.0.51", + "protobuf>=5.29.6,<6", + "pydantic==2.12.5", + "python-dotenv==1.2.2", + "requests==2.33.0", + "requirements-parser==0.13.0", + "rich==14.0.0", + "setuptools==80.8.0", + "snowflake-connector-python[secure-local-storage]==4.7.1", + 'snowflake-snowpark-python==1.53.0', + "snowflake.core==1.10.0", + "tomlkit==0.13.3", + "typer==0.17.3", + "urllib3>=2.6.3,<3", + "websocket-client>=1.6.0,<2", +] +classifiers = [ + "Development Status :: 5 - Production/Stable", + "Environment :: Console", + "Intended Audience :: Developers", + "Intended Audience :: Information Technology", + "Intended Audience :: System Administrators", + "License :: OSI Approved :: Apache Software License", + "Programming Language :: Python :: 3", + "Programming Language :: Python :: 3 :: Only", + "Programming Language :: Python :: 3.10", + "Programming Language :: Python :: 3.11", + "Programming Language :: Python :: 3.12", + "Programming Language :: Python :: 3.13", + "Programming Language :: SQL", + "Topic :: Database", +] + +[project.optional-dependencies] +development = [ + "Faker==37.4.0", + "coverage==7.10.4", + "factory-boy==3.3.3", + "pre-commit>=3.5.0", + "pytest-cov==6.0.0", + "pytest-httpserver==1.1.3", + "pytest-randomly==3.16.0", + "pytest-xdist==3.6.1", + "pytest==8.4.1", + "syrupy==4.9.1", + "uv==0.10.9", +] +packaging = [] + +[project.urls] +"Source code" = "https://github.com/snowflakedb/snowflake-cli" +"Bug Tracker" = "https://github.com/snowflakedb/snowflake-cli/issues" + +[project.scripts] +snow = "snowflake.cli._app.__main__:main" + +[tool.coverage.report] +exclude_also = ["@(abc\\.)?abstractmethod", "@(abc\\.)?abstractproperty"] + +[tool.hatch.version] +path = "src/snowflake/cli/__about__.py" + +[tool.hatch.build.targets.sdist] +exclude = ["/.github", "/.compat"] + +[tool.hatch.build.targets.wheel] +packages = ["src/snowflake"] + +[tool.hatch.envs.default] +python = "3.10" +features = ["development"] +# Use uv as the installer for faster environment creation (notably on CI +# cache-miss). Scoped to the default (unit-test) env; the integration/e2e +# envs still use pip because their `pip install` pre-install steps have not +# been verified against uv's pip-less virtualenvs. +installer = "uv" + +[tool.hatch.envs.default.scripts] +# Unit tests are parallelized with pytest-xdist. `loaded_modules` runs serially +# in a fresh process because it asserts on `sys.modules` after CLI startup, +# which is meaningless under a long-lived xdist worker. +test = [ + "pytest -n auto --dist=worksteal --durations=25 tests/", + "pytest -m loaded_modules tests/", +] +test-cov = [ + "pytest --cov=snowflake.cli --cov-report= -n auto --dist=worksteal --durations=25 tests/", + "pytest --cov=snowflake.cli --cov-append --cov-report= -m loaded_modules tests/", + "coverage report", +] +legacy-pypi-build = [".compat/build_snowflake-cli-labs.sh"] +lock-dependencies = [ + "uv pip compile pyproject.toml -o pylock.toml -p 3.10 --no-annotate --universal --index https://pypi.org/simple", +] +sync-dependencies = [ + "uv pip compile pyproject.toml -o snyk/requirements.txt -p 3.10 --no-annotate --universal --index https://pypi.org/simple", +] + +[tool.hatch.envs.packaging] +python = "3.11" +features = ["development", "packaging"] + +[tool.hatch.envs.packaging.scripts] +build-isolated-binary = [ + "python scripts/packaging/build_isolated_binary_with_hatch.py", +] +build-binaries = ["./scripts/packaging/build_binaries.sh"] +build-packages = ["./scripts/packaging/build_packages.sh"] +build-all = [ + "./scripts/packaging/build_binaries.sh", + "./scripts/packaging/build_packages.sh", +] +win-build-version = ["python ./scripts/packaging/win/build_version.py"] + +[tool.hatch.envs.e2e] +template = "e2e" +features = ["development"] + +[tool.hatch.envs.e2e.scripts] +test = ["pytest -m e2e --durations=0"] +cleanup = ["python scripts/cleanup.py"] + +[tool.hatch.envs.performance] +template = "performance" +features = ["development"] + +[tool.hatch.envs.performance.scripts] +test = ["pytest -m performance"] + +[tool.hatch.envs.integration] +template = "integration" +pre-install-commands = [ + # Disabled due to repo migration + # "pip install test_external_plugins/snowpark_hello_single_command", + # "pip install test_external_plugins/multilingual_hello_command_group", + "pip install pytest-xdist", +] +features = ["development"] + +[tool.hatch.envs.integration.scripts] +test = [ + 'pytest -m "integration and not qa_only" -n5 --dist=worksteal --deflake-test-type=integration --ignore=tests_integration/tests_using_container_services tests_integration/ {args}', +] +test_container_services = [ + "pytest -m integration -n5 --dist=worksteal --deflake-test-type=integration tests_integration/tests_using_container_services", +] +test_qa = [ + "pytest -m 'integration and not no_qa' -n5 --dist=worksteal --deflake-test-type=integration tests_integration/", +] + +[tool.hatch.envs.ud] +template = "ud" +pre-install-commands = [ + "pip install pytest-xdist", +] +features = ["development"] + +[tool.hatch.envs.ud.scripts] +check = [ + 'pip uninstall --yes snowflake-connector-python', + # Clean up orphaned files from the namespace package directory + 'echo "$(python -c "import site; print(site.getsitepackages()[0])")/snowflake/connector"', + 'rm -rf "$(python -c "import site; print(site.getsitepackages()[0])")/snowflake/connector"', + 'ls .hatch/ud/lib/python3.12/site-packages/snowflake', + 'pip install "git+https://github.com/snowflakedb/drivers@{env:UD_BRANCH:main}#subdirectory=python"', + "python -c \"from packaging.version import Version; import snowflake.connector; v = Version(snowflake.connector.__version__); assert v.major >= 5, 'Expected snowflake-connector-python >= 5, got ' + str(v)\"", + 'pytest -m "integration and not qa_only and not no_ud" -n8 --maxfail=1000 --dist=worksteal --ignore=tests_integration/tests_using_container_services --ignore=tests_integration/nativeapp tests_integration/ {args}' +] + + +[[tool.hatch.envs.local.matrix]] +python = ["3.10", "3.11", "3.12", "3.13"] + +[tool.coverage.run] +source = ["snowflake.cli"] + +[tool.ruff] +line-length = 88 +# Cookiecutter template paths contain Jinja placeholders that are not valid TOML/Python. +extend-exclude = ["*cookiecutter.plugin_name*"] + +[tool.ruff.lint] +select = [ + "N", + "I", # isort + "G", # flake8-logging-format + "N", # pep8 naming + "A", # flake 8 builtins + "TID252", # relative imports + "SLF", # Accessing private methods + "F401", # unused imports + "F403", # star imports + "FA100", # Missing from __future__ import annotations + "W605", # Invalid escape sequences +] + +[tool.pytest.ini_options] +addopts = "-vv --maxfail=10 -m 'not integration and not performance and not e2e and not spcs and not loaded_modules and not integration_experimental'" +markers = [ + "integration: mark test as integration test", + "performance: mark test as performance test", + "e2e: mark test to execute on Snowflake CLI installed in fresh virtual environment", + "loaded_modules: checks loaded modules", + "patch_app_version: sets app version to 0.0.0-test_patched", + "integration_experimental: experimental integration test", + "no_qa: mark test as not to be run in QA", + "qa_only: mark test as to be run only in QA", + "no_ud: mark test as incompatible with the Universal Driver", +] + + +[tool.codespell] +skip = 'tests/*,snow.spec' diff --git a/tests/fixtures/real-world-projects/README.md b/tests/fixtures/real-world-projects/README.md index 726ccb31..c186ada7 100644 --- a/tests/fixtures/real-world-projects/README.md +++ b/tests/fixtures/real-world-projects/README.md @@ -6,6 +6,17 @@ SPDX-License-Identifier: CC0-1.0 # Real-world project fixtures +This directory covers one fixture category: real packages vendored as +full sdist archives to validate build-backend **wheel-file discovery** +(the table below is organised by build backend). A sibling directory, +[../real-world-locks/README.md](../real-world-locks/README.md), covers a +different category -- real projects' **lock/pin files** (`poetry.lock`, +`pylock.toml`, and others), organised by lock format instead, and much +smaller per fixture (two text files, not a full sdist) since that's all +dependency-resolution testing needs. Kept as separate directories +because the two categories need different content and a different +validation shape, not because of any relationship between them. + See also: [../real_world.py](../real_world.py) (the shared sdist-extraction/manifest helper every test in this file uses); [../projects/README.md](../projects/README.md) (small, synthetic, @@ -48,9 +59,14 @@ archive a real `pip install ==` would download, which is also all `discover()` itself ever needs. This directory (like the rest of `tests/fixtures/`) is excluded from -Pitloom's own published sdist (`pyproject.toml`'s -`[tool.hatch.build.targets.sdist]`), to avoid redistributing vendored -third-party source in a release artifact. `tests/fixtures/real_world.py`'s +Pitloom's own published sdist and wheel (`pyproject.toml`'s +`[tool.hatch.build.targets.sdist]`/`[tool.hatch.build.targets.wheel]`), +to avoid redistributing vendored third-party source in a release +artifact -- the wheel was already implicitly safe (`packages = +["src/pitloom"]` never picks up `tests/`), but the sdist's own +`exclude` list didn't name this directory until it started actually +holding multi-megabyte vendored archives; both now list it explicitly. +`tests/fixtures/real_world.py`'s `sdist_available()` lets [`test_models_wheel_real_world.py`](../../core/models_wheel/test_models_wheel_real_world.py) skip cleanly (not error) when a fixture's archive isn't present -- e.g. @@ -70,7 +86,7 @@ to a different backend since, or (as with a compiled/Rust project) declare a Track B backend like `maturin` despite superficially looking like a `uv_build` case. -## Fixtures +## Backend file-discovery fixtures | Backend | Project | Version | License | Notes | | :--- | :--- | :--- | :--- | :--- | diff --git a/working-docs/implementation/lock-file-cascade.md b/working-docs/implementation/lock-file-cascade.md new file mode 100644 index 00000000..8c8c67c0 --- /dev/null +++ b/working-docs/implementation/lock-file-cascade.md @@ -0,0 +1,154 @@ +--- +Created: 2026-09-04 +Last-Modified: 2026-09-04 +SPDX-FileCopyrightText: 2026-present Arthit Suriyawongkul +SPDX-FileType: DOCUMENTATION +SPDX-License-Identifier: CC0-1.0 +--- + +# Lock/pin format priority cascade -- implementation notes + +See also: [poetry-support.md](poetry-support.md)'s "`poetry.lock` +transitive dependencies" section and [pep751-pylock-support.md](pep751-pylock-support.md) +for the two formats this cascade was generalized from; +[lock-files.md](../design/lock-files.md) for the broader multi-format +roadmap; [sbom-lifecycle-stages.md](sbom-lifecycle-stages.md) for the +source/build/deployed staging model that makes every source here +source-stage-only. + +## Motivation + +`poetry.lock` and `pylock.toml` (PEP 751) each shipped with their own +bespoke "extract, check if a result is already set, override with a +`WARNING:`" wiring (`_try_read_poetry()` for the former, +`_apply_pylock_dependencies()`, since deleted, for the latter). That +pattern doesn't scale to `uv.lock`, `pdm.lock`, `Pipfile.lock`, and +pinned `requirements.txt` landing on top -- five near-identical +bespoke functions is exactly the "pattern hand-copied across 3+ call +sites drifts" problem this repo's own conventions warn about. This +module (`src/pitloom/extract/_locked_dependencies.py`) replaces every +new format's would-be bespoke function with one shared, ordered cascade. + +## The cascade + +```python +_LockExtractor = Callable[[Path], list[str]] + +_LOCK_SOURCES: list[tuple[str, _LockExtractor, str]] = [ + ("pylock.toml", extract_pylock_dependencies, "resolved_lockfile"), + # uv.lock, pdm.lock, Pipfile.lock, requirements.txt land here as + # their own extractors ship -- see roadmap.md. +] + + +def apply_locked_dependencies(metadata: ProjectMetadata, project_dir: Path) -> None: + ... +``` + +Each entry pairs a source filename, an extractor (`project_dir -> list[str]` +of exact-pin PEP 508 strings, empty when absent/unusable -- the same +signature convention `_poetry_lock.py`/`_pylock.py` already established), +and a provenance `Method` tag. `apply_locked_dependencies()` tries each +entry in priority order (highest first) and applies the first non-empty +result, in place, onto `metadata.locked_dependencies` and +`metadata.provenance["locked_dependencies"]`. + +**`poetry.lock` is not in this table.** It stays exactly where it +shipped, gated inside `_try_read_poetry()`'s `include_locked_dependencies` +build-stage flag, since `poetry.lock` only ever makes sense alongside a +`[tool.poetry]` table -- which requires `pyproject.toml` to exist +regardless, so it needs no `read_project()`-level generalization. The +cascade runs *after* `_try_read_poetry()` in `read_pyproject()` +(indirectly, via `read_project()` -- see below), so a higher-priority +cascade entry can still override an already-set `poetry.lock` result. + +## Priority order + +Highest to lowest, per `working-docs/design/roadmap.md`'s "Remaining +lock formats" item and `lock-files.md`'s phase reasoning +(build-backend-agnostic and universal beats tool-specific; a real +resolver lock beats a merely-pinned file): + +1. `pylock.toml` (PEP 751) -- the interoperability standard. +2. `uv.lock` +3. `poetry.lock` (via `_try_read_poetry()`, not this cascade -- see above) +4. `pdm.lock` +5. `Pipfile.lock` +6. pinned `requirements.txt` -- weakest signal; only usable when every + line is an exact `==` pin (see that format's own implementation + notes once it lands). + +## Where the cascade is called from -- `read_project()`, not `read_pyproject()` + +This is the one deliberate divergence from `pylock.toml`'s original +wiring (which called `_apply_pylock_dependencies()` from inside +`read_pyproject()`'s three exit paths). `pitloom.extract.project.read_project()` +is the single dispatcher deciding between three metadata sources for a +directory: `pyproject.toml` (succeeds), a `pyproject.toml`-with-no-usable- +`[project]`-table fallback to `read_setuptools()`, or `setup.cfg`/`setup.py` +alone with no `pyproject.toml` at all. `Pipfile.lock` and pinned +`requirements.txt` predate PEP 621 almost entirely -- every real-world +project checked while sourcing test fixtures for this cascade +(`requests-html`, `responder` pre-`v3.0.0`) is `setup.py`-only, no +`pyproject.toml` -- so a cascade wired only inside `read_pyproject()` +would never run for the realistic case those two formats actually show +up in. + +`apply_locked_dependencies()` is called once, right before each of +`read_project()`'s three directory-based `return` statements (the +sdist-archive branch is skipped -- there's no sibling directory to +check for a lock file against a single archive file), so it runs +uniformly regardless of which metadata source won. + +## Provenance recording + +`metadata.provenance["locked_dependencies"]` is a single string, +`"Source: | Method: "`, consumed by `document.py`'s +`add_dependencies(dep_provenance=...)` to annotate every locked-transitive +`dependsOn` relationship. Two things beyond the pre-existing pattern: + +- **The cascade owns the string format**, built once from `_LOCK_SOURCES`' + `(source_name, method)` pair, rather than each extractor's call site + formatting its own copy -- the same "no hand-copied pattern" reasoning + as the cascade loop itself. +- **An override is recorded in the string, not only logged.** When a + higher-priority source supersedes an already-set one (from + `poetry.lock`, or a previous cascade winner -- though only one cascade + entry ever wins per call), the resulting string gets a trailing + `| Note: supersedes `, e.g. `"Source: pylock.toml | Method: + resolved_lockfile | Note: supersedes poetry.lock"`. This means a + reader of the *generated SBOM* -- not only Pitloom's own stderr at + generation time -- can see that more than one lock source existed and + which one Pitloom trusted, per this repo's "no silent deviations" + principle applied to the artifact itself. + +## Document UUID seeding + +`compute_doc_uuid()` (`src/pitloom/core/models.py`) folds +`locked_dependencies` (the resolved dependency *content*) into its seed, +but originally not *which source produced it*. With six lock/pin +formats now cascading instead of two, two different formats resolving +to an identical dependency set for a small project became a real, +checkable collision risk: two runs -- one with only `poetry.lock` +present, one with only `pylock.toml` present -- that happen to resolve +to the same exact pins would produce the *same* document UUID despite +the generated document's `provenance["locked_dependencies"]` field (and +any override note) differing, a real content difference the UUID is +meant to guard against. Fixed by adding a +`locked_dependencies_provenance: str | None` parameter, folded into the +seed alongside `locked_dependencies` itself whenever both are non-empty/ +given. Omitted (every pre-existing call site) leaves the seed +unaffected -- purely additive. + +## Adding a new format to the cascade + +1. Write `extract__dependencies(project_dir: Path) -> list[str]` + in its own `src/pitloom/extract/_.py`, following + `_pylock.py`'s shape: exact-pin PEP 508 strings, empty list when + absent/unusable, `WARNING:` (never a silent drop) for anything + malformed or non-registry-sourced. +2. Add one entry to `_LOCK_SOURCES` in `_locked_dependencies.py`, at the + priority position from the table above. +3. No changes needed anywhere else -- `read_project()`'s wiring, + provenance formatting, the override note, and UUID seeding are all + already generic across every entry in the table. diff --git a/working-docs/implementation/pep751-pylock-support.md b/working-docs/implementation/pep751-pylock-support.md index 37bda85f..a063d0da 100644 --- a/working-docs/implementation/pep751-pylock-support.md +++ b/working-docs/implementation/pep751-pylock-support.md @@ -1,6 +1,6 @@ --- Created: 2026-09-02 -Last-Modified: 2026-09-02 +Last-Modified: 2026-09-04 SPDX-FileCopyrightText: 2026-present Arthit Suriyawongkul SPDX-FileType: DOCUMENTATION SPDX-License-Identifier: CC0-1.0 @@ -13,11 +13,16 @@ transitive dependencies" section -- this feature reuses that shape almost unchanged; [lock-files.md](../design/lock-files.md) for the broader multi-format lock-file roadmap this closes Phase 1's headline item of; [sbom-lifecycle-stages.md](sbom-lifecycle-stages.md) for the -source/build/deployed staging model that makes this source-stage-only. +source/build/deployed staging model that makes this source-stage-only; +[lock-file-cascade.md](lock-file-cascade.md) for the shared priority +mechanism this format's wiring was generalized into once `uv.lock`, +`pdm.lock`, `Pipfile.lock`, and pinned `requirements.txt` needed the +same shape -- the "Wiring" and "Priority" sections below describe that +current, generalized mechanism, not this format's original bespoke one. ## Motivation -[PEP 751] standardizes `pylock.toml` as a build-backend-agnostic, +[PEP 751] standardises `pylock.toml` as a build-backend-agnostic, fully resolved dependency snapshot -- produced by `uv export --format pylock.toml`, `pdm lock --format pylock`, `poetry export --format=pylock.toml`, and similar, consumed only by installers. Per @@ -33,8 +38,9 @@ project regardless of build backend, unlike the already-shipped | File | Role | | :--- | :--- | | `src/pitloom/extract/_pylock.py` | `pylock.toml` resolved-dependency extraction (source-stage only) | -| `src/pitloom/extract/_pyproject.py` | Wires `pylock.toml` reading into `read_pyproject()`, unconditionally | +| `src/pitloom/extract/_locked_dependencies.py` | Cascade wiring `pylock.toml` (and every other lock format) into `read_project()` -- see [lock-file-cascade.md](lock-file-cascade.md) | | `tests/extract/test_pylock.py` | `pylock.toml` parsing unit and integration tests | +| `tests/extract/test_locked_dependencies.py` | Cascade mechanism tests (priority ordering, override note, `setup.py`-only wiring) | No changes were needed in `src/pitloom/assemble/spdx3/deps.py` or `document.py` -- both already operate on the generic @@ -77,42 +83,25 @@ mirrors `poetry.lock`'s equivalent `directory`/`file`/`git`/`url` skip in `_poetry_lock.py`. A package sourced via `sdist`/`wheels` (or with no source table at all) is included whenever it has a version. -## Wiring into `read_pyproject()` - -`_apply_pylock_dependencies()` is called unconditionally at the end of -every `read_pyproject()` code path (both the `[project]`-primary path -and the `[tool.poetry]`/no-`[project]` fallback path), overlaying -`pylock.toml`'s resolved dependencies onto `ProjectMetadata` in place -when a `pylock.toml` is present. - -This differs from `poetry.lock`'s wiring in one deliberate way: -`poetry.lock` reading is gated behind `[tool.poetry]` detection inside -`_try_read_poetry()`, since `poetry.lock` only makes sense for a Poetry -project. `pylock.toml` is build-backend-agnostic -- a plain PEP 621 -project with no Poetry involvement at all can have one -- so it's -checked unconditionally in `read_pyproject()` itself, independent of -which metadata-extraction branch ran. - -No `include_locked_dependencies`-style build-stage guard was needed -here: unlike `poetry.lock` (whose gap-fill helper `_try_read_poetry()` -is also called directly by the Hatchling build hook's -`_poetry_fallback_metadata()`, which must pass -`include_locked_dependencies=False` to avoid leaking a source-stage -artifact into a build-stage SBOM), `read_pyproject()` itself is never -called from the Hatchling build hook -- only from -`pitloom.extract.project.read_project()`, the CLI/library source-stage -path. So the unconditional call is already scoped correctly without -needing an extra parameter. - -## Priority when both `poetry.lock` and `pylock.toml` are present - -This is [lock-files.md](../design/lock-files.md)'s previously-open -"which lock file wins" question for the two-lock-files case: -`pylock.toml` -- the newer, build-backend-agnostic interoperability -standard -- always overrides an already-applied `poetry.lock`-resolved -set. `_apply_pylock_dependencies()` logs a `WARNING:` naming the -override whenever both are present, per this repo's "no silent -deviations" rule; it never merges the two sets. +## Wiring and priority + +`pylock.toml` is one entry (the highest-priority one) in the shared +lock/pin cascade -- see [lock-file-cascade.md](lock-file-cascade.md) for +the mechanism, priority order, provenance recording, and why the +cascade is called from `read_project()` rather than +`read_pyproject()`. Two points specific to `pylock.toml` itself: + +- It's build-backend-agnostic -- a plain PEP 621 project with no Poetry + involvement at all can have one -- unlike `poetry.lock`, which is + gated behind `[tool.poetry]` detection. +- No `include_locked_dependencies`-style build-stage guard was ever + needed for it: unlike `poetry.lock` (whose gap-fill helper + `_try_read_poetry()` is also called directly by the Hatchling build + hook's `_poetry_fallback_metadata()`, which must pass + `include_locked_dependencies=False` to avoid leaking a source-stage + artifact into a build-stage SBOM), `read_project()`'s cascade call is + never reached from the Hatchling build hook at all -- only from the + CLI/library source-stage path. ## Known limitations From 35cafb5dff21f9278ca989b32dc0db95db4ce2dd Mon Sep 17 00:00:00 2001 From: Arthit Suriyawongkul Date: Fri, 4 Sep 2026 17:08:02 +0700 Subject: [PATCH 03/35] Add uv.lock and pdm.lock Signed-off-by: Arthit Suriyawongkul --- CHANGELOG.md | 6 +- docs/cli.md | 12 +- docs/dependency-sources.md | 125 + docs/index.md | 6 +- src/pitloom/extract/_lock_common.py | 77 + src/pitloom/extract/_locked_dependencies.py | 80 +- src/pitloom/extract/_pdm_lock.py | 147 + src/pitloom/extract/_poetry_lock.py | 10 +- src/pitloom/extract/_pylock.py | 10 +- src/pitloom/extract/_uv_lock.py | 208 ++ tests/extract/test_lock_common.py | 81 + tests/extract/test_pdm_lock.py | 349 +++ tests/extract/test_uv_lock.py | 459 ++++ tests/fixtures/real-world-locks/README.md | 34 +- .../real-world-locks/pdm/pdm-2.29.0/LICENSE | 21 + .../real-world-locks/pdm/pdm-2.29.0/pdm.lock | 1966 ++++++++++++++ .../pdm/pdm-2.29.0/pyproject.toml | 260 ++ .../pdm/unearth-0.18.3/LICENSE | 21 + .../pdm/unearth-0.18.3/pdm.lock | 1152 ++++++++ .../pdm/unearth-0.18.3/pyproject.toml | 106 + .../uv/abi3audit-0.0.26/LICENSE | 21 + .../uv/abi3audit-0.0.26/pyproject.toml | 105 + .../uv/abi3audit-0.0.26/uv.lock | 826 ++++++ .../uv/fastapi-cli-0.0.32/LICENSE | 21 + .../uv/fastapi-cli-0.0.32/pyproject.toml | 180 ++ .../uv/fastapi-cli-0.0.32/uv.lock | 1663 ++++++++++++ .../uv/flask-3.1.3/LICENSE.txt | 28 + .../uv/flask-3.1.3/pyproject.toml | 279 ++ .../real-world-locks/uv/flask-3.1.3/uv.lock | 2405 +++++++++++++++++ .../implementation/lock-file-cascade.md | 176 +- 30 files changed, 10757 insertions(+), 77 deletions(-) create mode 100644 docs/dependency-sources.md create mode 100644 src/pitloom/extract/_lock_common.py create mode 100644 src/pitloom/extract/_pdm_lock.py create mode 100644 src/pitloom/extract/_uv_lock.py create mode 100644 tests/extract/test_lock_common.py create mode 100644 tests/extract/test_pdm_lock.py create mode 100644 tests/extract/test_uv_lock.py create mode 100644 tests/fixtures/real-world-locks/pdm/pdm-2.29.0/LICENSE create mode 100644 tests/fixtures/real-world-locks/pdm/pdm-2.29.0/pdm.lock create mode 100644 tests/fixtures/real-world-locks/pdm/pdm-2.29.0/pyproject.toml create mode 100644 tests/fixtures/real-world-locks/pdm/unearth-0.18.3/LICENSE create mode 100644 tests/fixtures/real-world-locks/pdm/unearth-0.18.3/pdm.lock create mode 100644 tests/fixtures/real-world-locks/pdm/unearth-0.18.3/pyproject.toml create mode 100644 tests/fixtures/real-world-locks/uv/abi3audit-0.0.26/LICENSE create mode 100644 tests/fixtures/real-world-locks/uv/abi3audit-0.0.26/pyproject.toml create mode 100644 tests/fixtures/real-world-locks/uv/abi3audit-0.0.26/uv.lock create mode 100644 tests/fixtures/real-world-locks/uv/fastapi-cli-0.0.32/LICENSE create mode 100644 tests/fixtures/real-world-locks/uv/fastapi-cli-0.0.32/pyproject.toml create mode 100644 tests/fixtures/real-world-locks/uv/fastapi-cli-0.0.32/uv.lock create mode 100644 tests/fixtures/real-world-locks/uv/flask-3.1.3/LICENSE.txt create mode 100644 tests/fixtures/real-world-locks/uv/flask-3.1.3/pyproject.toml create mode 100644 tests/fixtures/real-world-locks/uv/flask-3.1.3/uv.lock diff --git a/CHANGELOG.md b/CHANGELOG.md index 2969aab3..e8ca4d37 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -41,9 +41,9 @@ and this project adheres to - Add PEP 639 `[project.license-files]` support: each declared license file gets a `software_File` element at the real wheel's `.dist-info/licenses/` path and a `hasDeclaredLicense` relationship ([#207]) -- Add PEP 751 `pylock.toml` resolved-dependency parsing for `loom - project`/`loom generate`, taking priority over `poetry.lock` when both - are present +- Add resolved-dependency parsing for `loom project`/`loom generate` + from `pylock.toml` (PEP 751), `uv.lock`, and `pdm.lock`, more planned + -- see [Dependency sources and precedence](docs/dependency-sources.md) ### Fixed diff --git a/docs/cli.md b/docs/cli.md index c4a9fc70..171d88bd 100644 --- a/docs/cli.md +++ b/docs/cli.md @@ -1,6 +1,6 @@ --- Created: 2026-08-11 -Last-Modified: 2026-08-31 +Last-Modified: 2026-09-04 SPDX-FileCopyrightText: 2026-present Arthit Suriyawongkul SPDX-FileType: DOCUMENTATION SPDX-License-Identifier: CC0-1.0 @@ -66,10 +66,12 @@ loom project /path/to/project -o sbom.spdx3.json > Project-level metadata (name, version, dependencies, license, > authors) is read independently and unaffected either way. -A Poetry project with a `poetry.lock` next to `pyproject.toml` also gets -the lock's resolved `main`-group transitive dependencies added to the -Source SBOM's dependency list, on top of the direct -`[tool.poetry.dependencies]` constraints. +If a lock file (`pylock.toml`, `uv.lock`, `poetry.lock`, or `pdm.lock`) +is present next to `pyproject.toml`, its resolved transitive +dependencies are added to the Source SBOM's dependency list too -- see +[Dependency sources and precedence](dependency-sources.md) for which +one wins when more than one is present, and what counts as "resolved" +for each. Generate an **Analyzed SBOM** from a pre-built wheel (extracting bundled binaries as phantom dependencies): diff --git a/docs/dependency-sources.md b/docs/dependency-sources.md new file mode 100644 index 00000000..300723cf --- /dev/null +++ b/docs/dependency-sources.md @@ -0,0 +1,125 @@ +--- +Created: 2026-09-04 +Last-Modified: 2026-09-04 +SPDX-FileCopyrightText: 2026-present Arthit Suriyawongkul +SPDX-FileType: DOCUMENTATION +SPDX-License-Identifier: CC0-1.0 +--- + +# Dependency sources and precedence + +> **Note:** Background reading -- useful for understanding what shows up +> in a generated Source SBOM's dependency list and why, not needed to +> just generate one. + +## Two kinds of dependency information + +Every Source SBOM (`loom project`/`loom generate`) always includes your +project's **declared dependencies** -- the version ranges listed in +`pyproject.toml`'s `[project.dependencies]` (or `[tool.poetry.dependencies]` +for a Poetry project), e.g. `requests>=2.0`. These are read directly and +always present, with or without a lock file. + +If a **lock file** is also present next to `pyproject.toml`, Pitloom +additionally reads its exact resolved versions and adds any dependency +they introduce that your declared list doesn't already name -- your +project's *transitive* dependencies, pinned exactly (e.g. `idna==3.7`). + +**A direct dependency already in your declared list keeps its declared +range in the SBOM, even when a lock file has resolved it to an exact +version.** For example, if `pyproject.toml` declares `requests>=2.0` and +your lock file resolved it to `2.31.0`, the SBOM still shows +`requests>=2.0` for that entry -- only dependencies *not already +declared* (the ones the lock file alone reveals) get added, as new, +exactly-pinned entries. + +## Supported lock formats, and what counts as "resolved" + +| Priority | Format | File | What's included | +| :---: | :--- | :--- | :--- | +| 1 (highest) | PEP 751 | `pylock.toml` | Every resolved package the file records. | +| 2 | uv | `uv.lock` | Your project's own main/runtime dependencies (not `optional-dependencies` extras or `dev-dependencies` groups). A dependency pinned to more than one version for different Python versions is skipped, not guessed at -- see below. | +| 3 | Poetry | `poetry.lock` | Packages in the `main` dependency group only (not `[tool.poetry.group.*]` dev/extra groups). | +| 4 | PDM | `pdm.lock` | Packages in the `default` dependency group only. | + +Support for `Pipfile.lock` (Pipenv) and a fully pinned `requirements.txt` +is planned, ranked below the formats above. + +**Only the single highest-priority lock file present is used.** If more +than one lock file exists in the same project directory (uncommon, but +possible after a build-tool migration), Pitloom picks the one highest in +the table above and ignores the rest entirely -- it never merges two +lock files' resolutions together. + +**A lock entry that can't be resolved to one exact version is left out, +not guessed.** `uv.lock` in particular can record the same package +pinned to genuinely different versions for different Python versions in +one file; Pitloom doesn't evaluate environment markers to pick one, so +such a dependency is simply omitted from the additional (transitive) +list rather than added with a possibly-wrong version. Check stderr for a +`WARNING:` naming the skipped package if a dependency you expected is +missing. + +## Which commands use lock files at all + +Lock-file resolution only ever applies to a **Source SBOM** +(`loom project`, `loom generate`, and the equivalent +[Python API](python-api.md) call) -- describing your project as +declared in source, before a build happens. + +It's never consulted by: + +- `loom wheel`, `loom embed-wheel`, `loom verify-wheel`/`validate-wheel` + -- a built wheel's own installed metadata is the ground truth for an + **Analyzed SBOM**; a lock file (which describes what a *future* build + might resolve to) is beside the point once a real wheel exists. +- `loom env` -- describing what's actually installed in an environment + is more authoritative than a lock file that may be stale relative to + it. +- The [Hatchling build hook](hatchling-build-hook.md) -- SBOMs it embeds + during `hatch build`/`pip install .` describe the build artifact + itself, the same "real build, not a lock's prediction" reasoning as + `loom wheel` above. + +So it's normal for `loom project`'s SBOM to list more transitive +dependencies than an SBOM embedded by the Hatchling build hook for the +same project -- they're describing different things (a hypothetical +resolution vs. what a real build actually installed), not a bug in +either. + +## How to tell which source was used + +Every SBOM element built from a lock-resolved dependency carries a +provenance annotation naming the file and method Pitloom used, e.g. +`Source: pylock.toml | Method: resolved_lockfile`. If a lower-priority +lock file was present but ignored in favor of a higher-priority one, +the annotation also says so, e.g. `Source: pylock.toml | Method: +resolved_lockfile | Note: supersedes poetry.lock`. See [Metadata +provenance](metadata-provenance.md) for how to read these annotations +in the generated SBOM. + +## Configuration and flags + +There is currently no setting to change the priority order above, +choose a specific lock file, or turn lock-file reading off -- it's +automatic, based purely on which lock file (if any) is present next to +`pyproject.toml`. If you don't want a lock file's resolved dependencies +included, the only way is to not have that file present when you run +`loom project`/`loom generate`. + +`--offline` (also settable via `[tool.pitloom] offline` -- +see [Configuration](configuration.md)) is unrelated to lock-file +reading: it only controls whether Pitloom's own PyPI JSON API lookups +(used to fill in a dependency package's supplier/license/copyright gaps) +are attempted. A lock file is always read from disk regardless of this +setting -- there's no network involved in reading it. + +## See also + +- [Command line](cli.md) and [Python API](python-api.md) for how to run + a Source SBOM generation that reads lock files this way. +- [Metadata provenance](metadata-provenance.md) for the general + provenance-annotation mechanism this page's "how to tell which source + was used" section relies on. +- [Configuration](configuration.md) for `--offline` and every other + `[tool.pitloom]` setting. diff --git a/docs/index.md b/docs/index.md index e7a8b5d6..4b67885f 100644 --- a/docs/index.md +++ b/docs/index.md @@ -1,6 +1,6 @@ --- Created: 2026-07-08 -Last-Modified: 2026-08-09 +Last-Modified: 2026-09-04 SPDX-FileCopyrightText: 2026-present Arthit Suriyawongkul SPDX-FileType: DOCUMENTATION SPDX-License-Identifier: CC0-1.0 @@ -67,6 +67,10 @@ not needed to just generate one: - [Configuration](configuration.md) -- every `[tool.pitloom]` setting, its default, and how to reach it from each surface. +- [Dependency sources and precedence](dependency-sources.md) -- what + shows up in a Source SBOM's dependency list, which lock file wins + when more than one is present, and which commands use lock files at + all. - [Creation metadata](creation-metadata.md) -- who/what/when/how every Pitloom-generated element records about its own creation. - [Metadata provenance](metadata-provenance.md) -- how Pitloom tracks the diff --git a/src/pitloom/extract/_lock_common.py b/src/pitloom/extract/_lock_common.py new file mode 100644 index 00000000..7bf66beb --- /dev/null +++ b/src/pitloom/extract/_lock_common.py @@ -0,0 +1,77 @@ +# SPDX-FileContributor: Arthit Suriyawongkul +# SPDX-FileCopyrightText: 2026-present Arthit Suriyawongkul +# SPDX-FileType: SOURCE +# SPDX-License-Identifier: Apache-2.0 + +"""Shared helpers for lock/pin file extractors +(:mod:`pitloom.extract._poetry_lock`, :mod:`pitloom.extract._pylock`, +:mod:`pitloom.extract._uv_lock`, :mod:`pitloom.extract._pdm_lock`, and +future formats registered in +:mod:`pitloom.extract._locked_dependencies`). + +Factored out once the same two steps -- "load the lock file, handling +absence/parse errors the same way every format does" and "group a +lock's flat package-entry list by name, to detect a name resolved to +more than one version" -- started being hand-copied into each new +extractor. Per this repo's "a pattern hand-copied across 3+ call sites +drifts" convention, this module is the one place both now live; only +extraction logic genuinely specific to one format (its own field names, +its own group/source-key conventions) stays in that format's own module. +""" + +from __future__ import annotations + +import logging +from pathlib import Path +from typing import Any + +from pitloom.extract._toml_io import TOMLDecodeError, load_toml_file + +log = logging.getLogger(__name__) + +__all__ = ["index_packages_by_name", "load_lock_toml"] + + +def load_lock_toml(lock_path: Path) -> dict[str, Any] | None: + """Load *lock_path* as TOML, returning ``None`` (after a + ``WARNING:`` for a parse/read failure, silently for a simply-absent + file) instead of raising -- every lock format is optional + enrichment, never a requirement, so a caller's usual next step is + ``if data is None: return []``. + """ + try: + return load_toml_file(lock_path) + except FileNotFoundError: + return None + except (OSError, TOMLDecodeError) as exc: + log.warning("Failed to parse %s: %s", lock_path, exc) + return None + + +def index_packages_by_name(packages: list[Any]) -> dict[str, list[dict[str, Any]]]: + """Group every well-formed entry of *packages* (a lock format's flat + ``[[package]]``-style list) by its ``name`` field, preserving file + order both across and within names. + + A non-table entry, or a table with a missing/non-string/empty + ``name``, is silently excluded -- it can never be the target of a + real dependency reference by name, so it's inert here; the caller + validating that same list for other purposes (e.g. resolving a + specific referenced name) is where a malformed entry actually + matters and gets its own ``WARNING:``. + + Used to detect a name that resolves to more than one distinct + version within one lock file -- ambiguous without evaluating + markers/extras against a real environment, which no extractor using + this helper does; the caller decides whether to skip such a name + or (when every entry agrees on the same version, e.g. PDM's + per-extra duplicate records) treat it as unambiguous after all. + """ + by_name: dict[str, list[dict[str, Any]]] = {} + for pkg in packages: + if not isinstance(pkg, dict): + continue + name = pkg.get("name") + if isinstance(name, str) and name: + by_name.setdefault(name, []).append(pkg) + return by_name diff --git a/src/pitloom/extract/_locked_dependencies.py b/src/pitloom/extract/_locked_dependencies.py index 21d0a4b0..d57733d4 100644 --- a/src/pitloom/extract/_locked_dependencies.py +++ b/src/pitloom/extract/_locked_dependencies.py @@ -16,14 +16,16 @@ with a bare ``setup.py`` in real projects, never a ``pyproject.toml``, so a cascade wired only inside ``read_pyproject()`` would never see them. -``poetry.lock`` is *not* one of the sources listed here: it stays gated -inside :func:`pitloom.extract._pyproject._try_read_poetry`'s +``poetry.lock`` has no extractor entry in :data:`_LOCK_SOURCES` -- it +stays gated inside +:func:`pitloom.extract._pyproject._try_read_poetry`'s ``include_locked_dependencies`` build-stage flag, since it only ever makes sense alongside a ``[tool.poetry]`` table, which requires -``pyproject.toml`` to exist regardless. This cascade runs *after* that -poetry.lock resolution, so a higher-priority format here can still -override an already-set poetry.lock result -- see -:data:`_LOCK_SOURCES`'s ordering. +``pyproject.toml`` to exist regardless, so it's applied earlier, before +this cascade runs. It *is* still listed in :data:`_LOCK_SOURCES`, as a +placeholder entry with no extractor, purely to fix its rank in the one +priority order every source (cascade-tried or not) is compared against +-- see :func:`apply_locked_dependencies`. """ from __future__ import annotations @@ -34,7 +36,9 @@ from pitloom.assemble.spdx3._provenance_encoders import parse_provenance_value from pitloom.core.project import ProjectMetadata +from pitloom.extract._pdm_lock import extract_pdm_lock_dependencies from pitloom.extract._pylock import extract_pylock_dependencies +from pitloom.extract._uv_lock import extract_uv_lock_dependencies log = logging.getLogger(__name__) @@ -42,16 +46,22 @@ _LockExtractor = Callable[[Path], list[str]] -#: Priority-ordered (highest first) lock/pin sources this cascade -#: chooses among. Each entry is ``(source filename, extractor function, -#: provenance Method tag)``. The extractor always takes a project -#: directory and returns exact-pin PEP 508 strings, or an empty list -#: when the source is absent/unusable. See +#: Full priority order (highest first) across every lock/pin source, +#: including ``poetry.lock`` even though it has no extractor here (see +#: the module docstring). Each entry is ``(source name, extractor or +#: ``None``, provenance Method tag or ``None``)``. This is the single +#: place the *complete* order is declared -- both which extractors this +#: cascade tries, and where ``poetry.lock``'s already-applied result +#: ranks relative to them -- so the two can never drift apart the way +#: two independently-maintained lists could. See #: ``working-docs/design/roadmap.md``'s "Remaining lock formats" item #: for why this order was chosen (build-backend-agnostic and universal #: beats tool-specific; a real resolver lock beats a merely-pinned file). -_LOCK_SOURCES: list[tuple[str, _LockExtractor, str]] = [ +_LOCK_SOURCES: list[tuple[str, _LockExtractor | None, str | None]] = [ ("pylock.toml", extract_pylock_dependencies, "resolved_lockfile"), + ("uv.lock", extract_uv_lock_dependencies, "resolved_lockfile"), + ("poetry.lock", None, None), + ("pdm.lock", extract_pdm_lock_dependencies, "resolved_lockfile"), ] @@ -59,28 +69,52 @@ def apply_locked_dependencies(metadata: ProjectMetadata, project_dir: Path) -> N """Overlay the highest-priority available lock/pin source's resolved dependencies onto *metadata*, in place. - Tries each entry of :data:`_LOCK_SOURCES` in priority order; the - first one that yields a non-empty result wins and every lower - priority source is left unconsidered. If *metadata* already carries - a ``locked_dependencies`` result (from an already-applied - ``poetry.lock``, or nothing at all), a winning source here replaces - it and a ``WARNING:`` names the override -- and, per this repo's "no + Tries each extractor-bearing entry of :data:`_LOCK_SOURCES` in + priority order; the first one that yields a non-empty result wins. + Crucially, this respects *every* source's rank, not just the ones + this cascade itself tries: once the already-applied source (e.g. + ``poetry.lock``, applied earlier by ``_try_read_poetry()``) outranks + every remaining untried entry, the loop stops -- a lower-priority + format (``pdm.lock`` ranks below ``poetry.lock``) must never + silently clobber a higher-priority result just because it happens + to run later in this function's own loop. + + If *metadata* already carries a ``locked_dependencies`` result and a + higher-or-equal-priority source here wins, that source replaces it + and a ``WARNING:`` names the override -- and, per this repo's "no silent deviations" principle, the fact that a source was superseded is also recorded in the resulting ``provenance["locked_dependencies"]`` string itself (as a trailing ``| Note: supersedes ``), not only logged, so a reader of the generated SBOM can see it too. """ - for source_name, extractor, method in _LOCK_SOURCES: + previous = metadata.provenance.get("locked_dependencies") + previous_source = ( + parse_provenance_value(previous).get("source") if previous is not None else None + ) + previous_rank = next( + ( + rank + for rank, (name, _, _) in enumerate(_LOCK_SOURCES) + if name == previous_source + ), + None, + ) + + for rank, (source_name, extractor, method) in enumerate(_LOCK_SOURCES): + if extractor is None: + continue # e.g. poetry.lock: applied earlier, not tried here + if previous_rank is not None and rank > previous_rank: + # Every remaining entry ranks below whatever's already set -- + # none of them can win, so stop instead of scanning further. + break + dependencies = extractor(project_dir) if not dependencies: continue provenance = f"Source: {source_name} | Method: {method}" - previous = metadata.provenance.get("locked_dependencies") if previous is not None: - superseded = parse_provenance_value(previous).get( - "source", "unknown source" - ) + superseded = previous_source or "unknown source" log.warning( "%s: both %s and %s resolved-dependency data are present -- " "%s takes priority", diff --git a/src/pitloom/extract/_pdm_lock.py b/src/pitloom/extract/_pdm_lock.py new file mode 100644 index 00000000..eb347661 --- /dev/null +++ b/src/pitloom/extract/_pdm_lock.py @@ -0,0 +1,147 @@ +# SPDX-FileContributor: Arthit Suriyawongkul +# SPDX-FileCopyrightText: 2026-present Arthit Suriyawongkul +# SPDX-FileType: SOURCE +# SPDX-License-Identifier: Apache-2.0 + +"""Extractor for resolved dependencies from a PDM ``pdm.lock``. + +See also: :mod:`pitloom.extract._poetry_lock` (the ``poetry.lock`` +extractor this module mirrors in shape -- same ``groups``-based +main/default filtering, same source-stage-only scoping, same +``name==version`` output, same "no silent deviations" warning policy) +and :mod:`pitloom.extract._locked_dependencies` (the cascade module that +calls this extractor and overlays its output onto +``ProjectMetadata.locked_dependencies``, in priority order against every +other lock format). + +``pdm.lock`` is source-stage-only, the same class as every sibling lock +format: appropriate for ``loom project``/``loom generate``, never for +``loom wheel``/``embed-wheel`` (the real wheel's own metadata is ground +truth and never consults a lock) or ``loom env`` (live introspection of +what's actually installed is strictly more authoritative than a lock +that may be stale relative to it). + +Unlike ``uv.lock``, a ``pdm.lock`` resolves one Python-compatibility +range per file (its own ``metadata.targets``), not a whole matrix of +marker branches in one flat table -- so it has nothing structurally +equivalent to ``uv.lock``'s "the same name pinned at two genuinely +different versions" case. The same package name *can* still appear more +than once, but only to record separate per-extra variants (e.g. a bare +``httpx`` entry alongside an ``httpx`` entry with ``extras = ["socks"]``) +that always agree on ``version`` -- collapsed here via +:func:`pitloom.extract._lock_common.index_packages_by_name`, the same +helper ``_uv_lock.py`` uses for its (genuinely ambiguous) case. Only a +name whose entries actually *disagree* on version is treated as +ambiguous and skipped, matching ``uv.lock``'s "don't guess" policy for +that case. +""" + +from __future__ import annotations + +import logging +from pathlib import Path +from typing import Any + +from pitloom.extract._lock_common import index_packages_by_name, load_lock_toml + +log = logging.getLogger(__name__) + +__all__ = ["extract_pdm_lock_dependencies"] + +#: The default (main/runtime) group name in a ``pdm.lock``'s per-package +#: ``groups`` list -- PDM's equivalent of ``poetry.lock``'s ``"main"``. +_DEFAULT_GROUP = "default" + +#: ``pdm.lock`` keys, present directly on a ``[[package]]`` table (no +#: nested ``source`` table, unlike ``uv.lock``), that mark a package as +#: not resolvable to a meaningful PyPI version pin. +_NON_REGISTRY_KEYS = ("git", "path") + + +def _default_group_package_or_none(pkg: Any) -> dict[str, Any] | None: + """Return *pkg* itself when it's a well-formed, default-group, + registry-sourced, versioned ``[[package]]`` entry -- ``None`` + otherwise (with a ``WARNING:`` for anything malformed or + non-registry-sourced; silent for a package that's simply not in the + default group, the same "expected filtering" as ``poetry.lock``'s + non-``main`` group exclusion).""" + if not isinstance(pkg, dict): + log.warning( + "Skipping malformed pdm.lock [[package]] entry: expected a table, got %s", + type(pkg).__name__, + ) + return None + name = pkg.get("name") + if not isinstance(name, str) or not name: + log.warning( + "Skipping malformed pdm.lock [[package]] entry: missing or " + "non-string 'name' (name=%r)", + name, + ) + return None + + groups = pkg.get("groups", [_DEFAULT_GROUP]) + if not isinstance(groups, list) or _DEFAULT_GROUP not in groups: + return None + + non_registry_key = next((key for key in _NON_REGISTRY_KEYS if key in pkg), None) + if non_registry_key is not None: + log.warning( + "Skipping pdm.lock entry %r: %s-sourced dependencies cannot be " + "represented as a PEP 508 specifier", + name, + non_registry_key, + ) + return None + + version = pkg.get("version") + if not isinstance(version, str) or not version: + log.warning( + "Skipping pdm.lock entry %r: missing or non-string 'version'", + name, + ) + return None + return pkg + + +def extract_pdm_lock_dependencies(project_dir: Path) -> list[str]: + """Read ``pdm.lock`` next to ``pyproject.toml`` and return its + resolved ``default``-group packages as exact-pin PEP 508 strings. + + Returns an empty list when no ``pdm.lock`` is present, or when it + can't be parsed -- this is optional enrichment, never a requirement. + """ + lock_path = project_dir / "pdm.lock" + data = load_lock_toml(lock_path) + if data is None: + return [] + + packages = data.get("package", []) + if not isinstance(packages, list): + log.warning( + "%s: top-level 'package' key is %s, expected a list -- ignoring pdm.lock", + lock_path, + type(packages).__name__, + ) + return [] + + default_group_packages = [ + pkg + for pkg in (_default_group_package_or_none(raw) for raw in packages) + if pkg is not None + ] + + dependencies: list[str] = [] + for name, entries in index_packages_by_name(default_group_packages).items(): + versions = {entry["version"] for entry in entries} + if len(versions) > 1: + log.warning( + "Skipping pdm.lock entry %r: %d conflicting resolved " + "versions present (%s)", + name, + len(versions), + ", ".join(sorted(versions)), + ) + continue + dependencies.append(f"{name}=={entries[0]['version']}") + return dependencies diff --git a/src/pitloom/extract/_poetry_lock.py b/src/pitloom/extract/_poetry_lock.py index 43c2f8f1..6e53f409 100644 --- a/src/pitloom/extract/_poetry_lock.py +++ b/src/pitloom/extract/_poetry_lock.py @@ -27,7 +27,7 @@ from pathlib import Path from typing import Any -from pitloom.extract._toml_io import TOMLDecodeError, load_toml_file +from pitloom.extract._lock_common import load_lock_toml log = logging.getLogger(__name__) @@ -49,12 +49,8 @@ def extract_poetry_lock_dependencies(project_dir: Path) -> list[str]: A package listed under both ``main`` and another group still counts. """ lock_path = project_dir / "poetry.lock" - try: - data = load_toml_file(lock_path) - except FileNotFoundError: - return [] - except (OSError, TOMLDecodeError) as exc: - log.warning("Failed to parse %s: %s", lock_path, exc) + data = load_lock_toml(lock_path) + if data is None: return [] packages = data.get("package", []) diff --git a/src/pitloom/extract/_pylock.py b/src/pitloom/extract/_pylock.py index d200cc29..428253be 100644 --- a/src/pitloom/extract/_pylock.py +++ b/src/pitloom/extract/_pylock.py @@ -31,7 +31,7 @@ from pathlib import Path from typing import Any -from pitloom.extract._toml_io import TOMLDecodeError, load_toml_file +from pitloom.extract._lock_common import load_lock_toml log = logging.getLogger(__name__) @@ -54,12 +54,8 @@ def extract_pylock_dependencies(project_dir: Path) -> list[str]: entry is taken as-is. """ lock_path = project_dir / "pylock.toml" - try: - data = load_toml_file(lock_path) - except FileNotFoundError: - return [] - except (OSError, TOMLDecodeError) as exc: - log.warning("Failed to parse %s: %s", lock_path, exc) + data = load_lock_toml(lock_path) + if data is None: return [] if not isinstance(data.get("lock-version"), str): diff --git a/src/pitloom/extract/_uv_lock.py b/src/pitloom/extract/_uv_lock.py new file mode 100644 index 00000000..80381ad0 --- /dev/null +++ b/src/pitloom/extract/_uv_lock.py @@ -0,0 +1,208 @@ +# SPDX-FileContributor: Arthit Suriyawongkul +# SPDX-FileCopyrightText: 2026-present Arthit Suriyawongkul +# SPDX-FileType: SOURCE +# SPDX-License-Identifier: Apache-2.0 + +"""Extractor for resolved dependencies from a ``uv.lock``. + +See also: :mod:`pitloom.extract._poetry_lock` and +:mod:`pitloom.extract._pylock` (the sibling lock extractors this module +mirrors in shape -- same source-stage-only scoping, same +``name==version`` output, same "no silent deviations" warning policy) +and :mod:`pitloom.extract._locked_dependencies` (the cascade module that +calls this extractor and overlays its output onto +``ProjectMetadata.locked_dependencies``, in priority order against every +other lock format). + +``uv.lock`` is source-stage-only, the same class as ``poetry.lock`` and +``pylock.toml``: appropriate for ``loom project``/``loom generate``, +never for ``loom wheel``/``embed-wheel`` (the real wheel's own metadata +is ground truth and never consults a lock) or ``loom env`` (live +introspection of what's actually installed is strictly more +authoritative than a lock that may be stale relative to it). + +Unlike ``poetry.lock`` and ``pylock.toml``, a ``uv.lock`` resolves +*every* Python version/platform combination its ``resolution-markers`` +cover in one file: the top-level ``[[package]]`` table is a flat union +across all of them, so the same package name can legitimately appear +more than once at different versions (e.g. one entry pinned for +``python_full_version < '3.10'``, another for ``>= '3.10'``). Picking +one of those without evaluating markers against a real environment +would misrepresent the resolved set, so this extractor doesn't guess: +it reads the *project's own* ``[[package]]`` entry (identified by +``source.editable``/``source.virtual``, uv's markers for "this is a +local project, not a PyPI download") and only its ``dependencies`` list +(main/runtime only -- ``optional-dependencies``/``dev-dependencies`` are +extras and dev groups, excluded the same way ``poetry.lock``'s +non-``main`` groups are), then resolves each referenced name against +the flat table *only* when exactly one candidate exists for that name. +An ambiguous (multiple-version) or marker-conditional (inline +``version`` on the dependency reference itself) name is skipped with a +``WARNING:``, not guessed. +""" + +from __future__ import annotations + +import logging +from pathlib import Path +from typing import Any + +from pitloom.extract._lock_common import index_packages_by_name, load_lock_toml + +log = logging.getLogger(__name__) + +__all__ = ["extract_uv_lock_dependencies"] + +#: uv.lock ``source`` keys that mark a package as not resolvable to a +#: meaningful PyPI version pin -- mirrors ``poetry.lock``'s +#: ``directory``/``file``/``git``/``url`` skip and ``pylock.toml``'s +#: ``vcs``/``directory``/``archive`` skip. +_NON_REGISTRY_SOURCE_KEYS = ("git", "path", "directory", "editable", "virtual") + +#: ``source`` keys identifying the project's own package entry (a local +#: root/workspace member, not a PyPI download). +_ROOT_SOURCE_KEYS = ("editable", "virtual") + + +def _find_root_package(packages: list[Any]) -> dict[str, Any] | None: + """Return the first ``[[package]]`` entry that is the project's own + (identified by an ``editable``/``virtual`` ``source``), or ``None`` + if none is found.""" + for pkg in packages: + if not isinstance(pkg, dict): + continue + source = pkg.get("source") + if isinstance(source, dict) and any(key in source for key in _ROOT_SOURCE_KEYS): + return pkg + return None + + +def _pinned_dep_for_root_dependency( + dep_ref: Any, by_name: dict[str, list[dict[str, Any]]] +) -> str | None: + """Return ``name==version`` for one entry of the root package's own + ``dependencies`` list, or ``None`` when it can't be resolved to a + single, unambiguous, registry-sourced pin.""" + if not isinstance(dep_ref, dict): + log.warning( + "Skipping malformed uv.lock dependency reference: expected a table, got %s", + type(dep_ref).__name__, + ) + return None + name = dep_ref.get("name") + if not isinstance(name, str) or not name: + log.warning( + "Skipping malformed uv.lock dependency reference: missing or " + "non-string 'name' (name=%r)", + name, + ) + return None + if "version" in dep_ref: + # An inline version on the reference itself means this + # dependency resolves to a different version per environment + # marker -- ambiguous without evaluating markers against a real + # environment, which this extractor deliberately doesn't do. + log.warning( + "Skipping uv.lock dependency %r: marker-conditional version " + "on the root package's own dependency reference (no marker " + "evaluation)", + name, + ) + return None + + candidates = by_name.get(name, []) + if not candidates: + log.warning( + "Skipping uv.lock dependency %r: referenced but not found in " + "the lock file's package table", + name, + ) + return None + if len(candidates) > 1: + log.warning( + "Skipping uv.lock dependency %r: %d resolved versions present " + "(marker-conditional) -- no marker evaluation", + name, + len(candidates), + ) + return None + + return _pinned_dep_for_package(candidates[0]) + + +def _pinned_dep_for_package(pkg: dict[str, Any]) -> str | None: + """Return ``name==version`` for one top-level ``[[package]]`` entry, + or ``None`` when it's non-registry-sourced or missing a version.""" + name = pkg["name"] + source = pkg.get("source") + if isinstance(source, dict): + non_registry_source = next( + (key for key in _NON_REGISTRY_SOURCE_KEYS if key in source), None + ) + if non_registry_source is not None: + log.warning( + "Skipping uv.lock entry %r: %s-sourced dependencies cannot " + "be represented as a PEP 508 specifier", + name, + non_registry_source, + ) + return None + version = pkg.get("version") + if not isinstance(version, str) or not version: + log.warning( + "Skipping uv.lock entry %r: missing or non-string 'version'", + name, + ) + return None + return f"{name}=={version}" + + +def extract_uv_lock_dependencies(project_dir: Path) -> list[str]: + """Read ``uv.lock`` next to ``pyproject.toml`` and return the + project's own main/runtime dependencies as exact-pin PEP 508 + strings. + + Returns an empty list when no ``uv.lock`` is present, it can't be + parsed, or the project's own package entry can't be identified -- + this is optional enrichment, never a requirement. + """ + lock_path = project_dir / "uv.lock" + data = load_lock_toml(lock_path) + if data is None: + return [] + + packages = data.get("package", []) + if not isinstance(packages, list): + log.warning( + "%s: top-level 'package' key is %s, expected a list -- ignoring uv.lock", + lock_path, + type(packages).__name__, + ) + return [] + + root = _find_root_package(packages) + if root is None: + log.warning( + "%s: no project package found (no 'editable'/'virtual' " + "source entry) -- ignoring uv.lock", + lock_path, + ) + return [] + + root_dependencies = root.get("dependencies", []) + if not isinstance(root_dependencies, list): + log.warning( + "%s: project package's 'dependencies' key is %s, expected a " + "list -- ignoring uv.lock", + lock_path, + type(root_dependencies).__name__, + ) + return [] + + by_name = index_packages_by_name(packages) + dependencies: list[str] = [] + for dep_ref in root_dependencies: + dep = _pinned_dep_for_root_dependency(dep_ref, by_name) + if dep is not None: + dependencies.append(dep) + return dependencies diff --git a/tests/extract/test_lock_common.py b/tests/extract/test_lock_common.py new file mode 100644 index 00000000..ab62cfb2 --- /dev/null +++ b/tests/extract/test_lock_common.py @@ -0,0 +1,81 @@ +# SPDX-FileContributor: Arthit Suriyawongkul +# SPDX-FileCopyrightText: 2026-present Arthit Suriyawongkul +# SPDX-FileType: SOURCE +# SPDX-License-Identifier: Apache-2.0 + +"""Tests for :mod:`pitloom.extract._lock_common` -- the helpers shared +across every lock/pin extractor (:mod:`pitloom.extract._poetry_lock`, +:mod:`pitloom.extract._pylock`, :mod:`pitloom.extract._uv_lock`, +:mod:`pitloom.extract._pdm_lock`).""" + +import logging +import tempfile +from pathlib import Path + +import pytest + +from pitloom.extract._lock_common import index_packages_by_name, load_lock_toml + + +def test_load_lock_toml_missing_file_returns_none() -> None: + with tempfile.TemporaryDirectory() as tmp: + assert load_lock_toml(Path(tmp) / "does-not-exist.lock") is None + + +def test_load_lock_toml_malformed_returns_none_and_warns( + caplog: pytest.LogCaptureFixture, +) -> None: + with tempfile.TemporaryDirectory() as tmp: + lock_path = Path(tmp) / "some.lock" + lock_path.write_text("this is not [ valid toml", encoding="utf-8") + + with caplog.at_level(logging.WARNING): + result = load_lock_toml(lock_path) + + assert result is None + assert "Failed to parse" in caplog.text + + +def test_load_lock_toml_valid_file_returns_data() -> None: + with tempfile.TemporaryDirectory() as tmp: + lock_path = Path(tmp) / "some.lock" + lock_path.write_text('key = "value"\n', encoding="utf-8") + + assert load_lock_toml(lock_path) == {"key": "value"} + + +def test_index_packages_by_name_groups_by_name_preserving_order() -> None: + packages = [ + {"name": "a", "version": "1.0.0"}, + {"name": "b", "version": "2.0.0"}, + {"name": "a", "version": "1.0.1", "extras": ["x"]}, + ] + + result = index_packages_by_name(packages) + + assert list(result.keys()) == ["a", "b"] + assert result["a"] == [ + {"name": "a", "version": "1.0.0"}, + {"name": "a", "version": "1.0.1", "extras": ["x"]}, + ] + assert result["b"] == [{"name": "b", "version": "2.0.0"}] + + +def test_index_packages_by_name_ignores_non_dict_entries() -> None: + packages: list[object] = ["not-a-dict", ["still", "not", "a", "dict"]] + + assert not index_packages_by_name(packages) + + +def test_index_packages_by_name_ignores_entries_with_missing_or_bad_name() -> None: + packages: list[object] = [ + {"version": "1.0.0"}, # missing name + {"name": None, "version": "1.0.0"}, # non-string name + {"name": "", "version": "1.0.0"}, # empty name + ] + + assert not index_packages_by_name(packages) + + +def test_index_packages_by_name_empty_list_returns_empty_dict() -> None: + assert not index_packages_by_name([]) diff --git a/tests/extract/test_pdm_lock.py b/tests/extract/test_pdm_lock.py new file mode 100644 index 00000000..585d74fd --- /dev/null +++ b/tests/extract/test_pdm_lock.py @@ -0,0 +1,349 @@ +# SPDX-FileContributor: Arthit Suriyawongkul +# SPDX-FileCopyrightText: 2026-present Arthit Suriyawongkul +# SPDX-FileType: SOURCE +# SPDX-License-Identifier: Apache-2.0 + +"""Tests for PDM ``pdm.lock`` resolved-dependency parsing +(:mod:`pitloom.extract._pdm_lock`) and its overlay onto +``ProjectMetadata.locked_dependencies`` via ``read_project()``'s lock +cascade (:mod:`pitloom.extract._locked_dependencies`). + +See also: test_poetry_lock.py/test_uv_lock.py for the sibling lock +extractors this module's tests mirror in shape; +test_locked_dependencies.py for the cascade mechanism's own tests, +including the priority-order-consistency regression this format's +below-``poetry.lock`` rank exercises. +""" + +import logging +import tempfile +from pathlib import Path + +import pytest + +from pitloom.extract._pdm_lock import extract_pdm_lock_dependencies +from pitloom.extract.project import read_project + +REAL_WORLD_LOCKS = ( + Path(__file__).parent.parent / "fixtures" / "real-world-locks" / "pdm" +) + + +def _write_lock(tmp_dir: Path, body: str = "") -> None: + (tmp_dir / "pdm.lock").write_text(body, encoding="utf-8") + + +def test_no_lock_file_returns_empty_list() -> None: + with tempfile.TemporaryDirectory() as tmp: + assert not extract_pdm_lock_dependencies(Path(tmp)) + + +def test_malformed_toml_returns_empty_list_and_warns( + caplog: pytest.LogCaptureFixture, +) -> None: + with tempfile.TemporaryDirectory() as tmp: + tmp_path = Path(tmp) + _write_lock(tmp_path, "this is not [ valid toml") + + with caplog.at_level(logging.WARNING): + result = extract_pdm_lock_dependencies(tmp_path) + + assert not result + assert "Failed to parse" in caplog.text + + +def test_package_key_not_a_list_returns_empty_list_and_warns( + caplog: pytest.LogCaptureFixture, +) -> None: + with tempfile.TemporaryDirectory() as tmp: + tmp_path = Path(tmp) + _write_lock(tmp_path, 'package = "not-a-list"\n') + + with caplog.at_level(logging.WARNING): + result = extract_pdm_lock_dependencies(tmp_path) + + assert not result + assert "expected a list" in caplog.text + + +def test_default_group_package_included() -> None: + with tempfile.TemporaryDirectory() as tmp: + tmp_path = Path(tmp) + _write_lock( + tmp_path, + '[[package]]\nname = "requests"\nversion = "2.31.0"\n' + 'groups = ["default"]\n', + ) + + assert extract_pdm_lock_dependencies(tmp_path) == ["requests==2.31.0"] + + +def test_non_default_group_package_excluded() -> None: + with tempfile.TemporaryDirectory() as tmp: + tmp_path = Path(tmp) + _write_lock( + tmp_path, + '[[package]]\nname = "pytest"\nversion = "8.0.0"\ngroups = ["test"]\n', + ) + + assert not extract_pdm_lock_dependencies(tmp_path) + + +def test_package_in_default_and_other_group_included() -> None: + """A package listed under both `default` and another group still + counts -- only *exclusively* non-default packages are dropped.""" + with tempfile.TemporaryDirectory() as tmp: + tmp_path = Path(tmp) + _write_lock( + tmp_path, + '[[package]]\nname = "shared-pkg"\nversion = "1.0.0"\n' + 'groups = ["default", "test"]\n', + ) + + assert extract_pdm_lock_dependencies(tmp_path) == ["shared-pkg==1.0.0"] + + +def test_missing_groups_key_defaults_to_default_group() -> None: + with tempfile.TemporaryDirectory() as tmp: + tmp_path = Path(tmp) + _write_lock(tmp_path, '[[package]]\nname = "legacy-pkg"\nversion = "1.0.0"\n') + + assert extract_pdm_lock_dependencies(tmp_path) == ["legacy-pkg==1.0.0"] + + +def test_malformed_package_entry_skipped_and_warns( + caplog: pytest.LogCaptureFixture, +) -> None: + with tempfile.TemporaryDirectory() as tmp: + tmp_path = Path(tmp) + _write_lock( + tmp_path, + '[[package]]\nversion = "1.0.0"\ngroups = ["default"]\n\n' + '[[package]]\nname = "complete-pkg"\nversion = "2.0.0"\n' + 'groups = ["default"]\n', + ) + + with caplog.at_level(logging.WARNING): + result = extract_pdm_lock_dependencies(tmp_path) + + assert result == ["complete-pkg==2.0.0"] + assert "malformed" in caplog.text.lower() + + +def test_non_dict_package_entry_warns(caplog: pytest.LogCaptureFixture) -> None: + with tempfile.TemporaryDirectory() as tmp: + tmp_path = Path(tmp) + _write_lock(tmp_path, "package = [1, 2, 3]\n") + + with caplog.at_level(logging.WARNING): + result = extract_pdm_lock_dependencies(tmp_path) + + assert not result + assert "malformed" in caplog.text.lower() + + +def test_missing_version_skipped_and_warns(caplog: pytest.LogCaptureFixture) -> None: + with tempfile.TemporaryDirectory() as tmp: + tmp_path = Path(tmp) + _write_lock( + tmp_path, '[[package]]\nname = "no-version"\ngroups = ["default"]\n' + ) + + with caplog.at_level(logging.WARNING): + result = extract_pdm_lock_dependencies(tmp_path) + + assert not result + assert "missing" in caplog.text.lower() + + +@pytest.mark.parametrize("source_key", ["git", "path"]) +def test_non_registry_sourced_package_excluded( + source_key: str, caplog: pytest.LogCaptureFixture +) -> None: + with tempfile.TemporaryDirectory() as tmp: + tmp_path = Path(tmp) + _write_lock( + tmp_path, + f'[[package]]\nname = "local-dep"\nversion = "0.1.0"\n' + f'groups = ["default"]\n{source_key} = "some-value"\n', + ) + + with caplog.at_level(logging.WARNING): + result = extract_pdm_lock_dependencies(tmp_path) + + assert not result + assert "local-dep" in caplog.text + + +def test_same_name_same_version_duplicate_entries_deduped() -> None: + """PDM records a separate `[[package]]` entry per requested extra + variant of the same package, always agreeing on `version` -- these + must collapse to one `name==version`, not two, and not be treated + as an ambiguous conflict.""" + with tempfile.TemporaryDirectory() as tmp: + tmp_path = Path(tmp) + _write_lock( + tmp_path, + '[[package]]\nname = "httpx"\nversion = "0.28.1"\n' + 'groups = ["default"]\n\n' + '[[package]]\nname = "httpx"\nversion = "0.28.1"\n' + 'extras = ["socks"]\ngroups = ["default"]\n', + ) + + assert extract_pdm_lock_dependencies(tmp_path) == ["httpx==0.28.1"] + + +def test_same_name_conflicting_versions_skipped_and_warns( + caplog: pytest.LogCaptureFixture, +) -> None: + """Unlike the same-version extras-variant case, entries that + genuinely disagree on version are ambiguous -- skip, don't guess.""" + with tempfile.TemporaryDirectory() as tmp: + tmp_path = Path(tmp) + _write_lock( + tmp_path, + '[[package]]\nname = "conflicted"\nversion = "1.0.0"\n' + 'groups = ["default"]\n\n' + '[[package]]\nname = "conflicted"\nversion = "2.0.0"\n' + 'groups = ["default"]\n', + ) + + with caplog.at_level(logging.WARNING): + result = extract_pdm_lock_dependencies(tmp_path) + + assert not result + assert "2 conflicting resolved versions" in caplog.text + + +# --- read_project() cascade integration ----------------------------------- + + +def test_read_project_populates_locked_dependencies() -> None: + with tempfile.TemporaryDirectory() as tmp: + tmp_path = Path(tmp) + (tmp_path / "pyproject.toml").write_text( + '[project]\nname = "demo"\nversion = "1.0.0"\n', encoding="utf-8" + ) + _write_lock( + tmp_path, + '[[package]]\nname = "requests"\nversion = "2.31.0"\n' + 'groups = ["default"]\n', + ) + + metadata, _config, _path = read_project(tmp_path) + + assert metadata.locked_dependencies == ["requests==2.31.0"] + assert metadata.provenance["locked_dependencies"] == ( + "Source: pdm.lock | Method: resolved_lockfile" + ) + + +def test_read_project_pdm_lock_never_overrides_poetry_lock( + caplog: pytest.LogCaptureFixture, +) -> None: + """Regression: `pdm.lock` ranks *below* `poetry.lock` in the shared + priority order -- when both are present (an unusual but possible + project layout), `poetry.lock`'s already-applied result must win, + and `pdm.lock` must never silently override it. This is the exact + class of bug a naive "first entry with data wins" cascade would + introduce once a lower-than-`poetry.lock`-ranked format is added.""" + with tempfile.TemporaryDirectory() as tmp: + tmp_path = Path(tmp) + (tmp_path / "pyproject.toml").write_text( + '[tool.poetry]\nname = "demo"\nversion = "1.0.0"\n', encoding="utf-8" + ) + (tmp_path / "poetry.lock").write_text( + '[[package]]\nname = "requests"\nversion = "2.31.0"\ngroups = ["main"]\n', + encoding="utf-8", + ) + _write_lock( + tmp_path, + '[[package]]\nname = "httpx"\nversion = "0.28.1"\ngroups = ["default"]\n', + ) + + with caplog.at_level(logging.WARNING): + metadata, _config, _path = read_project(tmp_path) + + assert metadata.locked_dependencies == ["requests==2.31.0"] + assert metadata.provenance["locked_dependencies"] == ( + "Source: poetry.lock | Method: resolved_lockfile" + ) + assert "supersedes" not in caplog.text + + +def test_read_project_pdm_lock_used_when_no_poetry_lock_present() -> None: + """A non-Poetry project (no `[tool.poetry]` at all, so `poetry.lock` + was never applied) still gets `pdm.lock`'s data normally -- the + priority-order gate only blocks *actual* higher-ranked data, not the + mere possibility of it.""" + with tempfile.TemporaryDirectory() as tmp: + tmp_path = Path(tmp) + (tmp_path / "pyproject.toml").write_text( + '[project]\nname = "demo"\nversion = "1.0.0"\n', encoding="utf-8" + ) + _write_lock( + tmp_path, + '[[package]]\nname = "httpx"\nversion = "0.28.1"\ngroups = ["default"]\n', + ) + + metadata, _config, _path = read_project(tmp_path) + + assert metadata.locked_dependencies == ["httpx==0.28.1"] + assert metadata.provenance["locked_dependencies"] == ( + "Source: pdm.lock | Method: resolved_lockfile" + ) + + +def test_read_project_uv_lock_still_overrides_pdm_lock() -> None: + """`uv.lock` outranks `pdm.lock` -- confirm adding `pdm.lock` to the + cascade didn't disturb the existing higher-priority entries.""" + with tempfile.TemporaryDirectory() as tmp: + tmp_path = Path(tmp) + (tmp_path / "pyproject.toml").write_text( + '[project]\nname = "demo"\nversion = "1.0.0"\n', encoding="utf-8" + ) + (tmp_path / "uv.lock").write_text( + 'version = 1\nrevision = 1\nrequires-python = ">=3.10"\n' + '[[package]]\nname = "demo"\nversion = "1.0.0"\n' + 'source = { editable = "." }\n' + 'dependencies = [{ name = "requests" }]\n\n' + '[[package]]\nname = "requests"\nversion = "2.31.0"\n' + 'source = { registry = "https://pypi.org/simple" }\n', + encoding="utf-8", + ) + _write_lock( + tmp_path, + '[[package]]\nname = "httpx"\nversion = "0.28.1"\ngroups = ["default"]\n', + ) + + metadata, _config, _path = read_project(tmp_path) + + assert metadata.locked_dependencies == ["requests==2.31.0"] + assert metadata.provenance["locked_dependencies"] == ( + "Source: uv.lock | Method: resolved_lockfile" + ) + + +# --- real-world fixtures --------------------------------------------------- + + +def test_real_world_pdm() -> None: + """`pdm-project/pdm` -- PDM's own package (self-hosting case).""" + metadata, _config, _path = read_project(REAL_WORLD_LOCKS / "pdm-2.29.0") + + assert metadata.name == "pdm" + names = {dep.split("==", maxsplit=1)[0] for dep in metadata.locked_dependencies} + assert "httpx" in names + assert "coverage" not in names # test-group only + assert metadata.provenance["locked_dependencies"] == ( + "Source: pdm.lock | Method: resolved_lockfile" + ) + + +def test_real_world_unearth() -> None: + metadata, _config, _path = read_project(REAL_WORLD_LOCKS / "unearth-0.18.3") + + assert metadata.name == "unearth" + names = {dep.split("==", maxsplit=1)[0] for dep in metadata.locked_dependencies} + assert "anyio" in names + assert "sphinx" not in names # doc-group only, per the unearth pdm.lock sample diff --git a/tests/extract/test_uv_lock.py b/tests/extract/test_uv_lock.py new file mode 100644 index 00000000..0e0e3427 --- /dev/null +++ b/tests/extract/test_uv_lock.py @@ -0,0 +1,459 @@ +# SPDX-FileContributor: Arthit Suriyawongkul +# SPDX-FileCopyrightText: 2026-present Arthit Suriyawongkul +# SPDX-FileType: SOURCE +# SPDX-License-Identifier: Apache-2.0 + +"""Tests for ``uv.lock`` resolved-dependency parsing +(:mod:`pitloom.extract._uv_lock`) and its overlay onto +``ProjectMetadata.locked_dependencies`` via ``read_project()``'s lock +cascade (:mod:`pitloom.extract._locked_dependencies`). + +See also: test_pylock.py/test_poetry_lock.py for the sibling lock +extractors this module's tests mirror in shape; +test_locked_dependencies.py for the cascade mechanism's own tests. +""" + +import logging +import tempfile +from pathlib import Path + +import pytest + +from pitloom.extract._uv_lock import ( + _find_root_package, + _pinned_dep_for_package, + extract_uv_lock_dependencies, +) +from pitloom.extract.project import read_project + +_LOCK_HEADER = 'version = 1\nrevision = 1\nrequires-python = ">=3.10"\n' + +#: A minimal root/project package entry -- every test that needs one +#: root dependency composes this with its own `dependencies` block. +_ROOT_HEADER = ( + '[[package]]\nname = "demo"\nversion = "1.0.0"\nsource = { editable = "." }\n' +) + +REAL_WORLD_LOCKS = Path(__file__).parent.parent / "fixtures" / "real-world-locks" / "uv" + + +def _write_lock(tmp_dir: Path, body: str = "") -> None: + (tmp_dir / "uv.lock").write_text(_LOCK_HEADER + body, encoding="utf-8") + + +def test_no_lock_file_returns_empty_list() -> None: + with tempfile.TemporaryDirectory() as tmp: + assert not extract_uv_lock_dependencies(Path(tmp)) + + +def test_malformed_toml_returns_empty_list_and_warns( + caplog: pytest.LogCaptureFixture, +) -> None: + with tempfile.TemporaryDirectory() as tmp: + tmp_path = Path(tmp) + (tmp_path / "uv.lock").write_text("this is not [ valid toml", encoding="utf-8") + + with caplog.at_level(logging.WARNING): + result = extract_uv_lock_dependencies(tmp_path) + + assert not result + assert "Failed to parse" in caplog.text + + +def test_package_key_not_a_list_returns_empty_list_and_warns( + caplog: pytest.LogCaptureFixture, +) -> None: + with tempfile.TemporaryDirectory() as tmp: + tmp_path = Path(tmp) + _write_lock(tmp_path, 'package = "not-a-list"\n') + + with caplog.at_level(logging.WARNING): + result = extract_uv_lock_dependencies(tmp_path) + + assert not result + assert "expected a list" in caplog.text + + +def test_no_root_package_returns_empty_list_and_warns( + caplog: pytest.LogCaptureFixture, +) -> None: + """A uv.lock with no `editable`/`virtual`-sourced entry has no + identifiable project package -- nothing to resolve dependencies + for.""" + with tempfile.TemporaryDirectory() as tmp: + tmp_path = Path(tmp) + _write_lock( + tmp_path, + '[[package]]\nname = "requests"\nversion = "2.31.0"\n' + 'source = { registry = "https://pypi.org/simple" }\n', + ) + + with caplog.at_level(logging.WARNING): + result = extract_uv_lock_dependencies(tmp_path) + + assert not result + assert "no project package found" in caplog.text + + +def test_root_dependencies_not_a_list_returns_empty_list_and_warns( + caplog: pytest.LogCaptureFixture, +) -> None: + with tempfile.TemporaryDirectory() as tmp: + tmp_path = Path(tmp) + _write_lock( + tmp_path, + '[[package]]\nname = "demo"\nversion = "1.0.0"\n' + 'source = { editable = "." }\ndependencies = "not-a-list"\n', + ) + + with caplog.at_level(logging.WARNING): + result = extract_uv_lock_dependencies(tmp_path) + + assert not result + assert "expected a list" in caplog.text + + +def test_root_with_no_dependencies_key_returns_empty_list() -> None: + """A project with zero runtime dependencies is valid, not an error.""" + with tempfile.TemporaryDirectory() as tmp: + tmp_path = Path(tmp) + _write_lock(tmp_path, _ROOT_HEADER) + + assert not extract_uv_lock_dependencies(tmp_path) + + +def test_simple_dependency_resolved() -> None: + with tempfile.TemporaryDirectory() as tmp: + tmp_path = Path(tmp) + _write_lock( + tmp_path, + _ROOT_HEADER + 'dependencies = [{ name = "requests" }]\n\n' + '[[package]]\nname = "requests"\nversion = "2.31.0"\n' + 'source = { registry = "https://pypi.org/simple" }\n', + ) + + assert extract_uv_lock_dependencies(tmp_path) == ["requests==2.31.0"] + + +def test_dependency_with_marker_but_no_inline_version_still_resolved() -> None: + """A `marker` field alone (conditional presence, not a version + conflict) doesn't block resolution -- same "no marker evaluation, + include regardless" simplification as poetry.lock/pylock.toml.""" + with tempfile.TemporaryDirectory() as tmp: + tmp_path = Path(tmp) + _write_lock( + tmp_path, + _ROOT_HEADER + 'dependencies = [{ name = "requests", ' + "marker = \"python_full_version < '3.11'\" }]\n\n" + '[[package]]\nname = "requests"\nversion = "2.31.0"\n' + 'source = { registry = "https://pypi.org/simple" }\n', + ) + + assert extract_uv_lock_dependencies(tmp_path) == ["requests==2.31.0"] + + +def test_malformed_dependency_reference_skipped_and_warns( + caplog: pytest.LogCaptureFixture, +) -> None: + with tempfile.TemporaryDirectory() as tmp: + tmp_path = Path(tmp) + _write_lock(tmp_path, _ROOT_HEADER + 'dependencies = ["not-a-table"]\n') + + with caplog.at_level(logging.WARNING): + result = extract_uv_lock_dependencies(tmp_path) + + assert not result + assert "malformed" in caplog.text.lower() + + +def test_dependency_reference_missing_name_skipped_and_warns( + caplog: pytest.LogCaptureFixture, +) -> None: + with tempfile.TemporaryDirectory() as tmp: + tmp_path = Path(tmp) + _write_lock(tmp_path, _ROOT_HEADER + "dependencies = [{ extra = ['x'] }]\n") + + with caplog.at_level(logging.WARNING): + result = extract_uv_lock_dependencies(tmp_path) + + assert not result + assert "malformed" in caplog.text.lower() + + +def test_marker_conditional_root_dependency_skipped_and_warns( + caplog: pytest.LogCaptureFixture, +) -> None: + """An inline `version` directly on the root's own dependency + reference means the resolved version differs per environment marker + -- ambiguous without evaluating markers, so it's skipped, not + guessed.""" + with tempfile.TemporaryDirectory() as tmp: + tmp_path = Path(tmp) + _write_lock( + tmp_path, + _ROOT_HEADER + 'dependencies = [{ name = "click", version = "8.1.8", ' + 'source = { registry = "https://pypi.org/simple" }, ' + "marker = \"python_full_version < '3.10'\" }]\n", + ) + + with caplog.at_level(logging.WARNING): + result = extract_uv_lock_dependencies(tmp_path) + + assert not result + assert "marker-conditional" in caplog.text + + +def test_dependency_not_found_in_package_table_skipped_and_warns( + caplog: pytest.LogCaptureFixture, +) -> None: + with tempfile.TemporaryDirectory() as tmp: + tmp_path = Path(tmp) + _write_lock( + tmp_path, _ROOT_HEADER + 'dependencies = [{ name = "ghost-pkg" }]\n' + ) + + with caplog.at_level(logging.WARNING): + result = extract_uv_lock_dependencies(tmp_path) + + assert not result + assert "not found" in caplog.text + + +def test_ambiguous_multi_version_dependency_skipped_and_warns( + caplog: pytest.LogCaptureFixture, +) -> None: + """The same package name resolved at two different versions (e.g. + one per Python-version marker branch) can't be picked between + without marker evaluation -- skip both, don't guess.""" + with tempfile.TemporaryDirectory() as tmp: + tmp_path = Path(tmp) + _write_lock( + tmp_path, + _ROOT_HEADER + 'dependencies = [{ name = "click" }]\n\n' + '[[package]]\nname = "click"\nversion = "8.1.8"\n' + 'source = { registry = "https://pypi.org/simple" }\n\n' + '[[package]]\nname = "click"\nversion = "8.3.1"\n' + 'source = { registry = "https://pypi.org/simple" }\n', + ) + + with caplog.at_level(logging.WARNING): + result = extract_uv_lock_dependencies(tmp_path) + + assert not result + assert "2 resolved versions" in caplog.text + + +@pytest.mark.parametrize( + "source_key", ["git", "path", "directory", "editable", "virtual"] +) +def test_non_registry_sourced_dependency_excluded( + source_key: str, caplog: pytest.LogCaptureFixture +) -> None: + with tempfile.TemporaryDirectory() as tmp: + tmp_path = Path(tmp) + _write_lock( + tmp_path, + _ROOT_HEADER + 'dependencies = [{ name = "local-dep" }]\n\n' + f'[[package]]\nname = "local-dep"\nversion = "0.1.0"\n' + f'source = {{ {source_key} = "some-value" }}\n', + ) + + with caplog.at_level(logging.WARNING): + result = extract_uv_lock_dependencies(tmp_path) + + assert not result + assert "local-dep" in caplog.text + + +def test_dependency_missing_version_skipped_and_warns( + caplog: pytest.LogCaptureFixture, +) -> None: + with tempfile.TemporaryDirectory() as tmp: + tmp_path = Path(tmp) + _write_lock( + tmp_path, + _ROOT_HEADER + 'dependencies = [{ name = "no-version" }]\n\n' + '[[package]]\nname = "no-version"\n' + 'source = { registry = "https://pypi.org/simple" }\n', + ) + + with caplog.at_level(logging.WARNING): + result = extract_uv_lock_dependencies(tmp_path) + + assert not result + assert "missing" in caplog.text.lower() + + +def test_dependency_with_no_source_table_still_included() -> None: + """A package entry with no `source` key at all (unusual but not + invalid) is treated the same as a registry source -- only an + explicit non-registry key excludes it.""" + with tempfile.TemporaryDirectory() as tmp: + tmp_path = Path(tmp) + _write_lock( + tmp_path, + _ROOT_HEADER + 'dependencies = [{ name = "no-source" }]\n\n' + '[[package]]\nname = "no-source"\nversion = "1.2.3"\n', + ) + + assert extract_uv_lock_dependencies(tmp_path) == ["no-source==1.2.3"] + + +def test_find_root_package_returns_none_for_empty_list() -> None: + assert _find_root_package([]) is None + + +def test_find_root_package_ignores_malformed_entries() -> None: + """A malformed top-level `[[package]]` entry (not a table) is + silently skipped while searching for the root package -- see + test_lock_common.py for the equivalent `index_packages_by_name()` + coverage this and `_uv_lock.py`'s own extraction share.""" + packages: list[object] = [ + "not-a-dict", + {"version": "1.0.0"}, # missing name, still not editable/virtual + {"name": "requests", "version": "2.31.0"}, + ] + + assert _find_root_package(packages) is None + + +def test_pinned_dep_for_package_returns_none_when_source_not_a_dict() -> None: + """Defensive: a `source` value that isn't a table (malformed) is + skipped by the source-key check, not a crash -- version resolution + still proceeds normally.""" + assert ( + _pinned_dep_for_package( + {"name": "odd-pkg", "version": "1.0.0", "source": "not-a-table"} + ) + == "odd-pkg==1.0.0" + ) + + +# --- read_project() cascade integration ----------------------------------- + + +def test_read_project_populates_locked_dependencies() -> None: + with tempfile.TemporaryDirectory() as tmp: + tmp_path = Path(tmp) + (tmp_path / "pyproject.toml").write_text( + '[project]\nname = "demo"\nversion = "1.0.0"\n', encoding="utf-8" + ) + _write_lock( + tmp_path, + _ROOT_HEADER + 'dependencies = [{ name = "requests" }]\n\n' + '[[package]]\nname = "requests"\nversion = "2.31.0"\n' + 'source = { registry = "https://pypi.org/simple" }\n', + ) + + metadata, _config, _path = read_project(tmp_path) + + assert metadata.locked_dependencies == ["requests==2.31.0"] + assert metadata.provenance["locked_dependencies"] == ( + "Source: uv.lock | Method: resolved_lockfile" + ) + + +def test_read_project_uv_lock_takes_priority_over_poetry_lock( + caplog: pytest.LogCaptureFixture, +) -> None: + with tempfile.TemporaryDirectory() as tmp: + tmp_path = Path(tmp) + (tmp_path / "pyproject.toml").write_text( + '[tool.poetry]\nname = "demo"\nversion = "1.0.0"\n', encoding="utf-8" + ) + (tmp_path / "poetry.lock").write_text( + '[[package]]\nname = "requests"\nversion = "2.31.0"\ngroups = ["main"]\n', + encoding="utf-8", + ) + _write_lock( + tmp_path, + _ROOT_HEADER + 'dependencies = [{ name = "httpx" }]\n\n' + '[[package]]\nname = "httpx"\nversion = "0.27.0"\n' + 'source = { registry = "https://pypi.org/simple" }\n', + ) + + with caplog.at_level(logging.WARNING): + metadata, _config, _path = read_project(tmp_path) + + assert metadata.locked_dependencies == ["httpx==0.27.0"] + assert metadata.provenance["locked_dependencies"] == ( + "Source: uv.lock | Method: resolved_lockfile | Note: supersedes poetry.lock" + ) + + +def test_read_project_pylock_takes_priority_over_uv_lock() -> None: + """pylock.toml (PEP 751) outranks uv.lock in the cascade.""" + with tempfile.TemporaryDirectory() as tmp: + tmp_path = Path(tmp) + (tmp_path / "pyproject.toml").write_text( + '[project]\nname = "demo"\nversion = "1.0.0"\n', encoding="utf-8" + ) + (tmp_path / "pylock.toml").write_text( + 'lock-version = "1.0"\ncreated-by = "test"\n' + '[[packages]]\nname = "httpx"\nversion = "0.27.0"\n', + encoding="utf-8", + ) + _write_lock( + tmp_path, + _ROOT_HEADER + 'dependencies = [{ name = "requests" }]\n\n' + '[[package]]\nname = "requests"\nversion = "2.31.0"\n' + 'source = { registry = "https://pypi.org/simple" }\n', + ) + + metadata, _config, _path = read_project(tmp_path) + + assert metadata.locked_dependencies == ["httpx==0.27.0"] + assert metadata.provenance["locked_dependencies"] == ( + "Source: pylock.toml | Method: resolved_lockfile" + ) + + +# --- real-world fixtures --------------------------------------------------- + + +def test_real_world_flask() -> None: + """`pallets/flask` -- `uv.lock` ships in the PyPI sdist itself (the + only fixture where that's true, per real-world-locks/README.md). + Has multiple marker-conditional duplicate names (e.g. `click`), + exercising the ambiguity-skip path against real data.""" + metadata, _config, _path = read_project(REAL_WORLD_LOCKS / "flask-3.1.3") + + assert metadata.name == "Flask" + names = {dep.split("==", maxsplit=1)[0] for dep in metadata.locked_dependencies} + assert names == { + "blinker", + "importlib-metadata", + "itsdangerous", + "jinja2", + "markupsafe", + "werkzeug", + } + assert "click" not in names # ambiguous (ships two marker-conditional versions) + assert metadata.provenance["locked_dependencies"] == ( + "Source: uv.lock | Method: resolved_lockfile" + ) + + +def test_real_world_fastapi_cli() -> None: + metadata, _config, _path = read_project(REAL_WORLD_LOCKS / "fastapi-cli-0.0.32") + + assert metadata.name == "fastapi-cli" + names = {dep.split("==", maxsplit=1)[0] for dep in metadata.locked_dependencies} + assert names == {"rich-toolkit", "tomli", "typer", "uvicorn"} + + +def test_real_world_abi3audit() -> None: + metadata, _config, _path = read_project(REAL_WORLD_LOCKS / "abi3audit-0.0.26") + + assert metadata.name == "abi3audit" + names = {dep.split("==", maxsplit=1)[0] for dep in metadata.locked_dependencies} + assert names == { + "abi3info", + "kaitaistruct", + "packaging", + "pefile", + "pyelftools", + "requests", + "requests-cache", + "rich", + } diff --git a/tests/fixtures/real-world-locks/README.md b/tests/fixtures/real-world-locks/README.md index b9e9e5bb..ebced17a 100644 --- a/tests/fixtures/real-world-locks/README.md +++ b/tests/fixtures/real-world-locks/README.md @@ -89,6 +89,27 @@ artifact. `[tool.poetry]` (hybrid); the other three have `[tool.poetry]` only, no `[project]` table at all -- both of `read_pyproject()`'s Poetry-detection shapes get real coverage. +- **`flask`'s `uv.lock` genuinely exercises the marker-ambiguity skip + path.** Unlike `poetry.lock`/`pylock.toml`, a `uv.lock` resolves every + Python-version/platform combination in one file, so the same package + name can legitimately appear at more than one version (e.g. `click` + pinned differently for `python_full_version < '3.10'` vs `>= '3.10'`). + `flask`'s own runtime `dependencies` include exactly one such case -- + `extract_uv_lock_dependencies()` skips it with a `WARNING:` rather + than guessing, so `click` is deliberately absent from + `locked_dependencies` for this fixture. `fastapi-cli` and `abi3audit` + have no such ambiguity, so they cover the plain resolution path + instead. +- **`pdm`'s own `pdm.lock` exercises the extras-variant dedup path.** + `pdm.lock` records a separate `[[package]]` entry per requested extra + variant of the same package (e.g. a bare `httpx` entry alongside an + `httpx` entry with `extras = ["socks"]`) -- unlike `uv.lock`'s + genuinely conflicting duplicates, these always agree on `version`, so + `extract_pdm_lock_dependencies()` collapses them to one + `name==version` rather than skipping them as ambiguous. `pdm`'s own + lock has several real instances of this (`httpx`, `coverage`, + `mkdocstrings`, `hishel`); `unearth`'s doesn't, so together they cover + both the dedup path and the plain case. ## Fixtures @@ -100,8 +121,13 @@ artifact. | `poetry.lock` | [python-poetry/cleo](https://github.com/python-poetry/cleo) | 2.1.0 | MIT | PyPI sdist (`[tool.poetry]` only) | GitHub tag `2.1.0` | | `poetry.lock` | [python-poetry/tomlkit](https://github.com/python-poetry/tomlkit) | 0.15.1 | MIT | PyPI sdist (`[tool.poetry]` only) | GitHub tag `0.15.1` | | `poetry.lock` | [sdispater/pastel](https://github.com/sdispater/pastel) | 0.2.1 | MIT | PyPI sdist (`[tool.poetry]` only) | GitHub tag `0.2.1` | +| `uv.lock` | [pallets/flask](https://github.com/pallets/flask) | 3.1.3 | BSD-3-Clause | PyPI sdist | PyPI sdist (the only fixture where `uv.lock` ships in it) | +| `uv.lock` | [fastapi/fastapi-cli](https://github.com/fastapi/fastapi-cli) | 0.0.32 | MIT | GitHub tag `0.0.32` | GitHub tag `0.0.32` | +| `uv.lock` | [pypa/abi3audit](https://github.com/pypa/abi3audit) | 0.0.26 | MIT | GitHub tag `v0.0.26` | GitHub tag `v0.0.26` | +| `pdm.lock` | [pdm-project/pdm](https://github.com/pdm-project/pdm) | 2.29.0 | MIT | GitHub tag `2.29.0` | GitHub tag `2.29.0` | +| `pdm.lock` | [frostming/unearth](https://github.com/frostming/unearth) | 0.18.3 | MIT | GitHub tag `0.18.3` | GitHub tag `0.18.3` | -`uv.lock`, `pdm.lock`, `Pipfile.lock`, and pinned `requirements.txt` -fixtures land in their own follow-up changes, alongside each format's -own extractor -- see `working-docs/design/roadmap.md`'s "Remaining lock -formats as a resolved-dependency source" item. +`Pipfile.lock` and pinned `requirements.txt` fixtures land in their own +follow-up changes, alongside each format's own extractor -- see +`working-docs/design/roadmap.md`'s "Remaining lock formats as a +resolved-dependency source" item. diff --git a/tests/fixtures/real-world-locks/pdm/pdm-2.29.0/LICENSE b/tests/fixtures/real-world-locks/pdm/pdm-2.29.0/LICENSE new file mode 100644 index 00000000..e4e31fb3 --- /dev/null +++ b/tests/fixtures/real-world-locks/pdm/pdm-2.29.0/LICENSE @@ -0,0 +1,21 @@ +MIT License + +Copyright (c) 2019-present Frost Ming + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all +copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +SOFTWARE. diff --git a/tests/fixtures/real-world-locks/pdm/pdm-2.29.0/pdm.lock b/tests/fixtures/real-world-locks/pdm/pdm-2.29.0/pdm.lock new file mode 100644 index 00000000..bd724acd --- /dev/null +++ b/tests/fixtures/real-world-locks/pdm/pdm-2.29.0/pdm.lock @@ -0,0 +1,1966 @@ +# This file is @generated by PDM. +# It is not intended for manual editing. + +[metadata] +groups = ["default", "all", "doc", "keyring", "pytest", "test", "tox", "workflow"] +strategy = ["inherit_metadata"] +lock_version = "4.5.1" +content_hash = "sha256:c3f6d458b99d53437990fe3a5789d402dba4b471df42276a1492a80baf130777" + +[[metadata.targets]] +requires_python = ">=3.10" + +[[package]] +name = "anyio" +version = "4.13.0" +requires_python = ">=3.10" +summary = "High-level concurrency and networking framework on top of asyncio or Trio" +groups = ["default", "test"] +dependencies = [ + "exceptiongroup>=1.0.2; python_version < \"3.11\"", + "idna>=2.8", + "typing-extensions>=4.5; python_version < \"3.13\"", +] +files = [ + {file = "anyio-4.13.0-py3-none-any.whl", hash = "sha256:08b310f9e24a9594186fd75b4f73f4a4152069e3853f1ed8bfbf58369f4ad708"}, + {file = "anyio-4.13.0.tar.gz", hash = "sha256:334b70e641fd2221c1505b3890c69882fe4a2df910cba14d97019b90b24439dc"}, +] + +[[package]] +name = "anysqlite" +version = "0.0.5" +requires_python = ">=3.8" +summary = "" +groups = ["default"] +dependencies = [ + "anyio>3.4.0", +] +files = [ + {file = "anysqlite-0.0.5-py3-none-any.whl", hash = "sha256:cb345dc4f76f6b37f768d7a0b3e9cf5c700dfcb7a6356af8ab46a11f666edbe7"}, + {file = "anysqlite-0.0.5.tar.gz", hash = "sha256:9dfcf87baf6b93426ad1d9118088c41dbf24ef01b445eea4a5d486bac2755cce"}, +] + +[[package]] +name = "argcomplete" +version = "3.7.0" +requires_python = ">=3.8" +summary = "Bash tab completion for argparse" +groups = ["default"] +files = [ + {file = "argcomplete-3.7.0-py3-none-any.whl", hash = "sha256:d8f0f22d2a8a7caa383be1e22b6caf1ecaf0ebd10d8f83cc125e36540c95830c"}, + {file = "argcomplete-3.7.0.tar.gz", hash = "sha256:afde224f753f874807b1dc1414e883ab8fe0cda9c04807b6047dcb8e1ac23913"}, +] + +[[package]] +name = "arpeggio" +version = "2.0.3" +summary = "Packrat parser interpreter" +groups = ["workflow"] +files = [ + {file = "Arpeggio-2.0.3-py2.py3-none-any.whl", hash = "sha256:9374d9c531b62018b787635f37fd81c9a6ee69ef2d28c5db3cd18791b1f7db2f"}, + {file = "Arpeggio-2.0.3.tar.gz", hash = "sha256:9e85ad35cfc6c938676817c7ae9a1000a7c72a34c71db0c687136c460d12b85e"}, +] + +[[package]] +name = "attrs" +version = "26.1.0" +requires_python = ">=3.9" +summary = "Classes Without Boilerplate" +groups = ["workflow"] +files = [ + {file = "attrs-26.1.0-py3-none-any.whl", hash = "sha256:c647aa4a12dfbad9333ca4e71fe62ddc36f4e63b2d260a37a8b83d2f043ac309"}, + {file = "attrs-26.1.0.tar.gz", hash = "sha256:d03ceb89cb322a8fd706d4fb91940737b6642aa36998fe130a9bc96c985eff32"}, +] + +[[package]] +name = "backports-tarfile" +version = "1.2.0" +requires_python = ">=3.8" +summary = "Backport of CPython tarfile module" +groups = ["all", "keyring"] +marker = "python_version < \"3.12\"" +files = [ + {file = "backports.tarfile-1.2.0-py3-none-any.whl", hash = "sha256:77e284d754527b01fb1e6fa8a1afe577858ebe4e9dad8919e34c862cb399bc34"}, + {file = "backports_tarfile-1.2.0.tar.gz", hash = "sha256:d75e02c268746e1b8144c278978b6e98e85de6ad16f8e4b0844a154557eca991"}, +] + +[[package]] +name = "blinker" +version = "1.9.0" +requires_python = ">=3.9" +summary = "Fast, simple object-to-object and broadcast signaling" +groups = ["default"] +files = [ + {file = "blinker-1.9.0-py3-none-any.whl", hash = "sha256:ba0efaa9080b619ff2f3459d1d500c57bddea4a6b424b60a91141db6fd2f08bc"}, + {file = "blinker-1.9.0.tar.gz", hash = "sha256:b4ce2265a7abece45e7cc896e98dbebe6cead56bcf805a3d23136d145f5445bf"}, +] + +[[package]] +name = "cachetools" +version = "7.1.1" +requires_python = ">=3.10" +summary = "Extensible memoizing collections and decorators" +groups = ["tox"] +files = [ + {file = "cachetools-7.1.1-py3-none-any.whl", hash = "sha256:0335cd7a0952d2b22327441fb0628139e234c565559eeb91a8a4ac7551c5353d"}, + {file = "cachetools-7.1.1.tar.gz", hash = "sha256:27bdf856d68fd3c71c26c01b5edc312124ed427524d1ddb31aa2b7746fe20d4b"}, +] + +[[package]] +name = "certifi" +version = "2026.4.22" +requires_python = ">=3.7" +summary = "Python package for providing Mozilla's CA Bundle." +groups = ["default", "test"] +files = [ + {file = "certifi-2026.4.22-py3-none-any.whl", hash = "sha256:3cb2210c8f88ba2318d29b0388d1023c8492ff72ecdde4ebdaddbb13a31b1c4a"}, + {file = "certifi-2026.4.22.tar.gz", hash = "sha256:8d455352a37b71bf76a79caa83a3d6c25afee4a385d632127b6afb3963f1c580"}, +] + +[[package]] +name = "cffi" +version = "2.0.0" +requires_python = ">=3.9" +summary = "Foreign Function Interface for Python calling C code." +groups = ["all", "keyring"] +marker = "platform_python_implementation != \"PyPy\" and sys_platform == \"linux\"" +dependencies = [ + "pycparser; implementation_name != \"PyPy\"", +] +files = [ + {file = "cffi-2.0.0-cp310-cp310-macosx_10_13_x86_64.whl", hash = "sha256:0cf2d91ecc3fcc0625c2c530fe004f82c110405f101548512cce44322fa8ac44"}, + {file = "cffi-2.0.0-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:f73b96c41e3b2adedc34a7356e64c8eb96e03a3782b535e043a986276ce12a49"}, + {file = "cffi-2.0.0-cp310-cp310-manylinux1_i686.manylinux2014_i686.manylinux_2_17_i686.manylinux_2_5_i686.whl", hash = "sha256:53f77cbe57044e88bbd5ed26ac1d0514d2acf0591dd6bb02a3ae37f76811b80c"}, + {file = "cffi-2.0.0-cp310-cp310-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:3e837e369566884707ddaf85fc1744b47575005c0a229de3327f8f9a20f4efeb"}, + {file = "cffi-2.0.0-cp310-cp310-manylinux2014_ppc64le.manylinux_2_17_ppc64le.whl", hash = "sha256:5eda85d6d1879e692d546a078b44251cdd08dd1cfb98dfb77b670c97cee49ea0"}, + {file = "cffi-2.0.0-cp310-cp310-manylinux2014_s390x.manylinux_2_17_s390x.whl", hash = "sha256:9332088d75dc3241c702d852d4671613136d90fa6881da7d770a483fd05248b4"}, + {file = "cffi-2.0.0-cp310-cp310-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:fc7de24befaeae77ba923797c7c87834c73648a05a4bde34b3b7e5588973a453"}, + {file = "cffi-2.0.0-cp310-cp310-musllinux_1_2_aarch64.whl", hash = "sha256:cf364028c016c03078a23b503f02058f1814320a56ad535686f90565636a9495"}, + {file = "cffi-2.0.0-cp310-cp310-musllinux_1_2_i686.whl", hash = "sha256:e11e82b744887154b182fd3e7e8512418446501191994dbf9c9fc1f32cc8efd5"}, + {file = "cffi-2.0.0-cp310-cp310-musllinux_1_2_x86_64.whl", hash = "sha256:8ea985900c5c95ce9db1745f7933eeef5d314f0565b27625d9a10ec9881e1bfb"}, + {file = "cffi-2.0.0-cp310-cp310-win32.whl", hash = "sha256:1f72fb8906754ac8a2cc3f9f5aaa298070652a0ffae577e0ea9bd480dc3c931a"}, + {file = "cffi-2.0.0-cp310-cp310-win_amd64.whl", hash = "sha256:b18a3ed7d5b3bd8d9ef7a8cb226502c6bf8308df1525e1cc676c3680e7176739"}, + {file = "cffi-2.0.0-cp311-cp311-macosx_10_13_x86_64.whl", hash = "sha256:b4c854ef3adc177950a8dfc81a86f5115d2abd545751a304c5bcf2c2c7283cfe"}, + {file = "cffi-2.0.0-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:2de9a304e27f7596cd03d16f1b7c72219bd944e99cc52b84d0145aefb07cbd3c"}, + {file = "cffi-2.0.0-cp311-cp311-manylinux1_i686.manylinux2014_i686.manylinux_2_17_i686.manylinux_2_5_i686.whl", hash = "sha256:baf5215e0ab74c16e2dd324e8ec067ef59e41125d3eade2b863d294fd5035c92"}, + {file = "cffi-2.0.0-cp311-cp311-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:730cacb21e1bdff3ce90babf007d0a0917cc3e6492f336c2f0134101e0944f93"}, + {file = "cffi-2.0.0-cp311-cp311-manylinux2014_ppc64le.manylinux_2_17_ppc64le.whl", hash = "sha256:6824f87845e3396029f3820c206e459ccc91760e8fa24422f8b0c3d1731cbec5"}, + {file = "cffi-2.0.0-cp311-cp311-manylinux2014_s390x.manylinux_2_17_s390x.whl", hash = "sha256:9de40a7b0323d889cf8d23d1ef214f565ab154443c42737dfe52ff82cf857664"}, + {file = "cffi-2.0.0-cp311-cp311-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:8941aaadaf67246224cee8c3803777eed332a19d909b47e29c9842ef1e79ac26"}, + {file = "cffi-2.0.0-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:a05d0c237b3349096d3981b727493e22147f934b20f6f125a3eba8f994bec4a9"}, + {file = "cffi-2.0.0-cp311-cp311-musllinux_1_2_i686.whl", hash = "sha256:94698a9c5f91f9d138526b48fe26a199609544591f859c870d477351dc7b2414"}, + {file = "cffi-2.0.0-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:5fed36fccc0612a53f1d4d9a816b50a36702c28a2aa880cb8a122b3466638743"}, + {file = "cffi-2.0.0-cp311-cp311-win32.whl", hash = "sha256:c649e3a33450ec82378822b3dad03cc228b8f5963c0c12fc3b1e0ab940f768a5"}, + {file = "cffi-2.0.0-cp311-cp311-win_amd64.whl", hash = "sha256:66f011380d0e49ed280c789fbd08ff0d40968ee7b665575489afa95c98196ab5"}, + {file = "cffi-2.0.0-cp311-cp311-win_arm64.whl", hash = "sha256:c6638687455baf640e37344fe26d37c404db8b80d037c3d29f58fe8d1c3b194d"}, + {file = "cffi-2.0.0-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:6d02d6655b0e54f54c4ef0b94eb6be0607b70853c45ce98bd278dc7de718be5d"}, + {file = "cffi-2.0.0-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:8eca2a813c1cb7ad4fb74d368c2ffbbb4789d377ee5bb8df98373c2cc0dee76c"}, + {file = "cffi-2.0.0-cp312-cp312-manylinux1_i686.manylinux2014_i686.manylinux_2_17_i686.manylinux_2_5_i686.whl", hash = "sha256:21d1152871b019407d8ac3985f6775c079416c282e431a4da6afe7aefd2bccbe"}, + {file = "cffi-2.0.0-cp312-cp312-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:b21e08af67b8a103c71a250401c78d5e0893beff75e28c53c98f4de42f774062"}, + {file = "cffi-2.0.0-cp312-cp312-manylinux2014_ppc64le.manylinux_2_17_ppc64le.whl", hash = "sha256:1e3a615586f05fc4065a8b22b8152f0c1b00cdbc60596d187c2a74f9e3036e4e"}, + {file = "cffi-2.0.0-cp312-cp312-manylinux2014_s390x.manylinux_2_17_s390x.whl", hash = "sha256:81afed14892743bbe14dacb9e36d9e0e504cd204e0b165062c488942b9718037"}, + {file = "cffi-2.0.0-cp312-cp312-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:3e17ed538242334bf70832644a32a7aae3d83b57567f9fd60a26257e992b79ba"}, + {file = "cffi-2.0.0-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:3925dd22fa2b7699ed2617149842d2e6adde22b262fcbfada50e3d195e4b3a94"}, + {file = "cffi-2.0.0-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:2c8f814d84194c9ea681642fd164267891702542f028a15fc97d4674b6206187"}, + {file = "cffi-2.0.0-cp312-cp312-win32.whl", hash = "sha256:da902562c3e9c550df360bfa53c035b2f241fed6d9aef119048073680ace4a18"}, + {file = "cffi-2.0.0-cp312-cp312-win_amd64.whl", hash = "sha256:da68248800ad6320861f129cd9c1bf96ca849a2771a59e0344e88681905916f5"}, + {file = "cffi-2.0.0-cp312-cp312-win_arm64.whl", hash = "sha256:4671d9dd5ec934cb9a73e7ee9676f9362aba54f7f34910956b84d727b0d73fb6"}, + {file = "cffi-2.0.0-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:00bdf7acc5f795150faa6957054fbbca2439db2f775ce831222b66f192f03beb"}, + {file = "cffi-2.0.0-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:45d5e886156860dc35862657e1494b9bae8dfa63bf56796f2fb56e1679fc0bca"}, + {file = "cffi-2.0.0-cp313-cp313-manylinux1_i686.manylinux2014_i686.manylinux_2_17_i686.manylinux_2_5_i686.whl", hash = "sha256:07b271772c100085dd28b74fa0cd81c8fb1a3ba18b21e03d7c27f3436a10606b"}, + {file = "cffi-2.0.0-cp313-cp313-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:d48a880098c96020b02d5a1f7d9251308510ce8858940e6fa99ece33f610838b"}, + {file = "cffi-2.0.0-cp313-cp313-manylinux2014_ppc64le.manylinux_2_17_ppc64le.whl", hash = "sha256:f93fd8e5c8c0a4aa1f424d6173f14a892044054871c771f8566e4008eaa359d2"}, + {file = "cffi-2.0.0-cp313-cp313-manylinux2014_s390x.manylinux_2_17_s390x.whl", hash = "sha256:dd4f05f54a52fb558f1ba9f528228066954fee3ebe629fc1660d874d040ae5a3"}, + {file = "cffi-2.0.0-cp313-cp313-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:c8d3b5532fc71b7a77c09192b4a5a200ea992702734a2e9279a37f2478236f26"}, + {file = "cffi-2.0.0-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:d9b29c1f0ae438d5ee9acb31cadee00a58c46cc9c0b2f9038c6b0b3470877a8c"}, + {file = "cffi-2.0.0-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:6d50360be4546678fc1b79ffe7a66265e28667840010348dd69a314145807a1b"}, + {file = "cffi-2.0.0-cp313-cp313-win32.whl", hash = "sha256:74a03b9698e198d47562765773b4a8309919089150a0bb17d829ad7b44b60d27"}, + {file = "cffi-2.0.0-cp313-cp313-win_amd64.whl", hash = "sha256:19f705ada2530c1167abacb171925dd886168931e0a7b78f5bffcae5c6b5be75"}, + {file = "cffi-2.0.0-cp313-cp313-win_arm64.whl", hash = "sha256:256f80b80ca3853f90c21b23ee78cd008713787b1b1e93eae9f3d6a7134abd91"}, + {file = "cffi-2.0.0-cp314-cp314-macosx_10_13_x86_64.whl", hash = "sha256:fc33c5141b55ed366cfaad382df24fe7dcbc686de5be719b207bb248e3053dc5"}, + {file = "cffi-2.0.0-cp314-cp314-macosx_11_0_arm64.whl", hash = "sha256:c654de545946e0db659b3400168c9ad31b5d29593291482c43e3564effbcee13"}, + {file = "cffi-2.0.0-cp314-cp314-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:24b6f81f1983e6df8db3adc38562c83f7d4a0c36162885ec7f7b77c7dcbec97b"}, + {file = "cffi-2.0.0-cp314-cp314-manylinux2014_ppc64le.manylinux_2_17_ppc64le.whl", hash = "sha256:12873ca6cb9b0f0d3a0da705d6086fe911591737a59f28b7936bdfed27c0d47c"}, + {file = "cffi-2.0.0-cp314-cp314-manylinux2014_s390x.manylinux_2_17_s390x.whl", hash = "sha256:d9b97165e8aed9272a6bb17c01e3cc5871a594a446ebedc996e2397a1c1ea8ef"}, + {file = "cffi-2.0.0-cp314-cp314-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:afb8db5439b81cf9c9d0c80404b60c3cc9c3add93e114dcae767f1477cb53775"}, + {file = "cffi-2.0.0-cp314-cp314-musllinux_1_2_aarch64.whl", hash = "sha256:737fe7d37e1a1bffe70bd5754ea763a62a066dc5913ca57e957824b72a85e205"}, + {file = "cffi-2.0.0-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:38100abb9d1b1435bc4cc340bb4489635dc2f0da7456590877030c9b3d40b0c1"}, + {file = "cffi-2.0.0-cp314-cp314-win32.whl", hash = "sha256:087067fa8953339c723661eda6b54bc98c5625757ea62e95eb4898ad5e776e9f"}, + {file = "cffi-2.0.0-cp314-cp314-win_amd64.whl", hash = "sha256:203a48d1fb583fc7d78a4c6655692963b860a417c0528492a6bc21f1aaefab25"}, + {file = "cffi-2.0.0-cp314-cp314-win_arm64.whl", hash = "sha256:dbd5c7a25a7cb98f5ca55d258b103a2054f859a46ae11aaf23134f9cc0d356ad"}, + {file = "cffi-2.0.0-cp314-cp314t-macosx_10_13_x86_64.whl", hash = "sha256:9a67fc9e8eb39039280526379fb3a70023d77caec1852002b4da7e8b270c4dd9"}, + {file = "cffi-2.0.0-cp314-cp314t-macosx_11_0_arm64.whl", hash = "sha256:7a66c7204d8869299919db4d5069a82f1561581af12b11b3c9f48c584eb8743d"}, + {file = "cffi-2.0.0-cp314-cp314t-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:7cc09976e8b56f8cebd752f7113ad07752461f48a58cbba644139015ac24954c"}, + {file = "cffi-2.0.0-cp314-cp314t-manylinux2014_ppc64le.manylinux_2_17_ppc64le.whl", hash = "sha256:92b68146a71df78564e4ef48af17551a5ddd142e5190cdf2c5624d0c3ff5b2e8"}, + {file = "cffi-2.0.0-cp314-cp314t-manylinux2014_s390x.manylinux_2_17_s390x.whl", hash = "sha256:b1e74d11748e7e98e2f426ab176d4ed720a64412b6a15054378afdb71e0f37dc"}, + {file = "cffi-2.0.0-cp314-cp314t-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:28a3a209b96630bca57cce802da70c266eb08c6e97e5afd61a75611ee6c64592"}, + {file = "cffi-2.0.0-cp314-cp314t-musllinux_1_2_aarch64.whl", hash = "sha256:7553fb2090d71822f02c629afe6042c299edf91ba1bf94951165613553984512"}, + {file = "cffi-2.0.0-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:6c6c373cfc5c83a975506110d17457138c8c63016b563cc9ed6e056a82f13ce4"}, + {file = "cffi-2.0.0-cp314-cp314t-win32.whl", hash = "sha256:1fc9ea04857caf665289b7a75923f2c6ed559b8298a1b8c49e59f7dd95c8481e"}, + {file = "cffi-2.0.0-cp314-cp314t-win_amd64.whl", hash = "sha256:d68b6cef7827e8641e8ef16f4494edda8b36104d79773a334beaa1e3521430f6"}, + {file = "cffi-2.0.0-cp314-cp314t-win_arm64.whl", hash = "sha256:0a1527a803f0a659de1af2e1fd700213caba79377e27e4693648c2923da066f9"}, + {file = "cffi-2.0.0.tar.gz", hash = "sha256:44d1b5909021139fe36001ae048dbdde8214afa20200eda0f64c068cac5d5529"}, +] + +[[package]] +name = "click" +version = "8.3.3" +requires_python = ">=3.10" +summary = "Composable command line interface toolkit" +groups = ["doc", "workflow"] +dependencies = [ + "colorama; platform_system == \"Windows\"", +] +files = [ + {file = "click-8.3.3-py3-none-any.whl", hash = "sha256:a2bf429bb3033c89fa4936ffb35d5cb471e3719e1f3c8a7c3fff0b8314305613"}, + {file = "click-8.3.3.tar.gz", hash = "sha256:398329ad4837b2ff7cbe1dd166a4c0f8900c3ca3a218de04466f38f6497f18a2"}, +] + +[[package]] +name = "colorama" +version = "0.4.6" +requires_python = "!=3.0.*,!=3.1.*,!=3.2.*,!=3.3.*,!=3.4.*,!=3.5.*,!=3.6.*,>=2.7" +summary = "Cross-platform colored terminal text." +groups = ["doc", "pytest", "test", "tox", "workflow"] +files = [ + {file = "colorama-0.4.6-py2.py3-none-any.whl", hash = "sha256:4f1d9991f5acc0ca119f9d443620b77f9d6b33703e51011c16baf57afb285fc6"}, + {file = "colorama-0.4.6.tar.gz", hash = "sha256:08695f5cb7ed6e0531a20572697297273c47b8cae5a63ffc6d6ed5c201be6e44"}, +] + +[[package]] +name = "coverage" +version = "7.13.5" +requires_python = ">=3.10" +summary = "Code coverage measurement for Python" +groups = ["test"] +files = [ + {file = "coverage-7.13.5-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:e0723d2c96324561b9aa76fb982406e11d93cdb388a7a7da2b16e04719cf7ca5"}, + {file = "coverage-7.13.5-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:52f444e86475992506b32d4e5ca55c24fc88d73bcbda0e9745095b28ef4dc0cf"}, + {file = "coverage-7.13.5-cp310-cp310-manylinux1_i686.manylinux_2_28_i686.manylinux_2_5_i686.whl", hash = "sha256:704de6328e3d612a8f6c07000a878ff38181ec3263d5a11da1db294fa6a9bdf8"}, + {file = "coverage-7.13.5-cp310-cp310-manylinux1_x86_64.manylinux_2_28_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:a1a6d79a14e1ec1832cabc833898636ad5f3754a678ef8bb4908515208bf84f4"}, + {file = "coverage-7.13.5-cp310-cp310-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:79060214983769c7ba3f0cee10b54c97609dca4d478fa1aa32b914480fd5738d"}, + {file = "coverage-7.13.5-cp310-cp310-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:356e76b46783a98c2a2fe81ec79df4883a1e62895ea952968fb253c114e7f930"}, + {file = "coverage-7.13.5-cp310-cp310-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:0cef0cdec915d11254a7f549c1170afecce708d30610c6abdded1f74e581666d"}, + {file = "coverage-7.13.5-cp310-cp310-musllinux_1_2_aarch64.whl", hash = "sha256:dc022073d063b25a402454e5712ef9e007113e3a676b96c5f29b2bda29352f40"}, + {file = "coverage-7.13.5-cp310-cp310-musllinux_1_2_i686.whl", hash = "sha256:9b74db26dfea4f4e50d48a4602207cd1e78be33182bc9cbf22da94f332f99878"}, + {file = "coverage-7.13.5-cp310-cp310-musllinux_1_2_ppc64le.whl", hash = "sha256:ad146744ca4fd09b50c482650e3c1b1f4dfa1d4792e0a04a369c7f23336f0400"}, + {file = "coverage-7.13.5-cp310-cp310-musllinux_1_2_riscv64.whl", hash = "sha256:c555b48be1853fe3997c11c4bd521cdd9a9612352de01fa4508f16ec341e6fe0"}, + {file = "coverage-7.13.5-cp310-cp310-musllinux_1_2_x86_64.whl", hash = "sha256:7034b5c56a58ae5e85f23949d52c14aca2cfc6848a31764995b7de88f13a1ea0"}, + {file = "coverage-7.13.5-cp310-cp310-win32.whl", hash = "sha256:eb7fdf1ef130660e7415e0253a01a7d5a88c9c4d158bcf75cbbd922fd65a5b58"}, + {file = "coverage-7.13.5-cp310-cp310-win_amd64.whl", hash = "sha256:3e1bb5f6c78feeb1be3475789b14a0f0a5b47d505bfc7267126ccbd50289999e"}, + {file = "coverage-7.13.5-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:66a80c616f80181f4d643b0f9e709d97bcea413ecd9631e1dedc7401c8e6695d"}, + {file = "coverage-7.13.5-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:145ede53ccbafb297c1c9287f788d1bc3efd6c900da23bf6931b09eafc931587"}, + {file = "coverage-7.13.5-cp311-cp311-manylinux1_i686.manylinux_2_28_i686.manylinux_2_5_i686.whl", hash = "sha256:0672854dc733c342fa3e957e0605256d2bf5934feeac328da9e0b5449634a642"}, + {file = "coverage-7.13.5-cp311-cp311-manylinux1_x86_64.manylinux_2_28_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:ec10e2a42b41c923c2209b846126c6582db5e43a33157e9870ba9fb70dc7854b"}, + {file = "coverage-7.13.5-cp311-cp311-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:be3d4bbad9d4b037791794ddeedd7d64a56f5933a2c1373e18e9e568b9141686"}, + {file = "coverage-7.13.5-cp311-cp311-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:4d2afbc5cc54d286bfb54541aa50b64cdb07a718227168c87b9e2fb8f25e1743"}, + {file = "coverage-7.13.5-cp311-cp311-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:3ad050321264c49c2fa67bb599100456fc51d004b82534f379d16445da40fb75"}, + {file = "coverage-7.13.5-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:7300c8a6d13335b29bb76d7651c66af6bd8658517c43499f110ddc6717bfc209"}, + {file = "coverage-7.13.5-cp311-cp311-musllinux_1_2_i686.whl", hash = "sha256:eb07647a5738b89baab047f14edd18ded523de60f3b30e75c2acc826f79c839a"}, + {file = "coverage-7.13.5-cp311-cp311-musllinux_1_2_ppc64le.whl", hash = "sha256:9adb6688e3b53adffefd4a52d72cbd8b02602bfb8f74dcd862337182fd4d1a4e"}, + {file = "coverage-7.13.5-cp311-cp311-musllinux_1_2_riscv64.whl", hash = "sha256:7c8d4bc913dd70b93488d6c496c77f3aff5ea99a07e36a18f865bca55adef8bd"}, + {file = "coverage-7.13.5-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:0e3c426ffc4cd952f54ee9ffbdd10345709ecc78a3ecfd796a57236bfad0b9b8"}, + {file = "coverage-7.13.5-cp311-cp311-win32.whl", hash = "sha256:259b69bb83ad9894c4b25be2528139eecba9a82646ebdda2d9db1ba28424a6bf"}, + {file = "coverage-7.13.5-cp311-cp311-win_amd64.whl", hash = "sha256:258354455f4e86e3e9d0d17571d522e13b4e1e19bf0f8596bcf9476d61e7d8a9"}, + {file = "coverage-7.13.5-cp311-cp311-win_arm64.whl", hash = "sha256:bff95879c33ec8da99fc9b6fe345ddb5be6414b41d6d1ad1c8f188d26f36e028"}, + {file = "coverage-7.13.5-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:460cf0114c5016fa841214ff5564aa4864f11948da9440bc97e21ad1f4ba1e01"}, + {file = "coverage-7.13.5-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:0e223ce4b4ed47f065bfb123687686512e37629be25cc63728557ae7db261422"}, + {file = "coverage-7.13.5-cp312-cp312-manylinux1_i686.manylinux_2_28_i686.manylinux_2_5_i686.whl", hash = "sha256:6e3370441f4513c6252bf042b9c36d22491142385049243253c7e48398a15a9f"}, + {file = "coverage-7.13.5-cp312-cp312-manylinux1_x86_64.manylinux_2_28_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:03ccc709a17a1de074fb1d11f217342fb0d2b1582ed544f554fc9fc3f07e95f5"}, + {file = "coverage-7.13.5-cp312-cp312-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:3f4818d065964db3c1c66dc0fbdac5ac692ecbc875555e13374fdbe7eedb4376"}, + {file = "coverage-7.13.5-cp312-cp312-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:012d5319e66e9d5a218834642d6c35d265515a62f01157a45bcc036ecf947256"}, + {file = "coverage-7.13.5-cp312-cp312-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:8dd02af98971bdb956363e4827d34425cb3df19ee550ef92855b0acb9c7ce51c"}, + {file = "coverage-7.13.5-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:f08fd75c50a760c7eb068ae823777268daaf16a80b918fa58eea888f8e3919f5"}, + {file = "coverage-7.13.5-cp312-cp312-musllinux_1_2_i686.whl", hash = "sha256:843ea8643cf967d1ac7e8ecd4bb00c99135adf4816c0c0593fdcc47b597fcf09"}, + {file = "coverage-7.13.5-cp312-cp312-musllinux_1_2_ppc64le.whl", hash = "sha256:9d44d7aa963820b1b971dbecd90bfe5fe8f81cff79787eb6cca15750bd2f79b9"}, + {file = "coverage-7.13.5-cp312-cp312-musllinux_1_2_riscv64.whl", hash = "sha256:7132bed4bd7b836200c591410ae7d97bf7ae8be6fc87d160b2bd881df929e7bf"}, + {file = "coverage-7.13.5-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:a698e363641b98843c517817db75373c83254781426e94ada3197cabbc2c919c"}, + {file = "coverage-7.13.5-cp312-cp312-win32.whl", hash = "sha256:bdba0a6b8812e8c7df002d908a9a2ea3c36e92611b5708633c50869e6d922fdf"}, + {file = "coverage-7.13.5-cp312-cp312-win_amd64.whl", hash = "sha256:d2c87e0c473a10bffe991502eac389220533024c8082ec1ce849f4218dded810"}, + {file = "coverage-7.13.5-cp312-cp312-win_arm64.whl", hash = "sha256:bf69236a9a81bdca3bff53796237aab096cdbf8d78a66ad61e992d9dac7eb2de"}, + {file = "coverage-7.13.5-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:5ec4af212df513e399cf11610cc27063f1586419e814755ab362e50a85ea69c1"}, + {file = "coverage-7.13.5-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:941617e518602e2d64942c88ec8499f7fbd49d3f6c4327d3a71d43a1973032f3"}, + {file = "coverage-7.13.5-cp313-cp313-manylinux1_i686.manylinux_2_28_i686.manylinux_2_5_i686.whl", hash = "sha256:da305e9937617ee95c2e39d8ff9f040e0487cbf1ac174f777ed5eddd7a7c1f26"}, + {file = "coverage-7.13.5-cp313-cp313-manylinux1_x86_64.manylinux_2_28_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:78e696e1cc714e57e8b25760b33a8b1026b7048d270140d25dafe1b0a1ee05a3"}, + {file = "coverage-7.13.5-cp313-cp313-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:02ca0eed225b2ff301c474aeeeae27d26e2537942aa0f87491d3e147e784a82b"}, + {file = "coverage-7.13.5-cp313-cp313-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:04690832cbea4e4663d9149e05dba142546ca05cb1848816760e7f58285c970a"}, + {file = "coverage-7.13.5-cp313-cp313-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:0590e44dd2745c696a778f7bab6aa95256de2cbc8b8cff4f7db8ff09813d6969"}, + {file = "coverage-7.13.5-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:d7cfad2d6d81dd298ab6b89fe72c3b7b05ec7544bdda3b707ddaecff8d25c161"}, + {file = "coverage-7.13.5-cp313-cp313-musllinux_1_2_i686.whl", hash = "sha256:e092b9499de38ae0fbfbc603a74660eb6ff3e869e507b50d85a13b6db9863e15"}, + {file = "coverage-7.13.5-cp313-cp313-musllinux_1_2_ppc64le.whl", hash = "sha256:48c39bc4a04d983a54a705a6389512883d4a3b9862991b3617d547940e9f52b1"}, + {file = "coverage-7.13.5-cp313-cp313-musllinux_1_2_riscv64.whl", hash = "sha256:2d3807015f138ffea1ed9afeeb8624fd781703f2858b62a8dd8da5a0994c57b6"}, + {file = "coverage-7.13.5-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:ee2aa19e03161671ec964004fb74b2257805d9710bf14a5c704558b9d8dbaf17"}, + {file = "coverage-7.13.5-cp313-cp313-win32.whl", hash = "sha256:ce1998c0483007608c8382f4ff50164bfc5bd07a2246dd272aa4043b75e61e85"}, + {file = "coverage-7.13.5-cp313-cp313-win_amd64.whl", hash = "sha256:631efb83f01569670a5e866ceb80fe483e7c159fac6f167e6571522636104a0b"}, + {file = "coverage-7.13.5-cp313-cp313-win_arm64.whl", hash = "sha256:f4cd16206ad171cbc2470dbea9103cf9a7607d5fe8c242fdf1edf36174020664"}, + {file = "coverage-7.13.5-cp313-cp313t-macosx_10_13_x86_64.whl", hash = "sha256:0428cbef5783ad91fe240f673cc1f76b25e74bbfe1a13115e4aa30d3f538162d"}, + {file = "coverage-7.13.5-cp313-cp313t-macosx_11_0_arm64.whl", hash = "sha256:e0b216a19534b2427cc201a26c25da4a48633f29a487c61258643e89d28200c0"}, + {file = "coverage-7.13.5-cp313-cp313t-manylinux1_i686.manylinux_2_28_i686.manylinux_2_5_i686.whl", hash = "sha256:972a9cd27894afe4bc2b1480107054e062df08e671df7c2f18c205e805ccd806"}, + {file = "coverage-7.13.5-cp313-cp313t-manylinux1_x86_64.manylinux_2_28_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:4b59148601efcd2bac8c4dbf1f0ad6391693ccf7a74b8205781751637076aee3"}, + {file = "coverage-7.13.5-cp313-cp313t-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:505d7083c8b0c87a8fa8c07370c285847c1f77739b22e299ad75a6af6c32c5c9"}, + {file = "coverage-7.13.5-cp313-cp313t-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:60365289c3741e4db327e7baff2a4aaacf22f788e80fa4683393891b70a89fbd"}, + {file = "coverage-7.13.5-cp313-cp313t-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:1b88c69c8ef5d4b6fe7dea66d6636056a0f6a7527c440e890cf9259011f5e606"}, + {file = "coverage-7.13.5-cp313-cp313t-musllinux_1_2_aarch64.whl", hash = "sha256:5b13955d31d1633cf9376908089b7cebe7d15ddad7aeaabcbe969a595a97e95e"}, + {file = "coverage-7.13.5-cp313-cp313t-musllinux_1_2_i686.whl", hash = "sha256:f70c9ab2595c56f81a89620e22899eea8b212a4041bd728ac6f4a28bf5d3ddd0"}, + {file = "coverage-7.13.5-cp313-cp313t-musllinux_1_2_ppc64le.whl", hash = "sha256:084b84a8c63e8d6fc7e3931b316a9bcafca1458d753c539db82d31ed20091a87"}, + {file = "coverage-7.13.5-cp313-cp313t-musllinux_1_2_riscv64.whl", hash = "sha256:ad14385487393e386e2ea988b09d62dd42c397662ac2dabc3832d71253eee479"}, + {file = "coverage-7.13.5-cp313-cp313t-musllinux_1_2_x86_64.whl", hash = "sha256:7f2c47b36fe7709a6e83bfadf4eefb90bd25fbe4014d715224c4316f808e59a2"}, + {file = "coverage-7.13.5-cp313-cp313t-win32.whl", hash = "sha256:67e9bc5449801fad0e5dff329499fb090ba4c5800b86805c80617b4e29809b2a"}, + {file = "coverage-7.13.5-cp313-cp313t-win_amd64.whl", hash = "sha256:da86cdcf10d2519e10cabb8ac2de03da1bcb6e4853790b7fbd48523332e3a819"}, + {file = "coverage-7.13.5-cp313-cp313t-win_arm64.whl", hash = "sha256:0ecf12ecb326fe2c339d93fc131816f3a7367d223db37817208905c89bded911"}, + {file = "coverage-7.13.5-cp314-cp314-macosx_10_15_x86_64.whl", hash = "sha256:fbabfaceaeb587e16f7008f7795cd80d20ec548dc7f94fbb0d4ec2e038ce563f"}, + {file = "coverage-7.13.5-cp314-cp314-macosx_11_0_arm64.whl", hash = "sha256:9bb2a28101a443669a423b665939381084412b81c3f8c0fcfbac57f4e30b5b8e"}, + {file = "coverage-7.13.5-cp314-cp314-manylinux1_i686.manylinux_2_28_i686.manylinux_2_5_i686.whl", hash = "sha256:bd3a2fbc1c6cccb3c5106140d87cc6a8715110373ef42b63cf5aea29df8c217a"}, + {file = "coverage-7.13.5-cp314-cp314-manylinux1_x86_64.manylinux_2_28_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:6c36ddb64ed9d7e496028d1d00dfec3e428e0aabf4006583bb1839958d280510"}, + {file = "coverage-7.13.5-cp314-cp314-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:380e8e9084d8eb38db3a9176a1a4f3c0082c3806fa0dc882d1d87abc3c789247"}, + {file = "coverage-7.13.5-cp314-cp314-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:e808af52a0513762df4d945ea164a24b37f2f518cbe97e03deaa0ee66139b4d6"}, + {file = "coverage-7.13.5-cp314-cp314-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:e301d30dd7e95ae068671d746ba8c34e945a82682e62918e41b2679acd2051a0"}, + {file = "coverage-7.13.5-cp314-cp314-musllinux_1_2_aarch64.whl", hash = "sha256:800bc829053c80d240a687ceeb927a94fd108bbdc68dfbe505d0d75ab578a882"}, + {file = "coverage-7.13.5-cp314-cp314-musllinux_1_2_i686.whl", hash = "sha256:0b67af5492adb31940ee418a5a655c28e48165da5afab8c7fa6fd72a142f8740"}, + {file = "coverage-7.13.5-cp314-cp314-musllinux_1_2_ppc64le.whl", hash = "sha256:c9136ff29c3a91e25b1d1552b5308e53a1e0653a23e53b6366d7c2dcbbaf8a16"}, + {file = "coverage-7.13.5-cp314-cp314-musllinux_1_2_riscv64.whl", hash = "sha256:cff784eef7f0b8f6cb28804fbddcfa99f89efe4cc35fb5627e3ac58f91ed3ac0"}, + {file = "coverage-7.13.5-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:68a4953be99b17ac3c23b6efbc8a38330d99680c9458927491d18700ef23ded0"}, + {file = "coverage-7.13.5-cp314-cp314-win32.whl", hash = "sha256:35a31f2b1578185fbe6aa2e74cea1b1d0bbf4c552774247d9160d29b80ed56cc"}, + {file = "coverage-7.13.5-cp314-cp314-win_amd64.whl", hash = "sha256:2aa055ae1857258f9e0045be26a6d62bdb47a72448b62d7b55f4820f361a2633"}, + {file = "coverage-7.13.5-cp314-cp314-win_arm64.whl", hash = "sha256:1b11eef33edeae9d142f9b4358edb76273b3bfd30bc3df9a4f95d0e49caf94e8"}, + {file = "coverage-7.13.5-cp314-cp314t-macosx_10_15_x86_64.whl", hash = "sha256:10a0c37f0b646eaff7cce1874c31d1f1ccb297688d4c747291f4f4c70741cc8b"}, + {file = "coverage-7.13.5-cp314-cp314t-macosx_11_0_arm64.whl", hash = "sha256:b5db73ba3c41c7008037fa731ad5459fc3944cb7452fc0aa9f822ad3533c583c"}, + {file = "coverage-7.13.5-cp314-cp314t-manylinux1_i686.manylinux_2_28_i686.manylinux_2_5_i686.whl", hash = "sha256:750db93a81e3e5a9831b534be7b1229df848b2e125a604fe6651e48aa070e5f9"}, + {file = "coverage-7.13.5-cp314-cp314t-manylinux1_x86_64.manylinux_2_28_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:9ddb4f4a5479f2539644be484da179b653273bca1a323947d48ab107b3ed1f29"}, + {file = "coverage-7.13.5-cp314-cp314t-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:d8a7a2049c14f413163e2bdabd37e41179b1d1ccb10ffc6ccc4b7a718429c607"}, + {file = "coverage-7.13.5-cp314-cp314t-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:e1c85e0b6c05c592ea6d8768a66a254bfb3874b53774b12d4c89c481eb78cb90"}, + {file = "coverage-7.13.5-cp314-cp314t-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:777c4d1eff1b67876139d24288aaf1817f6c03d6bae9c5cc8d27b83bcfe38fe3"}, + {file = "coverage-7.13.5-cp314-cp314t-musllinux_1_2_aarch64.whl", hash = "sha256:6697e29b93707167687543480a40f0db8f356e86d9f67ddf2e37e2dfd91a9dab"}, + {file = "coverage-7.13.5-cp314-cp314t-musllinux_1_2_i686.whl", hash = "sha256:8fdf453a942c3e4d99bd80088141c4c6960bb232c409d9c3558e2dbaa3998562"}, + {file = "coverage-7.13.5-cp314-cp314t-musllinux_1_2_ppc64le.whl", hash = "sha256:32ca0c0114c9834a43f045a87dcebd69d108d8ffb666957ea65aa132f50332e2"}, + {file = "coverage-7.13.5-cp314-cp314t-musllinux_1_2_riscv64.whl", hash = "sha256:8769751c10f339021e2638cd354e13adeac54004d1941119b2c96fe5276d45ea"}, + {file = "coverage-7.13.5-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:cec2d83125531bd153175354055cdb7a09987af08a9430bd173c937c6d0fba2a"}, + {file = "coverage-7.13.5-cp314-cp314t-win32.whl", hash = "sha256:0cd9ed7a8b181775459296e402ca4fb27db1279740a24e93b3b41942ebe4b215"}, + {file = "coverage-7.13.5-cp314-cp314t-win_amd64.whl", hash = "sha256:301e3b7dfefecaca37c9f1aa6f0049b7d4ab8dd933742b607765d757aca77d43"}, + {file = "coverage-7.13.5-cp314-cp314t-win_arm64.whl", hash = "sha256:9dacc2ad679b292709e0f5fc1ac74a6d4d5562e424058962c7bb0c658ad25e45"}, + {file = "coverage-7.13.5-py3-none-any.whl", hash = "sha256:34b02417cf070e173989b3db962f7ed56d2f644307b2cf9d5a0f258e13084a61"}, + {file = "coverage-7.13.5.tar.gz", hash = "sha256:c81f6515c4c40141f83f502b07bbfa5c240ba25bbe73da7b33f1e5b6120ff179"}, +] + +[[package]] +name = "coverage" +version = "7.13.5" +extras = ["toml"] +requires_python = ">=3.10" +summary = "Code coverage measurement for Python" +groups = ["test"] +dependencies = [ + "coverage==7.13.5", + "tomli; python_full_version <= \"3.11.0a6\"", +] +files = [ + {file = "coverage-7.13.5-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:e0723d2c96324561b9aa76fb982406e11d93cdb388a7a7da2b16e04719cf7ca5"}, + {file = "coverage-7.13.5-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:52f444e86475992506b32d4e5ca55c24fc88d73bcbda0e9745095b28ef4dc0cf"}, + {file = "coverage-7.13.5-cp310-cp310-manylinux1_i686.manylinux_2_28_i686.manylinux_2_5_i686.whl", hash = "sha256:704de6328e3d612a8f6c07000a878ff38181ec3263d5a11da1db294fa6a9bdf8"}, + {file = "coverage-7.13.5-cp310-cp310-manylinux1_x86_64.manylinux_2_28_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:a1a6d79a14e1ec1832cabc833898636ad5f3754a678ef8bb4908515208bf84f4"}, + {file = "coverage-7.13.5-cp310-cp310-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:79060214983769c7ba3f0cee10b54c97609dca4d478fa1aa32b914480fd5738d"}, + {file = "coverage-7.13.5-cp310-cp310-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:356e76b46783a98c2a2fe81ec79df4883a1e62895ea952968fb253c114e7f930"}, + {file = "coverage-7.13.5-cp310-cp310-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:0cef0cdec915d11254a7f549c1170afecce708d30610c6abdded1f74e581666d"}, + {file = "coverage-7.13.5-cp310-cp310-musllinux_1_2_aarch64.whl", hash = "sha256:dc022073d063b25a402454e5712ef9e007113e3a676b96c5f29b2bda29352f40"}, + {file = "coverage-7.13.5-cp310-cp310-musllinux_1_2_i686.whl", hash = "sha256:9b74db26dfea4f4e50d48a4602207cd1e78be33182bc9cbf22da94f332f99878"}, + {file = "coverage-7.13.5-cp310-cp310-musllinux_1_2_ppc64le.whl", hash = "sha256:ad146744ca4fd09b50c482650e3c1b1f4dfa1d4792e0a04a369c7f23336f0400"}, + {file = "coverage-7.13.5-cp310-cp310-musllinux_1_2_riscv64.whl", hash = "sha256:c555b48be1853fe3997c11c4bd521cdd9a9612352de01fa4508f16ec341e6fe0"}, + {file = "coverage-7.13.5-cp310-cp310-musllinux_1_2_x86_64.whl", hash = "sha256:7034b5c56a58ae5e85f23949d52c14aca2cfc6848a31764995b7de88f13a1ea0"}, + {file = "coverage-7.13.5-cp310-cp310-win32.whl", hash = "sha256:eb7fdf1ef130660e7415e0253a01a7d5a88c9c4d158bcf75cbbd922fd65a5b58"}, + {file = "coverage-7.13.5-cp310-cp310-win_amd64.whl", hash = "sha256:3e1bb5f6c78feeb1be3475789b14a0f0a5b47d505bfc7267126ccbd50289999e"}, + {file = "coverage-7.13.5-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:66a80c616f80181f4d643b0f9e709d97bcea413ecd9631e1dedc7401c8e6695d"}, + {file = "coverage-7.13.5-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:145ede53ccbafb297c1c9287f788d1bc3efd6c900da23bf6931b09eafc931587"}, + {file = "coverage-7.13.5-cp311-cp311-manylinux1_i686.manylinux_2_28_i686.manylinux_2_5_i686.whl", hash = "sha256:0672854dc733c342fa3e957e0605256d2bf5934feeac328da9e0b5449634a642"}, + {file = "coverage-7.13.5-cp311-cp311-manylinux1_x86_64.manylinux_2_28_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:ec10e2a42b41c923c2209b846126c6582db5e43a33157e9870ba9fb70dc7854b"}, + {file = "coverage-7.13.5-cp311-cp311-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:be3d4bbad9d4b037791794ddeedd7d64a56f5933a2c1373e18e9e568b9141686"}, + {file = "coverage-7.13.5-cp311-cp311-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:4d2afbc5cc54d286bfb54541aa50b64cdb07a718227168c87b9e2fb8f25e1743"}, + {file = "coverage-7.13.5-cp311-cp311-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:3ad050321264c49c2fa67bb599100456fc51d004b82534f379d16445da40fb75"}, + {file = "coverage-7.13.5-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:7300c8a6d13335b29bb76d7651c66af6bd8658517c43499f110ddc6717bfc209"}, + {file = "coverage-7.13.5-cp311-cp311-musllinux_1_2_i686.whl", hash = "sha256:eb07647a5738b89baab047f14edd18ded523de60f3b30e75c2acc826f79c839a"}, + {file = "coverage-7.13.5-cp311-cp311-musllinux_1_2_ppc64le.whl", hash = "sha256:9adb6688e3b53adffefd4a52d72cbd8b02602bfb8f74dcd862337182fd4d1a4e"}, + {file = "coverage-7.13.5-cp311-cp311-musllinux_1_2_riscv64.whl", hash = "sha256:7c8d4bc913dd70b93488d6c496c77f3aff5ea99a07e36a18f865bca55adef8bd"}, + {file = "coverage-7.13.5-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:0e3c426ffc4cd952f54ee9ffbdd10345709ecc78a3ecfd796a57236bfad0b9b8"}, + {file = "coverage-7.13.5-cp311-cp311-win32.whl", hash = "sha256:259b69bb83ad9894c4b25be2528139eecba9a82646ebdda2d9db1ba28424a6bf"}, + {file = "coverage-7.13.5-cp311-cp311-win_amd64.whl", hash = "sha256:258354455f4e86e3e9d0d17571d522e13b4e1e19bf0f8596bcf9476d61e7d8a9"}, + {file = "coverage-7.13.5-cp311-cp311-win_arm64.whl", hash = "sha256:bff95879c33ec8da99fc9b6fe345ddb5be6414b41d6d1ad1c8f188d26f36e028"}, + {file = "coverage-7.13.5-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:460cf0114c5016fa841214ff5564aa4864f11948da9440bc97e21ad1f4ba1e01"}, + {file = "coverage-7.13.5-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:0e223ce4b4ed47f065bfb123687686512e37629be25cc63728557ae7db261422"}, + {file = "coverage-7.13.5-cp312-cp312-manylinux1_i686.manylinux_2_28_i686.manylinux_2_5_i686.whl", hash = "sha256:6e3370441f4513c6252bf042b9c36d22491142385049243253c7e48398a15a9f"}, + {file = "coverage-7.13.5-cp312-cp312-manylinux1_x86_64.manylinux_2_28_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:03ccc709a17a1de074fb1d11f217342fb0d2b1582ed544f554fc9fc3f07e95f5"}, + {file = "coverage-7.13.5-cp312-cp312-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:3f4818d065964db3c1c66dc0fbdac5ac692ecbc875555e13374fdbe7eedb4376"}, + {file = "coverage-7.13.5-cp312-cp312-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:012d5319e66e9d5a218834642d6c35d265515a62f01157a45bcc036ecf947256"}, + {file = "coverage-7.13.5-cp312-cp312-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:8dd02af98971bdb956363e4827d34425cb3df19ee550ef92855b0acb9c7ce51c"}, + {file = "coverage-7.13.5-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:f08fd75c50a760c7eb068ae823777268daaf16a80b918fa58eea888f8e3919f5"}, + {file = "coverage-7.13.5-cp312-cp312-musllinux_1_2_i686.whl", hash = "sha256:843ea8643cf967d1ac7e8ecd4bb00c99135adf4816c0c0593fdcc47b597fcf09"}, + {file = "coverage-7.13.5-cp312-cp312-musllinux_1_2_ppc64le.whl", hash = "sha256:9d44d7aa963820b1b971dbecd90bfe5fe8f81cff79787eb6cca15750bd2f79b9"}, + {file = "coverage-7.13.5-cp312-cp312-musllinux_1_2_riscv64.whl", hash = "sha256:7132bed4bd7b836200c591410ae7d97bf7ae8be6fc87d160b2bd881df929e7bf"}, + {file = "coverage-7.13.5-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:a698e363641b98843c517817db75373c83254781426e94ada3197cabbc2c919c"}, + {file = "coverage-7.13.5-cp312-cp312-win32.whl", hash = "sha256:bdba0a6b8812e8c7df002d908a9a2ea3c36e92611b5708633c50869e6d922fdf"}, + {file = "coverage-7.13.5-cp312-cp312-win_amd64.whl", hash = "sha256:d2c87e0c473a10bffe991502eac389220533024c8082ec1ce849f4218dded810"}, + {file = "coverage-7.13.5-cp312-cp312-win_arm64.whl", hash = "sha256:bf69236a9a81bdca3bff53796237aab096cdbf8d78a66ad61e992d9dac7eb2de"}, + {file = "coverage-7.13.5-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:5ec4af212df513e399cf11610cc27063f1586419e814755ab362e50a85ea69c1"}, + {file = "coverage-7.13.5-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:941617e518602e2d64942c88ec8499f7fbd49d3f6c4327d3a71d43a1973032f3"}, + {file = "coverage-7.13.5-cp313-cp313-manylinux1_i686.manylinux_2_28_i686.manylinux_2_5_i686.whl", hash = "sha256:da305e9937617ee95c2e39d8ff9f040e0487cbf1ac174f777ed5eddd7a7c1f26"}, + {file = "coverage-7.13.5-cp313-cp313-manylinux1_x86_64.manylinux_2_28_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:78e696e1cc714e57e8b25760b33a8b1026b7048d270140d25dafe1b0a1ee05a3"}, + {file = "coverage-7.13.5-cp313-cp313-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:02ca0eed225b2ff301c474aeeeae27d26e2537942aa0f87491d3e147e784a82b"}, + {file = "coverage-7.13.5-cp313-cp313-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:04690832cbea4e4663d9149e05dba142546ca05cb1848816760e7f58285c970a"}, + {file = "coverage-7.13.5-cp313-cp313-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:0590e44dd2745c696a778f7bab6aa95256de2cbc8b8cff4f7db8ff09813d6969"}, + {file = "coverage-7.13.5-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:d7cfad2d6d81dd298ab6b89fe72c3b7b05ec7544bdda3b707ddaecff8d25c161"}, + {file = "coverage-7.13.5-cp313-cp313-musllinux_1_2_i686.whl", hash = "sha256:e092b9499de38ae0fbfbc603a74660eb6ff3e869e507b50d85a13b6db9863e15"}, + {file = "coverage-7.13.5-cp313-cp313-musllinux_1_2_ppc64le.whl", hash = "sha256:48c39bc4a04d983a54a705a6389512883d4a3b9862991b3617d547940e9f52b1"}, + {file = "coverage-7.13.5-cp313-cp313-musllinux_1_2_riscv64.whl", hash = "sha256:2d3807015f138ffea1ed9afeeb8624fd781703f2858b62a8dd8da5a0994c57b6"}, + {file = "coverage-7.13.5-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:ee2aa19e03161671ec964004fb74b2257805d9710bf14a5c704558b9d8dbaf17"}, + {file = "coverage-7.13.5-cp313-cp313-win32.whl", hash = "sha256:ce1998c0483007608c8382f4ff50164bfc5bd07a2246dd272aa4043b75e61e85"}, + {file = "coverage-7.13.5-cp313-cp313-win_amd64.whl", hash = "sha256:631efb83f01569670a5e866ceb80fe483e7c159fac6f167e6571522636104a0b"}, + {file = "coverage-7.13.5-cp313-cp313-win_arm64.whl", hash = "sha256:f4cd16206ad171cbc2470dbea9103cf9a7607d5fe8c242fdf1edf36174020664"}, + {file = "coverage-7.13.5-cp313-cp313t-macosx_10_13_x86_64.whl", hash = "sha256:0428cbef5783ad91fe240f673cc1f76b25e74bbfe1a13115e4aa30d3f538162d"}, + {file = "coverage-7.13.5-cp313-cp313t-macosx_11_0_arm64.whl", hash = "sha256:e0b216a19534b2427cc201a26c25da4a48633f29a487c61258643e89d28200c0"}, + {file = "coverage-7.13.5-cp313-cp313t-manylinux1_i686.manylinux_2_28_i686.manylinux_2_5_i686.whl", hash = "sha256:972a9cd27894afe4bc2b1480107054e062df08e671df7c2f18c205e805ccd806"}, + {file = "coverage-7.13.5-cp313-cp313t-manylinux1_x86_64.manylinux_2_28_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:4b59148601efcd2bac8c4dbf1f0ad6391693ccf7a74b8205781751637076aee3"}, + {file = "coverage-7.13.5-cp313-cp313t-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:505d7083c8b0c87a8fa8c07370c285847c1f77739b22e299ad75a6af6c32c5c9"}, + {file = "coverage-7.13.5-cp313-cp313t-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:60365289c3741e4db327e7baff2a4aaacf22f788e80fa4683393891b70a89fbd"}, + {file = "coverage-7.13.5-cp313-cp313t-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:1b88c69c8ef5d4b6fe7dea66d6636056a0f6a7527c440e890cf9259011f5e606"}, + {file = "coverage-7.13.5-cp313-cp313t-musllinux_1_2_aarch64.whl", hash = "sha256:5b13955d31d1633cf9376908089b7cebe7d15ddad7aeaabcbe969a595a97e95e"}, + {file = "coverage-7.13.5-cp313-cp313t-musllinux_1_2_i686.whl", hash = "sha256:f70c9ab2595c56f81a89620e22899eea8b212a4041bd728ac6f4a28bf5d3ddd0"}, + {file = "coverage-7.13.5-cp313-cp313t-musllinux_1_2_ppc64le.whl", hash = "sha256:084b84a8c63e8d6fc7e3931b316a9bcafca1458d753c539db82d31ed20091a87"}, + {file = "coverage-7.13.5-cp313-cp313t-musllinux_1_2_riscv64.whl", hash = "sha256:ad14385487393e386e2ea988b09d62dd42c397662ac2dabc3832d71253eee479"}, + {file = "coverage-7.13.5-cp313-cp313t-musllinux_1_2_x86_64.whl", hash = "sha256:7f2c47b36fe7709a6e83bfadf4eefb90bd25fbe4014d715224c4316f808e59a2"}, + {file = "coverage-7.13.5-cp313-cp313t-win32.whl", hash = "sha256:67e9bc5449801fad0e5dff329499fb090ba4c5800b86805c80617b4e29809b2a"}, + {file = "coverage-7.13.5-cp313-cp313t-win_amd64.whl", hash = "sha256:da86cdcf10d2519e10cabb8ac2de03da1bcb6e4853790b7fbd48523332e3a819"}, + {file = "coverage-7.13.5-cp313-cp313t-win_arm64.whl", hash = "sha256:0ecf12ecb326fe2c339d93fc131816f3a7367d223db37817208905c89bded911"}, + {file = "coverage-7.13.5-cp314-cp314-macosx_10_15_x86_64.whl", hash = "sha256:fbabfaceaeb587e16f7008f7795cd80d20ec548dc7f94fbb0d4ec2e038ce563f"}, + {file = "coverage-7.13.5-cp314-cp314-macosx_11_0_arm64.whl", hash = "sha256:9bb2a28101a443669a423b665939381084412b81c3f8c0fcfbac57f4e30b5b8e"}, + {file = "coverage-7.13.5-cp314-cp314-manylinux1_i686.manylinux_2_28_i686.manylinux_2_5_i686.whl", hash = "sha256:bd3a2fbc1c6cccb3c5106140d87cc6a8715110373ef42b63cf5aea29df8c217a"}, + {file = "coverage-7.13.5-cp314-cp314-manylinux1_x86_64.manylinux_2_28_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:6c36ddb64ed9d7e496028d1d00dfec3e428e0aabf4006583bb1839958d280510"}, + {file = "coverage-7.13.5-cp314-cp314-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:380e8e9084d8eb38db3a9176a1a4f3c0082c3806fa0dc882d1d87abc3c789247"}, + {file = "coverage-7.13.5-cp314-cp314-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:e808af52a0513762df4d945ea164a24b37f2f518cbe97e03deaa0ee66139b4d6"}, + {file = "coverage-7.13.5-cp314-cp314-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:e301d30dd7e95ae068671d746ba8c34e945a82682e62918e41b2679acd2051a0"}, + {file = "coverage-7.13.5-cp314-cp314-musllinux_1_2_aarch64.whl", hash = "sha256:800bc829053c80d240a687ceeb927a94fd108bbdc68dfbe505d0d75ab578a882"}, + {file = "coverage-7.13.5-cp314-cp314-musllinux_1_2_i686.whl", hash = "sha256:0b67af5492adb31940ee418a5a655c28e48165da5afab8c7fa6fd72a142f8740"}, + {file = "coverage-7.13.5-cp314-cp314-musllinux_1_2_ppc64le.whl", hash = "sha256:c9136ff29c3a91e25b1d1552b5308e53a1e0653a23e53b6366d7c2dcbbaf8a16"}, + {file = "coverage-7.13.5-cp314-cp314-musllinux_1_2_riscv64.whl", hash = "sha256:cff784eef7f0b8f6cb28804fbddcfa99f89efe4cc35fb5627e3ac58f91ed3ac0"}, + {file = "coverage-7.13.5-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:68a4953be99b17ac3c23b6efbc8a38330d99680c9458927491d18700ef23ded0"}, + {file = "coverage-7.13.5-cp314-cp314-win32.whl", hash = "sha256:35a31f2b1578185fbe6aa2e74cea1b1d0bbf4c552774247d9160d29b80ed56cc"}, + {file = "coverage-7.13.5-cp314-cp314-win_amd64.whl", hash = "sha256:2aa055ae1857258f9e0045be26a6d62bdb47a72448b62d7b55f4820f361a2633"}, + {file = "coverage-7.13.5-cp314-cp314-win_arm64.whl", hash = "sha256:1b11eef33edeae9d142f9b4358edb76273b3bfd30bc3df9a4f95d0e49caf94e8"}, + {file = "coverage-7.13.5-cp314-cp314t-macosx_10_15_x86_64.whl", hash = "sha256:10a0c37f0b646eaff7cce1874c31d1f1ccb297688d4c747291f4f4c70741cc8b"}, + {file = "coverage-7.13.5-cp314-cp314t-macosx_11_0_arm64.whl", hash = "sha256:b5db73ba3c41c7008037fa731ad5459fc3944cb7452fc0aa9f822ad3533c583c"}, + {file = "coverage-7.13.5-cp314-cp314t-manylinux1_i686.manylinux_2_28_i686.manylinux_2_5_i686.whl", hash = "sha256:750db93a81e3e5a9831b534be7b1229df848b2e125a604fe6651e48aa070e5f9"}, + {file = "coverage-7.13.5-cp314-cp314t-manylinux1_x86_64.manylinux_2_28_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:9ddb4f4a5479f2539644be484da179b653273bca1a323947d48ab107b3ed1f29"}, + {file = "coverage-7.13.5-cp314-cp314t-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:d8a7a2049c14f413163e2bdabd37e41179b1d1ccb10ffc6ccc4b7a718429c607"}, + {file = "coverage-7.13.5-cp314-cp314t-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:e1c85e0b6c05c592ea6d8768a66a254bfb3874b53774b12d4c89c481eb78cb90"}, + {file = "coverage-7.13.5-cp314-cp314t-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:777c4d1eff1b67876139d24288aaf1817f6c03d6bae9c5cc8d27b83bcfe38fe3"}, + {file = "coverage-7.13.5-cp314-cp314t-musllinux_1_2_aarch64.whl", hash = "sha256:6697e29b93707167687543480a40f0db8f356e86d9f67ddf2e37e2dfd91a9dab"}, + {file = "coverage-7.13.5-cp314-cp314t-musllinux_1_2_i686.whl", hash = "sha256:8fdf453a942c3e4d99bd80088141c4c6960bb232c409d9c3558e2dbaa3998562"}, + {file = "coverage-7.13.5-cp314-cp314t-musllinux_1_2_ppc64le.whl", hash = "sha256:32ca0c0114c9834a43f045a87dcebd69d108d8ffb666957ea65aa132f50332e2"}, + {file = "coverage-7.13.5-cp314-cp314t-musllinux_1_2_riscv64.whl", hash = "sha256:8769751c10f339021e2638cd354e13adeac54004d1941119b2c96fe5276d45ea"}, + {file = "coverage-7.13.5-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:cec2d83125531bd153175354055cdb7a09987af08a9430bd173c937c6d0fba2a"}, + {file = "coverage-7.13.5-cp314-cp314t-win32.whl", hash = "sha256:0cd9ed7a8b181775459296e402ca4fb27db1279740a24e93b3b41942ebe4b215"}, + {file = "coverage-7.13.5-cp314-cp314t-win_amd64.whl", hash = "sha256:301e3b7dfefecaca37c9f1aa6f0049b7d4ab8dd933742b607765d757aca77d43"}, + {file = "coverage-7.13.5-cp314-cp314t-win_arm64.whl", hash = "sha256:9dacc2ad679b292709e0f5fc1ac74a6d4d5562e424058962c7bb0c658ad25e45"}, + {file = "coverage-7.13.5-py3-none-any.whl", hash = "sha256:34b02417cf070e173989b3db962f7ed56d2f644307b2cf9d5a0f258e13084a61"}, + {file = "coverage-7.13.5.tar.gz", hash = "sha256:c81f6515c4c40141f83f502b07bbfa5c240ba25bbe73da7b33f1e5b6120ff179"}, +] + +[[package]] +name = "cryptography" +version = "48.0.0" +requires_python = "!=3.9.0,!=3.9.1,>=3.9" +summary = "cryptography is a package which provides cryptographic recipes and primitives to Python developers." +groups = ["all", "keyring"] +marker = "sys_platform == \"linux\"" +dependencies = [ + "cffi>=2.0.0; platform_python_implementation != \"PyPy\"", + "typing-extensions>=4.13.2; python_full_version < \"3.11\"", +] +files = [ + {file = "cryptography-48.0.0-cp311-abi3-macosx_10_9_universal2.whl", hash = "sha256:0c558d2cdffd8f4bbb30fc7134c74d2ca9a476f830bb053074498fbc86f41ed6"}, + {file = "cryptography-48.0.0-cp311-abi3-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:f5333311663ea94f75dd408665686aaf426563556bb5283554a3539177e03b8c"}, + {file = "cryptography-48.0.0-cp311-abi3-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:7995ef305d7165c3f11ae07f2517e5a4f1d5c18da1376a0a9ed496336b69e5f3"}, + {file = "cryptography-48.0.0-cp311-abi3-manylinux_2_28_aarch64.whl", hash = "sha256:40ba1f85eaa6959837b1d51c9767e230e14612eea4ef110ee8854ada22da1bf5"}, + {file = "cryptography-48.0.0-cp311-abi3-manylinux_2_28_ppc64le.whl", hash = "sha256:369a6348999f94bbd53435c894377b20ab95f25a9065c283570e70150d8abc3c"}, + {file = "cryptography-48.0.0-cp311-abi3-manylinux_2_28_x86_64.whl", hash = "sha256:a0e692c683f4df67815a2d258b324e66f4738bd7a96a218c826dce4f4bd05d8f"}, + {file = "cryptography-48.0.0-cp311-abi3-manylinux_2_31_armv7l.whl", hash = "sha256:18349bbc56f4743c8b12dc32e2bccb2cf83ee8b69a3bba74ef8ae857e26b3d25"}, + {file = "cryptography-48.0.0-cp311-abi3-manylinux_2_34_aarch64.whl", hash = "sha256:7e8eac43dfca5c4cccc6dad9a80504436fca53bb9bc3100a2386d730fbe6b602"}, + {file = "cryptography-48.0.0-cp311-abi3-manylinux_2_34_ppc64le.whl", hash = "sha256:9ccdac7d40688ecb5a3b4a604b8a88c8002e3442d6c60aead1db2a89a041560c"}, + {file = "cryptography-48.0.0-cp311-abi3-manylinux_2_34_x86_64.whl", hash = "sha256:bd72e68b06bb1e96913f97dd4901119bc17f39d4586a5adf2d3e47bc2b9d58b5"}, + {file = "cryptography-48.0.0-cp311-abi3-musllinux_1_2_aarch64.whl", hash = "sha256:59baa2cb386c4f0b9905bd6eb4c2a79a69a128408fd31d32ca4d7102d4156321"}, + {file = "cryptography-48.0.0-cp311-abi3-musllinux_1_2_x86_64.whl", hash = "sha256:9249e3cd978541d665967ac2cb2787fd6a62bddf1e75b3e347a594d7dacf4f74"}, + {file = "cryptography-48.0.0-cp311-abi3-win32.whl", hash = "sha256:9c459db21422be75e2809370b829a87eb37f74cd785fc4aa9ea1e5f43b47cda4"}, + {file = "cryptography-48.0.0-cp311-abi3-win_amd64.whl", hash = "sha256:5b012212e08b8dd5edc78ef54da83dd9892fd9105323b3993eff6bea65dc21d7"}, + {file = "cryptography-48.0.0-cp314-cp314t-macosx_10_9_universal2.whl", hash = "sha256:3cb07a3ed6431663cd321ea8a000a1314c74211f823e4177fefa2255e057d1ec"}, + {file = "cryptography-48.0.0-cp314-cp314t-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:8c7378637d7d88016fa6791c159f698b3d3eed28ebf844ac36b9dc04a14dae18"}, + {file = "cryptography-48.0.0-cp314-cp314t-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:cc90c0b39b2e3c65ef52c804b72e3c58f8a04ab2a1871272798e5f9572c17d20"}, + {file = "cryptography-48.0.0-cp314-cp314t-manylinux_2_28_aarch64.whl", hash = "sha256:76341972e1eff8b4bea859f09c0d3e64b96ce931b084f9b9b7db8ef364c30eff"}, + {file = "cryptography-48.0.0-cp314-cp314t-manylinux_2_28_ppc64le.whl", hash = "sha256:55b7718303bf06a5753dcdccf2f3945cf18ad7bffde41b61226e4db31ab89a9c"}, + {file = "cryptography-48.0.0-cp314-cp314t-manylinux_2_28_x86_64.whl", hash = "sha256:a64697c641c7b1b2178e573cbc31c7c6684cd56883a478d75143dbb7118036db"}, + {file = "cryptography-48.0.0-cp314-cp314t-manylinux_2_31_armv7l.whl", hash = "sha256:561215ea3879cb1cbbf272867e2efda62476f240fb58c64de6b393ae19246741"}, + {file = "cryptography-48.0.0-cp314-cp314t-manylinux_2_34_aarch64.whl", hash = "sha256:ad64688338ed4bc1a6618076ba75fd7194a5f1797ac60b47afe926285adb3166"}, + {file = "cryptography-48.0.0-cp314-cp314t-manylinux_2_34_ppc64le.whl", hash = "sha256:906cbf0670286c6e0044156bc7d4af9cbb0ef6db9f73e52c3ec56ba6bdde5336"}, + {file = "cryptography-48.0.0-cp314-cp314t-manylinux_2_34_x86_64.whl", hash = "sha256:ea8990436d914540a40ab24b6a77c0969695ed52f4a4874c5137ccf7045a7057"}, + {file = "cryptography-48.0.0-cp314-cp314t-musllinux_1_2_aarch64.whl", hash = "sha256:c18684a7f0cc9a3cb60328f496b8e3372def7c5d2df39ac267878b05565aaaae"}, + {file = "cryptography-48.0.0-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:9be5aafa5736574f8f15f262adc81b2a9869e2cfe9014d52a44633905b40d52c"}, + {file = "cryptography-48.0.0-cp314-cp314t-win32.whl", hash = "sha256:c17dfe85494deaeddc5ce251aebd1d60bbe6afc8b62071bb0b469431a000124f"}, + {file = "cryptography-48.0.0-cp314-cp314t-win_amd64.whl", hash = "sha256:27241b1dc9962e056062a8eef1991d02c3a24569c95975bd2322a8a52c6e5e12"}, + {file = "cryptography-48.0.0-cp39-abi3-macosx_10_9_universal2.whl", hash = "sha256:58d00498e8933e4a194f3076aee1b4a97dfec1a6da444535755822fe5d8b0b86"}, + {file = "cryptography-48.0.0-cp39-abi3-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:614d0949f4790582d2cc25553abd09dd723025f0c0e7c67376a1d77196743d6e"}, + {file = "cryptography-48.0.0-cp39-abi3-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:7ce4bfae76319a532a2dc68f82cc32f5676ee792a983187dac07183690e5c66f"}, + {file = "cryptography-48.0.0-cp39-abi3-manylinux_2_28_aarch64.whl", hash = "sha256:2eb992bbd4661238c5a397594c83f5b4dc2bc5b848c365c8f991b6780efcc5c7"}, + {file = "cryptography-48.0.0-cp39-abi3-manylinux_2_28_ppc64le.whl", hash = "sha256:22a5cb272895dce158b2cacdfdc3debd299019659f42947dbdac6f32d68fe832"}, + {file = "cryptography-48.0.0-cp39-abi3-manylinux_2_28_x86_64.whl", hash = "sha256:2b4d59804e8408e2fea7d1fbaf218e5ec984325221db76e6a241a9abd6cdd95c"}, + {file = "cryptography-48.0.0-cp39-abi3-manylinux_2_31_armv7l.whl", hash = "sha256:984a20b0f62a26f48a3396c72e4bc34c66e356d356bf370053066b3b6d54634a"}, + {file = "cryptography-48.0.0-cp39-abi3-manylinux_2_34_aarch64.whl", hash = "sha256:5a5ed8fde7a1d09376ca0b40e68cd59c69fe23b1f9768bd5824f54681626032a"}, + {file = "cryptography-48.0.0-cp39-abi3-manylinux_2_34_ppc64le.whl", hash = "sha256:8cd666227ef7af430aa5914a9910e0ddd703e75f039cef0825cd0da71b6b711a"}, + {file = "cryptography-48.0.0-cp39-abi3-manylinux_2_34_x86_64.whl", hash = "sha256:9071196d81abc88b3516ac8cdfad32e2b66dd4a5393a8e68a961e9161ddc6239"}, + {file = "cryptography-48.0.0-cp39-abi3-musllinux_1_2_aarch64.whl", hash = "sha256:1e2d54c8be6152856a36f0882ab231e70f8ec7f14e93cf87db8a2ed056bf160c"}, + {file = "cryptography-48.0.0-cp39-abi3-musllinux_1_2_x86_64.whl", hash = "sha256:a5da777e32ffed6f85a7b2b3f7c5cbc88c146bfcd0a1d7baf5fcc6c52ee35dd4"}, + {file = "cryptography-48.0.0-cp39-abi3-win32.whl", hash = "sha256:77a2ccbbe917f6710e05ba9adaa25fb5075620bf3ea6fb751997875aff4ae4bd"}, + {file = "cryptography-48.0.0-cp39-abi3-win_amd64.whl", hash = "sha256:16cd65b9330583e4619939b3a3843eec1e6e789744bb01e7c7e2e62e33c239c8"}, + {file = "cryptography-48.0.0-pp311-pypy311_pp73-macosx_11_0_arm64.whl", hash = "sha256:84cf79f0dc8b36ac5da873481716e87aef31fcfa0444f9e1d8b4b2cece142855"}, + {file = "cryptography-48.0.0-pp311-pypy311_pp73-manylinux_2_28_aarch64.whl", hash = "sha256:fdfef35d751d510fcef5252703621574364fec16418c4a1e5e1055248401054b"}, + {file = "cryptography-48.0.0-pp311-pypy311_pp73-manylinux_2_28_x86_64.whl", hash = "sha256:0890f502ddf7d9c6426129c3f49f5c0a39278ed7cd6322c8755ffca6ee675a13"}, + {file = "cryptography-48.0.0-pp311-pypy311_pp73-manylinux_2_34_aarch64.whl", hash = "sha256:ecde28a596bead48b0cfd2a1b4416c3d43074c2d785e3a398d7ec1fc4d0f7fbb"}, + {file = "cryptography-48.0.0-pp311-pypy311_pp73-manylinux_2_34_x86_64.whl", hash = "sha256:4defde8685ae324a9eb9d818717e93b4638ef67070ac9bc15b8ca85f63048355"}, + {file = "cryptography-48.0.0-pp311-pypy311_pp73-win_amd64.whl", hash = "sha256:db63bf618e5dea46c07de12e900fe1cdd2541e6dc9dbae772a70b7d4d4765f6a"}, + {file = "cryptography-48.0.0.tar.gz", hash = "sha256:5c3932f4436d1cccb036cb0eaef46e6e2db91035166f1ad6505c3c9d5a635920"}, +] + +[[package]] +name = "deepmerge" +version = "2.0" +requires_python = ">=3.8" +summary = "A toolset for deeply merging Python dictionaries." +groups = ["doc"] +marker = "python_version >= \"3.10\"" +dependencies = [ + "typing-extensions; python_version <= \"3.9\"", +] +files = [ + {file = "deepmerge-2.0-py3-none-any.whl", hash = "sha256:6de9ce507115cff0bed95ff0ce9ecc31088ef50cbdf09bc90a09349a318b3d00"}, + {file = "deepmerge-2.0.tar.gz", hash = "sha256:5c3d86081fbebd04dd5de03626a0607b809a98fb6ccba5770b62466fe940ff20"}, +] + +[[package]] +name = "dep-logic" +version = "0.7.1" +requires_python = ">=3.10" +summary = "Python dependency specifications supporting logical operations" +groups = ["default"] +dependencies = [ + "packaging>=22", +] +files = [ + {file = "dep_logic-0.7.1-py3-none-any.whl", hash = "sha256:38b96555083a8efdd62a6987e49f65eb3778fe4c8189e83b3429ca6b6412d196"}, + {file = "dep_logic-0.7.1.tar.gz", hash = "sha256:4bf66e3b323e0c30d2ed268c44efd69c179c10cdf499c28ca316c2cb49e95ee2"}, +] + +[[package]] +name = "distlib" +version = "0.4.0" +summary = "Distribution utilities" +groups = ["default", "tox"] +files = [ + {file = "distlib-0.4.0-py2.py3-none-any.whl", hash = "sha256:9659f7d87e46584a30b5780e43ac7a2143098441670ff0a49d5f9034c54a6c16"}, + {file = "distlib-0.4.0.tar.gz", hash = "sha256:feec40075be03a04501a973d81f633735b4b69f98b05450592310c0f401a4e0d"}, +] + +[[package]] +name = "exceptiongroup" +version = "1.3.1" +requires_python = ">=3.7" +summary = "Backport of PEP 654 (exception groups)" +groups = ["default", "pytest", "test"] +marker = "python_version < \"3.11\"" +dependencies = [ + "typing-extensions>=4.6.0; python_version < \"3.13\"", +] +files = [ + {file = "exceptiongroup-1.3.1-py3-none-any.whl", hash = "sha256:a7a39a3bd276781e98394987d3a5701d0c4edffb633bb7a5144577f82c773598"}, + {file = "exceptiongroup-1.3.1.tar.gz", hash = "sha256:8b412432c6055b0b7d14c310000ae93352ed6754f70fa8f7c34141f91c4e3219"}, +] + +[[package]] +name = "execnet" +version = "2.1.2" +requires_python = ">=3.8" +summary = "execnet: rapid multi-Python deployment" +groups = ["test"] +files = [ + {file = "execnet-2.1.2-py3-none-any.whl", hash = "sha256:67fba928dd5a544b783f6056f449e5e3931a5c378b128bc18501f7ea79e296ec"}, + {file = "execnet-2.1.2.tar.gz", hash = "sha256:63d83bfdd9a23e35b9c6a3261412324f964c2ec8dcd8d3c6916ee9373e0befcd"}, +] + +[[package]] +name = "filelock" +version = "3.29.0" +requires_python = ">=3.10" +summary = "A platform independent file lock." +groups = ["default", "tox"] +files = [ + {file = "filelock-3.29.0-py3-none-any.whl", hash = "sha256:96f5f6344709aa1572bbf631c640e4ebeeb519e08da902c39a001882f30ac258"}, + {file = "filelock-3.29.0.tar.gz", hash = "sha256:69974355e960702e789734cb4871f884ea6fe50bd8404051a3530bc07809cf90"}, +] + +[[package]] +name = "findpython" +version = "0.8.0" +requires_python = ">=3.9" +summary = "A utility to find python versions on your system" +groups = ["default"] +dependencies = [ + "packaging>=20", + "platformdirs>=4.3.6", +] +files = [ + {file = "findpython-0.8.0-py3-none-any.whl", hash = "sha256:4a61ee1618a8b55014f7d41f59345d322be93f6ce62395bdccccc651b3f7e28a"}, + {file = "findpython-0.8.0.tar.gz", hash = "sha256:53b32264874dfa5990bd09d717819386d8db3149d89fe20f88fe1078de286bae"}, +] + +[[package]] +name = "ghp-import" +version = "2.1.0" +summary = "Copy your docs directly to the gh-pages branch." +groups = ["doc"] +dependencies = [ + "python-dateutil>=2.8.1", +] +files = [ + {file = "ghp-import-2.1.0.tar.gz", hash = "sha256:9c535c4c61193c2df8871222567d7fd7e5014d835f97dc7b7439069e2413d343"}, + {file = "ghp_import-2.1.0-py3-none-any.whl", hash = "sha256:8337dd7b50877f163d4c0289bc1f1c7f127550241988d568c1db512c4324a619"}, +] + +[[package]] +name = "griffelib" +version = "2.0.2" +requires_python = ">=3.10" +summary = "Signatures for entire Python programs. Extract the structure, the frame, the skeleton of your project, to generate API documentation or find breaking changes in your API." +groups = ["doc"] +files = [ + {file = "griffelib-2.0.2-py3-none-any.whl", hash = "sha256:925c857658fb1ba40c0772c37acbc2ab650bd794d9c1b9726922e36ea4117ea1"}, + {file = "griffelib-2.0.2.tar.gz", hash = "sha256:3cf20b3bc470e83763ffbf236e0076b1211bac1bc67de13daf494640f2de707e"}, +] + +[[package]] +name = "h11" +version = "0.16.0" +requires_python = ">=3.8" +summary = "A pure-Python, bring-your-own-I/O implementation of HTTP/1.1" +groups = ["default", "test"] +files = [ + {file = "h11-0.16.0-py3-none-any.whl", hash = "sha256:63cf8bbe7522de3bf65932fda1d9c2772064ffb3dae62d55932da54b31cb6c86"}, + {file = "h11-0.16.0.tar.gz", hash = "sha256:4e35b956cf45792e4caa5885e69fba00bdbc6ffafbfa020300e549b208ee5ff1"}, +] + +[[package]] +name = "hishel" +version = "1.2.1" +requires_python = ">=3.10" +summary = "Elegant HTTP Caching for Python" +groups = ["default"] +dependencies = [ + "msgpack>=1.1.2", + "typing-extensions>=4.14.1", +] +files = [ + {file = "hishel-1.2.1-py3-none-any.whl", hash = "sha256:5e1c1e38f4c970eeef69bc8528e85fd5c02eab15a22f9845d30fc880da32b881"}, + {file = "hishel-1.2.1.tar.gz", hash = "sha256:87212cd31a7a6904352ec5bd7119ed3b43d0ab1259d5995751a2a2b173d7c305"}, +] + +[[package]] +name = "hishel" +version = "1.2.1" +extras = ["httpx"] +requires_python = ">=3.10" +summary = "Elegant HTTP Caching for Python" +groups = ["default"] +dependencies = [ + "anyio>=4.9.0", + "anysqlite>=0.0.5", + "hishel==1.2.1", + "httpx>=0.28.1", +] +files = [ + {file = "hishel-1.2.1-py3-none-any.whl", hash = "sha256:5e1c1e38f4c970eeef69bc8528e85fd5c02eab15a22f9845d30fc880da32b881"}, + {file = "hishel-1.2.1.tar.gz", hash = "sha256:87212cd31a7a6904352ec5bd7119ed3b43d0ab1259d5995751a2a2b173d7c305"}, +] + +[[package]] +name = "httpcore" +version = "1.0.9" +requires_python = ">=3.8" +summary = "A minimal low-level HTTP client." +groups = ["default", "test"] +dependencies = [ + "certifi", + "h11>=0.16", +] +files = [ + {file = "httpcore-1.0.9-py3-none-any.whl", hash = "sha256:2d400746a40668fc9dec9810239072b40b4484b640a8c38fd654a024c7a1bf55"}, + {file = "httpcore-1.0.9.tar.gz", hash = "sha256:6e34463af53fd2ab5d807f399a9b45ea31c3dfa2276f15a2c3f00afff6e176e8"}, +] + +[[package]] +name = "httpx" +version = "0.28.1" +requires_python = ">=3.8" +summary = "The next generation HTTP client." +groups = ["default", "test"] +dependencies = [ + "anyio", + "certifi", + "httpcore==1.*", + "idna", +] +files = [ + {file = "httpx-0.28.1-py3-none-any.whl", hash = "sha256:d909fcccc110f8c7faf814ca82a9a4d816bc5a6dbfea25d6591d6985b8ba59ad"}, + {file = "httpx-0.28.1.tar.gz", hash = "sha256:75e98c5f16b0f35b567856f597f06ff2270a374470a5c2392242528e3e3e42fc"}, +] + +[[package]] +name = "httpx" +version = "0.28.1" +extras = ["socks"] +requires_python = ">=3.8" +summary = "The next generation HTTP client." +groups = ["default"] +dependencies = [ + "httpx==0.28.1", + "socksio==1.*", +] +files = [ + {file = "httpx-0.28.1-py3-none-any.whl", hash = "sha256:d909fcccc110f8c7faf814ca82a9a4d816bc5a6dbfea25d6591d6985b8ba59ad"}, + {file = "httpx-0.28.1.tar.gz", hash = "sha256:75e98c5f16b0f35b567856f597f06ff2270a374470a5c2392242528e3e3e42fc"}, +] + +[[package]] +name = "id" +version = "1.6.1" +requires_python = ">=3.9" +summary = "A tool for generating OIDC identities" +groups = ["default"] +dependencies = [ + "urllib3<3,>=2", +] +files = [ + {file = "id-1.6.1-py3-none-any.whl", hash = "sha256:f5ec41ed2629a508f5d0988eda142e190c9c6da971100612c4de9ad9f9b237ca"}, + {file = "id-1.6.1.tar.gz", hash = "sha256:d0732d624fb46fd4e7bc4e5152f00214450953b9e772c182c1c22964def1a069"}, +] + +[[package]] +name = "idna" +version = "3.13" +requires_python = ">=3.8" +summary = "Internationalized Domain Names in Applications (IDNA)" +groups = ["default", "test"] +files = [ + {file = "idna-3.13-py3-none-any.whl", hash = "sha256:892ea0cde124a99ce773decba204c5552b69c3c67ffd5f232eb7696135bc8bb3"}, + {file = "idna-3.13.tar.gz", hash = "sha256:585ea8fe5d69b9181ec1afba340451fba6ba764af97026f92a91d4eef164a242"}, +] + +[[package]] +name = "importlib-metadata" +version = "9.0.0" +requires_python = ">=3.10" +summary = "Read metadata from Python packages" +groups = ["all", "keyring"] +marker = "python_version < \"3.12\"" +dependencies = [ + "zipp>=3.20", +] +files = [ + {file = "importlib_metadata-9.0.0-py3-none-any.whl", hash = "sha256:2d21d1cc5a017bd0559e36150c21c830ab1dc304dedd1b7ea85d20f45ef3edd7"}, + {file = "importlib_metadata-9.0.0.tar.gz", hash = "sha256:a4f57ab599e6a2e3016d7595cfd72eb4661a5106e787a95bcc90c7105b831efc"}, +] + +[[package]] +name = "iniconfig" +version = "2.3.0" +requires_python = ">=3.10" +summary = "brain-dead simple config-ini parsing" +groups = ["pytest", "test"] +files = [ + {file = "iniconfig-2.3.0-py3-none-any.whl", hash = "sha256:f631c04d2c48c52b84d0d0549c99ff3859c98df65b3101406327ecc7d53fbf12"}, + {file = "iniconfig-2.3.0.tar.gz", hash = "sha256:c76315c77db068650d49c5b56314774a7804df16fee4402c1f19d6d15d8c4730"}, +] + +[[package]] +name = "installer" +version = "1.0.0" +requires_python = ">=3.10" +summary = "A library for installing Python wheels." +groups = ["default"] +files = [ + {file = "installer-1.0.0-py3-none-any.whl", hash = "sha256:7b46327ded20d8544bfe2d8561618bbcd12d88e7e3645333af1ed141d8bc1bfe"}, + {file = "installer-1.0.0.tar.gz", hash = "sha256:c6d691331621cf3fec4822f5c6f83cab3705f79b316225dc454127411677c71f"}, +] + +[[package]] +name = "jaraco-classes" +version = "3.4.0" +requires_python = ">=3.8" +summary = "Utility functions for Python class constructs" +groups = ["all", "keyring"] +dependencies = [ + "more-itertools", +] +files = [ + {file = "jaraco.classes-3.4.0-py3-none-any.whl", hash = "sha256:f662826b6bed8cace05e7ff873ce0f9283b5c924470fe664fff1c2f00f581790"}, + {file = "jaraco.classes-3.4.0.tar.gz", hash = "sha256:47a024b51d0239c0dd8c8540c6c7f484be3b8fcf0b2d85c13825780d3b3f3acd"}, +] + +[[package]] +name = "jaraco-context" +version = "6.1.2" +requires_python = ">=3.10" +summary = "Useful decorators and context managers" +groups = ["all", "keyring"] +dependencies = [ + "backports-tarfile; python_version < \"3.12\"", +] +files = [ + {file = "jaraco_context-6.1.2-py3-none-any.whl", hash = "sha256:bf8150b79a2d5d91ae48629d8b427a8f7ba0e1097dd6202a9059f29a36379535"}, + {file = "jaraco_context-6.1.2.tar.gz", hash = "sha256:f1a6c9d391e661cc5b8d39861ff077a7dc24dc23833ccee564b234b81c82dfe3"}, +] + +[[package]] +name = "jaraco-functools" +version = "4.4.0" +requires_python = ">=3.9" +summary = "Functools like those found in stdlib" +groups = ["all", "keyring"] +dependencies = [ + "more-itertools", +] +files = [ + {file = "jaraco_functools-4.4.0-py3-none-any.whl", hash = "sha256:9eec1e36f45c818d9bf307c8948eb03b2b56cd44087b3cdc989abca1f20b9176"}, + {file = "jaraco_functools-4.4.0.tar.gz", hash = "sha256:da21933b0417b89515562656547a77b4931f98176eb173644c0d35032a33d6bb"}, +] + +[[package]] +name = "jeepney" +version = "0.9.0" +requires_python = ">=3.7" +summary = "Low-level, pure Python DBus protocol wrapper." +groups = ["all", "keyring"] +marker = "sys_platform == \"linux\"" +files = [ + {file = "jeepney-0.9.0-py3-none-any.whl", hash = "sha256:97e5714520c16fc0a45695e5365a2e11b81ea79bba796e26f9f1d178cb182683"}, + {file = "jeepney-0.9.0.tar.gz", hash = "sha256:cf0e9e845622b81e4a28df94c40345400256ec608d0e55bb8a3feaa9163f5732"}, +] + +[[package]] +name = "jinja2" +version = "3.1.6" +requires_python = ">=3.7" +summary = "A very fast and expressive template engine." +groups = ["doc", "workflow"] +dependencies = [ + "MarkupSafe>=2.0", +] +files = [ + {file = "jinja2-3.1.6-py3-none-any.whl", hash = "sha256:85ece4451f492d0c13c5dd7c13a64681a86afae63a5f347908daf103ce6d2f67"}, + {file = "jinja2-3.1.6.tar.gz", hash = "sha256:0137fb05990d35f1275a587e9aee6d56da821fc83491a0fb838183be43f66d6d"}, +] + +[[package]] +name = "keyring" +version = "25.7.0" +requires_python = ">=3.9" +summary = "Store and access your passwords safely." +groups = ["all", "keyring"] +dependencies = [ + "SecretStorage>=3.2; sys_platform == \"linux\"", + "importlib-metadata>=4.11.4; python_version < \"3.12\"", + "jaraco-classes", + "jaraco-context", + "jaraco-functools", + "jeepney>=0.4.2; sys_platform == \"linux\"", + "pywin32-ctypes>=0.2.0; sys_platform == \"win32\"", +] +files = [ + {file = "keyring-25.7.0-py3-none-any.whl", hash = "sha256:be4a0b195f149690c166e850609a477c532ddbfbaed96a404d4e43f8d5e2689f"}, + {file = "keyring-25.7.0.tar.gz", hash = "sha256:fe01bd85eb3f8fb3dd0405defdeac9a5b4f6f0439edbb3149577f244a2e8245b"}, +] + +[[package]] +name = "markdown" +version = "3.9" +requires_python = ">=3.9" +summary = "Python implementation of John Gruber's Markdown." +groups = ["doc"] +dependencies = [ + "importlib-metadata>=4.4; python_version < \"3.10\"", +] +files = [ + {file = "markdown-3.9-py3-none-any.whl", hash = "sha256:9f4d91ed810864ea88a6f32c07ba8bee1346c0cc1f6b1f9f6c822f2a9667d280"}, + {file = "markdown-3.9.tar.gz", hash = "sha256:d2900fe1782bd33bdbbd56859defef70c2e78fc46668f8eb9df3128138f2cb6a"}, +] + +[[package]] +name = "markdown-it-py" +version = "4.1.0" +requires_python = ">=3.10" +summary = "Python port of markdown-it. Markdown parsing, done right!" +groups = ["default"] +dependencies = [ + "mdurl~=0.1", +] +files = [ + {file = "markdown_it_py-4.1.0-py3-none-any.whl", hash = "sha256:d4939a62a2dd0cd9cb80a191a711ba1d39bac8ed5ef9e9966895b0171c01c46d"}, + {file = "markdown_it_py-4.1.0.tar.gz", hash = "sha256:760e3f87b2787c044c5138a5ba107b7c2be26c03b13cc7f8fe42756b65b1df6c"}, +] + +[[package]] +name = "markupsafe" +version = "3.0.3" +requires_python = ">=3.9" +summary = "Safely add untrusted strings to HTML/XML markup." +groups = ["doc", "test", "workflow"] +files = [ + {file = "markupsafe-3.0.3-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:2f981d352f04553a7171b8e44369f2af4055f888dfb147d55e42d29e29e74559"}, + {file = "markupsafe-3.0.3-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:e1c1493fb6e50ab01d20a22826e57520f1284df32f2d8601fdd90b6304601419"}, + {file = "markupsafe-3.0.3-cp310-cp310-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:1ba88449deb3de88bd40044603fafffb7bc2b055d626a330323a9ed736661695"}, + {file = "markupsafe-3.0.3-cp310-cp310-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:f42d0984e947b8adf7dd6dde396e720934d12c506ce84eea8476409563607591"}, + {file = "markupsafe-3.0.3-cp310-cp310-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:c0c0b3ade1c0b13b936d7970b1d37a57acde9199dc2aecc4c336773e1d86049c"}, + {file = "markupsafe-3.0.3-cp310-cp310-musllinux_1_2_aarch64.whl", hash = "sha256:0303439a41979d9e74d18ff5e2dd8c43ed6c6001fd40e5bf2e43f7bd9bbc523f"}, + {file = "markupsafe-3.0.3-cp310-cp310-musllinux_1_2_riscv64.whl", hash = "sha256:d2ee202e79d8ed691ceebae8e0486bd9a2cd4794cec4824e1c99b6f5009502f6"}, + {file = "markupsafe-3.0.3-cp310-cp310-musllinux_1_2_x86_64.whl", hash = "sha256:177b5253b2834fe3678cb4a5f0059808258584c559193998be2601324fdeafb1"}, + {file = "markupsafe-3.0.3-cp310-cp310-win32.whl", hash = "sha256:2a15a08b17dd94c53a1da0438822d70ebcd13f8c3a95abe3a9ef9f11a94830aa"}, + {file = "markupsafe-3.0.3-cp310-cp310-win_amd64.whl", hash = "sha256:c4ffb7ebf07cfe8931028e3e4c85f0357459a3f9f9490886198848f4fa002ec8"}, + {file = "markupsafe-3.0.3-cp310-cp310-win_arm64.whl", hash = "sha256:e2103a929dfa2fcaf9bb4e7c091983a49c9ac3b19c9061b6d5427dd7d14d81a1"}, + {file = "markupsafe-3.0.3-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:1cc7ea17a6824959616c525620e387f6dd30fec8cb44f649e31712db02123dad"}, + {file = "markupsafe-3.0.3-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:4bd4cd07944443f5a265608cc6aab442e4f74dff8088b0dfc8238647b8f6ae9a"}, + {file = "markupsafe-3.0.3-cp311-cp311-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:6b5420a1d9450023228968e7e6a9ce57f65d148ab56d2313fcd589eee96a7a50"}, + {file = "markupsafe-3.0.3-cp311-cp311-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:0bf2a864d67e76e5c9a34dc26ec616a66b9888e25e7b9460e1c76d3293bd9dbf"}, + {file = "markupsafe-3.0.3-cp311-cp311-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:bc51efed119bc9cfdf792cdeaa4d67e8f6fcccab66ed4bfdd6bde3e59bfcbb2f"}, + {file = "markupsafe-3.0.3-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:068f375c472b3e7acbe2d5318dea141359e6900156b5b2ba06a30b169086b91a"}, + {file = "markupsafe-3.0.3-cp311-cp311-musllinux_1_2_riscv64.whl", hash = "sha256:7be7b61bb172e1ed687f1754f8e7484f1c8019780f6f6b0786e76bb01c2ae115"}, + {file = "markupsafe-3.0.3-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:f9e130248f4462aaa8e2552d547f36ddadbeaa573879158d721bbd33dfe4743a"}, + {file = "markupsafe-3.0.3-cp311-cp311-win32.whl", hash = "sha256:0db14f5dafddbb6d9208827849fad01f1a2609380add406671a26386cdf15a19"}, + {file = "markupsafe-3.0.3-cp311-cp311-win_amd64.whl", hash = "sha256:de8a88e63464af587c950061a5e6a67d3632e36df62b986892331d4620a35c01"}, + {file = "markupsafe-3.0.3-cp311-cp311-win_arm64.whl", hash = "sha256:3b562dd9e9ea93f13d53989d23a7e775fdfd1066c33494ff43f5418bc8c58a5c"}, + {file = "markupsafe-3.0.3-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:d53197da72cc091b024dd97249dfc7794d6a56530370992a5e1a08983ad9230e"}, + {file = "markupsafe-3.0.3-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:1872df69a4de6aead3491198eaf13810b565bdbeec3ae2dc8780f14458ec73ce"}, + {file = "markupsafe-3.0.3-cp312-cp312-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:3a7e8ae81ae39e62a41ec302f972ba6ae23a5c5396c8e60113e9066ef893da0d"}, + {file = "markupsafe-3.0.3-cp312-cp312-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:d6dd0be5b5b189d31db7cda48b91d7e0a9795f31430b7f271219ab30f1d3ac9d"}, + {file = "markupsafe-3.0.3-cp312-cp312-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:94c6f0bb423f739146aec64595853541634bde58b2135f27f61c1ffd1cd4d16a"}, + {file = "markupsafe-3.0.3-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:be8813b57049a7dc738189df53d69395eba14fb99345e0a5994914a3864c8a4b"}, + {file = "markupsafe-3.0.3-cp312-cp312-musllinux_1_2_riscv64.whl", hash = "sha256:83891d0e9fb81a825d9a6d61e3f07550ca70a076484292a70fde82c4b807286f"}, + {file = "markupsafe-3.0.3-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:77f0643abe7495da77fb436f50f8dab76dbc6e5fd25d39589a0f1fe6548bfa2b"}, + {file = "markupsafe-3.0.3-cp312-cp312-win32.whl", hash = "sha256:d88b440e37a16e651bda4c7c2b930eb586fd15ca7406cb39e211fcff3bf3017d"}, + {file = "markupsafe-3.0.3-cp312-cp312-win_amd64.whl", hash = "sha256:26a5784ded40c9e318cfc2bdb30fe164bdb8665ded9cd64d500a34fb42067b1c"}, + {file = "markupsafe-3.0.3-cp312-cp312-win_arm64.whl", hash = "sha256:35add3b638a5d900e807944a078b51922212fb3dedb01633a8defc4b01a3c85f"}, + {file = "markupsafe-3.0.3-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:e1cf1972137e83c5d4c136c43ced9ac51d0e124706ee1c8aa8532c1287fa8795"}, + {file = "markupsafe-3.0.3-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:116bb52f642a37c115f517494ea5feb03889e04df47eeff5b130b1808ce7c219"}, + {file = "markupsafe-3.0.3-cp313-cp313-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:133a43e73a802c5562be9bbcd03d090aa5a1fe899db609c29e8c8d815c5f6de6"}, + {file = "markupsafe-3.0.3-cp313-cp313-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:ccfcd093f13f0f0b7fdd0f198b90053bf7b2f02a3927a30e63f3ccc9df56b676"}, + {file = "markupsafe-3.0.3-cp313-cp313-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:509fa21c6deb7a7a273d629cf5ec029bc209d1a51178615ddf718f5918992ab9"}, + {file = "markupsafe-3.0.3-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:a4afe79fb3de0b7097d81da19090f4df4f8d3a2b3adaa8764138aac2e44f3af1"}, + {file = "markupsafe-3.0.3-cp313-cp313-musllinux_1_2_riscv64.whl", hash = "sha256:795e7751525cae078558e679d646ae45574b47ed6e7771863fcc079a6171a0fc"}, + {file = "markupsafe-3.0.3-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:8485f406a96febb5140bfeca44a73e3ce5116b2501ac54fe953e488fb1d03b12"}, + {file = "markupsafe-3.0.3-cp313-cp313-win32.whl", hash = "sha256:bdd37121970bfd8be76c5fb069c7751683bdf373db1ed6c010162b2a130248ed"}, + {file = "markupsafe-3.0.3-cp313-cp313-win_amd64.whl", hash = "sha256:9a1abfdc021a164803f4d485104931fb8f8c1efd55bc6b748d2f5774e78b62c5"}, + {file = "markupsafe-3.0.3-cp313-cp313-win_arm64.whl", hash = "sha256:7e68f88e5b8799aa49c85cd116c932a1ac15caaa3f5db09087854d218359e485"}, + {file = "markupsafe-3.0.3-cp313-cp313t-macosx_10_13_x86_64.whl", hash = "sha256:218551f6df4868a8d527e3062d0fb968682fe92054e89978594c28e642c43a73"}, + {file = "markupsafe-3.0.3-cp313-cp313t-macosx_11_0_arm64.whl", hash = "sha256:3524b778fe5cfb3452a09d31e7b5adefeea8c5be1d43c4f810ba09f2ceb29d37"}, + {file = "markupsafe-3.0.3-cp313-cp313t-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:4e885a3d1efa2eadc93c894a21770e4bc67899e3543680313b09f139e149ab19"}, + {file = "markupsafe-3.0.3-cp313-cp313t-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:8709b08f4a89aa7586de0aadc8da56180242ee0ada3999749b183aa23df95025"}, + {file = "markupsafe-3.0.3-cp313-cp313t-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:b8512a91625c9b3da6f127803b166b629725e68af71f8184ae7e7d54686a56d6"}, + {file = "markupsafe-3.0.3-cp313-cp313t-musllinux_1_2_aarch64.whl", hash = "sha256:9b79b7a16f7fedff2495d684f2b59b0457c3b493778c9eed31111be64d58279f"}, + {file = "markupsafe-3.0.3-cp313-cp313t-musllinux_1_2_riscv64.whl", hash = "sha256:12c63dfb4a98206f045aa9563db46507995f7ef6d83b2f68eda65c307c6829eb"}, + {file = "markupsafe-3.0.3-cp313-cp313t-musllinux_1_2_x86_64.whl", hash = "sha256:8f71bc33915be5186016f675cd83a1e08523649b0e33efdb898db577ef5bb009"}, + {file = "markupsafe-3.0.3-cp313-cp313t-win32.whl", hash = "sha256:69c0b73548bc525c8cb9a251cddf1931d1db4d2258e9599c28c07ef3580ef354"}, + {file = "markupsafe-3.0.3-cp313-cp313t-win_amd64.whl", hash = "sha256:1b4b79e8ebf6b55351f0d91fe80f893b4743f104bff22e90697db1590e47a218"}, + {file = "markupsafe-3.0.3-cp313-cp313t-win_arm64.whl", hash = "sha256:ad2cf8aa28b8c020ab2fc8287b0f823d0a7d8630784c31e9ee5edea20f406287"}, + {file = "markupsafe-3.0.3-cp314-cp314-macosx_10_13_x86_64.whl", hash = "sha256:eaa9599de571d72e2daf60164784109f19978b327a3910d3e9de8c97b5b70cfe"}, + {file = "markupsafe-3.0.3-cp314-cp314-macosx_11_0_arm64.whl", hash = "sha256:c47a551199eb8eb2121d4f0f15ae0f923d31350ab9280078d1e5f12b249e0026"}, + {file = "markupsafe-3.0.3-cp314-cp314-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:f34c41761022dd093b4b6896d4810782ffbabe30f2d443ff5f083e0cbbb8c737"}, + {file = "markupsafe-3.0.3-cp314-cp314-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:457a69a9577064c05a97c41f4e65148652db078a3a509039e64d3467b9e7ef97"}, + {file = "markupsafe-3.0.3-cp314-cp314-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:e8afc3f2ccfa24215f8cb28dcf43f0113ac3c37c2f0f0806d8c70e4228c5cf4d"}, + {file = "markupsafe-3.0.3-cp314-cp314-musllinux_1_2_aarch64.whl", hash = "sha256:ec15a59cf5af7be74194f7ab02d0f59a62bdcf1a537677ce67a2537c9b87fcda"}, + {file = "markupsafe-3.0.3-cp314-cp314-musllinux_1_2_riscv64.whl", hash = "sha256:0eb9ff8191e8498cca014656ae6b8d61f39da5f95b488805da4bb029cccbfbaf"}, + {file = "markupsafe-3.0.3-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:2713baf880df847f2bece4230d4d094280f4e67b1e813eec43b4c0e144a34ffe"}, + {file = "markupsafe-3.0.3-cp314-cp314-win32.whl", hash = "sha256:729586769a26dbceff69f7a7dbbf59ab6572b99d94576a5592625d5b411576b9"}, + {file = "markupsafe-3.0.3-cp314-cp314-win_amd64.whl", hash = "sha256:bdc919ead48f234740ad807933cdf545180bfbe9342c2bb451556db2ed958581"}, + {file = "markupsafe-3.0.3-cp314-cp314-win_arm64.whl", hash = "sha256:5a7d5dc5140555cf21a6fefbdbf8723f06fcd2f63ef108f2854de715e4422cb4"}, + {file = "markupsafe-3.0.3-cp314-cp314t-macosx_10_13_x86_64.whl", hash = "sha256:1353ef0c1b138e1907ae78e2f6c63ff67501122006b0f9abad68fda5f4ffc6ab"}, + {file = "markupsafe-3.0.3-cp314-cp314t-macosx_11_0_arm64.whl", hash = "sha256:1085e7fbddd3be5f89cc898938f42c0b3c711fdcb37d75221de2666af647c175"}, + {file = "markupsafe-3.0.3-cp314-cp314t-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:1b52b4fb9df4eb9ae465f8d0c228a00624de2334f216f178a995ccdcf82c4634"}, + {file = "markupsafe-3.0.3-cp314-cp314t-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:fed51ac40f757d41b7c48425901843666a6677e3e8eb0abcff09e4ba6e664f50"}, + {file = "markupsafe-3.0.3-cp314-cp314t-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:f190daf01f13c72eac4efd5c430a8de82489d9cff23c364c3ea822545032993e"}, + {file = "markupsafe-3.0.3-cp314-cp314t-musllinux_1_2_aarch64.whl", hash = "sha256:e56b7d45a839a697b5eb268c82a71bd8c7f6c94d6fd50c3d577fa39a9f1409f5"}, + {file = "markupsafe-3.0.3-cp314-cp314t-musllinux_1_2_riscv64.whl", hash = "sha256:f3e98bb3798ead92273dc0e5fd0f31ade220f59a266ffd8a4f6065e0a3ce0523"}, + {file = "markupsafe-3.0.3-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:5678211cb9333a6468fb8d8be0305520aa073f50d17f089b5b4b477ea6e67fdc"}, + {file = "markupsafe-3.0.3-cp314-cp314t-win32.whl", hash = "sha256:915c04ba3851909ce68ccc2b8e2cd691618c4dc4c4232fb7982bca3f41fd8c3d"}, + {file = "markupsafe-3.0.3-cp314-cp314t-win_amd64.whl", hash = "sha256:4faffd047e07c38848ce017e8725090413cd80cbc23d86e55c587bf979e579c9"}, + {file = "markupsafe-3.0.3-cp314-cp314t-win_arm64.whl", hash = "sha256:32001d6a8fc98c8cb5c947787c5d08b0a50663d139f1305bac5885d98d9b40fa"}, + {file = "markupsafe-3.0.3.tar.gz", hash = "sha256:722695808f4b6457b320fdc131280796bdceb04ab50fe1795cd540799ebe1698"}, +] + +[[package]] +name = "mdurl" +version = "0.1.2" +requires_python = ">=3.7" +summary = "Markdown URL utilities" +groups = ["default"] +files = [ + {file = "mdurl-0.1.2-py3-none-any.whl", hash = "sha256:84008a41e51615a49fc9966191ff91509e3c40b939176e643fd50a5c2196b8f8"}, + {file = "mdurl-0.1.2.tar.gz", hash = "sha256:bb413d29f5eea38f31dd4754dd7377d4465116fb207585f97bf925588687c1ba"}, +] + +[[package]] +name = "mergedeep" +version = "1.3.4" +requires_python = ">=3.6" +summary = "A deep merge function for 🐍." +groups = ["doc"] +files = [ + {file = "mergedeep-1.3.4-py3-none-any.whl", hash = "sha256:70775750742b25c0d8f36c55aed03d24c3384d17c951b3175d898bd778ef0307"}, + {file = "mergedeep-1.3.4.tar.gz", hash = "sha256:0096d52e9dad9939c3d975a774666af186eda617e6ca84df4c94dec30004f2a8"}, +] + +[[package]] +name = "mkdocs" +version = "1.6.1" +requires_python = ">=3.8" +summary = "Project documentation with Markdown." +groups = ["doc"] +dependencies = [ + "click>=7.0", + "colorama>=0.4; platform_system == \"Windows\"", + "ghp-import>=1.0", + "importlib-metadata>=4.4; python_version < \"3.10\"", + "jinja2>=2.11.1", + "markdown>=3.3.6", + "markupsafe>=2.0.1", + "mergedeep>=1.3.4", + "mkdocs-get-deps>=0.2.0", + "packaging>=20.5", + "pathspec>=0.11.1", + "pyyaml-env-tag>=0.1", + "pyyaml>=5.1", + "watchdog>=2.0", +] +files = [ + {file = "mkdocs-1.6.1-py3-none-any.whl", hash = "sha256:db91759624d1647f3f34aa0c3f327dd2601beae39a366d6e064c03468d35c20e"}, + {file = "mkdocs-1.6.1.tar.gz", hash = "sha256:7b432f01d928c084353ab39c57282f29f92136665bdd6abf7c1ec8d822ef86f2"}, +] + +[[package]] +name = "mkdocs-autorefs" +version = "1.4.4" +requires_python = ">=3.9" +summary = "Automatically link across pages in MkDocs." +groups = ["doc"] +dependencies = [ + "Markdown>=3.3", + "markupsafe>=2.0.1", + "mkdocs>=1.1", +] +files = [ + {file = "mkdocs_autorefs-1.4.4-py3-none-any.whl", hash = "sha256:834ef5408d827071ad1bc69e0f39704fa34c7fc05bc8e1c72b227dfdc5c76089"}, + {file = "mkdocs_autorefs-1.4.4.tar.gz", hash = "sha256:d54a284f27a7346b9c38f1f852177940c222da508e66edc816a0fa55fc6da197"}, +] + +[[package]] +name = "mkdocs-get-deps" +version = "0.2.2" +requires_python = ">=3.9" +summary = "An extra command for MkDocs that infers required PyPI packages from `plugins` in mkdocs.yml" +groups = ["doc"] +dependencies = [ + "importlib-metadata>=4.3; python_version < \"3.10\"", + "mergedeep>=1.3.4", + "platformdirs>=2.2.0", + "pyyaml>=5.1", +] +files = [ + {file = "mkdocs_get_deps-0.2.2-py3-none-any.whl", hash = "sha256:e7878cbeac04860b8b5e0ca31d3abad3df9411a75a32cde82f8e44b6c16ff650"}, + {file = "mkdocs_get_deps-0.2.2.tar.gz", hash = "sha256:8ee8d5f316cdbbb2834bc1df6e69c08fe769a83e040060de26d3c19fad3599a1"}, +] + +[[package]] +name = "mkdocstrings" +version = "1.0.4" +requires_python = ">=3.10" +summary = "Automatic documentation from sources, for MkDocs." +groups = ["doc"] +dependencies = [ + "Jinja2>=3.1", + "Markdown>=3.6", + "MarkupSafe>=1.1", + "mkdocs-autorefs>=1.4", + "mkdocs>=1.6", + "pymdown-extensions>=6.3", +] +files = [ + {file = "mkdocstrings-1.0.4-py3-none-any.whl", hash = "sha256:63464b4b29053514f32a1dbbf604e52876d5e638111b0c295ab7ed3cac73ca9b"}, + {file = "mkdocstrings-1.0.4.tar.gz", hash = "sha256:3969a6515b77db65fd097b53c1b7aa4ae840bd71a2ee62a6a3e89503446d7172"}, +] + +[[package]] +name = "mkdocstrings-python" +version = "2.0.3" +requires_python = ">=3.10" +summary = "A Python handler for mkdocstrings." +groups = ["doc"] +dependencies = [ + "griffelib>=2.0", + "mkdocs-autorefs>=1.4", + "mkdocstrings>=0.30", + "typing-extensions>=4.0; python_version < \"3.11\"", +] +files = [ + {file = "mkdocstrings_python-2.0.3-py3-none-any.whl", hash = "sha256:0b83513478bdfd803ff05aa43e9b1fca9dd22bcd9471f09ca6257f009bc5ee12"}, + {file = "mkdocstrings_python-2.0.3.tar.gz", hash = "sha256:c518632751cc869439b31c9d3177678ad2bfa5c21b79b863956ad68fc92c13b8"}, +] + +[[package]] +name = "mkdocstrings" +version = "1.0.4" +extras = ["python"] +requires_python = ">=3.10" +summary = "Automatic documentation from sources, for MkDocs." +groups = ["doc"] +dependencies = [ + "mkdocstrings-python>=1.16.2", + "mkdocstrings==1.0.4", +] +files = [ + {file = "mkdocstrings-1.0.4-py3-none-any.whl", hash = "sha256:63464b4b29053514f32a1dbbf604e52876d5e638111b0c295ab7ed3cac73ca9b"}, + {file = "mkdocstrings-1.0.4.tar.gz", hash = "sha256:3969a6515b77db65fd097b53c1b7aa4ae840bd71a2ee62a6a3e89503446d7172"}, +] + +[[package]] +name = "more-itertools" +version = "11.0.2" +requires_python = ">=3.10" +summary = "More routines for operating on iterables, beyond itertools" +groups = ["all", "keyring"] +files = [ + {file = "more_itertools-11.0.2-py3-none-any.whl", hash = "sha256:6e35b35f818b01f691643c6c611bc0902f2e92b46c18fffa77ae1e7c46e912e4"}, + {file = "more_itertools-11.0.2.tar.gz", hash = "sha256:392a9e1e362cbc106a2457d37cabf9b36e5e12efd4ebff1654630e76597df804"}, +] + +[[package]] +name = "msgpack" +version = "1.1.2" +requires_python = ">=3.9" +summary = "MessagePack serializer" +groups = ["default"] +files = [ + {file = "msgpack-1.1.2-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:0051fffef5a37ca2cd16978ae4f0aef92f164df86823871b5162812bebecd8e2"}, + {file = "msgpack-1.1.2-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:a605409040f2da88676e9c9e5853b3449ba8011973616189ea5ee55ddbc5bc87"}, + {file = "msgpack-1.1.2-cp310-cp310-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:8b696e83c9f1532b4af884045ba7f3aa741a63b2bc22617293a2c6a7c645f251"}, + {file = "msgpack-1.1.2-cp310-cp310-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:365c0bbe981a27d8932da71af63ef86acc59ed5c01ad929e09a0b88c6294e28a"}, + {file = "msgpack-1.1.2-cp310-cp310-musllinux_1_2_aarch64.whl", hash = "sha256:41d1a5d875680166d3ac5c38573896453bbbea7092936d2e107214daf43b1d4f"}, + {file = "msgpack-1.1.2-cp310-cp310-musllinux_1_2_x86_64.whl", hash = "sha256:354e81bcdebaab427c3df4281187edc765d5d76bfb3a7c125af9da7a27e8458f"}, + {file = "msgpack-1.1.2-cp310-cp310-win32.whl", hash = "sha256:e64c8d2f5e5d5fda7b842f55dec6133260ea8f53c4257d64494c534f306bf7a9"}, + {file = "msgpack-1.1.2-cp310-cp310-win_amd64.whl", hash = "sha256:db6192777d943bdaaafb6ba66d44bf65aa0e9c5616fa1d2da9bb08828c6b39aa"}, + {file = "msgpack-1.1.2-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:2e86a607e558d22985d856948c12a3fa7b42efad264dca8a3ebbcfa2735d786c"}, + {file = "msgpack-1.1.2-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:283ae72fc89da59aa004ba147e8fc2f766647b1251500182fac0350d8af299c0"}, + {file = "msgpack-1.1.2-cp311-cp311-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:61c8aa3bd513d87c72ed0b37b53dd5c5a0f58f2ff9f26e1555d3bd7948fb7296"}, + {file = "msgpack-1.1.2-cp311-cp311-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:454e29e186285d2ebe65be34629fa0e8605202c60fbc7c4c650ccd41870896ef"}, + {file = "msgpack-1.1.2-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:7bc8813f88417599564fafa59fd6f95be417179f76b40325b500b3c98409757c"}, + {file = "msgpack-1.1.2-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:bafca952dc13907bdfdedfc6a5f579bf4f292bdd506fadb38389afa3ac5b208e"}, + {file = "msgpack-1.1.2-cp311-cp311-win32.whl", hash = "sha256:602b6740e95ffc55bfb078172d279de3773d7b7db1f703b2f1323566b878b90e"}, + {file = "msgpack-1.1.2-cp311-cp311-win_amd64.whl", hash = "sha256:d198d275222dc54244bf3327eb8cbe00307d220241d9cec4d306d49a44e85f68"}, + {file = "msgpack-1.1.2-cp311-cp311-win_arm64.whl", hash = "sha256:86f8136dfa5c116365a8a651a7d7484b65b13339731dd6faebb9a0242151c406"}, + {file = "msgpack-1.1.2-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:70a0dff9d1f8da25179ffcf880e10cf1aad55fdb63cd59c9a49a1b82290062aa"}, + {file = "msgpack-1.1.2-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:446abdd8b94b55c800ac34b102dffd2f6aa0ce643c55dfc017ad89347db3dbdb"}, + {file = "msgpack-1.1.2-cp312-cp312-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:c63eea553c69ab05b6747901b97d620bb2a690633c77f23feb0c6a947a8a7b8f"}, + {file = "msgpack-1.1.2-cp312-cp312-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:372839311ccf6bdaf39b00b61288e0557916c3729529b301c52c2d88842add42"}, + {file = "msgpack-1.1.2-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:2929af52106ca73fcb28576218476ffbb531a036c2adbcf54a3664de124303e9"}, + {file = "msgpack-1.1.2-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:be52a8fc79e45b0364210eef5234a7cf8d330836d0a64dfbb878efa903d84620"}, + {file = "msgpack-1.1.2-cp312-cp312-win32.whl", hash = "sha256:1fff3d825d7859ac888b0fbda39a42d59193543920eda9d9bea44d958a878029"}, + {file = "msgpack-1.1.2-cp312-cp312-win_amd64.whl", hash = "sha256:1de460f0403172cff81169a30b9a92b260cb809c4cb7e2fc79ae8d0510c78b6b"}, + {file = "msgpack-1.1.2-cp312-cp312-win_arm64.whl", hash = "sha256:be5980f3ee0e6bd44f3a9e9dea01054f175b50c3e6cdb692bc9424c0bbb8bf69"}, + {file = "msgpack-1.1.2-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:4efd7b5979ccb539c221a4c4e16aac1a533efc97f3b759bb5a5ac9f6d10383bf"}, + {file = "msgpack-1.1.2-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:42eefe2c3e2af97ed470eec850facbe1b5ad1d6eacdbadc42ec98e7dcf68b4b7"}, + {file = "msgpack-1.1.2-cp313-cp313-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:1fdf7d83102bf09e7ce3357de96c59b627395352a4024f6e2458501f158bf999"}, + {file = "msgpack-1.1.2-cp313-cp313-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:fac4be746328f90caa3cd4bc67e6fe36ca2bf61d5c6eb6d895b6527e3f05071e"}, + {file = "msgpack-1.1.2-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:fffee09044073e69f2bad787071aeec727183e7580443dfeb8556cbf1978d162"}, + {file = "msgpack-1.1.2-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:5928604de9b032bc17f5099496417f113c45bc6bc21b5c6920caf34b3c428794"}, + {file = "msgpack-1.1.2-cp313-cp313-win32.whl", hash = "sha256:a7787d353595c7c7e145e2331abf8b7ff1e6673a6b974ded96e6d4ec09f00c8c"}, + {file = "msgpack-1.1.2-cp313-cp313-win_amd64.whl", hash = "sha256:a465f0dceb8e13a487e54c07d04ae3ba131c7c5b95e2612596eafde1dccf64a9"}, + {file = "msgpack-1.1.2-cp313-cp313-win_arm64.whl", hash = "sha256:e69b39f8c0aa5ec24b57737ebee40be647035158f14ed4b40e6f150077e21a84"}, + {file = "msgpack-1.1.2-cp314-cp314-macosx_10_13_x86_64.whl", hash = "sha256:e23ce8d5f7aa6ea6d2a2b326b4ba46c985dbb204523759984430db7114f8aa00"}, + {file = "msgpack-1.1.2-cp314-cp314-macosx_11_0_arm64.whl", hash = "sha256:6c15b7d74c939ebe620dd8e559384be806204d73b4f9356320632d783d1f7939"}, + {file = "msgpack-1.1.2-cp314-cp314-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:99e2cb7b9031568a2a5c73aa077180f93dd2e95b4f8d3b8e14a73ae94a9e667e"}, + {file = "msgpack-1.1.2-cp314-cp314-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:180759d89a057eab503cf62eeec0aa61c4ea1200dee709f3a8e9397dbb3b6931"}, + {file = "msgpack-1.1.2-cp314-cp314-musllinux_1_2_aarch64.whl", hash = "sha256:04fb995247a6e83830b62f0b07bf36540c213f6eac8e851166d8d86d83cbd014"}, + {file = "msgpack-1.1.2-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:8e22ab046fa7ede9e36eeb4cfad44d46450f37bb05d5ec482b02868f451c95e2"}, + {file = "msgpack-1.1.2-cp314-cp314-win32.whl", hash = "sha256:80a0ff7d4abf5fecb995fcf235d4064b9a9a8a40a3ab80999e6ac1e30b702717"}, + {file = "msgpack-1.1.2-cp314-cp314-win_amd64.whl", hash = "sha256:9ade919fac6a3e7260b7f64cea89df6bec59104987cbea34d34a2fa15d74310b"}, + {file = "msgpack-1.1.2-cp314-cp314-win_arm64.whl", hash = "sha256:59415c6076b1e30e563eb732e23b994a61c159cec44deaf584e5cc1dd662f2af"}, + {file = "msgpack-1.1.2-cp314-cp314t-macosx_10_13_x86_64.whl", hash = "sha256:897c478140877e5307760b0ea66e0932738879e7aa68144d9b78ea4c8302a84a"}, + {file = "msgpack-1.1.2-cp314-cp314t-macosx_11_0_arm64.whl", hash = "sha256:a668204fa43e6d02f89dbe79a30b0d67238d9ec4c5bd8a940fc3a004a47b721b"}, + {file = "msgpack-1.1.2-cp314-cp314t-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:5559d03930d3aa0f3aacb4c42c776af1a2ace2611871c84a75afe436695e6245"}, + {file = "msgpack-1.1.2-cp314-cp314t-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:70c5a7a9fea7f036b716191c29047374c10721c389c21e9ffafad04df8c52c90"}, + {file = "msgpack-1.1.2-cp314-cp314t-musllinux_1_2_aarch64.whl", hash = "sha256:f2cb069d8b981abc72b41aea1c580ce92d57c673ec61af4c500153a626cb9e20"}, + {file = "msgpack-1.1.2-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:d62ce1f483f355f61adb5433ebfd8868c5f078d1a52d042b0a998682b4fa8c27"}, + {file = "msgpack-1.1.2-cp314-cp314t-win32.whl", hash = "sha256:1d1418482b1ee984625d88aa9585db570180c286d942da463533b238b98b812b"}, + {file = "msgpack-1.1.2-cp314-cp314t-win_amd64.whl", hash = "sha256:5a46bf7e831d09470ad92dff02b8b1ac92175ca36b087f904a0519857c6be3ff"}, + {file = "msgpack-1.1.2-cp314-cp314t-win_arm64.whl", hash = "sha256:d99ef64f349d5ec3293688e91486c5fdb925ed03807f64d98d205d2713c60b46"}, + {file = "msgpack-1.1.2.tar.gz", hash = "sha256:3b60763c1373dd60f398488069bcdc703cd08a711477b5d480eecc9f9626f47e"}, +] + +[[package]] +name = "packaging" +version = "26.2" +requires_python = ">=3.8" +summary = "Core utilities for Python packages" +groups = ["default", "doc", "pytest", "test", "tox"] +files = [ + {file = "packaging-26.2-py3-none-any.whl", hash = "sha256:5fc45236b9446107ff2415ce77c807cee2862cb6fac22b8a73826d0693b0980e"}, + {file = "packaging-26.2.tar.gz", hash = "sha256:ff452ff5a3e828ce110190feff1178bb1f2ea2281fa2075aadb987c2fb221661"}, +] + +[[package]] +name = "parver" +version = "0.5" +requires_python = ">=3.8" +summary = "Parse and manipulate version numbers." +groups = ["workflow"] +dependencies = [ + "arpeggio>=1.7", + "attrs>=19.2", + "typing-extensions; python_version < \"3.10\"", +] +files = [ + {file = "parver-0.5-py3-none-any.whl", hash = "sha256:2281b187276c8e8e3c15634f62287b2fb6fe0efe3010f739a6bd1e45fa2bf2b2"}, + {file = "parver-0.5.tar.gz", hash = "sha256:b9fde1e6bb9ce9f07e08e9c4bea8d8825c5e78e18a0052d02e02bf9517eb4777"}, +] + +[[package]] +name = "pathspec" +version = "1.1.1" +requires_python = ">=3.9" +summary = "Utility library for gitignore style pattern matching of file paths." +groups = ["doc"] +files = [ + {file = "pathspec-1.1.1-py3-none-any.whl", hash = "sha256:a00ce642f577bf7f473932318056212bc4f8bfdf53128c78bbd5af0b9b20b189"}, + {file = "pathspec-1.1.1.tar.gz", hash = "sha256:17db5ecd524104a120e173814c90367a96a98d07c45b2e10c2f3919fff91bf5a"}, +] + +[[package]] +name = "pbs-installer" +version = "2026.5.4" +requires_python = ">=3.9" +summary = "Installer for Python Build Standalone" +groups = ["default"] +files = [ + {file = "pbs_installer-2026.5.4-py3-none-any.whl", hash = "sha256:f585ed8252e31a88807969c5113015db343bf49c38ff26eb7d0b1eedf7bb3b9a"}, + {file = "pbs_installer-2026.5.4.tar.gz", hash = "sha256:e32323dc299f8d6b485bb622ff2348e45a04691263fec5d6f617c8109fc290f7"}, +] + +[[package]] +name = "platformdirs" +version = "4.9.6" +requires_python = ">=3.10" +summary = "A small Python package for determining appropriate platform-specific dirs, e.g. a `user data dir`." +groups = ["default", "doc", "tox"] +files = [ + {file = "platformdirs-4.9.6-py3-none-any.whl", hash = "sha256:e61adb1d5e5cb3441b4b7710bea7e4c12250ca49439228cc1021c00dcfac0917"}, + {file = "platformdirs-4.9.6.tar.gz", hash = "sha256:3bfa75b0ad0db84096ae777218481852c0ebc6c727b3168c1b9e0118e458cf0a"}, +] + +[[package]] +name = "pluggy" +version = "1.6.0" +requires_python = ">=3.9" +summary = "plugin and hook calling mechanisms for python" +groups = ["pytest", "test", "tox"] +files = [ + {file = "pluggy-1.6.0-py3-none-any.whl", hash = "sha256:e920276dd6813095e9377c0bc5566d94c932c33b27a3e3945d8389c374dd4746"}, + {file = "pluggy-1.6.0.tar.gz", hash = "sha256:7dcc130b76258d33b90f61b658791dede3486c3e6bfb003ee5c9bfb396dd22f3"}, +] + +[[package]] +name = "pycparser" +version = "3.0" +requires_python = ">=3.10" +summary = "C parser in Python" +groups = ["all", "keyring"] +marker = "platform_python_implementation != \"PyPy\" and sys_platform == \"linux\" and implementation_name != \"PyPy\"" +files = [ + {file = "pycparser-3.0-py3-none-any.whl", hash = "sha256:b727414169a36b7d524c1c3e31839a521725078d7b2ff038656844266160a992"}, + {file = "pycparser-3.0.tar.gz", hash = "sha256:600f49d217304a5902ac3c37e1281c9fe94e4d0489de643a9504c5cdfdfc6b29"}, +] + +[[package]] +name = "pygments" +version = "2.20.0" +requires_python = ">=3.9" +summary = "Pygments is a syntax highlighting package written in Python." +groups = ["default", "doc", "pytest", "test"] +files = [ + {file = "pygments-2.20.0-py3-none-any.whl", hash = "sha256:81a9e26dd42fd28a23a2d169d86d7ac03b46e2f8b59ed4698fb4785f946d0176"}, + {file = "pygments-2.20.0.tar.gz", hash = "sha256:6757cd03768053ff99f3039c1a36d6c0aa0b263438fcab17520b30a303a82b5f"}, +] + +[[package]] +name = "pymdown-extensions" +version = "10.21.2" +requires_python = ">=3.9" +summary = "Extension pack for Python Markdown." +groups = ["doc"] +dependencies = [ + "markdown>=3.6", + "pyyaml", +] +files = [ + {file = "pymdown_extensions-10.21.2-py3-none-any.whl", hash = "sha256:5c0fd2a2bea14eb39af8ff284f1066d898ab2187d81b889b75d46d4348c01638"}, + {file = "pymdown_extensions-10.21.2.tar.gz", hash = "sha256:c3f55a5b8a1d0edf6699e35dcbea71d978d34ff3fa79f3d807b8a5b3fa90fbdc"}, +] + +[[package]] +name = "pyproject-api" +version = "1.10.0" +requires_python = ">=3.10" +summary = "API to interact with the python pyproject.toml based projects" +groups = ["tox"] +dependencies = [ + "packaging>=25", + "tomli>=2.3; python_version < \"3.11\"", +] +files = [ + {file = "pyproject_api-1.10.0-py3-none-any.whl", hash = "sha256:8757c41a79c0f4ab71b99abed52b97ecf66bd20b04fa59da43b5840bac105a09"}, + {file = "pyproject_api-1.10.0.tar.gz", hash = "sha256:40c6f2d82eebdc4afee61c773ed208c04c19db4c4a60d97f8d7be3ebc0bbb330"}, +] + +[[package]] +name = "pyproject-hooks" +version = "1.2.0" +requires_python = ">=3.7" +summary = "Wrappers to call pyproject.toml-based build backend hooks." +groups = ["default"] +files = [ + {file = "pyproject_hooks-1.2.0-py3-none-any.whl", hash = "sha256:9e5c6bfa8dcc30091c74b0cf803c81fdd29d94f01992a7707bc97babb1141913"}, + {file = "pyproject_hooks-1.2.0.tar.gz", hash = "sha256:1e859bd5c40fae9448642dd871adf459e5e2084186e8d2c2a79a824c970da1f8"}, +] + +[[package]] +name = "pytest" +version = "9.0.3" +requires_python = ">=3.10" +summary = "pytest: simple powerful testing with Python" +groups = ["pytest", "test"] +dependencies = [ + "colorama>=0.4; sys_platform == \"win32\"", + "exceptiongroup>=1; python_version < \"3.11\"", + "iniconfig>=1.0.1", + "packaging>=22", + "pluggy<2,>=1.5", + "pygments>=2.7.2", + "tomli>=1; python_version < \"3.11\"", +] +files = [ + {file = "pytest-9.0.3-py3-none-any.whl", hash = "sha256:2c5efc453d45394fdd706ade797c0a81091eccd1d6e4bccfcd476e2b8e0ab5d9"}, + {file = "pytest-9.0.3.tar.gz", hash = "sha256:b86ada508af81d19edeb213c681b1d48246c1a91d304c6c81a427674c17eb91c"}, +] + +[[package]] +name = "pytest-cov" +version = "7.1.0" +requires_python = ">=3.9" +summary = "Pytest plugin for measuring coverage." +groups = ["test"] +dependencies = [ + "coverage[toml]>=7.10.6", + "pluggy>=1.2", + "pytest>=7", +] +files = [ + {file = "pytest_cov-7.1.0-py3-none-any.whl", hash = "sha256:a0461110b7865f9a271aa1b51e516c9a95de9d696734a2f71e3e78f46e1d4678"}, + {file = "pytest_cov-7.1.0.tar.gz", hash = "sha256:30674f2b5f6351aa09702a9c8c364f6a01c27aae0c1366ae8016160d1efc56b2"}, +] + +[[package]] +name = "pytest-httpserver" +version = "1.1.5" +requires_python = ">=3.10" +summary = "pytest-httpserver is a httpserver for pytest" +groups = ["test"] +dependencies = [ + "Werkzeug>=2.0.0", +] +files = [ + {file = "pytest_httpserver-1.1.5-py3-none-any.whl", hash = "sha256:ee83feb587ab652c0c6729598db2820e9048233bac8df756818b7845a1621d0a"}, + {file = "pytest_httpserver-1.1.5.tar.gz", hash = "sha256:dc3d82e1fe00e491829d8939c549bf4bd9b39a260f87113c619b9d517c2f8ff1"}, +] + +[[package]] +name = "pytest-httpx" +version = "0.36.2" +requires_python = ">=3.10" +summary = "Send responses to httpx." +groups = ["test"] +dependencies = [ + "httpx==0.28.*", + "pytest==9.*", +] +files = [ + {file = "pytest_httpx-0.36.2-py3-none-any.whl", hash = "sha256:d42ebd5679442dc7bfb0c48e0767b6562e9bc4534d805127b0084171886a5e22"}, + {file = "pytest_httpx-0.36.2.tar.gz", hash = "sha256:05a56527484f7f4e8c856419ea379b8dc359c36801c4992fdb330f294c690356"}, +] + +[[package]] +name = "pytest-mock" +version = "3.15.1" +requires_python = ">=3.9" +summary = "Thin-wrapper around the mock package for easier use with pytest" +groups = ["pytest", "test"] +dependencies = [ + "pytest>=6.2.5", +] +files = [ + {file = "pytest_mock-3.15.1-py3-none-any.whl", hash = "sha256:0a25e2eb88fe5168d535041d09a4529a188176ae608a6d249ee65abc0949630d"}, + {file = "pytest_mock-3.15.1.tar.gz", hash = "sha256:1849a238f6f396da19762269de72cb1814ab44416fa73a8686deac10b0d87a0f"}, +] + +[[package]] +name = "pytest-rerunfailures" +version = "16.1" +requires_python = ">=3.10" +summary = "pytest plugin to re-run tests to eliminate flaky failures" +groups = ["test"] +dependencies = [ + "packaging>=17.1", + "pytest!=8.2.2,>=7.4", +] +files = [ + {file = "pytest_rerunfailures-16.1-py3-none-any.whl", hash = "sha256:5d11b12c0ca9a1665b5054052fcc1084f8deadd9328962745ef6b04e26382e86"}, + {file = "pytest_rerunfailures-16.1.tar.gz", hash = "sha256:c38b266db8a808953ebd71ac25c381cb1981a78ff9340a14bcb9f1b9bff1899e"}, +] + +[[package]] +name = "pytest-xdist" +version = "3.8.0" +requires_python = ">=3.9" +summary = "pytest xdist plugin for distributed testing, most importantly across multiple CPUs" +groups = ["test"] +dependencies = [ + "execnet>=2.1", + "pytest>=7.0.0", +] +files = [ + {file = "pytest_xdist-3.8.0-py3-none-any.whl", hash = "sha256:202ca578cfeb7370784a8c33d6d05bc6e13b4f25b5053c30a152269fd10f0b88"}, + {file = "pytest_xdist-3.8.0.tar.gz", hash = "sha256:7e578125ec9bc6050861aa93f2d59f1d8d085595d6551c2c90b6f4fad8d3a9f1"}, +] + +[[package]] +name = "python-dateutil" +version = "2.9.0.post0" +requires_python = "!=3.0.*,!=3.1.*,!=3.2.*,>=2.7" +summary = "Extensions to the standard Python datetime module" +groups = ["doc"] +dependencies = [ + "six>=1.5", +] +files = [ + {file = "python-dateutil-2.9.0.post0.tar.gz", hash = "sha256:37dd54208da7e1cd875388217d5e00ebd4179249f90fb72437e91a35459a0ad3"}, + {file = "python_dateutil-2.9.0.post0-py2.py3-none-any.whl", hash = "sha256:a8b2bc7bffae282281c8140a97d3aa9c14da0b136dfe83f850eea9a5f7470427"}, +] + +[[package]] +name = "python-discovery" +version = "1.3.0" +requires_python = ">=3.8" +summary = "Python interpreter discovery" +groups = ["default", "tox"] +dependencies = [ + "filelock>=3.15.4", + "platformdirs<5,>=4.3.6", +] +files = [ + {file = "python_discovery-1.3.0-py3-none-any.whl", hash = "sha256:441d9ced3dfce36e113beb35ca302c71c7ef06f3c0f9c227a0b9bb3bd49b9e9f"}, + {file = "python_discovery-1.3.0.tar.gz", hash = "sha256:d098f1e86be5d45fe4d14bf1029294aabbd332f4321179dec85e76cddce834b0"}, +] + +[[package]] +name = "python-dotenv" +version = "1.2.2" +requires_python = ">=3.10" +summary = "Read key-value pairs from a .env file and set them as environment variables" +groups = ["default"] +files = [ + {file = "python_dotenv-1.2.2-py3-none-any.whl", hash = "sha256:1d8214789a24de455a8b8bd8ae6fe3c6b69a5e3d64aa8a8e5d68e694bbcb285a"}, + {file = "python_dotenv-1.2.2.tar.gz", hash = "sha256:2c371a91fbd7ba082c2c1dc1f8bf89ca22564a087c2c287cd9b662adde799cf3"}, +] + +[[package]] +name = "pywin32-ctypes" +version = "0.2.3" +requires_python = ">=3.6" +summary = "A (partial) reimplementation of pywin32 using ctypes/cffi" +groups = ["all", "keyring"] +marker = "sys_platform == \"win32\"" +files = [ + {file = "pywin32-ctypes-0.2.3.tar.gz", hash = "sha256:d162dc04946d704503b2edc4d55f3dba5c1d539ead017afa00142c38b9885755"}, + {file = "pywin32_ctypes-0.2.3-py3-none-any.whl", hash = "sha256:8a1513379d709975552d202d942d9837758905c8d01eb82b8bcc30918929e7b8"}, +] + +[[package]] +name = "pyyaml" +version = "6.0.3" +requires_python = ">=3.8" +summary = "YAML parser and emitter for Python" +groups = ["doc"] +files = [ + {file = "pyyaml-6.0.3-cp310-cp310-macosx_10_13_x86_64.whl", hash = "sha256:214ed4befebe12df36bcc8bc2b64b396ca31be9304b8f59e25c11cf94a4c033b"}, + {file = "pyyaml-6.0.3-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:02ea2dfa234451bbb8772601d7b8e426c2bfa197136796224e50e35a78777956"}, + {file = "pyyaml-6.0.3-cp310-cp310-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:b30236e45cf30d2b8e7b3e85881719e98507abed1011bf463a8fa23e9c3e98a8"}, + {file = "pyyaml-6.0.3-cp310-cp310-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:66291b10affd76d76f54fad28e22e51719ef9ba22b29e1d7d03d6777a9174198"}, + {file = "pyyaml-6.0.3-cp310-cp310-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:9c7708761fccb9397fe64bbc0395abcae8c4bf7b0eac081e12b809bf47700d0b"}, + {file = "pyyaml-6.0.3-cp310-cp310-musllinux_1_2_aarch64.whl", hash = "sha256:418cf3f2111bc80e0933b2cd8cd04f286338bb88bdc7bc8e6dd775ebde60b5e0"}, + {file = "pyyaml-6.0.3-cp310-cp310-musllinux_1_2_x86_64.whl", hash = "sha256:5e0b74767e5f8c593e8c9b5912019159ed0533c70051e9cce3e8b6aa699fcd69"}, + {file = "pyyaml-6.0.3-cp310-cp310-win32.whl", hash = "sha256:28c8d926f98f432f88adc23edf2e6d4921ac26fb084b028c733d01868d19007e"}, + {file = "pyyaml-6.0.3-cp310-cp310-win_amd64.whl", hash = "sha256:bdb2c67c6c1390b63c6ff89f210c8fd09d9a1217a465701eac7316313c915e4c"}, + {file = "pyyaml-6.0.3-cp311-cp311-macosx_10_13_x86_64.whl", hash = "sha256:44edc647873928551a01e7a563d7452ccdebee747728c1080d881d68af7b997e"}, + {file = "pyyaml-6.0.3-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:652cb6edd41e718550aad172851962662ff2681490a8a711af6a4d288dd96824"}, + {file = "pyyaml-6.0.3-cp311-cp311-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:10892704fc220243f5305762e276552a0395f7beb4dbf9b14ec8fd43b57f126c"}, + {file = "pyyaml-6.0.3-cp311-cp311-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:850774a7879607d3a6f50d36d04f00ee69e7fc816450e5f7e58d7f17f1ae5c00"}, + {file = "pyyaml-6.0.3-cp311-cp311-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:b8bb0864c5a28024fac8a632c443c87c5aa6f215c0b126c449ae1a150412f31d"}, + {file = "pyyaml-6.0.3-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:1d37d57ad971609cf3c53ba6a7e365e40660e3be0e5175fa9f2365a379d6095a"}, + {file = "pyyaml-6.0.3-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:37503bfbfc9d2c40b344d06b2199cf0e96e97957ab1c1b546fd4f87e53e5d3e4"}, + {file = "pyyaml-6.0.3-cp311-cp311-win32.whl", hash = "sha256:8098f252adfa6c80ab48096053f512f2321f0b998f98150cea9bd23d83e1467b"}, + {file = "pyyaml-6.0.3-cp311-cp311-win_amd64.whl", hash = "sha256:9f3bfb4965eb874431221a3ff3fdcddc7e74e3b07799e0e84ca4a0f867d449bf"}, + {file = "pyyaml-6.0.3-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:7f047e29dcae44602496db43be01ad42fc6f1cc0d8cd6c83d342306c32270196"}, + {file = "pyyaml-6.0.3-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:fc09d0aa354569bc501d4e787133afc08552722d3ab34836a80547331bb5d4a0"}, + {file = "pyyaml-6.0.3-cp312-cp312-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:9149cad251584d5fb4981be1ecde53a1ca46c891a79788c0df828d2f166bda28"}, + {file = "pyyaml-6.0.3-cp312-cp312-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:5fdec68f91a0c6739b380c83b951e2c72ac0197ace422360e6d5a959d8d97b2c"}, + {file = "pyyaml-6.0.3-cp312-cp312-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:ba1cc08a7ccde2d2ec775841541641e4548226580ab850948cbfda66a1befcdc"}, + {file = "pyyaml-6.0.3-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:8dc52c23056b9ddd46818a57b78404882310fb473d63f17b07d5c40421e47f8e"}, + {file = "pyyaml-6.0.3-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:41715c910c881bc081f1e8872880d3c650acf13dfa8214bad49ed4cede7c34ea"}, + {file = "pyyaml-6.0.3-cp312-cp312-win32.whl", hash = "sha256:96b533f0e99f6579b3d4d4995707cf36df9100d67e0c8303a0c55b27b5f99bc5"}, + {file = "pyyaml-6.0.3-cp312-cp312-win_amd64.whl", hash = "sha256:5fcd34e47f6e0b794d17de1b4ff496c00986e1c83f7ab2fb8fcfe9616ff7477b"}, + {file = "pyyaml-6.0.3-cp312-cp312-win_arm64.whl", hash = "sha256:64386e5e707d03a7e172c0701abfb7e10f0fb753ee1d773128192742712a98fd"}, + {file = "pyyaml-6.0.3-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:8da9669d359f02c0b91ccc01cac4a67f16afec0dac22c2ad09f46bee0697eba8"}, + {file = "pyyaml-6.0.3-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:2283a07e2c21a2aa78d9c4442724ec1eb15f5e42a723b99cb3d822d48f5f7ad1"}, + {file = "pyyaml-6.0.3-cp313-cp313-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:ee2922902c45ae8ccada2c5b501ab86c36525b883eff4255313a253a3160861c"}, + {file = "pyyaml-6.0.3-cp313-cp313-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:a33284e20b78bd4a18c8c2282d549d10bc8408a2a7ff57653c0cf0b9be0afce5"}, + {file = "pyyaml-6.0.3-cp313-cp313-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:0f29edc409a6392443abf94b9cf89ce99889a1dd5376d94316ae5145dfedd5d6"}, + {file = "pyyaml-6.0.3-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:f7057c9a337546edc7973c0d3ba84ddcdf0daa14533c2065749c9075001090e6"}, + {file = "pyyaml-6.0.3-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:eda16858a3cab07b80edaf74336ece1f986ba330fdb8ee0d6c0d68fe82bc96be"}, + {file = "pyyaml-6.0.3-cp313-cp313-win32.whl", hash = "sha256:d0eae10f8159e8fdad514efdc92d74fd8d682c933a6dd088030f3834bc8e6b26"}, + {file = "pyyaml-6.0.3-cp313-cp313-win_amd64.whl", hash = "sha256:79005a0d97d5ddabfeeea4cf676af11e647e41d81c9a7722a193022accdb6b7c"}, + {file = "pyyaml-6.0.3-cp313-cp313-win_arm64.whl", hash = "sha256:5498cd1645aa724a7c71c8f378eb29ebe23da2fc0d7a08071d89469bf1d2defb"}, + {file = "pyyaml-6.0.3-cp314-cp314-macosx_10_13_x86_64.whl", hash = "sha256:8d1fab6bb153a416f9aeb4b8763bc0f22a5586065f86f7664fc23339fc1c1fac"}, + {file = "pyyaml-6.0.3-cp314-cp314-macosx_11_0_arm64.whl", hash = "sha256:34d5fcd24b8445fadc33f9cf348c1047101756fd760b4dacb5c3e99755703310"}, + {file = "pyyaml-6.0.3-cp314-cp314-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:501a031947e3a9025ed4405a168e6ef5ae3126c59f90ce0cd6f2bfc477be31b7"}, + {file = "pyyaml-6.0.3-cp314-cp314-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:b3bc83488de33889877a0f2543ade9f70c67d66d9ebb4ac959502e12de895788"}, + {file = "pyyaml-6.0.3-cp314-cp314-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:c458b6d084f9b935061bc36216e8a69a7e293a2f1e68bf956dcd9e6cbcd143f5"}, + {file = "pyyaml-6.0.3-cp314-cp314-musllinux_1_2_aarch64.whl", hash = "sha256:7c6610def4f163542a622a73fb39f534f8c101d690126992300bf3207eab9764"}, + {file = "pyyaml-6.0.3-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:5190d403f121660ce8d1d2c1bb2ef1bd05b5f68533fc5c2ea899bd15f4399b35"}, + {file = "pyyaml-6.0.3-cp314-cp314-win_amd64.whl", hash = "sha256:4a2e8cebe2ff6ab7d1050ecd59c25d4c8bd7e6f400f5f82b96557ac0abafd0ac"}, + {file = "pyyaml-6.0.3-cp314-cp314-win_arm64.whl", hash = "sha256:93dda82c9c22deb0a405ea4dc5f2d0cda384168e466364dec6255b293923b2f3"}, + {file = "pyyaml-6.0.3-cp314-cp314t-macosx_10_13_x86_64.whl", hash = "sha256:02893d100e99e03eda1c8fd5c441d8c60103fd175728e23e431db1b589cf5ab3"}, + {file = "pyyaml-6.0.3-cp314-cp314t-macosx_11_0_arm64.whl", hash = "sha256:c1ff362665ae507275af2853520967820d9124984e0f7466736aea23d8611fba"}, + {file = "pyyaml-6.0.3-cp314-cp314t-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:6adc77889b628398debc7b65c073bcb99c4a0237b248cacaf3fe8a557563ef6c"}, + {file = "pyyaml-6.0.3-cp314-cp314t-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:a80cb027f6b349846a3bf6d73b5e95e782175e52f22108cfa17876aaeff93702"}, + {file = "pyyaml-6.0.3-cp314-cp314t-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:00c4bdeba853cc34e7dd471f16b4114f4162dc03e6b7afcc2128711f0eca823c"}, + {file = "pyyaml-6.0.3-cp314-cp314t-musllinux_1_2_aarch64.whl", hash = "sha256:66e1674c3ef6f541c35191caae2d429b967b99e02040f5ba928632d9a7f0f065"}, + {file = "pyyaml-6.0.3-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:16249ee61e95f858e83976573de0f5b2893b3677ba71c9dd36b9cf8be9ac6d65"}, + {file = "pyyaml-6.0.3-cp314-cp314t-win_amd64.whl", hash = "sha256:4ad1906908f2f5ae4e5a8ddfce73c320c2a1429ec52eafd27138b7f1cbe341c9"}, + {file = "pyyaml-6.0.3-cp314-cp314t-win_arm64.whl", hash = "sha256:ebc55a14a21cb14062aa4162f906cd962b28e2e9ea38f9b4391244cd8de4ae0b"}, + {file = "pyyaml-6.0.3.tar.gz", hash = "sha256:d76623373421df22fb4cf8817020cbb7ef15c725b9d5e45f17e189bfc384190f"}, +] + +[[package]] +name = "pyyaml-env-tag" +version = "1.1" +requires_python = ">=3.9" +summary = "A custom YAML tag for referencing environment variables in YAML files." +groups = ["doc"] +dependencies = [ + "pyyaml", +] +files = [ + {file = "pyyaml_env_tag-1.1-py3-none-any.whl", hash = "sha256:17109e1a528561e32f026364712fee1264bc2ea6715120891174ed1b980d2e04"}, + {file = "pyyaml_env_tag-1.1.tar.gz", hash = "sha256:2eb38b75a2d21ee0475d6d97ec19c63287a7e140231e4214969d0eac923cd7ff"}, +] + +[[package]] +name = "resolvelib" +version = "1.2.1" +requires_python = ">=3.9" +summary = "Resolve abstract dependencies into concrete ones" +groups = ["default"] +files = [ + {file = "resolvelib-1.2.1-py3-none-any.whl", hash = "sha256:fb06b66c8da04172d9e72a21d7d06186d8919e32ae5ab5cdf5b9d920be805ac2"}, + {file = "resolvelib-1.2.1.tar.gz", hash = "sha256:7d08a2022f6e16ce405d60b68c390f054efcfd0477d4b9bd019cc941c28fad1c"}, +] + +[[package]] +name = "rich" +version = "15.0.0" +requires_python = ">=3.9.0" +summary = "Render rich text, tables, progress bars, syntax highlighting, markdown and more to the terminal" +groups = ["default"] +dependencies = [ + "markdown-it-py>=2.2.0", + "pygments<3.0.0,>=2.13.0", +] +files = [ + {file = "rich-15.0.0-py3-none-any.whl", hash = "sha256:33bd4ef74232fb73fe9279a257718407f169c09b78a87ad3d296f548e27de0bb"}, + {file = "rich-15.0.0.tar.gz", hash = "sha256:edd07a4824c6b40189fb7ac9bc4c52536e9780fbbfbddf6f1e2502c31b068c36"}, +] + +[[package]] +name = "secretstorage" +version = "3.5.0" +requires_python = ">=3.10" +summary = "Python bindings to FreeDesktop.org Secret Service API" +groups = ["all", "keyring"] +marker = "sys_platform == \"linux\"" +dependencies = [ + "cryptography>=2.0", + "jeepney>=0.6", +] +files = [ + {file = "secretstorage-3.5.0-py3-none-any.whl", hash = "sha256:0ce65888c0725fcb2c5bc0fdb8e5438eece02c523557ea40ce0703c266248137"}, + {file = "secretstorage-3.5.0.tar.gz", hash = "sha256:f04b8e4689cbce351744d5537bf6b1329c6fc68f91fa666f60a380edddcd11be"}, +] + +[[package]] +name = "setuptools" +version = "82.0.1" +requires_python = ">=3.9" +summary = "Most extensible Python build backend with support for C/C++ extension modules" +groups = ["doc"] +files = [ + {file = "setuptools-82.0.1-py3-none-any.whl", hash = "sha256:a59e362652f08dcd477c78bb6e7bd9d80a7995bc73ce773050228a348ce2e5bb"}, + {file = "setuptools-82.0.1.tar.gz", hash = "sha256:7d872682c5d01cfde07da7bccc7b65469d3dca203318515ada1de5eda35efbf9"}, +] + +[[package]] +name = "shellingham" +version = "1.5.4" +requires_python = ">=3.7" +summary = "Tool to Detect Surrounding Shell" +groups = ["default"] +files = [ + {file = "shellingham-1.5.4-py2.py3-none-any.whl", hash = "sha256:7ecfff8f2fd72616f7481040475a65b2bf8af90a56c89140852d1120324e8686"}, + {file = "shellingham-1.5.4.tar.gz", hash = "sha256:8dbca0739d487e5bd35ab3ca4b36e11c4078f3a234bfce294b0a0291363404de"}, +] + +[[package]] +name = "six" +version = "1.17.0" +requires_python = "!=3.0.*,!=3.1.*,!=3.2.*,>=2.7" +summary = "Python 2 and 3 compatibility utilities" +groups = ["doc"] +files = [ + {file = "six-1.17.0-py2.py3-none-any.whl", hash = "sha256:4721f391ed90541fddacab5acf947aa0d3dc7d27b2e1e8eda2be8970586c3274"}, + {file = "six-1.17.0.tar.gz", hash = "sha256:ff70335d468e7eb6ec65b95b99d3a2836546063f63acc5171de367e834932a81"}, +] + +[[package]] +name = "socksio" +version = "1.0.0" +requires_python = ">=3.6" +summary = "Sans-I/O implementation of SOCKS4, SOCKS4A, and SOCKS5." +groups = ["default"] +files = [ + {file = "socksio-1.0.0-py3-none-any.whl", hash = "sha256:95dc1f15f9b34e8d7b16f06d74b8ccf48f609af32ab33c608d08761c5dcbb1f3"}, + {file = "socksio-1.0.0.tar.gz", hash = "sha256:f88beb3da5b5c38b9890469de67d0cb0f9d494b78b106ca1845f96c10b91c4ac"}, +] + +[[package]] +name = "tomli" +version = "2.4.1" +requires_python = ">=3.8" +summary = "A lil' TOML parser" +groups = ["default", "doc", "pytest", "test", "tox", "workflow"] +files = [ + {file = "tomli-2.4.1-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:f8f0fc26ec2cc2b965b7a3b87cd19c5c6b8c5e5f436b984e85f486d652285c30"}, + {file = "tomli-2.4.1-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:4ab97e64ccda8756376892c53a72bd1f964e519c77236368527f758fbc36a53a"}, + {file = "tomli-2.4.1-cp311-cp311-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:96481a5786729fd470164b47cdb3e0e58062a496f455ee41b4403be77cb5a076"}, + {file = "tomli-2.4.1-cp311-cp311-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:5a881ab208c0baf688221f8cecc5401bd291d67e38a1ac884d6736cbcd8247e9"}, + {file = "tomli-2.4.1-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:47149d5bd38761ac8be13a84864bf0b7b70bc051806bc3669ab1cbc56216b23c"}, + {file = "tomli-2.4.1-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:ec9bfaf3ad2df51ace80688143a6a4ebc09a248f6ff781a9945e51937008fcbc"}, + {file = "tomli-2.4.1-cp311-cp311-win32.whl", hash = "sha256:ff2983983d34813c1aeb0fa89091e76c3a22889ee83ab27c5eeb45100560c049"}, + {file = "tomli-2.4.1-cp311-cp311-win_amd64.whl", hash = "sha256:5ee18d9ebdb417e384b58fe414e8d6af9f4e7a0ae761519fb50f721de398dd4e"}, + {file = "tomli-2.4.1-cp311-cp311-win_arm64.whl", hash = "sha256:c2541745709bad0264b7d4705ad453b76ccd191e64aa6f0fc66b69a293a45ece"}, + {file = "tomli-2.4.1-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:c742f741d58a28940ce01d58f0ab2ea3ced8b12402f162f4d534dfe18ba1cd6a"}, + {file = "tomli-2.4.1-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:7f86fd587c4ed9dd76f318225e7d9b29cfc5a9d43de44e5754db8d1128487085"}, + {file = "tomli-2.4.1-cp312-cp312-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:ff18e6a727ee0ab0388507b89d1bc6a22b138d1e2fa56d1ad494586d61d2eae9"}, + {file = "tomli-2.4.1-cp312-cp312-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:136443dbd7e1dee43c68ac2694fde36b2849865fa258d39bf822c10e8068eac5"}, + {file = "tomli-2.4.1-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:5e262d41726bc187e69af7825504c933b6794dc3fbd5945e41a79bb14c31f585"}, + {file = "tomli-2.4.1-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:5cb41aa38891e073ee49d55fbc7839cfdb2bc0e600add13874d048c94aadddd1"}, + {file = "tomli-2.4.1-cp312-cp312-win32.whl", hash = "sha256:da25dc3563bff5965356133435b757a795a17b17d01dbc0f42fb32447ddfd917"}, + {file = "tomli-2.4.1-cp312-cp312-win_amd64.whl", hash = "sha256:52c8ef851d9a240f11a88c003eacb03c31fc1c9c4ec64a99a0f922b93874fda9"}, + {file = "tomli-2.4.1-cp312-cp312-win_arm64.whl", hash = "sha256:f758f1b9299d059cc3f6546ae2af89670cb1c4d48ea29c3cacc4fe7de3058257"}, + {file = "tomli-2.4.1-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:36d2bd2ad5fb9eaddba5226aa02c8ec3fa4f192631e347b3ed28186d43be6b54"}, + {file = "tomli-2.4.1-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:eb0dc4e38e6a1fd579e5d50369aa2e10acfc9cace504579b2faabb478e76941a"}, + {file = "tomli-2.4.1-cp313-cp313-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:c7f2c7f2b9ca6bdeef8f0fa897f8e05085923eb091721675170254cbc5b02897"}, + {file = "tomli-2.4.1-cp313-cp313-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:f3c6818a1a86dd6dca7ddcaaf76947d5ba31aecc28cb1b67009a5877c9a64f3f"}, + {file = "tomli-2.4.1-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:d312ef37c91508b0ab2cee7da26ec0b3ed2f03ce12bd87a588d771ae15dcf82d"}, + {file = "tomli-2.4.1-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:51529d40e3ca50046d7606fa99ce3956a617f9b36380da3b7f0dd3dd28e68cb5"}, + {file = "tomli-2.4.1-cp313-cp313-win32.whl", hash = "sha256:2190f2e9dd7508d2a90ded5ed369255980a1bcdd58e52f7fe24b8162bf9fedbd"}, + {file = "tomli-2.4.1-cp313-cp313-win_amd64.whl", hash = "sha256:8d65a2fbf9d2f8352685bc1364177ee3923d6baf5e7f43ea4959d7d8bc326a36"}, + {file = "tomli-2.4.1-cp313-cp313-win_arm64.whl", hash = "sha256:4b605484e43cdc43f0954ddae319fb75f04cc10dd80d830540060ee7cd0243cd"}, + {file = "tomli-2.4.1-cp314-cp314-macosx_10_15_x86_64.whl", hash = "sha256:fd0409a3653af6c147209d267a0e4243f0ae46b011aa978b1080359fddc9b6cf"}, + {file = "tomli-2.4.1-cp314-cp314-macosx_11_0_arm64.whl", hash = "sha256:a120733b01c45e9a0c34aeef92bf0cf1d56cfe81ed9d47d562f9ed591a9828ac"}, + {file = "tomli-2.4.1-cp314-cp314-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:559db847dc486944896521f68d8190be1c9e719fced785720d2216fe7022b662"}, + {file = "tomli-2.4.1-cp314-cp314-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:01f520d4f53ef97964a240a035ec2a869fe1a37dde002b57ebc4417a27ccd853"}, + {file = "tomli-2.4.1-cp314-cp314-musllinux_1_2_aarch64.whl", hash = "sha256:7f94b27a62cfad8496c8d2513e1a222dd446f095fca8987fceef261225538a15"}, + {file = "tomli-2.4.1-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:ede3e6487c5ef5d28634ba3f31f989030ad6af71edfb0055cbbd14189ff240ba"}, + {file = "tomli-2.4.1-cp314-cp314-win32.whl", hash = "sha256:3d48a93ee1c9b79c04bb38772ee1b64dcf18ff43085896ea460ca8dec96f35f6"}, + {file = "tomli-2.4.1-cp314-cp314-win_amd64.whl", hash = "sha256:88dceee75c2c63af144e456745e10101eb67361050196b0b6af5d717254dddf7"}, + {file = "tomli-2.4.1-cp314-cp314-win_arm64.whl", hash = "sha256:b8c198f8c1805dc42708689ed6864951fd2494f924149d3e4bce7710f8eb5232"}, + {file = "tomli-2.4.1-cp314-cp314t-macosx_10_15_x86_64.whl", hash = "sha256:d4d8fe59808a54658fcc0160ecfb1b30f9089906c50b23bcb4c69eddc19ec2b4"}, + {file = "tomli-2.4.1-cp314-cp314t-macosx_11_0_arm64.whl", hash = "sha256:7008df2e7655c495dd12d2a4ad038ff878d4ca4b81fccaf82b714e07eae4402c"}, + {file = "tomli-2.4.1-cp314-cp314t-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:1d8591993e228b0c930c4bb0db464bdad97b3289fb981255d6c9a41aedc84b2d"}, + {file = "tomli-2.4.1-cp314-cp314t-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:734e20b57ba95624ecf1841e72b53f6e186355e216e5412de414e3c51e5e3c41"}, + {file = "tomli-2.4.1-cp314-cp314t-musllinux_1_2_aarch64.whl", hash = "sha256:8a650c2dbafa08d42e51ba0b62740dae4ecb9338eefa093aa5c78ceb546fcd5c"}, + {file = "tomli-2.4.1-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:504aa796fe0569bb43171066009ead363de03675276d2d121ac1a4572397870f"}, + {file = "tomli-2.4.1-cp314-cp314t-win32.whl", hash = "sha256:b1d22e6e9387bf4739fbe23bfa80e93f6b0373a7f1b96c6227c32bef95a4d7a8"}, + {file = "tomli-2.4.1-cp314-cp314t-win_amd64.whl", hash = "sha256:2c1c351919aca02858f740c6d33adea0c5deea37f9ecca1cc1ef9e884a619d26"}, + {file = "tomli-2.4.1-cp314-cp314t-win_arm64.whl", hash = "sha256:eab21f45c7f66c13f2a9e0e1535309cee140182a9cdae1e041d02e47291e8396"}, + {file = "tomli-2.4.1-py3-none-any.whl", hash = "sha256:0d85819802132122da43cb86656f8d1f8c6587d54ae7dcaf30e90533028b49fe"}, + {file = "tomli-2.4.1.tar.gz", hash = "sha256:7c7e1a961a0b2f2472c1ac5b69affa0ae1132c39adcb67aba98568702b9cc23f"}, +] + +[[package]] +name = "tomli-w" +version = "1.2.0" +requires_python = ">=3.9" +summary = "A lil' TOML writer" +groups = ["tox"] +files = [ + {file = "tomli_w-1.2.0-py3-none-any.whl", hash = "sha256:188306098d013b691fcadc011abd66727d3c414c571bb01b1a174ba8c983cf90"}, + {file = "tomli_w-1.2.0.tar.gz", hash = "sha256:2dd14fac5a47c27be9cd4c976af5a12d87fb1f0b4512f81d69cce3b35ae25021"}, +] + +[[package]] +name = "tomlkit" +version = "0.14.0" +requires_python = ">=3.9" +summary = "Style preserving TOML library" +groups = ["default"] +files = [ + {file = "tomlkit-0.14.0-py3-none-any.whl", hash = "sha256:592064ed85b40fa213469f81ac584f67a4f2992509a7c3ea2d632208623a3680"}, + {file = "tomlkit-0.14.0.tar.gz", hash = "sha256:cf00efca415dbd57575befb1f6634c4f42d2d87dbba376128adb42c121b87064"}, +] + +[[package]] +name = "towncrier" +version = "25.8.0" +requires_python = ">=3.9" +summary = "Building newsfiles for your project." +groups = ["workflow"] +dependencies = [ + "click", + "importlib-metadata>=4.6; python_version < \"3.10\"", + "importlib-resources>=5; python_version < \"3.10\"", + "jinja2", + "tomli; python_version < \"3.11\"", +] +files = [ + {file = "towncrier-25.8.0-py3-none-any.whl", hash = "sha256:b953d133d98f9aeae9084b56a3563fd2519dfc6ec33f61c9cd2c61ff243fb513"}, + {file = "towncrier-25.8.0.tar.gz", hash = "sha256:eef16d29f831ad57abb3ae32a0565739866219f1ebfbdd297d32894eb9940eb1"}, +] + +[[package]] +name = "tox" +version = "4.53.1" +requires_python = ">=3.10" +summary = "tox is a generic virtualenv management and test command line tool" +groups = ["tox"] +dependencies = [ + "cachetools>=7.0.3", + "colorama>=0.4.6", + "filelock>=3.25", + "packaging>=26", + "platformdirs>=4.9.4", + "pluggy>=1.6", + "pyproject-api>=1.10", + "python-discovery>=1.2.2", + "tomli-w>=1.2", + "tomli>=2.4; python_version < \"3.11\"", + "typing-extensions>=4.15; python_version < \"3.11\"", + "virtualenv>=21.1", +] +files = [ + {file = "tox-4.53.1-py3-none-any.whl", hash = "sha256:4a9948607e976a337c22d64a1b4fafd486125e82f00ab6ce32fa6cacc23f48b1"}, + {file = "tox-4.53.1.tar.gz", hash = "sha256:7be9805ed4a34242510c7acc9a7e3a01a35942e08f31f8bd69067c3a37130afc"}, +] + +[[package]] +name = "tox-pdm" +version = "0.7.2" +requires_python = ">=3.7" +summary = "A plugin for tox that utilizes PDM as the package manager and installer" +groups = ["tox"] +dependencies = [ + "tomli; python_version < \"3.11\"", + "tox>=4.0", +] +files = [ + {file = "tox_pdm-0.7.2-py3-none-any.whl", hash = "sha256:12f6215416b7acd00a80a9e7128f3dc3e3c89308d60707f5d0a24abdf83ac104"}, + {file = "tox_pdm-0.7.2.tar.gz", hash = "sha256:a841a7e1e942a71805624703b9a6d286663bd6af79bba6130ba756975c315308"}, +] + +[[package]] +name = "truststore" +version = "0.10.4" +requires_python = ">=3.10" +summary = "Verify certificates using native system trust stores" +groups = ["default"] +marker = "python_version >= \"3.10\"" +files = [ + {file = "truststore-0.10.4-py3-none-any.whl", hash = "sha256:adaeaecf1cbb5f4de3b1959b42d41f6fab57b2b1666adb59e89cb0b53361d981"}, + {file = "truststore-0.10.4.tar.gz", hash = "sha256:9d91bd436463ad5e4ee4aba766628dd6cd7010cf3e2461756b3303710eebc301"}, +] + +[[package]] +name = "typing-extensions" +version = "4.15.0" +requires_python = ">=3.9" +summary = "Backported and Experimental Type Hints for Python 3.9+" +groups = ["default", "all", "doc", "keyring", "pytest", "test", "tox"] +files = [ + {file = "typing_extensions-4.15.0-py3-none-any.whl", hash = "sha256:f0fa19c6845758ab08074a0cfa8b7aecb71c999ca73d62883bc25cc018c4e548"}, + {file = "typing_extensions-4.15.0.tar.gz", hash = "sha256:0cea48d173cc12fa28ecabc3b837ea3cf6f38c6d1136f85cbaaf598984861466"}, +] + +[[package]] +name = "unearth" +version = "0.18.2" +requires_python = ">=3.9" +summary = "A utility to fetch and download python packages" +groups = ["default"] +dependencies = [ + "httpx<1,>=0.27.0", + "packaging>=20", +] +files = [ + {file = "unearth-0.18.2-py3-none-any.whl", hash = "sha256:31fd55d67c0e46a1ebb78993a2010568e6c4231334a3207d18d5d4a549d8d692"}, + {file = "unearth-0.18.2.tar.gz", hash = "sha256:1e53d7f52f46dd5f875e77ff1c55b12477e215a092e4b66c9764a77df4a9b520"}, +] + +[[package]] +name = "urllib3" +version = "2.6.3" +requires_python = ">=3.9" +summary = "HTTP library with thread-safe connection pooling, file post, and more." +groups = ["default"] +files = [ + {file = "urllib3-2.6.3-py3-none-any.whl", hash = "sha256:bf272323e553dfb2e87d9bfd225ca7b0f467b919d7bbd355436d3fd37cb0acd4"}, + {file = "urllib3-2.6.3.tar.gz", hash = "sha256:1b62b6884944a57dbe321509ab94fd4d3b307075e0c2eae991ac71ee15ad38ed"}, +] + +[[package]] +name = "virtualenv" +version = "21.3.1" +requires_python = ">=3.8" +summary = "Virtual Python Environment builder" +groups = ["default", "tox"] +dependencies = [ + "distlib<1,>=0.3.7", + "filelock<4,>=3.24.2; python_version >= \"3.10\"", + "filelock<=3.19.1,>=3.16.1; python_version < \"3.10\"", + "importlib-metadata>=6.6; python_version < \"3.8\"", + "platformdirs<5,>=3.9.1", + "python-discovery>=1.2.2", + "typing-extensions>=4.13.2; python_version < \"3.11\"", +] +files = [ + {file = "virtualenv-21.3.1-py3-none-any.whl", hash = "sha256:d1a71cf58f2f9228fff23a1f6ec15d39785c6b32e03658d104974247145edd35"}, + {file = "virtualenv-21.3.1.tar.gz", hash = "sha256:c2305bc1fddeec40699b8370d13f8d431b0701f00ce895061ce493aeded4426b"}, +] + +[[package]] +name = "watchdog" +version = "6.0.0" +requires_python = ">=3.9" +summary = "Filesystem events monitoring" +groups = ["doc"] +files = [ + {file = "watchdog-6.0.0-cp310-cp310-macosx_10_9_universal2.whl", hash = "sha256:d1cdb490583ebd691c012b3d6dae011000fe42edb7a82ece80965b42abd61f26"}, + {file = "watchdog-6.0.0-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:bc64ab3bdb6a04d69d4023b29422170b74681784ffb9463ed4870cf2f3e66112"}, + {file = "watchdog-6.0.0-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:c897ac1b55c5a1461e16dae288d22bb2e412ba9807df8397a635d88f671d36c3"}, + {file = "watchdog-6.0.0-cp311-cp311-macosx_10_9_universal2.whl", hash = "sha256:6eb11feb5a0d452ee41f824e271ca311a09e250441c262ca2fd7ebcf2461a06c"}, + {file = "watchdog-6.0.0-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:ef810fbf7b781a5a593894e4f439773830bdecb885e6880d957d5b9382a960d2"}, + {file = "watchdog-6.0.0-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:afd0fe1b2270917c5e23c2a65ce50c2a4abb63daafb0d419fde368e272a76b7c"}, + {file = "watchdog-6.0.0-cp312-cp312-macosx_10_13_universal2.whl", hash = "sha256:bdd4e6f14b8b18c334febb9c4425a878a2ac20efd1e0b231978e7b150f92a948"}, + {file = "watchdog-6.0.0-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:c7c15dda13c4eb00d6fb6fc508b3c0ed88b9d5d374056b239c4ad1611125c860"}, + {file = "watchdog-6.0.0-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:6f10cb2d5902447c7d0da897e2c6768bca89174d0c6e1e30abec5421af97a5b0"}, + {file = "watchdog-6.0.0-cp313-cp313-macosx_10_13_universal2.whl", hash = "sha256:490ab2ef84f11129844c23fb14ecf30ef3d8a6abafd3754a6f75ca1e6654136c"}, + {file = "watchdog-6.0.0-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:76aae96b00ae814b181bb25b1b98076d5fc84e8a53cd8885a318b42b6d3a5134"}, + {file = "watchdog-6.0.0-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:a175f755fc2279e0b7312c0035d52e27211a5bc39719dd529625b1930917345b"}, + {file = "watchdog-6.0.0-pp310-pypy310_pp73-macosx_10_15_x86_64.whl", hash = "sha256:c7ac31a19f4545dd92fc25d200694098f42c9a8e391bc00bdd362c5736dbf881"}, + {file = "watchdog-6.0.0-pp310-pypy310_pp73-macosx_11_0_arm64.whl", hash = "sha256:9513f27a1a582d9808cf21a07dae516f0fab1cf2d7683a742c498b93eedabb11"}, + {file = "watchdog-6.0.0-py3-none-manylinux2014_aarch64.whl", hash = "sha256:7607498efa04a3542ae3e05e64da8202e58159aa1fa4acddf7678d34a35d4f13"}, + {file = "watchdog-6.0.0-py3-none-manylinux2014_armv7l.whl", hash = "sha256:9041567ee8953024c83343288ccc458fd0a2d811d6a0fd68c4c22609e3490379"}, + {file = "watchdog-6.0.0-py3-none-manylinux2014_i686.whl", hash = "sha256:82dc3e3143c7e38ec49d61af98d6558288c415eac98486a5c581726e0737c00e"}, + {file = "watchdog-6.0.0-py3-none-manylinux2014_ppc64.whl", hash = "sha256:212ac9b8bf1161dc91bd09c048048a95ca3a4c4f5e5d4a7d1b1a7d5752a7f96f"}, + {file = "watchdog-6.0.0-py3-none-manylinux2014_ppc64le.whl", hash = "sha256:e3df4cbb9a450c6d49318f6d14f4bbc80d763fa587ba46ec86f99f9e6876bb26"}, + {file = "watchdog-6.0.0-py3-none-manylinux2014_s390x.whl", hash = "sha256:2cce7cfc2008eb51feb6aab51251fd79b85d9894e98ba847408f662b3395ca3c"}, + {file = "watchdog-6.0.0-py3-none-manylinux2014_x86_64.whl", hash = "sha256:20ffe5b202af80ab4266dcd3e91aae72bf2da48c0d33bdb15c66658e685e94e2"}, + {file = "watchdog-6.0.0-py3-none-win32.whl", hash = "sha256:07df1fdd701c5d4c8e55ef6cf55b8f0120fe1aef7ef39a1c6fc6bc2e606d517a"}, + {file = "watchdog-6.0.0-py3-none-win_amd64.whl", hash = "sha256:cbafb470cf848d93b5d013e2ecb245d4aa1c8fd0504e863ccefa32445359d680"}, + {file = "watchdog-6.0.0-py3-none-win_ia64.whl", hash = "sha256:a1914259fa9e1454315171103c6a30961236f508b9b623eae470268bbcc6a22f"}, + {file = "watchdog-6.0.0.tar.gz", hash = "sha256:9ddf7c82fda3ae8e24decda1338ede66e1c99883db93711d8fb941eaa2d8c282"}, +] + +[[package]] +name = "werkzeug" +version = "3.1.8" +requires_python = ">=3.9" +summary = "The comprehensive WSGI web application library." +groups = ["test"] +dependencies = [ + "markupsafe>=2.1.1", +] +files = [ + {file = "werkzeug-3.1.8-py3-none-any.whl", hash = "sha256:63a77fb8892bf28ebc3178683445222aa500e48ebad5ec77b0ad80f8726b1f50"}, + {file = "werkzeug-3.1.8.tar.gz", hash = "sha256:9bad61a4268dac112f1c5cd4630a56ede601b6ed420300677a869083d70a4c44"}, +] + +[[package]] +name = "zensical" +version = "0.0.40" +requires_python = ">=3.10" +summary = "A modern static site generator built by the creators of Material for MkDocs" +groups = ["doc"] +marker = "python_version >= \"3.10\"" +dependencies = [ + "click>=8.1.8", + "deepmerge>=2.0", + "jinja2>=3.1", + "markdown>=3.7", + "pygments>=2.20", + "pymdown-extensions>=10.21.2", + "pyyaml>=6.0.2", + "tomli>=2.4.0", +] +files = [ + {file = "zensical-0.0.40-cp310-abi3-macosx_10_12_x86_64.whl", hash = "sha256:b65a7143c9c6a460880bf3e65b777952bd2dcede9dd17a6c6bac9b4a0686ad9b"}, + {file = "zensical-0.0.40-cp310-abi3-macosx_11_0_arm64.whl", hash = "sha256:045bdcb6d00a11ddcab7d379d0d986cdf78dba8e9287d8e628ef11958241507d"}, + {file = "zensical-0.0.40-cp310-abi3-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:d48ec476c2e8ce3f8585a1278083aabc35ec80361f2c4fc4a53b9a525778f7fc"}, + {file = "zensical-0.0.40-cp310-abi3-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:48c38e0ae314c25f2e5e64210bbad9be6e970f2d40fe9da106586ad90ce5e85e"}, + {file = "zensical-0.0.40-cp310-abi3-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:f25f62dcd61f6306cab890dfa34c81d2709f5db290b4c3f2675343771db28c90"}, + {file = "zensical-0.0.40-cp310-abi3-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:168fe3489dd93ae92978b4db11d9300c63e10d382b81634232c2872ce9e746c2"}, + {file = "zensical-0.0.40-cp310-abi3-musllinux_1_2_aarch64.whl", hash = "sha256:8652ba203bd588ebf2d66bda4457a4a7d8e193c886960859c75081c0e3b946de"}, + {file = "zensical-0.0.40-cp310-abi3-musllinux_1_2_armv7l.whl", hash = "sha256:9ffa6cf208b7ab6b771703be827d4d8c7f07f173abeffb35a8015a0b832b2a40"}, + {file = "zensical-0.0.40-cp310-abi3-musllinux_1_2_i686.whl", hash = "sha256:7101ba0c739c78bc3a57d22130b59b9e6fdf96c21c8a6b4244070de6b34527d4"}, + {file = "zensical-0.0.40-cp310-abi3-musllinux_1_2_x86_64.whl", hash = "sha256:39bf728a68a5418feeda8f3385cd1063fdb8d896a6812c3dede4267b2868df12"}, + {file = "zensical-0.0.40-cp310-abi3-win32.whl", hash = "sha256:bc750c3ba8d11833d9b9ac8fc14adc3435225b6d17314a21a91eb60209511ca5"}, + {file = "zensical-0.0.40-cp310-abi3-win_amd64.whl", hash = "sha256:c5c86ac468df2dfe515ff54ffa97725c38226f1e5c970059b7e88078abab89ab"}, + {file = "zensical-0.0.40.tar.gz", hash = "sha256:5c294751977a664614cb84e987186ad8e282af77ce0d0d800fe48ee57791279d"}, +] + +[[package]] +name = "zipp" +version = "3.23.1" +requires_python = ">=3.9" +summary = "Backport of pathlib-compatible object wrapper for zip files" +groups = ["all", "keyring"] +marker = "python_version < \"3.12\"" +files = [ + {file = "zipp-3.23.1-py3-none-any.whl", hash = "sha256:0b3596c50a5c700c9cb40ba8d86d9f2cc4807e9bedb06bcdf7fac85633e444dc"}, + {file = "zipp-3.23.1.tar.gz", hash = "sha256:32120e378d32cd9714ad503c1d024619063ec28aad2248dc6672ad13edfa5110"}, +] diff --git a/tests/fixtures/real-world-locks/pdm/pdm-2.29.0/pyproject.toml b/tests/fixtures/real-world-locks/pdm/pdm-2.29.0/pyproject.toml new file mode 100644 index 00000000..9f5334ac --- /dev/null +++ b/tests/fixtures/real-world-locks/pdm/pdm-2.29.0/pyproject.toml @@ -0,0 +1,260 @@ +[build-system] +requires = ["pdm-backend", "pdm-build-locked"] +build-backend = "pdm.backend" + +[project] +# PEP 621 project metadata +# See https://www.python.org/dev/peps/pep-0621/ +name = "pdm" +description = "A modern Python package and dependency manager supporting the latest PEP standards" +authors = [ + {name = "Frost Ming", email = "mianghong@gmail.com"}, +] +dynamic = ["version"] +requires-python = ">=3.10" +license = "MIT" +license-files = ["LICENSE"] +dependencies = [ + "argcomplete>=3.6.3", + "blinker", + "packaging>22.0", + "platformdirs", + "rich>=12.3.0", + "virtualenv>=20", + "pyproject-hooks", + "unearth>=0.17.5", + "dep-logic>=0.5", + "findpython>=0.7.0,<1.0.0a0", + "tomlkit>=0.11.1,<1", + "shellingham>=1.3.2", + "python-dotenv>=0.15", + "resolvelib>=1.1", + "installer>=1", + "truststore>=0.10.4", + "tomli>=1.1.0; python_version < \"3.11\"", + "hishel[httpx]>=1.0.0", + "pbs-installer>=2025.10.7", + "httpx[socks]<1,>0.20", + "filelock>=3.13", + "httpcore>=1.0.6", + "certifi>=2024.8.30", + "id>=1.5.0" +] +readme = "README.md" +keywords = ["packaging", "dependency", "workflow"] +classifiers = [ + "Topic :: Software Development :: Build Tools", + "Programming Language :: Python :: 3", + "Programming Language :: Python :: 3.10", + "Programming Language :: Python :: 3.11", + "Programming Language :: Python :: 3.12", + "Programming Language :: Python :: 3.13", + "Programming Language :: Python :: 3.14", +] + +[project.urls] +Homepage = "https://pdm-project.org" +Repository = "https://github.com/pdm-project/pdm" +Documentation = "https://pdm-project.org" +Changelog = "https://pdm-project.org/latest/dev/changelog/" + +[project.optional-dependencies] +pytest = [ + "pytest", + "pytest-mock", +] +copier = ["copier>=8.0.0"] +cookiecutter = ["cookiecutter"] +keyring = ["keyring"] +template = [ + "pdm[copier,cookiecutter]", +] +all = [ + "pdm[keyring,template]", +] + +[project.scripts] +pdm = "pdm.core:main" + +[dependency-groups] +test = [ + "pdm[pytest]", + "pytest-cov", + "pytest-xdist>=1.31.0", + "pytest-rerunfailures>=10.2", + "pytest-httpserver>=1.0.6", + "pytest-httpx>=0.34.0", +] +tox = [ + "tox", + "tox-pdm>=0.5", +] +doc = [ + "zensical>=0.0.28; python_version >= '3.10'", + "mkdocstrings[python]>=0.18", + "setuptools>=62.3.3", +] +workflow = [ + "parver>=0.3.1", + "towncrier>=20", +] + +[tool.ruff] +line-length = 120 +exclude = ["tests/fixtures"] +target-version = "py310" +src = ["src"] + +[tool.ruff.lint] +extend-select = [ + "I", # isort + "B", # flake8-bugbear + "C4", # flake8-comprehensions + "FA", # flake8-future-annotations + "PGH", # pygrep-hooks + "RUF", # ruff + "W", # pycodestyle + "UP", # pyupgrade + "YTT", # flake8-2020 +] +extend-ignore = ["B018", "B019", "BLE001", "RUF018", "PLW1510"] + +[tool.ruff.lint.mccabe] +max-complexity = 10 + +[tool.ruff.lint.isort] +known-first-party = ["pdm"] + +[tool.towncrier] +package = "pdm" +filename = "CHANGELOG.md" +issue_format = "[#{issue}](https://github.com/pdm-project/pdm/issues/{issue})" +directory = "news/" +title_format = "## Release v{version} ({project_date})" +underlines = ["", "", ""] + + [[tool.towncrier.type]] + directory = "break" + name = "Breaking Changes" + showcontent = true + + [[tool.towncrier.type]] + directory = "feature" + name = "Features & Improvements" + showcontent = true + + [[tool.towncrier.type]] + directory = "bugfix" + name = "Bug Fixes" + showcontent = true + + [[tool.towncrier.type]] + directory = "doc" + name = "Documentation" + showcontent = true + + [[tool.towncrier.type]] + directory = "dep" + name = "Dependencies" + showcontent = true + + [[tool.towncrier.type]] + directory = "removal" + name = "Removals and Deprecations" + showcontent = true + + [[tool.towncrier.type]] + directory = "misc" + name = "Miscellany" + showcontent = true + +[tool.pytest.ini_options] +filterwarnings = [ + "default::DeprecationWarning", +] +markers = [ + "network: Tests that require network", + "integration: Run with all Python versions", + "path: Tests that compare with the system paths", + "deprecated: Tests about deprecated features", + "uv: Tests that require uv to be installed", +] +addopts = "-r aR" +testpaths = [ + "tests/", +] + +[tool.codespell] +ignore-words-list = "ba,overriden,te,instal" + +[tool.coverage.run] +branch = true +source = ["pdm"] +omit = [ + "*/pdm/__main__.py", + "*/pdm/pep582/sitecustomize.py", + "*/pdm/models/in_process/*.py", + "*/pdm-test-*-env/*", + "*/pdm/misc/sysconfig_patcher.py", +] + +[tool.coverage.report] +fail_under = 84 +# Regexes for lines to exclude from consideration +exclude_lines = [ + "pragma: no cover", + # Don't complain about missing debug-only code: + "def __repr__", + "if self.debug", + # Don't complain if tests don't hit defensive assertion code: + "raise AssertionError", + "raise NotImplementedError", + # Don't complain if non-runnable code isn't run: + "if __name__ == .__main__.:", + "if TYPE_CHECKING:", +] +ignore_errors = true + +[tool.mypy] +follow_imports = "silent" +ignore_missing_imports = true +disallow_incomplete_defs = true +disallow_untyped_defs = true +disallow_untyped_decorators = true +exclude = "pdm/(pep582/|models/in_process/.+\\.py|misc)" +namespace_packages = true +mypy_path = "src" +explicit_package_bases = true + +[tool.pdm.version] +source = "scm" +write_to = "pdm/VERSION" + +[tool.pdm.build] +excludes = ["./**/.git"] +package-dir = "src" +includes = ["src/pdm"] +source-includes = ["tests", "typings", "CHANGELOG.md", "LICENSE", "README.md", "tox.ini"] +# editables backend doesn't work well with namespace packages +editable-backend = "path" +locked = true +locked-groups = ["default", "all"] + +[tool.pdm.scripts] +pre_release = "python tasks/max_versions.py" +release = "python tasks/release.py" +test = "pytest" +coverage = {shell = """\ + python -m pytest \ + --verbosity=3 \ + --cov=src/pdm \ + --cov-branch \ + --cov-report term-missing \ + tests/ + """} +tox = "tox" +pre_doc = "python tasks/render_reference_docs.py" +doc = {cmd= "zensical serve", help = "Start the dev server for docs preview"} +doc-build = {composite = ["pre_doc", "zensical build"], help = "Build the documentation site"} +lint = "prek run --all-files" +complete = {call = "tasks.complete:main", help = "Create autocomplete files with argcomplete"} diff --git a/tests/fixtures/real-world-locks/pdm/unearth-0.18.3/LICENSE b/tests/fixtures/real-world-locks/pdm/unearth-0.18.3/LICENSE new file mode 100644 index 00000000..622261fc --- /dev/null +++ b/tests/fixtures/real-world-locks/pdm/unearth-0.18.3/LICENSE @@ -0,0 +1,21 @@ +MIT + +Copyright (c) 2022 Frost Ming + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all +copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +SOFTWARE. diff --git a/tests/fixtures/real-world-locks/pdm/unearth-0.18.3/pdm.lock b/tests/fixtures/real-world-locks/pdm/unearth-0.18.3/pdm.lock new file mode 100644 index 00000000..4008d560 --- /dev/null +++ b/tests/fixtures/real-world-locks/pdm/unearth-0.18.3/pdm.lock @@ -0,0 +1,1152 @@ +# This file is @generated by PDM. +# It is not intended for manual editing. + +[metadata] +groups = ["default", "doc", "keyring", "legacy", "test"] +strategy = ["inherit_metadata"] +lock_version = "4.5.0" +content_hash = "sha256:c320d97858c2efacd48679ed0e3bee18298660dc045d638f6a237f6d2acf7a73" + +[[metadata.targets]] +requires_python = ">=3.9" + +[[package]] +name = "accessible-pygments" +version = "0.0.5" +requires_python = ">=3.9" +summary = "A collection of accessible pygments styles" +groups = ["doc"] +dependencies = [ + "pygments>=1.5", +] +files = [ + {file = "accessible_pygments-0.0.5-py3-none-any.whl", hash = "sha256:88ae3211e68a1d0b011504b2ffc1691feafce124b845bd072ab6f9f66f34d4b7"}, + {file = "accessible_pygments-0.0.5.tar.gz", hash = "sha256:40918d3e6a2b619ad424cb91e556bd3bd8865443d9f22f1dcdf79e33c8046872"}, +] + +[[package]] +name = "alabaster" +version = "0.7.16" +requires_python = ">=3.9" +summary = "A light, configurable Sphinx theme" +groups = ["doc"] +files = [ + {file = "alabaster-0.7.16-py3-none-any.whl", hash = "sha256:b46733c07dce03ae4e150330b975c75737fa60f0a7c591b6c8bf4928a28e2c92"}, + {file = "alabaster-0.7.16.tar.gz", hash = "sha256:75a8b99c28a5dad50dd7f8ccdd447a121ddb3892da9e53d1ca5cca3106d58d65"}, +] + +[[package]] +name = "anyio" +version = "4.11.0" +requires_python = ">=3.9" +summary = "High-level concurrency and networking framework on top of asyncio or Trio" +groups = ["default"] +dependencies = [ + "exceptiongroup>=1.0.2; python_version < \"3.11\"", + "idna>=2.8", + "sniffio>=1.1", + "typing-extensions>=4.5; python_version < \"3.13\"", +] +files = [ + {file = "anyio-4.11.0-py3-none-any.whl", hash = "sha256:0287e96f4d26d4149305414d4e3bc32f0dcd0862365a4bddea19d7a1ec38c4fc"}, + {file = "anyio-4.11.0.tar.gz", hash = "sha256:82a8d0b81e318cc5ce71a5f1f8b5c4e63619620b63141ef8c995fa0db95a57c4"}, +] + +[[package]] +name = "babel" +version = "2.14.0" +requires_python = ">=3.7" +summary = "Internationalization utilities" +groups = ["doc"] +dependencies = [ + "pytz>=2015.7; python_version < \"3.9\"", +] +files = [ + {file = "Babel-2.14.0-py3-none-any.whl", hash = "sha256:efb1a25b7118e67ce3a259bed20545c29cb68be8ad2c784c83689981b7a57287"}, + {file = "Babel-2.14.0.tar.gz", hash = "sha256:6919867db036398ba21eb5c7a0f6b28ab8cbc3ae7a73a44ebe34ae74a4e7d363"}, +] + +[[package]] +name = "beautifulsoup4" +version = "4.12.2" +requires_python = ">=3.6.0" +summary = "Screen-scraping library" +groups = ["doc"] +dependencies = [ + "soupsieve>1.2", +] +files = [ + {file = "beautifulsoup4-4.12.2-py3-none-any.whl", hash = "sha256:bd2520ca0d9d7d12694a53d44ac482d181b4ec1888909b035a3dbf40d0f57d4a"}, + {file = "beautifulsoup4-4.12.2.tar.gz", hash = "sha256:492bbc69dca35d12daac71c4db1bfff0c876c00ef4a2ffacce226d4638eb72da"}, +] + +[[package]] +name = "blinker" +version = "1.9.0" +requires_python = ">=3.9" +summary = "Fast, simple object-to-object and broadcast signaling" +groups = ["test"] +files = [ + {file = "blinker-1.9.0-py3-none-any.whl", hash = "sha256:ba0efaa9080b619ff2f3459d1d500c57bddea4a6b424b60a91141db6fd2f08bc"}, + {file = "blinker-1.9.0.tar.gz", hash = "sha256:b4ce2265a7abece45e7cc896e98dbebe6cead56bcf805a3d23136d145f5445bf"}, +] + +[[package]] +name = "certifi" +version = "2025.10.5" +requires_python = ">=3.7" +summary = "Python package for providing Mozilla's CA Bundle." +groups = ["default", "doc", "legacy", "test"] +files = [ + {file = "certifi-2025.10.5-py3-none-any.whl", hash = "sha256:0f212c2744a9bb6de0c56639a6f68afe01ecd92d91f14ae897c4fe7bbeeef0de"}, + {file = "certifi-2025.10.5.tar.gz", hash = "sha256:47c09d31ccf2acf0be3f701ea53595ee7e0b8fa08801c6624be771df09ae7b43"}, +] + +[[package]] +name = "cffi" +version = "1.16.0" +requires_python = ">=3.8" +summary = "Foreign Function Interface for Python calling C code." +groups = ["keyring", "test"] +marker = "python_version < \"3.13\" and platform_python_implementation != \"PyPy\" or sys_platform == \"linux\" and platform_python_implementation != \"PyPy\"" +dependencies = [ + "pycparser", +] +files = [ + {file = "cffi-1.16.0-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:6b3d6606d369fc1da4fd8c357d026317fbb9c9b75d36dc16e90e84c26854b088"}, + {file = "cffi-1.16.0-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:ac0f5edd2360eea2f1daa9e26a41db02dd4b0451b48f7c318e217ee092a213e9"}, + {file = "cffi-1.16.0-cp310-cp310-manylinux_2_12_i686.manylinux2010_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:7e61e3e4fa664a8588aa25c883eab612a188c725755afff6289454d6362b9673"}, + {file = "cffi-1.16.0-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:a72e8961a86d19bdb45851d8f1f08b041ea37d2bd8d4fd19903bc3083d80c896"}, + {file = "cffi-1.16.0-cp310-cp310-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:5b50bf3f55561dac5438f8e70bfcdfd74543fd60df5fa5f62d94e5867deca684"}, + {file = "cffi-1.16.0-cp310-cp310-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:7651c50c8c5ef7bdb41108b7b8c5a83013bfaa8a935590c5d74627c047a583c7"}, + {file = "cffi-1.16.0-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:e4108df7fe9b707191e55f33efbcb2d81928e10cea45527879a4749cbe472614"}, + {file = "cffi-1.16.0-cp310-cp310-musllinux_1_1_i686.whl", hash = "sha256:32c68ef735dbe5857c810328cb2481e24722a59a2003018885514d4c09af9743"}, + {file = "cffi-1.16.0-cp310-cp310-musllinux_1_1_x86_64.whl", hash = "sha256:673739cb539f8cdaa07d92d02efa93c9ccf87e345b9a0b556e3ecc666718468d"}, + {file = "cffi-1.16.0-cp310-cp310-win32.whl", hash = "sha256:9f90389693731ff1f659e55c7d1640e2ec43ff725cc61b04b2f9c6d8d017df6a"}, + {file = "cffi-1.16.0-cp310-cp310-win_amd64.whl", hash = "sha256:e6024675e67af929088fda399b2094574609396b1decb609c55fa58b028a32a1"}, + {file = "cffi-1.16.0-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:b84834d0cf97e7d27dd5b7f3aca7b6e9263c56308ab9dc8aae9784abb774d404"}, + {file = "cffi-1.16.0-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:1b8ebc27c014c59692bb2664c7d13ce7a6e9a629be20e54e7271fa696ff2b417"}, + {file = "cffi-1.16.0-cp311-cp311-manylinux_2_12_i686.manylinux2010_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:ee07e47c12890ef248766a6e55bd38ebfb2bb8edd4142d56db91b21ea68b7627"}, + {file = "cffi-1.16.0-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:d8a9d3ebe49f084ad71f9269834ceccbf398253c9fac910c4fd7053ff1386936"}, + {file = "cffi-1.16.0-cp311-cp311-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:e70f54f1796669ef691ca07d046cd81a29cb4deb1e5f942003f401c0c4a2695d"}, + {file = "cffi-1.16.0-cp311-cp311-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:5bf44d66cdf9e893637896c7faa22298baebcd18d1ddb6d2626a6e39793a1d56"}, + {file = "cffi-1.16.0-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:7b78010e7b97fef4bee1e896df8a4bbb6712b7f05b7ef630f9d1da00f6444d2e"}, + {file = "cffi-1.16.0-cp311-cp311-musllinux_1_1_i686.whl", hash = "sha256:c6a164aa47843fb1b01e941d385aab7215563bb8816d80ff3a363a9f8448a8dc"}, + {file = "cffi-1.16.0-cp311-cp311-musllinux_1_1_x86_64.whl", hash = "sha256:e09f3ff613345df5e8c3667da1d918f9149bd623cd9070c983c013792a9a62eb"}, + {file = "cffi-1.16.0-cp311-cp311-win32.whl", hash = "sha256:2c56b361916f390cd758a57f2e16233eb4f64bcbeee88a4881ea90fca14dc6ab"}, + {file = "cffi-1.16.0-cp311-cp311-win_amd64.whl", hash = "sha256:db8e577c19c0fda0beb7e0d4e09e0ba74b1e4c092e0e40bfa12fe05b6f6d75ba"}, + {file = "cffi-1.16.0-cp312-cp312-macosx_10_9_x86_64.whl", hash = "sha256:fa3a0128b152627161ce47201262d3140edb5a5c3da88d73a1b790a959126956"}, + {file = "cffi-1.16.0-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:68e7c44931cc171c54ccb702482e9fc723192e88d25a0e133edd7aff8fcd1f6e"}, + {file = "cffi-1.16.0-cp312-cp312-manylinux_2_12_i686.manylinux2010_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:abd808f9c129ba2beda4cfc53bde801e5bcf9d6e0f22f095e45327c038bfe68e"}, + {file = "cffi-1.16.0-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:88e2b3c14bdb32e440be531ade29d3c50a1a59cd4e51b1dd8b0865c54ea5d2e2"}, + {file = "cffi-1.16.0-cp312-cp312-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:fcc8eb6d5902bb1cf6dc4f187ee3ea80a1eba0a89aba40a5cb20a5087d961357"}, + {file = "cffi-1.16.0-cp312-cp312-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:b7be2d771cdba2942e13215c4e340bfd76398e9227ad10402a8767ab1865d2e6"}, + {file = "cffi-1.16.0-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:e715596e683d2ce000574bae5d07bd522c781a822866c20495e52520564f0969"}, + {file = "cffi-1.16.0-cp312-cp312-musllinux_1_1_x86_64.whl", hash = "sha256:2d92b25dbf6cae33f65005baf472d2c245c050b1ce709cc4588cdcdd5495b520"}, + {file = "cffi-1.16.0-cp312-cp312-win32.whl", hash = "sha256:b2ca4e77f9f47c55c194982e10f058db063937845bb2b7a86c84a6cfe0aefa8b"}, + {file = "cffi-1.16.0-cp312-cp312-win_amd64.whl", hash = "sha256:68678abf380b42ce21a5f2abde8efee05c114c2fdb2e9eef2efdb0257fba1235"}, + {file = "cffi-1.16.0-cp38-cp38-macosx_10_9_x86_64.whl", hash = "sha256:0c9ef6ff37e974b73c25eecc13952c55bceed9112be2d9d938ded8e856138bcc"}, + {file = "cffi-1.16.0-cp38-cp38-manylinux_2_12_i686.manylinux2010_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:a09582f178759ee8128d9270cd1344154fd473bb77d94ce0aeb2a93ebf0feaf0"}, + {file = "cffi-1.16.0-cp38-cp38-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:e760191dd42581e023a68b758769e2da259b5d52e3103c6060ddc02c9edb8d7b"}, + {file = "cffi-1.16.0-cp38-cp38-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:80876338e19c951fdfed6198e70bc88f1c9758b94578d5a7c4c91a87af3cf31c"}, + {file = "cffi-1.16.0-cp38-cp38-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:a6a14b17d7e17fa0d207ac08642c8820f84f25ce17a442fd15e27ea18d67c59b"}, + {file = "cffi-1.16.0-cp38-cp38-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:6602bc8dc6f3a9e02b6c22c4fc1e47aa50f8f8e6d3f78a5e16ac33ef5fefa324"}, + {file = "cffi-1.16.0-cp38-cp38-win32.whl", hash = "sha256:131fd094d1065b19540c3d72594260f118b231090295d8c34e19a7bbcf2e860a"}, + {file = "cffi-1.16.0-cp38-cp38-win_amd64.whl", hash = "sha256:31d13b0f99e0836b7ff893d37af07366ebc90b678b6664c955b54561fc36ef36"}, + {file = "cffi-1.16.0-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:582215a0e9adbe0e379761260553ba11c58943e4bbe9c36430c4ca6ac74b15ed"}, + {file = "cffi-1.16.0-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:b29ebffcf550f9da55bec9e02ad430c992a87e5f512cd63388abb76f1036d8d2"}, + {file = "cffi-1.16.0-cp39-cp39-manylinux_2_12_i686.manylinux2010_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:dc9b18bf40cc75f66f40a7379f6a9513244fe33c0e8aa72e2d56b0196a7ef872"}, + {file = "cffi-1.16.0-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:9cb4a35b3642fc5c005a6755a5d17c6c8b6bcb6981baf81cea8bfbc8903e8ba8"}, + {file = "cffi-1.16.0-cp39-cp39-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:b86851a328eedc692acf81fb05444bdf1891747c25af7529e39ddafaf68a4f3f"}, + {file = "cffi-1.16.0-cp39-cp39-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:c0f31130ebc2d37cdd8e44605fb5fa7ad59049298b3f745c74fa74c62fbfcfc4"}, + {file = "cffi-1.16.0-cp39-cp39-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:8f8e709127c6c77446a8c0a8c8bf3c8ee706a06cd44b1e827c3e6a2ee6b8c098"}, + {file = "cffi-1.16.0-cp39-cp39-musllinux_1_1_i686.whl", hash = "sha256:748dcd1e3d3d7cd5443ef03ce8685043294ad6bd7c02a38d1bd367cfd968e000"}, + {file = "cffi-1.16.0-cp39-cp39-musllinux_1_1_x86_64.whl", hash = "sha256:8895613bcc094d4a1b2dbe179d88d7fb4a15cee43c052e8885783fac397d91fe"}, + {file = "cffi-1.16.0-cp39-cp39-win32.whl", hash = "sha256:ed86a35631f7bfbb28e108dd96773b9d5a6ce4811cf6ea468bb6a359b256b1e4"}, + {file = "cffi-1.16.0-cp39-cp39-win_amd64.whl", hash = "sha256:3686dffb02459559c74dd3d81748269ffb0eb027c39a6fc99502de37d501faa8"}, + {file = "cffi-1.16.0.tar.gz", hash = "sha256:bcb3ef43e58665bbda2fb198698fcae6776483e0c4a631aa5647806c25e02cc0"}, +] + +[[package]] +name = "charset-normalizer" +version = "3.3.2" +requires_python = ">=3.7.0" +summary = "The Real First Universal Charset Detector. Open, modern and actively maintained alternative to Chardet." +groups = ["doc", "legacy", "test"] +files = [ + {file = "charset-normalizer-3.3.2.tar.gz", hash = "sha256:f30c3cb33b24454a82faecaf01b19c18562b1e89558fb6c56de4d9118a032fd5"}, + {file = "charset_normalizer-3.3.2-cp310-cp310-macosx_10_9_universal2.whl", hash = "sha256:25baf083bf6f6b341f4121c2f3c548875ee6f5339300e08be3f2b2ba1721cdd3"}, + {file = "charset_normalizer-3.3.2-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:06435b539f889b1f6f4ac1758871aae42dc3a8c0e24ac9e60c2384973ad73027"}, + {file = "charset_normalizer-3.3.2-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:9063e24fdb1e498ab71cb7419e24622516c4a04476b17a2dab57e8baa30d6e03"}, + {file = "charset_normalizer-3.3.2-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:6897af51655e3691ff853668779c7bad41579facacf5fd7253b0133308cf000d"}, + {file = "charset_normalizer-3.3.2-cp310-cp310-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:1d3193f4a680c64b4b6a9115943538edb896edc190f0b222e73761716519268e"}, + {file = "charset_normalizer-3.3.2-cp310-cp310-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:cd70574b12bb8a4d2aaa0094515df2463cb429d8536cfb6c7ce983246983e5a6"}, + {file = "charset_normalizer-3.3.2-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:8465322196c8b4d7ab6d1e049e4c5cb460d0394da4a27d23cc242fbf0034b6b5"}, + {file = "charset_normalizer-3.3.2-cp310-cp310-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:a9a8e9031d613fd2009c182b69c7b2c1ef8239a0efb1df3f7c8da66d5dd3d537"}, + {file = "charset_normalizer-3.3.2-cp310-cp310-musllinux_1_1_aarch64.whl", hash = "sha256:beb58fe5cdb101e3a055192ac291b7a21e3b7ef4f67fa1d74e331a7f2124341c"}, + {file = "charset_normalizer-3.3.2-cp310-cp310-musllinux_1_1_i686.whl", hash = "sha256:e06ed3eb3218bc64786f7db41917d4e686cc4856944f53d5bdf83a6884432e12"}, + {file = "charset_normalizer-3.3.2-cp310-cp310-musllinux_1_1_ppc64le.whl", hash = "sha256:2e81c7b9c8979ce92ed306c249d46894776a909505d8f5a4ba55b14206e3222f"}, + {file = "charset_normalizer-3.3.2-cp310-cp310-musllinux_1_1_s390x.whl", hash = "sha256:572c3763a264ba47b3cf708a44ce965d98555f618ca42c926a9c1616d8f34269"}, + {file = "charset_normalizer-3.3.2-cp310-cp310-musllinux_1_1_x86_64.whl", hash = "sha256:fd1abc0d89e30cc4e02e4064dc67fcc51bd941eb395c502aac3ec19fab46b519"}, + {file = "charset_normalizer-3.3.2-cp310-cp310-win32.whl", hash = "sha256:3d47fa203a7bd9c5b6cee4736ee84ca03b8ef23193c0d1ca99b5089f72645c73"}, + {file = "charset_normalizer-3.3.2-cp310-cp310-win_amd64.whl", hash = "sha256:10955842570876604d404661fbccbc9c7e684caf432c09c715ec38fbae45ae09"}, + {file = "charset_normalizer-3.3.2-cp311-cp311-macosx_10_9_universal2.whl", hash = "sha256:802fe99cca7457642125a8a88a084cef28ff0cf9407060f7b93dca5aa25480db"}, + {file = "charset_normalizer-3.3.2-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:573f6eac48f4769d667c4442081b1794f52919e7edada77495aaed9236d13a96"}, + {file = "charset_normalizer-3.3.2-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:549a3a73da901d5bc3ce8d24e0600d1fa85524c10287f6004fbab87672bf3e1e"}, + {file = "charset_normalizer-3.3.2-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:f27273b60488abe721a075bcca6d7f3964f9f6f067c8c4c605743023d7d3944f"}, + {file = "charset_normalizer-3.3.2-cp311-cp311-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:1ceae2f17a9c33cb48e3263960dc5fc8005351ee19db217e9b1bb15d28c02574"}, + {file = "charset_normalizer-3.3.2-cp311-cp311-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:65f6f63034100ead094b8744b3b97965785388f308a64cf8d7c34f2f2e5be0c4"}, + {file = "charset_normalizer-3.3.2-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:753f10e867343b4511128c6ed8c82f7bec3bd026875576dfd88483c5c73b2fd8"}, + {file = "charset_normalizer-3.3.2-cp311-cp311-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:4a78b2b446bd7c934f5dcedc588903fb2f5eec172f3d29e52a9096a43722adfc"}, + {file = "charset_normalizer-3.3.2-cp311-cp311-musllinux_1_1_aarch64.whl", hash = "sha256:e537484df0d8f426ce2afb2d0f8e1c3d0b114b83f8850e5f2fbea0e797bd82ae"}, + {file = "charset_normalizer-3.3.2-cp311-cp311-musllinux_1_1_i686.whl", hash = "sha256:eb6904c354526e758fda7167b33005998fb68c46fbc10e013ca97f21ca5c8887"}, + {file = "charset_normalizer-3.3.2-cp311-cp311-musllinux_1_1_ppc64le.whl", hash = "sha256:deb6be0ac38ece9ba87dea880e438f25ca3eddfac8b002a2ec3d9183a454e8ae"}, + {file = "charset_normalizer-3.3.2-cp311-cp311-musllinux_1_1_s390x.whl", hash = "sha256:4ab2fe47fae9e0f9dee8c04187ce5d09f48eabe611be8259444906793ab7cbce"}, + {file = "charset_normalizer-3.3.2-cp311-cp311-musllinux_1_1_x86_64.whl", hash = "sha256:80402cd6ee291dcb72644d6eac93785fe2c8b9cb30893c1af5b8fdd753b9d40f"}, + {file = "charset_normalizer-3.3.2-cp311-cp311-win32.whl", hash = "sha256:7cd13a2e3ddeed6913a65e66e94b51d80a041145a026c27e6bb76c31a853c6ab"}, + {file = "charset_normalizer-3.3.2-cp311-cp311-win_amd64.whl", hash = "sha256:663946639d296df6a2bb2aa51b60a2454ca1cb29835324c640dafb5ff2131a77"}, + {file = "charset_normalizer-3.3.2-cp312-cp312-macosx_10_9_universal2.whl", hash = "sha256:0b2b64d2bb6d3fb9112bafa732def486049e63de9618b5843bcdd081d8144cd8"}, + {file = "charset_normalizer-3.3.2-cp312-cp312-macosx_10_9_x86_64.whl", hash = "sha256:ddbb2551d7e0102e7252db79ba445cdab71b26640817ab1e3e3648dad515003b"}, + {file = "charset_normalizer-3.3.2-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:55086ee1064215781fff39a1af09518bc9255b50d6333f2e4c74ca09fac6a8f6"}, + {file = "charset_normalizer-3.3.2-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:8f4a014bc36d3c57402e2977dada34f9c12300af536839dc38c0beab8878f38a"}, + {file = "charset_normalizer-3.3.2-cp312-cp312-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:a10af20b82360ab00827f916a6058451b723b4e65030c5a18577c8b2de5b3389"}, + {file = "charset_normalizer-3.3.2-cp312-cp312-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:8d756e44e94489e49571086ef83b2bb8ce311e730092d2c34ca8f7d925cb20aa"}, + {file = "charset_normalizer-3.3.2-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:90d558489962fd4918143277a773316e56c72da56ec7aa3dc3dbbe20fdfed15b"}, + {file = "charset_normalizer-3.3.2-cp312-cp312-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:6ac7ffc7ad6d040517be39eb591cac5ff87416c2537df6ba3cba3bae290c0fed"}, + {file = "charset_normalizer-3.3.2-cp312-cp312-musllinux_1_1_aarch64.whl", hash = "sha256:7ed9e526742851e8d5cc9e6cf41427dfc6068d4f5a3bb03659444b4cabf6bc26"}, + {file = "charset_normalizer-3.3.2-cp312-cp312-musllinux_1_1_i686.whl", hash = "sha256:8bdb58ff7ba23002a4c5808d608e4e6c687175724f54a5dade5fa8c67b604e4d"}, + {file = "charset_normalizer-3.3.2-cp312-cp312-musllinux_1_1_ppc64le.whl", hash = "sha256:6b3251890fff30ee142c44144871185dbe13b11bab478a88887a639655be1068"}, + {file = "charset_normalizer-3.3.2-cp312-cp312-musllinux_1_1_s390x.whl", hash = "sha256:b4a23f61ce87adf89be746c8a8974fe1c823c891d8f86eb218bb957c924bb143"}, + {file = "charset_normalizer-3.3.2-cp312-cp312-musllinux_1_1_x86_64.whl", hash = "sha256:efcb3f6676480691518c177e3b465bcddf57cea040302f9f4e6e191af91174d4"}, + {file = "charset_normalizer-3.3.2-cp312-cp312-win32.whl", hash = "sha256:d965bba47ddeec8cd560687584e88cf699fd28f192ceb452d1d7ee807c5597b7"}, + {file = "charset_normalizer-3.3.2-cp312-cp312-win_amd64.whl", hash = "sha256:96b02a3dc4381e5494fad39be677abcb5e6634bf7b4fa83a6dd3112607547001"}, + {file = "charset_normalizer-3.3.2-cp38-cp38-macosx_10_9_universal2.whl", hash = "sha256:6463effa3186ea09411d50efc7d85360b38d5f09b870c48e4600f63af490e56a"}, + {file = "charset_normalizer-3.3.2-cp38-cp38-macosx_10_9_x86_64.whl", hash = "sha256:6c4caeef8fa63d06bd437cd4bdcf3ffefe6738fb1b25951440d80dc7df8c03ac"}, + {file = "charset_normalizer-3.3.2-cp38-cp38-macosx_11_0_arm64.whl", hash = "sha256:37e55c8e51c236f95b033f6fb391d7d7970ba5fe7ff453dad675e88cf303377a"}, + {file = "charset_normalizer-3.3.2-cp38-cp38-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:fb69256e180cb6c8a894fee62b3afebae785babc1ee98b81cdf68bbca1987f33"}, + {file = "charset_normalizer-3.3.2-cp38-cp38-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:ae5f4161f18c61806f411a13b0310bea87f987c7d2ecdbdaad0e94eb2e404238"}, + {file = "charset_normalizer-3.3.2-cp38-cp38-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:b2b0a0c0517616b6869869f8c581d4eb2dd83a4d79e0ebcb7d373ef9956aeb0a"}, + {file = "charset_normalizer-3.3.2-cp38-cp38-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:45485e01ff4d3630ec0d9617310448a8702f70e9c01906b0d0118bdf9d124cf2"}, + {file = "charset_normalizer-3.3.2-cp38-cp38-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:eb00ed941194665c332bf8e078baf037d6c35d7c4f3102ea2d4f16ca94a26dc8"}, + {file = "charset_normalizer-3.3.2-cp38-cp38-musllinux_1_1_aarch64.whl", hash = "sha256:2127566c664442652f024c837091890cb1942c30937add288223dc895793f898"}, + {file = "charset_normalizer-3.3.2-cp38-cp38-musllinux_1_1_i686.whl", hash = "sha256:a50aebfa173e157099939b17f18600f72f84eed3049e743b68ad15bd69b6bf99"}, + {file = "charset_normalizer-3.3.2-cp38-cp38-musllinux_1_1_ppc64le.whl", hash = "sha256:4d0d1650369165a14e14e1e47b372cfcb31d6ab44e6e33cb2d4e57265290044d"}, + {file = "charset_normalizer-3.3.2-cp38-cp38-musllinux_1_1_s390x.whl", hash = "sha256:923c0c831b7cfcb071580d3f46c4baf50f174be571576556269530f4bbd79d04"}, + {file = "charset_normalizer-3.3.2-cp38-cp38-musllinux_1_1_x86_64.whl", hash = "sha256:06a81e93cd441c56a9b65d8e1d043daeb97a3d0856d177d5c90ba85acb3db087"}, + {file = "charset_normalizer-3.3.2-cp38-cp38-win32.whl", hash = "sha256:6ef1d82a3af9d3eecdba2321dc1b3c238245d890843e040e41e470ffa64c3e25"}, + {file = "charset_normalizer-3.3.2-cp38-cp38-win_amd64.whl", hash = "sha256:eb8821e09e916165e160797a6c17edda0679379a4be5c716c260e836e122f54b"}, + {file = "charset_normalizer-3.3.2-cp39-cp39-macosx_10_9_universal2.whl", hash = "sha256:c235ebd9baae02f1b77bcea61bce332cb4331dc3617d254df3323aa01ab47bd4"}, + {file = "charset_normalizer-3.3.2-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:5b4c145409bef602a690e7cfad0a15a55c13320ff7a3ad7ca59c13bb8ba4d45d"}, + {file = "charset_normalizer-3.3.2-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:68d1f8a9e9e37c1223b656399be5d6b448dea850bed7d0f87a8311f1ff3dabb0"}, + {file = "charset_normalizer-3.3.2-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:22afcb9f253dac0696b5a4be4a1c0f8762f8239e21b99680099abd9b2b1b2269"}, + {file = "charset_normalizer-3.3.2-cp39-cp39-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:e27ad930a842b4c5eb8ac0016b0a54f5aebbe679340c26101df33424142c143c"}, + {file = "charset_normalizer-3.3.2-cp39-cp39-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:1f79682fbe303db92bc2b1136016a38a42e835d932bab5b3b1bfcfbf0640e519"}, + {file = "charset_normalizer-3.3.2-cp39-cp39-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:b261ccdec7821281dade748d088bb6e9b69e6d15b30652b74cbbac25e280b796"}, + {file = "charset_normalizer-3.3.2-cp39-cp39-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:122c7fa62b130ed55f8f285bfd56d5f4b4a5b503609d181f9ad85e55c89f4185"}, + {file = "charset_normalizer-3.3.2-cp39-cp39-musllinux_1_1_aarch64.whl", hash = "sha256:d0eccceffcb53201b5bfebb52600a5fb483a20b61da9dbc885f8b103cbe7598c"}, + {file = "charset_normalizer-3.3.2-cp39-cp39-musllinux_1_1_i686.whl", hash = "sha256:9f96df6923e21816da7e0ad3fd47dd8f94b2a5ce594e00677c0013018b813458"}, + {file = "charset_normalizer-3.3.2-cp39-cp39-musllinux_1_1_ppc64le.whl", hash = "sha256:7f04c839ed0b6b98b1a7501a002144b76c18fb1c1850c8b98d458ac269e26ed2"}, + {file = "charset_normalizer-3.3.2-cp39-cp39-musllinux_1_1_s390x.whl", hash = "sha256:34d1c8da1e78d2e001f363791c98a272bb734000fcef47a491c1e3b0505657a8"}, + {file = "charset_normalizer-3.3.2-cp39-cp39-musllinux_1_1_x86_64.whl", hash = "sha256:ff8fa367d09b717b2a17a052544193ad76cd49979c805768879cb63d9ca50561"}, + {file = "charset_normalizer-3.3.2-cp39-cp39-win32.whl", hash = "sha256:aed38f6e4fb3f5d6bf81bfa990a07806be9d83cf7bacef998ab1a9bd660a581f"}, + {file = "charset_normalizer-3.3.2-cp39-cp39-win_amd64.whl", hash = "sha256:b01b88d45a6fcb69667cd6d2f7a9aeb4bf53760d7fc536bf679ec94fe9f3ff3d"}, + {file = "charset_normalizer-3.3.2-py3-none-any.whl", hash = "sha256:3e4d1f6587322d2788836a99c69062fbb091331ec940e02d12d179c1d53e25fc"}, +] + +[[package]] +name = "click" +version = "8.1.7" +requires_python = ">=3.7" +summary = "Composable command line interface toolkit" +groups = ["test"] +dependencies = [ + "colorama; platform_system == \"Windows\"", + "importlib-metadata; python_version < \"3.8\"", +] +files = [ + {file = "click-8.1.7-py3-none-any.whl", hash = "sha256:ae74fb96c20a0277a1d615f1e4d73c8414f5a98db8b799a7931d1582f3390c28"}, + {file = "click-8.1.7.tar.gz", hash = "sha256:ca9853ad459e787e2192211578cc907e7594e294c7ccc834310722b41b9ca6de"}, +] + +[[package]] +name = "colorama" +version = "0.4.6" +requires_python = "!=3.0.*,!=3.1.*,!=3.2.*,!=3.3.*,!=3.4.*,!=3.5.*,!=3.6.*,>=2.7" +summary = "Cross-platform colored terminal text." +groups = ["doc", "test"] +marker = "sys_platform == \"win32\" or platform_system == \"Windows\"" +files = [ + {file = "colorama-0.4.6-py2.py3-none-any.whl", hash = "sha256:4f1d9991f5acc0ca119f9d443620b77f9d6b33703e51011c16baf57afb285fc6"}, + {file = "colorama-0.4.6.tar.gz", hash = "sha256:08695f5cb7ed6e0531a20572697297273c47b8cae5a63ffc6d6ed5c201be6e44"}, +] + +[[package]] +name = "cryptography" +version = "42.0.7" +requires_python = ">=3.7" +summary = "cryptography is a package which provides cryptographic recipes and primitives to Python developers." +groups = ["keyring", "test"] +marker = "python_version < \"3.13\" or sys_platform == \"linux\"" +dependencies = [ + "cffi>=1.12; platform_python_implementation != \"PyPy\"", +] +files = [ + {file = "cryptography-42.0.7-cp37-abi3-macosx_10_12_universal2.whl", hash = "sha256:a987f840718078212fdf4504d0fd4c6effe34a7e4740378e59d47696e8dfb477"}, + {file = "cryptography-42.0.7-cp37-abi3-macosx_10_12_x86_64.whl", hash = "sha256:bd13b5e9b543532453de08bcdc3cc7cebec6f9883e886fd20a92f26940fd3e7a"}, + {file = "cryptography-42.0.7-cp37-abi3-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:a79165431551042cc9d1d90e6145d5d0d3ab0f2d66326c201d9b0e7f5bf43604"}, + {file = "cryptography-42.0.7-cp37-abi3-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:a47787a5e3649008a1102d3df55424e86606c9bae6fb77ac59afe06d234605f8"}, + {file = "cryptography-42.0.7-cp37-abi3-manylinux_2_28_aarch64.whl", hash = "sha256:02c0eee2d7133bdbbc5e24441258d5d2244beb31da5ed19fbb80315f4bbbff55"}, + {file = "cryptography-42.0.7-cp37-abi3-manylinux_2_28_x86_64.whl", hash = "sha256:5e44507bf8d14b36b8389b226665d597bc0f18ea035d75b4e53c7b1ea84583cc"}, + {file = "cryptography-42.0.7-cp37-abi3-musllinux_1_1_aarch64.whl", hash = "sha256:7f8b25fa616d8b846aef64b15c606bb0828dbc35faf90566eb139aa9cff67af2"}, + {file = "cryptography-42.0.7-cp37-abi3-musllinux_1_1_x86_64.whl", hash = "sha256:93a3209f6bb2b33e725ed08ee0991b92976dfdcf4e8b38646540674fc7508e13"}, + {file = "cryptography-42.0.7-cp37-abi3-musllinux_1_2_aarch64.whl", hash = "sha256:e6b8f1881dac458c34778d0a424ae5769de30544fc678eac51c1c8bb2183e9da"}, + {file = "cryptography-42.0.7-cp37-abi3-musllinux_1_2_x86_64.whl", hash = "sha256:3de9a45d3b2b7d8088c3fbf1ed4395dfeff79d07842217b38df14ef09ce1d8d7"}, + {file = "cryptography-42.0.7-cp37-abi3-win32.whl", hash = "sha256:789caea816c6704f63f6241a519bfa347f72fbd67ba28d04636b7c6b7da94b0b"}, + {file = "cryptography-42.0.7-cp37-abi3-win_amd64.whl", hash = "sha256:8cb8ce7c3347fcf9446f201dc30e2d5a3c898d009126010cbd1f443f28b52678"}, + {file = "cryptography-42.0.7-cp39-abi3-macosx_10_12_universal2.whl", hash = "sha256:a3a5ac8b56fe37f3125e5b72b61dcde43283e5370827f5233893d461b7360cd4"}, + {file = "cryptography-42.0.7-cp39-abi3-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:779245e13b9a6638df14641d029add5dc17edbef6ec915688f3acb9e720a5858"}, + {file = "cryptography-42.0.7-cp39-abi3-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:0d563795db98b4cd57742a78a288cdbdc9daedac29f2239793071fe114f13785"}, + {file = "cryptography-42.0.7-cp39-abi3-manylinux_2_28_aarch64.whl", hash = "sha256:31adb7d06fe4383226c3e963471f6837742889b3c4caa55aac20ad951bc8ffda"}, + {file = "cryptography-42.0.7-cp39-abi3-manylinux_2_28_x86_64.whl", hash = "sha256:efd0bf5205240182e0f13bcaea41be4fdf5c22c5129fc7ced4a0282ac86998c9"}, + {file = "cryptography-42.0.7-cp39-abi3-musllinux_1_1_aarch64.whl", hash = "sha256:a9bc127cdc4ecf87a5ea22a2556cab6c7eda2923f84e4f3cc588e8470ce4e42e"}, + {file = "cryptography-42.0.7-cp39-abi3-musllinux_1_1_x86_64.whl", hash = "sha256:3577d029bc3f4827dd5bf8bf7710cac13527b470bbf1820a3f394adb38ed7d5f"}, + {file = "cryptography-42.0.7-cp39-abi3-musllinux_1_2_aarch64.whl", hash = "sha256:2e47577f9b18723fa294b0ea9a17d5e53a227867a0a4904a1a076d1646d45ca1"}, + {file = "cryptography-42.0.7-cp39-abi3-musllinux_1_2_x86_64.whl", hash = "sha256:1a58839984d9cb34c855197043eaae2c187d930ca6d644612843b4fe8513c886"}, + {file = "cryptography-42.0.7-cp39-abi3-win32.whl", hash = "sha256:e6b79d0adb01aae87e8a44c2b64bc3f3fe59515280e00fb6d57a7267a2583cda"}, + {file = "cryptography-42.0.7-cp39-abi3-win_amd64.whl", hash = "sha256:16268d46086bb8ad5bf0a2b5544d8a9ed87a0e33f5e77dd3c3301e63d941a83b"}, + {file = "cryptography-42.0.7-pp310-pypy310_pp73-macosx_10_12_x86_64.whl", hash = "sha256:2954fccea107026512b15afb4aa664a5640cd0af630e2ee3962f2602693f0c82"}, + {file = "cryptography-42.0.7-pp310-pypy310_pp73-manylinux_2_28_aarch64.whl", hash = "sha256:362e7197754c231797ec45ee081f3088a27a47c6c01eff2ac83f60f85a50fe60"}, + {file = "cryptography-42.0.7-pp310-pypy310_pp73-manylinux_2_28_x86_64.whl", hash = "sha256:4f698edacf9c9e0371112792558d2f705b5645076cc0aaae02f816a0171770fd"}, + {file = "cryptography-42.0.7-pp310-pypy310_pp73-win_amd64.whl", hash = "sha256:5482e789294854c28237bba77c4c83be698be740e31a3ae5e879ee5444166582"}, + {file = "cryptography-42.0.7-pp39-pypy39_pp73-macosx_10_12_x86_64.whl", hash = "sha256:e9b2a6309f14c0497f348d08a065d52f3020656f675819fc405fb63bbcd26562"}, + {file = "cryptography-42.0.7-pp39-pypy39_pp73-manylinux_2_28_aarch64.whl", hash = "sha256:d8e3098721b84392ee45af2dd554c947c32cc52f862b6a3ae982dbb90f577f14"}, + {file = "cryptography-42.0.7-pp39-pypy39_pp73-manylinux_2_28_x86_64.whl", hash = "sha256:c65f96dad14f8528a447414125e1fc8feb2ad5a272b8f68477abbcc1ea7d94b9"}, + {file = "cryptography-42.0.7-pp39-pypy39_pp73-win_amd64.whl", hash = "sha256:36017400817987670037fbb0324d71489b6ead6231c9604f8fc1f7d008087c68"}, + {file = "cryptography-42.0.7.tar.gz", hash = "sha256:ecbfbc00bf55888edda9868a4cf927205de8499e7fabe6c050322298382953f2"}, +] + +[[package]] +name = "docutils" +version = "0.20.1" +requires_python = ">=3.7" +summary = "Docutils -- Python Documentation Utilities" +groups = ["doc"] +files = [ + {file = "docutils-0.20.1-py3-none-any.whl", hash = "sha256:96f387a2c5562db4476f09f13bbab2192e764cac08ebbf3a34a95d9b1e4a59d6"}, + {file = "docutils-0.20.1.tar.gz", hash = "sha256:f08a4e276c3a1583a86dce3e34aba3fe04d02bba2dd51ed16106244e8a923e3b"}, +] + +[[package]] +name = "exceptiongroup" +version = "1.3.0" +requires_python = ">=3.7" +summary = "Backport of PEP 654 (exception groups)" +groups = ["default", "test"] +marker = "python_version < \"3.11\"" +dependencies = [ + "typing-extensions>=4.6.0; python_version < \"3.13\"", +] +files = [ + {file = "exceptiongroup-1.3.0-py3-none-any.whl", hash = "sha256:4d111e6e0c13d0644cad6ddaa7ed0261a0b36971f6d23e7ec9b4b9097da78a10"}, + {file = "exceptiongroup-1.3.0.tar.gz", hash = "sha256:b241f5885f560bc56a59ee63ca4c6a8bfa46ae4ad651af316d4e81817bb9fd88"}, +] + +[[package]] +name = "flask" +version = "3.1.3" +requires_python = ">=3.9" +summary = "A simple framework for building complex web applications." +groups = ["test"] +dependencies = [ + "blinker>=1.9.0", + "click>=8.1.3", + "importlib-metadata>=3.6.0; python_version < \"3.10\"", + "itsdangerous>=2.2.0", + "jinja2>=3.1.2", + "markupsafe>=2.1.1", + "werkzeug>=3.1.0", +] +files = [ + {file = "flask-3.1.3-py3-none-any.whl", hash = "sha256:f4bcbefc124291925f1a26446da31a5178f9483862233b23c0c96a20701f670c"}, + {file = "flask-3.1.3.tar.gz", hash = "sha256:0ef0e52b8a9cd932855379197dd8f94047b359ca0a78695144304cb45f87c9eb"}, +] + +[[package]] +name = "furo" +version = "2025.12.19" +requires_python = ">=3.8" +summary = "A clean customisable Sphinx documentation theme." +groups = ["doc"] +dependencies = [ + "accessible-pygments>=0.0.5", + "beautifulsoup4", + "pygments>=2.7", + "sphinx-basic-ng>=1.0.0.beta2", + "sphinx<10.0,>=7.0", +] +files = [ + {file = "furo-2025.12.19-py3-none-any.whl", hash = "sha256:bb0ead5309f9500130665a26bee87693c41ce4dbdff864dbfb6b0dae4673d24f"}, + {file = "furo-2025.12.19.tar.gz", hash = "sha256:188d1f942037d8b37cd3985b955839fea62baa1730087dc29d157677c857e2a7"}, +] + +[[package]] +name = "h11" +version = "0.16.0" +requires_python = ">=3.8" +summary = "A pure-Python, bring-your-own-I/O implementation of HTTP/1.1" +groups = ["default"] +files = [ + {file = "h11-0.16.0-py3-none-any.whl", hash = "sha256:63cf8bbe7522de3bf65932fda1d9c2772064ffb3dae62d55932da54b31cb6c86"}, + {file = "h11-0.16.0.tar.gz", hash = "sha256:4e35b956cf45792e4caa5885e69fba00bdbc6ffafbfa020300e549b208ee5ff1"}, +] + +[[package]] +name = "httpcore" +version = "1.0.9" +requires_python = ">=3.8" +summary = "A minimal low-level HTTP client." +groups = ["default"] +dependencies = [ + "certifi", + "h11>=0.16", +] +files = [ + {file = "httpcore-1.0.9-py3-none-any.whl", hash = "sha256:2d400746a40668fc9dec9810239072b40b4484b640a8c38fd654a024c7a1bf55"}, + {file = "httpcore-1.0.9.tar.gz", hash = "sha256:6e34463af53fd2ab5d807f399a9b45ea31c3dfa2276f15a2c3f00afff6e176e8"}, +] + +[[package]] +name = "httpx" +version = "0.28.1" +requires_python = ">=3.8" +summary = "The next generation HTTP client." +groups = ["default"] +dependencies = [ + "anyio", + "certifi", + "httpcore==1.*", + "idna", +] +files = [ + {file = "httpx-0.28.1-py3-none-any.whl", hash = "sha256:d909fcccc110f8c7faf814ca82a9a4d816bc5a6dbfea25d6591d6985b8ba59ad"}, + {file = "httpx-0.28.1.tar.gz", hash = "sha256:75e98c5f16b0f35b567856f597f06ff2270a374470a5c2392242528e3e3e42fc"}, +] + +[[package]] +name = "idna" +version = "3.11" +requires_python = ">=3.8" +summary = "Internationalized Domain Names in Applications (IDNA)" +groups = ["default", "doc", "legacy", "test"] +files = [ + {file = "idna-3.11-py3-none-any.whl", hash = "sha256:771a87f49d9defaf64091e6e6fe9c18d4833f140bd19464795bc32d966ca37ea"}, + {file = "idna-3.11.tar.gz", hash = "sha256:795dafcc9c04ed0c1fb032c2aa73654d8e8c5023a7df64a53f39190ada629902"}, +] + +[[package]] +name = "imagesize" +version = "1.4.1" +requires_python = ">=2.7, !=3.0.*, !=3.1.*, !=3.2.*, !=3.3.*" +summary = "Getting image size from png/jpeg/jpeg2000/gif file" +groups = ["doc"] +files = [ + {file = "imagesize-1.4.1-py2.py3-none-any.whl", hash = "sha256:0d8d18d08f840c19d0ee7ca1fd82490fdc3729b7ac93f49870406ddde8ef8d8b"}, + {file = "imagesize-1.4.1.tar.gz", hash = "sha256:69150444affb9cb0d5cc5a92b3676f0b2fb7cd9ae39e947a5e11a36b4497cd4a"}, +] + +[[package]] +name = "importlib-metadata" +version = "7.0.1" +requires_python = ">=3.8" +summary = "Read metadata from Python packages" +groups = ["doc", "keyring", "test"] +marker = "python_version < \"3.12\"" +dependencies = [ + "typing-extensions>=3.6.4; python_version < \"3.8\"", + "zipp>=0.5", +] +files = [ + {file = "importlib_metadata-7.0.1-py3-none-any.whl", hash = "sha256:4805911c3a4ec7c3966410053e9ec6a1fecd629117df5adee56dfc9432a1081e"}, + {file = "importlib_metadata-7.0.1.tar.gz", hash = "sha256:f238736bb06590ae52ac1fab06a3a9ef1d8dce2b7a35b5ab329371d6c8f5d2cc"}, +] + +[[package]] +name = "iniconfig" +version = "2.0.0" +requires_python = ">=3.7" +summary = "brain-dead simple config-ini parsing" +groups = ["test"] +files = [ + {file = "iniconfig-2.0.0-py3-none-any.whl", hash = "sha256:b6a85871a79d2e3b22d2d1b94ac2824226a63c6b741c88f7ae975f18b6778374"}, + {file = "iniconfig-2.0.0.tar.gz", hash = "sha256:2d91e135bf72d31a410b17c16da610a82cb55f6b0477d1a902134b24a455b8b3"}, +] + +[[package]] +name = "itsdangerous" +version = "2.2.0" +requires_python = ">=3.8" +summary = "Safely pass data to untrusted environments and back." +groups = ["test"] +files = [ + {file = "itsdangerous-2.2.0-py3-none-any.whl", hash = "sha256:c6242fc49e35958c8b15141343aa660db5fc54d4f13a1db01a3f5891b98700ef"}, + {file = "itsdangerous-2.2.0.tar.gz", hash = "sha256:e0050c0b7da1eea53ffaf149c0cfbb5c6e2e2b69c4bef22c81fa6eb73e5f6173"}, +] + +[[package]] +name = "jaraco-classes" +version = "3.3.0" +requires_python = ">=3.8" +summary = "Utility functions for Python class constructs" +groups = ["keyring"] +dependencies = [ + "more-itertools", +] +files = [ + {file = "jaraco.classes-3.3.0-py3-none-any.whl", hash = "sha256:10afa92b6743f25c0cf5f37c6bb6e18e2c5bb84a16527ccfc0040ea377e7aaeb"}, + {file = "jaraco.classes-3.3.0.tar.gz", hash = "sha256:c063dd08e89217cee02c8d5e5ec560f2c8ce6cdc2fcdc2e68f7b2e5547ed3621"}, +] + +[[package]] +name = "jaraco-context" +version = "4.3.0" +requires_python = ">=3.7" +summary = "Context managers by jaraco" +groups = ["keyring"] +files = [ + {file = "jaraco.context-4.3.0-py3-none-any.whl", hash = "sha256:5d9e95ca0faa78943ed66f6bc658dd637430f16125d86988e77844c741ff2f11"}, + {file = "jaraco.context-4.3.0.tar.gz", hash = "sha256:4dad2404540b936a20acedec53355bdaea223acb88fd329fa6de9261c941566e"}, +] + +[[package]] +name = "jaraco-functools" +version = "4.0.0" +requires_python = ">=3.8" +summary = "Functools like those found in stdlib" +groups = ["keyring"] +dependencies = [ + "more-itertools", +] +files = [ + {file = "jaraco.functools-4.0.0-py3-none-any.whl", hash = "sha256:daf276ddf234bea897ef14f43c4e1bf9eefeac7b7a82a4dd69228ac20acff68d"}, + {file = "jaraco.functools-4.0.0.tar.gz", hash = "sha256:c279cb24c93d694ef7270f970d499cab4d3813f4e08273f95398651a634f0925"}, +] + +[[package]] +name = "jeepney" +version = "0.8.0" +requires_python = ">=3.7" +summary = "Low-level, pure Python DBus protocol wrapper." +groups = ["keyring"] +marker = "sys_platform == \"linux\"" +files = [ + {file = "jeepney-0.8.0-py3-none-any.whl", hash = "sha256:c0a454ad016ca575060802ee4d590dd912e35c122fa04e70306de3d076cce755"}, + {file = "jeepney-0.8.0.tar.gz", hash = "sha256:5efe48d255973902f6badc3ce55e2aa6c5c3b3bc642059ef3a91247bcfcc5806"}, +] + +[[package]] +name = "jinja2" +version = "3.1.3" +requires_python = ">=3.7" +summary = "A very fast and expressive template engine." +groups = ["doc", "test"] +dependencies = [ + "MarkupSafe>=2.0", +] +files = [ + {file = "Jinja2-3.1.3-py3-none-any.whl", hash = "sha256:7d6d50dd97d52cbc355597bd845fabfbac3f551e1f99619e39a35ce8c370b5fa"}, + {file = "Jinja2-3.1.3.tar.gz", hash = "sha256:ac8bd6544d4bb2c9792bf3a159e80bba8fda7f07e81bc3aed565432d5925ba90"}, +] + +[[package]] +name = "keyring" +version = "25.7.0" +requires_python = ">=3.9" +summary = "Store and access your passwords safely." +groups = ["keyring"] +dependencies = [ + "SecretStorage>=3.2; sys_platform == \"linux\"", + "importlib-metadata>=4.11.4; python_version < \"3.12\"", + "jaraco-classes", + "jaraco-context", + "jaraco-functools", + "jeepney>=0.4.2; sys_platform == \"linux\"", + "pywin32-ctypes>=0.2.0; sys_platform == \"win32\"", +] +files = [ + {file = "keyring-25.7.0-py3-none-any.whl", hash = "sha256:be4a0b195f149690c166e850609a477c532ddbfbaed96a404d4e43f8d5e2689f"}, + {file = "keyring-25.7.0.tar.gz", hash = "sha256:fe01bd85eb3f8fb3dd0405defdeac9a5b4f6f0439edbb3149577f244a2e8245b"}, +] + +[[package]] +name = "markdown-it-py" +version = "3.0.0" +requires_python = ">=3.8" +summary = "Python port of markdown-it. Markdown parsing, done right!" +groups = ["doc"] +dependencies = [ + "mdurl~=0.1", +] +files = [ + {file = "markdown-it-py-3.0.0.tar.gz", hash = "sha256:e3f60a94fa066dc52ec76661e37c851cb232d92f9886b15cb560aaada2df8feb"}, + {file = "markdown_it_py-3.0.0-py3-none-any.whl", hash = "sha256:355216845c60bd96232cd8d8c40e8f9765cc86f46880e43a8fd22dc1a1a8cab1"}, +] + +[[package]] +name = "markupsafe" +version = "2.1.3" +requires_python = ">=3.7" +summary = "Safely add untrusted strings to HTML/XML markup." +groups = ["doc", "test"] +files = [ + {file = "MarkupSafe-2.1.3-cp310-cp310-macosx_10_9_universal2.whl", hash = "sha256:cd0f502fe016460680cd20aaa5a76d241d6f35a1c3350c474bac1273803893fa"}, + {file = "MarkupSafe-2.1.3-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:e09031c87a1e51556fdcb46e5bd4f59dfb743061cf93c4d6831bf894f125eb57"}, + {file = "MarkupSafe-2.1.3-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:68e78619a61ecf91e76aa3e6e8e33fc4894a2bebe93410754bd28fce0a8a4f9f"}, + {file = "MarkupSafe-2.1.3-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:65c1a9bcdadc6c28eecee2c119465aebff8f7a584dd719facdd9e825ec61ab52"}, + {file = "MarkupSafe-2.1.3-cp310-cp310-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:525808b8019e36eb524b8c68acdd63a37e75714eac50e988180b169d64480a00"}, + {file = "MarkupSafe-2.1.3-cp310-cp310-musllinux_1_1_aarch64.whl", hash = "sha256:962f82a3086483f5e5f64dbad880d31038b698494799b097bc59c2edf392fce6"}, + {file = "MarkupSafe-2.1.3-cp310-cp310-musllinux_1_1_i686.whl", hash = "sha256:aa7bd130efab1c280bed0f45501b7c8795f9fdbeb02e965371bbef3523627779"}, + {file = "MarkupSafe-2.1.3-cp310-cp310-musllinux_1_1_x86_64.whl", hash = "sha256:c9c804664ebe8f83a211cace637506669e7890fec1b4195b505c214e50dd4eb7"}, + {file = "MarkupSafe-2.1.3-cp310-cp310-win32.whl", hash = "sha256:10bbfe99883db80bdbaff2dcf681dfc6533a614f700da1287707e8a5d78a8431"}, + {file = "MarkupSafe-2.1.3-cp310-cp310-win_amd64.whl", hash = "sha256:1577735524cdad32f9f694208aa75e422adba74f1baee7551620e43a3141f559"}, + {file = "MarkupSafe-2.1.3-cp311-cp311-macosx_10_9_universal2.whl", hash = "sha256:ad9e82fb8f09ade1c3e1b996a6337afac2b8b9e365f926f5a61aacc71adc5b3c"}, + {file = "MarkupSafe-2.1.3-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:3c0fae6c3be832a0a0473ac912810b2877c8cb9d76ca48de1ed31e1c68386575"}, + {file = "MarkupSafe-2.1.3-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:b076b6226fb84157e3f7c971a47ff3a679d837cf338547532ab866c57930dbee"}, + {file = "MarkupSafe-2.1.3-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:bfce63a9e7834b12b87c64d6b155fdd9b3b96191b6bd334bf37db7ff1fe457f2"}, + {file = "MarkupSafe-2.1.3-cp311-cp311-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:338ae27d6b8745585f87218a3f23f1512dbf52c26c28e322dbe54bcede54ccb9"}, + {file = "MarkupSafe-2.1.3-cp311-cp311-musllinux_1_1_aarch64.whl", hash = "sha256:e4dd52d80b8c83fdce44e12478ad2e85c64ea965e75d66dbeafb0a3e77308fcc"}, + {file = "MarkupSafe-2.1.3-cp311-cp311-musllinux_1_1_i686.whl", hash = "sha256:df0be2b576a7abbf737b1575f048c23fb1d769f267ec4358296f31c2479db8f9"}, + {file = "MarkupSafe-2.1.3-cp311-cp311-musllinux_1_1_x86_64.whl", hash = "sha256:5bbe06f8eeafd38e5d0a4894ffec89378b6c6a625ff57e3028921f8ff59318ac"}, + {file = "MarkupSafe-2.1.3-cp311-cp311-win32.whl", hash = "sha256:dd15ff04ffd7e05ffcb7fe79f1b98041b8ea30ae9234aed2a9168b5797c3effb"}, + {file = "MarkupSafe-2.1.3-cp311-cp311-win_amd64.whl", hash = "sha256:134da1eca9ec0ae528110ccc9e48041e0828d79f24121a1a146161103c76e686"}, + {file = "MarkupSafe-2.1.3-cp312-cp312-macosx_10_9_universal2.whl", hash = "sha256:f698de3fd0c4e6972b92290a45bd9b1536bffe8c6759c62471efaa8acb4c37bc"}, + {file = "MarkupSafe-2.1.3-cp312-cp312-macosx_10_9_x86_64.whl", hash = "sha256:aa57bd9cf8ae831a362185ee444e15a93ecb2e344c8e52e4d721ea3ab6ef1823"}, + {file = "MarkupSafe-2.1.3-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:ffcc3f7c66b5f5b7931a5aa68fc9cecc51e685ef90282f4a82f0f5e9b704ad11"}, + {file = "MarkupSafe-2.1.3-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:47d4f1c5f80fc62fdd7777d0d40a2e9dda0a05883ab11374334f6c4de38adffd"}, + {file = "MarkupSafe-2.1.3-cp312-cp312-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:1f67c7038d560d92149c060157d623c542173016c4babc0c1913cca0564b9939"}, + {file = "MarkupSafe-2.1.3-cp312-cp312-musllinux_1_1_aarch64.whl", hash = "sha256:9aad3c1755095ce347e26488214ef77e0485a3c34a50c5a5e2471dff60b9dd9c"}, + {file = "MarkupSafe-2.1.3-cp312-cp312-musllinux_1_1_i686.whl", hash = "sha256:14ff806850827afd6b07a5f32bd917fb7f45b046ba40c57abdb636674a8b559c"}, + {file = "MarkupSafe-2.1.3-cp312-cp312-musllinux_1_1_x86_64.whl", hash = "sha256:8f9293864fe09b8149f0cc42ce56e3f0e54de883a9de90cd427f191c346eb2e1"}, + {file = "MarkupSafe-2.1.3-cp312-cp312-win32.whl", hash = "sha256:715d3562f79d540f251b99ebd6d8baa547118974341db04f5ad06d5ea3eb8007"}, + {file = "MarkupSafe-2.1.3-cp312-cp312-win_amd64.whl", hash = "sha256:1b8dd8c3fd14349433c79fa8abeb573a55fc0fdd769133baac1f5e07abf54aeb"}, + {file = "MarkupSafe-2.1.3-cp38-cp38-macosx_10_9_universal2.whl", hash = "sha256:2ef12179d3a291be237280175b542c07a36e7f60718296278d8593d21ca937d4"}, + {file = "MarkupSafe-2.1.3-cp38-cp38-macosx_10_9_x86_64.whl", hash = "sha256:2c1b19b3aaacc6e57b7e25710ff571c24d6c3613a45e905b1fde04d691b98ee0"}, + {file = "MarkupSafe-2.1.3-cp38-cp38-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:8afafd99945ead6e075b973fefa56379c5b5c53fd8937dad92c662da5d8fd5ee"}, + {file = "MarkupSafe-2.1.3-cp38-cp38-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:8c41976a29d078bb235fea9b2ecd3da465df42a562910f9022f1a03107bd02be"}, + {file = "MarkupSafe-2.1.3-cp38-cp38-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:d080e0a5eb2529460b30190fcfcc4199bd7f827663f858a226a81bc27beaa97e"}, + {file = "MarkupSafe-2.1.3-cp38-cp38-musllinux_1_1_aarch64.whl", hash = "sha256:69c0f17e9f5a7afdf2cc9fb2d1ce6aabdb3bafb7f38017c0b77862bcec2bbad8"}, + {file = "MarkupSafe-2.1.3-cp38-cp38-musllinux_1_1_i686.whl", hash = "sha256:504b320cd4b7eff6f968eddf81127112db685e81f7e36e75f9f84f0df46041c3"}, + {file = "MarkupSafe-2.1.3-cp38-cp38-musllinux_1_1_x86_64.whl", hash = "sha256:42de32b22b6b804f42c5d98be4f7e5e977ecdd9ee9b660fda1a3edf03b11792d"}, + {file = "MarkupSafe-2.1.3-cp38-cp38-win32.whl", hash = "sha256:ceb01949af7121f9fc39f7d27f91be8546f3fb112c608bc4029aef0bab86a2a5"}, + {file = "MarkupSafe-2.1.3-cp38-cp38-win_amd64.whl", hash = "sha256:1b40069d487e7edb2676d3fbdb2b0829ffa2cd63a2ec26c4938b2d34391b4ecc"}, + {file = "MarkupSafe-2.1.3-cp39-cp39-macosx_10_9_universal2.whl", hash = "sha256:8023faf4e01efadfa183e863fefde0046de576c6f14659e8782065bcece22198"}, + {file = "MarkupSafe-2.1.3-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:6b2b56950d93e41f33b4223ead100ea0fe11f8e6ee5f641eb753ce4b77a7042b"}, + {file = "MarkupSafe-2.1.3-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:9dcdfd0eaf283af041973bff14a2e143b8bd64e069f4c383416ecd79a81aab58"}, + {file = "MarkupSafe-2.1.3-cp39-cp39-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:05fb21170423db021895e1ea1e1f3ab3adb85d1c2333cbc2310f2a26bc77272e"}, + {file = "MarkupSafe-2.1.3-cp39-cp39-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:282c2cb35b5b673bbcadb33a585408104df04f14b2d9b01d4c345a3b92861c2c"}, + {file = "MarkupSafe-2.1.3-cp39-cp39-musllinux_1_1_aarch64.whl", hash = "sha256:ab4a0df41e7c16a1392727727e7998a467472d0ad65f3ad5e6e765015df08636"}, + {file = "MarkupSafe-2.1.3-cp39-cp39-musllinux_1_1_i686.whl", hash = "sha256:7ef3cb2ebbf91e330e3bb937efada0edd9003683db6b57bb108c4001f37a02ea"}, + {file = "MarkupSafe-2.1.3-cp39-cp39-musllinux_1_1_x86_64.whl", hash = "sha256:0a4e4a1aff6c7ac4cd55792abf96c915634c2b97e3cc1c7129578aa68ebd754e"}, + {file = "MarkupSafe-2.1.3-cp39-cp39-win32.whl", hash = "sha256:fec21693218efe39aa7f8599346e90c705afa52c5b31ae019b2e57e8f6542bb2"}, + {file = "MarkupSafe-2.1.3-cp39-cp39-win_amd64.whl", hash = "sha256:3fd4abcb888d15a94f32b75d8fd18ee162ca0c064f35b11134be77050296d6ba"}, + {file = "MarkupSafe-2.1.3.tar.gz", hash = "sha256:af598ed32d6ae86f1b747b82783958b1a4ab8f617b06fe68795c7f026abbdcad"}, +] + +[[package]] +name = "mdit-py-plugins" +version = "0.4.0" +requires_python = ">=3.8" +summary = "Collection of plugins for markdown-it-py" +groups = ["doc"] +dependencies = [ + "markdown-it-py<4.0.0,>=1.0.0", +] +files = [ + {file = "mdit_py_plugins-0.4.0-py3-none-any.whl", hash = "sha256:b51b3bb70691f57f974e257e367107857a93b36f322a9e6d44ca5bf28ec2def9"}, + {file = "mdit_py_plugins-0.4.0.tar.gz", hash = "sha256:d8ab27e9aed6c38aa716819fedfde15ca275715955f8a185a8e1cf90fb1d2c1b"}, +] + +[[package]] +name = "mdurl" +version = "0.1.2" +requires_python = ">=3.7" +summary = "Markdown URL utilities" +groups = ["doc"] +files = [ + {file = "mdurl-0.1.2-py3-none-any.whl", hash = "sha256:84008a41e51615a49fc9966191ff91509e3c40b939176e643fd50a5c2196b8f8"}, + {file = "mdurl-0.1.2.tar.gz", hash = "sha256:bb413d29f5eea38f31dd4754dd7377d4465116fb207585f97bf925588687c1ba"}, +] + +[[package]] +name = "more-itertools" +version = "10.2.0" +requires_python = ">=3.8" +summary = "More routines for operating on iterables, beyond itertools" +groups = ["keyring"] +files = [ + {file = "more-itertools-10.2.0.tar.gz", hash = "sha256:8fccb480c43d3e99a00087634c06dd02b0d50fbf088b380de5a41a015ec239e1"}, + {file = "more_itertools-10.2.0-py3-none-any.whl", hash = "sha256:686b06abe565edfab151cb8fd385a05651e1fdf8f0a14191e4439283421f8684"}, +] + +[[package]] +name = "myst-parser" +version = "3.0.1" +requires_python = ">=3.8" +summary = "An extended [CommonMark](https://spec.commonmark.org/) compliant parser," +groups = ["doc"] +dependencies = [ + "docutils<0.22,>=0.18", + "jinja2", + "markdown-it-py~=3.0", + "mdit-py-plugins~=0.4", + "pyyaml", + "sphinx<8,>=6", +] +files = [ + {file = "myst_parser-3.0.1-py3-none-any.whl", hash = "sha256:6457aaa33a5d474aca678b8ead9b3dc298e89c68e67012e73146ea6fd54babf1"}, + {file = "myst_parser-3.0.1.tar.gz", hash = "sha256:88f0cb406cb363b077d176b51c476f62d60604d68a8dcdf4832e080441301a87"}, +] + +[[package]] +name = "packaging" +version = "26.3" +requires_python = ">=3.9" +summary = "Core utilities for Python packages" +groups = ["default", "doc", "test"] +files = [ + {file = "packaging-26.3-py3-none-any.whl", hash = "sha256:d7193f7c8e4e93f444fde0262bf90af30e16fa0ad0ad44cb553c87339b23cd1c"}, + {file = "packaging-26.3.tar.gz", hash = "sha256:94edc256424af38762eb31306eed28beb9f0efc50a8837492c9d6fd6004aed79"}, +] + +[[package]] +name = "pluggy" +version = "1.5.0" +requires_python = ">=3.8" +summary = "plugin and hook calling mechanisms for python" +groups = ["test"] +files = [ + {file = "pluggy-1.5.0-py3-none-any.whl", hash = "sha256:44e1ad92c8ca002de6377e165f3e0f1be63266ab4d554740532335b9d75ea669"}, + {file = "pluggy-1.5.0.tar.gz", hash = "sha256:2cffa88e94fdc978c4c574f15f9e59b7f4201d439195c3715ca9e2486f1d0cf1"}, +] + +[[package]] +name = "pycparser" +version = "2.22" +requires_python = ">=3.8" +summary = "C parser in Python" +groups = ["keyring", "test"] +marker = "python_version < \"3.13\" and platform_python_implementation != \"PyPy\" or sys_platform == \"linux\" and platform_python_implementation != \"PyPy\"" +files = [ + {file = "pycparser-2.22-py3-none-any.whl", hash = "sha256:c3702b6d3dd8c7abc1afa565d7e63d53a1d0bd86cdc24edd75470f4de499cfcc"}, + {file = "pycparser-2.22.tar.gz", hash = "sha256:491c8be9c040f5390f5bf44a5b07752bd07f56edf992381b05c701439eec10f6"}, +] + +[[package]] +name = "pygments" +version = "2.17.2" +requires_python = ">=3.7" +summary = "Pygments is a syntax highlighting package written in Python." +groups = ["doc", "test"] +files = [ + {file = "pygments-2.17.2-py3-none-any.whl", hash = "sha256:b27c2826c47d0f3219f29554824c30c5e8945175d888647acd804ddd04af846c"}, + {file = "pygments-2.17.2.tar.gz", hash = "sha256:da46cec9fd2de5be3a8a784f434e4c4ab670b4ff54d605c4c2717e9d49c4c367"}, +] + +[[package]] +name = "pytest" +version = "8.4.2" +requires_python = ">=3.9" +summary = "pytest: simple powerful testing with Python" +groups = ["test"] +dependencies = [ + "colorama>=0.4; sys_platform == \"win32\"", + "exceptiongroup>=1; python_version < \"3.11\"", + "iniconfig>=1", + "packaging>=20", + "pluggy<2,>=1.5", + "pygments>=2.7.2", + "tomli>=1; python_version < \"3.11\"", +] +files = [ + {file = "pytest-8.4.2-py3-none-any.whl", hash = "sha256:872f880de3fc3a5bdc88a11b39c9710c3497a547cfa9320bc3c5e62fbf272e79"}, + {file = "pytest-8.4.2.tar.gz", hash = "sha256:86c0d0b93306b961d58d62a4db4879f27fe25513d4b969df351abdddb3c30e01"}, +] + +[[package]] +name = "pytest-httpserver" +version = "1.1.3" +requires_python = ">=3.9" +summary = "pytest-httpserver is a httpserver for pytest" +groups = ["test"] +dependencies = [ + "Werkzeug>=2.0.0", +] +files = [ + {file = "pytest_httpserver-1.1.3-py3-none-any.whl", hash = "sha256:5f84757810233e19e2bb5287f3826a71c97a3740abe3a363af9155c0f82fdbb9"}, + {file = "pytest_httpserver-1.1.3.tar.gz", hash = "sha256:af819d6b533f84b4680b9416a5b3f67f1df3701f1da54924afd4d6e4ba5917ec"}, +] + +[[package]] +name = "pytest-mock" +version = "3.15.1" +requires_python = ">=3.9" +summary = "Thin-wrapper around the mock package for easier use with pytest" +groups = ["test"] +dependencies = [ + "pytest>=6.2.5", +] +files = [ + {file = "pytest_mock-3.15.1-py3-none-any.whl", hash = "sha256:0a25e2eb88fe5168d535041d09a4529a188176ae608a6d249ee65abc0949630d"}, + {file = "pytest_mock-3.15.1.tar.gz", hash = "sha256:1849a238f6f396da19762269de72cb1814ab44416fa73a8686deac10b0d87a0f"}, +] + +[[package]] +name = "pywin32-ctypes" +version = "0.2.2" +requires_python = ">=3.6" +summary = "A (partial) reimplementation of pywin32 using ctypes/cffi" +groups = ["keyring"] +marker = "sys_platform == \"win32\"" +files = [ + {file = "pywin32-ctypes-0.2.2.tar.gz", hash = "sha256:3426e063bdd5fd4df74a14fa3cf80a0b42845a87e1d1e81f6549f9daec593a60"}, + {file = "pywin32_ctypes-0.2.2-py3-none-any.whl", hash = "sha256:bf490a1a709baf35d688fe0ecf980ed4de11d2b3e37b51e5442587a75d9957e7"}, +] + +[[package]] +name = "pyyaml" +version = "6.0.1" +requires_python = ">=3.6" +summary = "YAML parser and emitter for Python" +groups = ["doc"] +files = [ + {file = "PyYAML-6.0.1-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:d858aa552c999bc8a8d57426ed01e40bef403cd8ccdd0fc5f6f04a00414cac2a"}, + {file = "PyYAML-6.0.1-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:fd66fc5d0da6d9815ba2cebeb4205f95818ff4b79c3ebe268e75d961704af52f"}, + {file = "PyYAML-6.0.1-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:69b023b2b4daa7548bcfbd4aa3da05b3a74b772db9e23b982788168117739938"}, + {file = "PyYAML-6.0.1-cp310-cp310-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:81e0b275a9ecc9c0c0c07b4b90ba548307583c125f54d5b6946cfee6360c733d"}, + {file = "PyYAML-6.0.1-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:ba336e390cd8e4d1739f42dfe9bb83a3cc2e80f567d8805e11b46f4a943f5515"}, + {file = "PyYAML-6.0.1-cp310-cp310-musllinux_1_1_x86_64.whl", hash = "sha256:326c013efe8048858a6d312ddd31d56e468118ad4cdeda36c719bf5bb6192290"}, + {file = "PyYAML-6.0.1-cp310-cp310-win32.whl", hash = "sha256:bd4af7373a854424dabd882decdc5579653d7868b8fb26dc7d0e99f823aa5924"}, + {file = "PyYAML-6.0.1-cp310-cp310-win_amd64.whl", hash = "sha256:fd1592b3fdf65fff2ad0004b5e363300ef59ced41c2e6b3a99d4089fa8c5435d"}, + {file = "PyYAML-6.0.1-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:6965a7bc3cf88e5a1c3bd2e0b5c22f8d677dc88a455344035f03399034eb3007"}, + {file = "PyYAML-6.0.1-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:f003ed9ad21d6a4713f0a9b5a7a0a79e08dd0f221aff4525a2be4c346ee60aab"}, + {file = "PyYAML-6.0.1-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:42f8152b8dbc4fe7d96729ec2b99c7097d656dc1213a3229ca5383f973a5ed6d"}, + {file = "PyYAML-6.0.1-cp311-cp311-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:062582fca9fabdd2c8b54a3ef1c978d786e0f6b3a1510e0ac93ef59e0ddae2bc"}, + {file = "PyYAML-6.0.1-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:d2b04aac4d386b172d5b9692e2d2da8de7bfb6c387fa4f801fbf6fb2e6ba4673"}, + {file = "PyYAML-6.0.1-cp311-cp311-musllinux_1_1_x86_64.whl", hash = "sha256:e7d73685e87afe9f3b36c799222440d6cf362062f78be1013661b00c5c6f678b"}, + {file = "PyYAML-6.0.1-cp311-cp311-win32.whl", hash = "sha256:1635fd110e8d85d55237ab316b5b011de701ea0f29d07611174a1b42f1444741"}, + {file = "PyYAML-6.0.1-cp311-cp311-win_amd64.whl", hash = "sha256:bf07ee2fef7014951eeb99f56f39c9bb4af143d8aa3c21b1677805985307da34"}, + {file = "PyYAML-6.0.1-cp312-cp312-macosx_10_9_x86_64.whl", hash = "sha256:855fb52b0dc35af121542a76b9a84f8d1cd886ea97c84703eaa6d88e37a2ad28"}, + {file = "PyYAML-6.0.1-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:40df9b996c2b73138957fe23a16a4f0ba614f4c0efce1e9406a184b6d07fa3a9"}, + {file = "PyYAML-6.0.1-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:6c22bec3fbe2524cde73d7ada88f6566758a8f7227bfbf93a408a9d86bcc12a0"}, + {file = "PyYAML-6.0.1-cp312-cp312-musllinux_1_1_x86_64.whl", hash = "sha256:8d4e9c88387b0f5c7d5f281e55304de64cf7f9c0021a3525bd3b1c542da3b0e4"}, + {file = "PyYAML-6.0.1-cp312-cp312-win32.whl", hash = "sha256:d483d2cdf104e7c9fa60c544d92981f12ad66a457afae824d146093b8c294c54"}, + {file = "PyYAML-6.0.1-cp312-cp312-win_amd64.whl", hash = "sha256:0d3304d8c0adc42be59c5f8a4d9e3d7379e6955ad754aa9d6ab7a398b59dd1df"}, + {file = "PyYAML-6.0.1-cp38-cp38-macosx_10_9_x86_64.whl", hash = "sha256:1d4c7e777c441b20e32f52bd377e0c409713e8bb1386e1099c2415f26e479595"}, + {file = "PyYAML-6.0.1-cp38-cp38-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:a0cd17c15d3bb3fa06978b4e8958dcdc6e0174ccea823003a106c7d4d7899ac5"}, + {file = "PyYAML-6.0.1-cp38-cp38-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:28c119d996beec18c05208a8bd78cbe4007878c6dd15091efb73a30e90539696"}, + {file = "PyYAML-6.0.1-cp38-cp38-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:7e07cbde391ba96ab58e532ff4803f79c4129397514e1413a7dc761ccd755735"}, + {file = "PyYAML-6.0.1-cp38-cp38-musllinux_1_1_x86_64.whl", hash = "sha256:49a183be227561de579b4a36efbb21b3eab9651dd81b1858589f796549873dd6"}, + {file = "PyYAML-6.0.1-cp38-cp38-win32.whl", hash = "sha256:184c5108a2aca3c5b3d3bf9395d50893a7ab82a38004c8f61c258d4428e80206"}, + {file = "PyYAML-6.0.1-cp38-cp38-win_amd64.whl", hash = "sha256:1e2722cc9fbb45d9b87631ac70924c11d3a401b2d7f410cc0e3bbf249f2dca62"}, + {file = "PyYAML-6.0.1-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:9eb6caa9a297fc2c2fb8862bc5370d0303ddba53ba97e71f08023b6cd73d16a8"}, + {file = "PyYAML-6.0.1-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:c8098ddcc2a85b61647b2590f825f3db38891662cfc2fc776415143f599bb859"}, + {file = "PyYAML-6.0.1-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:5773183b6446b2c99bb77e77595dd486303b4faab2b086e7b17bc6bef28865f6"}, + {file = "PyYAML-6.0.1-cp39-cp39-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:b786eecbdf8499b9ca1d697215862083bd6d2a99965554781d0d8d1ad31e13a0"}, + {file = "PyYAML-6.0.1-cp39-cp39-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:bc1bf2925a1ecd43da378f4db9e4f799775d6367bdb94671027b73b393a7c42c"}, + {file = "PyYAML-6.0.1-cp39-cp39-musllinux_1_1_x86_64.whl", hash = "sha256:04ac92ad1925b2cff1db0cfebffb6ffc43457495c9b3c39d3fcae417d7125dc5"}, + {file = "PyYAML-6.0.1-cp39-cp39-win32.whl", hash = "sha256:faca3bdcf85b2fc05d06ff3fbc1f83e1391b3e724afa3feba7d13eeab355484c"}, + {file = "PyYAML-6.0.1-cp39-cp39-win_amd64.whl", hash = "sha256:510c9deebc5c0225e8c96813043e62b680ba2f9c50a08d3724c7f28a747d1486"}, + {file = "PyYAML-6.0.1.tar.gz", hash = "sha256:bfdf460b1736c775f2ba9f6a92bca30bc2095067b8a9d77876d1fad6cc3b4a43"}, +] + +[[package]] +name = "requests" +version = "2.32.5" +requires_python = ">=3.9" +summary = "Python HTTP for Humans." +groups = ["doc", "legacy", "test"] +dependencies = [ + "certifi>=2017.4.17", + "charset-normalizer<4,>=2", + "idna<4,>=2.5", + "urllib3<3,>=1.21.1", +] +files = [ + {file = "requests-2.32.5-py3-none-any.whl", hash = "sha256:2462f94637a34fd532264295e186976db0f5d453d1cdd31473c85a6a161affb6"}, + {file = "requests-2.32.5.tar.gz", hash = "sha256:dbba0bac56e100853db0ea71b82b4dfd5fe2bf6d3754a8893c3af500cec7d7cf"}, +] + +[[package]] +name = "requests-wsgi-adapter" +version = "0.4.1" +summary = "WSGI Transport Adapter for Requests" +groups = ["test"] +dependencies = [ + "requests>=1.0", +] +files = [ + {file = "requests-wsgi-adapter-0.4.1.tar.gz", hash = "sha256:5a7709e90abf49d181f6c32aa37794537f725de0f6dd42362bc8c8c90812c878"}, +] + +[[package]] +name = "secretstorage" +version = "3.3.3" +requires_python = ">=3.6" +summary = "Python bindings to FreeDesktop.org Secret Service API" +groups = ["keyring"] +marker = "sys_platform == \"linux\"" +dependencies = [ + "cryptography>=2.0", + "jeepney>=0.6", +] +files = [ + {file = "SecretStorage-3.3.3-py3-none-any.whl", hash = "sha256:f356e6628222568e3af06f2eba8df495efa13b3b63081dafd4f7d9a7b7bc9f99"}, + {file = "SecretStorage-3.3.3.tar.gz", hash = "sha256:2403533ef369eca6d2ba81718576c5e0f564d5cca1b58f73a8b23e7d4eeebd77"}, +] + +[[package]] +name = "sniffio" +version = "1.3.1" +requires_python = ">=3.7" +summary = "Sniff out which async library your code is running under" +groups = ["default"] +files = [ + {file = "sniffio-1.3.1-py3-none-any.whl", hash = "sha256:2f6da418d1f1e0fddd844478f41680e794e6051915791a034ff65e5f100525a2"}, + {file = "sniffio-1.3.1.tar.gz", hash = "sha256:f4324edc670a0f49750a81b895f35c3adb843cca46f0530f79fc1babb23789dc"}, +] + +[[package]] +name = "snowballstemmer" +version = "2.2.0" +summary = "This package provides 29 stemmers for 28 languages generated from Snowball algorithms." +groups = ["doc"] +files = [ + {file = "snowballstemmer-2.2.0-py2.py3-none-any.whl", hash = "sha256:c8e1716e83cc398ae16824e5572ae04e0d9fc2c6b985fb0f900f5f0c96ecba1a"}, + {file = "snowballstemmer-2.2.0.tar.gz", hash = "sha256:09b16deb8547d3412ad7b590689584cd0fe25ec8db3be37788be3810cbf19cb1"}, +] + +[[package]] +name = "soupsieve" +version = "2.5" +requires_python = ">=3.8" +summary = "A modern CSS selector implementation for Beautiful Soup." +groups = ["doc"] +files = [ + {file = "soupsieve-2.5-py3-none-any.whl", hash = "sha256:eaa337ff55a1579b6549dc679565eac1e3d000563bcb1c8ab0d0fefbc0c2cdc7"}, + {file = "soupsieve-2.5.tar.gz", hash = "sha256:5663d5a7b3bfaeee0bc4372e7fc48f9cff4940b3eec54a6451cc5299f1097690"}, +] + +[[package]] +name = "sphinx" +version = "7.4.7" +requires_python = ">=3.9" +summary = "Python documentation generator" +groups = ["doc"] +dependencies = [ + "Jinja2>=3.1", + "Pygments>=2.17", + "alabaster~=0.7.14", + "babel>=2.13", + "colorama>=0.4.6; sys_platform == \"win32\"", + "docutils<0.22,>=0.20", + "imagesize>=1.3", + "importlib-metadata>=6.0; python_version < \"3.10\"", + "packaging>=23.0", + "requests>=2.30.0", + "snowballstemmer>=2.2", + "sphinxcontrib-applehelp", + "sphinxcontrib-devhelp", + "sphinxcontrib-htmlhelp>=2.0.0", + "sphinxcontrib-jsmath", + "sphinxcontrib-qthelp", + "sphinxcontrib-serializinghtml>=1.1.9", + "tomli>=2; python_version < \"3.11\"", +] +files = [ + {file = "sphinx-7.4.7-py3-none-any.whl", hash = "sha256:c2419e2135d11f1951cd994d6eb18a1835bd8fdd8429f9ca375dc1f3281bd239"}, + {file = "sphinx-7.4.7.tar.gz", hash = "sha256:242f92a7ea7e6c5b406fdc2615413890ba9f699114a9c09192d7dfead2ee9cfe"}, +] + +[[package]] +name = "sphinx-argparse" +version = "0.4.0" +requires_python = ">=3.7" +summary = "A sphinx extension that automatically documents argparse commands and options" +groups = ["doc"] +dependencies = [ + "sphinx>=1.2.0", +] +files = [ + {file = "sphinx_argparse-0.4.0-py3-none-any.whl", hash = "sha256:73bee01f7276fae2bf621ccfe4d167af7306e7288e3482005405d9f826f9b037"}, + {file = "sphinx_argparse-0.4.0.tar.gz", hash = "sha256:e0f34184eb56f12face774fbc87b880abdb9017a0998d1ec559b267e9697e449"}, +] + +[[package]] +name = "sphinx-basic-ng" +version = "1.0.0b2" +requires_python = ">=3.7" +summary = "A modern skeleton for Sphinx themes." +groups = ["doc"] +dependencies = [ + "sphinx>=4.0", +] +files = [ + {file = "sphinx_basic_ng-1.0.0b2-py3-none-any.whl", hash = "sha256:eb09aedbabfb650607e9b4b68c9d240b90b1e1be221d6ad71d61c52e29f7932b"}, + {file = "sphinx_basic_ng-1.0.0b2.tar.gz", hash = "sha256:9ec55a47c90c8c002b5960c57492ec3021f5193cb26cebc2dc4ea226848651c9"}, +] + +[[package]] +name = "sphinx-copybutton" +version = "0.5.2" +requires_python = ">=3.7" +summary = "Add a copy button to each of your code cells." +groups = ["doc"] +dependencies = [ + "sphinx>=1.8", +] +files = [ + {file = "sphinx-copybutton-0.5.2.tar.gz", hash = "sha256:4cf17c82fb9646d1bc9ca92ac280813a3b605d8c421225fd9913154103ee1fbd"}, + {file = "sphinx_copybutton-0.5.2-py3-none-any.whl", hash = "sha256:fb543fd386d917746c9a2c50360c7905b605726b9355cd26e9974857afeae06e"}, +] + +[[package]] +name = "sphinxcontrib-applehelp" +version = "1.0.4" +requires_python = ">=3.8" +summary = "sphinxcontrib-applehelp is a Sphinx extension which outputs Apple help books" +groups = ["doc"] +files = [ + {file = "sphinxcontrib-applehelp-1.0.4.tar.gz", hash = "sha256:828f867945bbe39817c210a1abfd1bc4895c8b73fcaade56d45357a348a07d7e"}, + {file = "sphinxcontrib_applehelp-1.0.4-py3-none-any.whl", hash = "sha256:29d341f67fb0f6f586b23ad80e072c8e6ad0b48417db2bde114a4c9746feb228"}, +] + +[[package]] +name = "sphinxcontrib-devhelp" +version = "1.0.2" +requires_python = ">=3.5" +summary = "sphinxcontrib-devhelp is a sphinx extension which outputs Devhelp document." +groups = ["doc"] +files = [ + {file = "sphinxcontrib-devhelp-1.0.2.tar.gz", hash = "sha256:ff7f1afa7b9642e7060379360a67e9c41e8f3121f2ce9164266f61b9f4b338e4"}, + {file = "sphinxcontrib_devhelp-1.0.2-py2.py3-none-any.whl", hash = "sha256:8165223f9a335cc1af7ffe1ed31d2871f325254c0423bc0c4c7cd1c1e4734a2e"}, +] + +[[package]] +name = "sphinxcontrib-htmlhelp" +version = "2.0.1" +requires_python = ">=3.8" +summary = "sphinxcontrib-htmlhelp is a sphinx extension which renders HTML help files" +groups = ["doc"] +files = [ + {file = "sphinxcontrib-htmlhelp-2.0.1.tar.gz", hash = "sha256:0cbdd302815330058422b98a113195c9249825d681e18f11e8b1f78a2f11efff"}, + {file = "sphinxcontrib_htmlhelp-2.0.1-py3-none-any.whl", hash = "sha256:c38cb46dccf316c79de6e5515e1770414b797162b23cd3d06e67020e1d2a6903"}, +] + +[[package]] +name = "sphinxcontrib-jsmath" +version = "1.0.1" +requires_python = ">=3.5" +summary = "A sphinx extension which renders display math in HTML via JavaScript" +groups = ["doc"] +files = [ + {file = "sphinxcontrib-jsmath-1.0.1.tar.gz", hash = "sha256:a9925e4a4587247ed2191a22df5f6970656cb8ca2bd6284309578f2153e0c4b8"}, + {file = "sphinxcontrib_jsmath-1.0.1-py2.py3-none-any.whl", hash = "sha256:2ec2eaebfb78f3f2078e73666b1415417a116cc848b72e5172e596c871103178"}, +] + +[[package]] +name = "sphinxcontrib-qthelp" +version = "1.0.3" +requires_python = ">=3.5" +summary = "sphinxcontrib-qthelp is a sphinx extension which outputs QtHelp document." +groups = ["doc"] +files = [ + {file = "sphinxcontrib-qthelp-1.0.3.tar.gz", hash = "sha256:4c33767ee058b70dba89a6fc5c1892c0d57a54be67ddd3e7875a18d14cba5a72"}, + {file = "sphinxcontrib_qthelp-1.0.3-py2.py3-none-any.whl", hash = "sha256:bd9fc24bcb748a8d51fd4ecaade681350aa63009a347a8c14e637895444dfab6"}, +] + +[[package]] +name = "sphinxcontrib-serializinghtml" +version = "2.0.0" +requires_python = ">=3.9" +summary = "sphinxcontrib-serializinghtml is a sphinx extension which outputs \"serialized\" HTML files (json and pickle)" +groups = ["doc"] +files = [ + {file = "sphinxcontrib_serializinghtml-2.0.0-py3-none-any.whl", hash = "sha256:6e2cb0eef194e10c27ec0023bfeb25badbbb5868244cf5bc5bdc04e4464bf331"}, + {file = "sphinxcontrib_serializinghtml-2.0.0.tar.gz", hash = "sha256:e9d912827f872c029017a53f0ef2180b327c3f7fd23c87229f7a8e8b70031d4d"}, +] + +[[package]] +name = "tomli" +version = "2.0.1" +requires_python = ">=3.7" +summary = "A lil' TOML parser" +groups = ["doc", "test"] +marker = "python_version < \"3.11\"" +files = [ + {file = "tomli-2.0.1-py3-none-any.whl", hash = "sha256:939de3e7a6161af0c887ef91b7d41a53e7c5a1ca976325f429cb46ea9bc30ecc"}, + {file = "tomli-2.0.1.tar.gz", hash = "sha256:de526c12914f0c550d15924c62d72abc48d6fe7364aa87328337a31007fe8a4f"}, +] + +[[package]] +name = "trustme" +version = "1.2.1" +requires_python = ">=3.9" +summary = "#1 quality TLS certs while you wait, for the discerning tester" +groups = ["test"] +marker = "python_version < \"3.13\"" +dependencies = [ + "cryptography>=3.1", + "idna>=2.0", +] +files = [ + {file = "trustme-1.2.1-py3-none-any.whl", hash = "sha256:d768e5fc57c86dfc5ec9365102e9b092541cd6954b35d8c1eea01a84f35a762a"}, + {file = "trustme-1.2.1.tar.gz", hash = "sha256:6528ba2bbc7f2db41f33825c8dd13e3e3eb9d334ba0f909713c8c3139f4ae47f"}, +] + +[[package]] +name = "typing-extensions" +version = "4.15.0" +requires_python = ">=3.9" +summary = "Backported and Experimental Type Hints for Python 3.9+" +groups = ["default", "test"] +marker = "python_version < \"3.13\"" +files = [ + {file = "typing_extensions-4.15.0-py3-none-any.whl", hash = "sha256:f0fa19c6845758ab08074a0cfa8b7aecb71c999ca73d62883bc25cc018c4e548"}, + {file = "typing_extensions-4.15.0.tar.gz", hash = "sha256:0cea48d173cc12fa28ecabc3b837ea3cf6f38c6d1136f85cbaaf598984861466"}, +] + +[[package]] +name = "urllib3" +version = "2.1.0" +requires_python = ">=3.8" +summary = "HTTP library with thread-safe connection pooling, file post, and more." +groups = ["doc", "legacy", "test"] +files = [ + {file = "urllib3-2.1.0-py3-none-any.whl", hash = "sha256:55901e917a5896a349ff771be919f8bd99aff50b79fe58fec595eb37bbc56bb3"}, + {file = "urllib3-2.1.0.tar.gz", hash = "sha256:df7aa8afb0148fa78488e7899b2c59b5f4ffcfa82e6c54ccb9dd37c1d7b52d54"}, +] + +[[package]] +name = "werkzeug" +version = "3.1.3" +requires_python = ">=3.9" +summary = "The comprehensive WSGI web application library." +groups = ["test"] +dependencies = [ + "MarkupSafe>=2.1.1", +] +files = [ + {file = "werkzeug-3.1.3-py3-none-any.whl", hash = "sha256:54b78bf3716d19a65be4fceccc0d1d7b89e608834989dfae50ea87564639213e"}, + {file = "werkzeug-3.1.3.tar.gz", hash = "sha256:60723ce945c19328679790e3282cc758aa4a6040e4bb330f53d30fa546d44746"}, +] + +[[package]] +name = "zipp" +version = "3.17.0" +requires_python = ">=3.8" +summary = "Backport of pathlib-compatible object wrapper for zip files" +groups = ["doc", "keyring", "test"] +marker = "python_version < \"3.12\"" +files = [ + {file = "zipp-3.17.0-py3-none-any.whl", hash = "sha256:0e923e726174922dce09c53c59ad483ff7bbb8e572e00c7f7c46b88556409f31"}, + {file = "zipp-3.17.0.tar.gz", hash = "sha256:84e64a1c28cf7e91ed2078bb8cc8c259cb19b76942096c8d7b84947690cabaf0"}, +] diff --git a/tests/fixtures/real-world-locks/pdm/unearth-0.18.3/pyproject.toml b/tests/fixtures/real-world-locks/pdm/unearth-0.18.3/pyproject.toml new file mode 100644 index 00000000..f161ed9e --- /dev/null +++ b/tests/fixtures/real-world-locks/pdm/unearth-0.18.3/pyproject.toml @@ -0,0 +1,106 @@ +[build-system] +requires = ["pdm-backend"] +build-backend = "pdm.backend" + +[project] +name = "unearth" +description = "A utility to fetch and download python packages" +authors = [ + {name = "Frost Ming", email = "me@frostming.com"} +] +license = "MIT" +license-files = [ + "LICENSE", +] +readme = "README.md" +requires-python = ">=3.9" +dependencies = [ + "packaging>=20", + "httpx>=0.27.0,<1", +] +dynamic = ["version"] + +classifiers = [ + "Development Status :: 3 - Alpha", + "Intended Audience :: Developers", + "Programming Language :: Python :: 3", + "Programming Language :: Python :: 3.9", + "Programming Language :: Python :: 3.10", + "Programming Language :: Python :: 3.11", + "Programming Language :: Python :: 3.12", + "Programming Language :: Python :: 3.13", + "Programming Language :: Python :: 3.14", + "Programming Language :: Python :: 3 :: Only", +] + +[project.urls] +Homepage = "https://github.com/frostming/unearth" +Documentation = "https://unearth.readthedocs.io" +Changelog = "https://github.com/frostming/unearth/releases" + +[project.optional-dependencies] +keyring = [ + "keyring", +] +legacy = [ + "requests>=2.25", +] + +[project.scripts] +unearth = "unearth.__main__:cli" + +[tool.pdm.version] +source = "scm" + +[tool.pdm.build] +package-dir = "src" + +[tool.pdm.dev-dependencies] +test = [ + "pytest>=6.1", + "pytest-httpserver>=1.0.4", + "flask>=2.1.2", + "requests-wsgi-adapter>=0.4.1", + "trustme>=0.9.0; python_version < \"3.13\"", + "pytest-mock>=3.12.0", +] +doc = [ + "furo", + "sphinx", + "myst-parser", + "sphinx-copybutton", + "sphinx-argparse", +] + +[tool.pdm.scripts] +test = "pytest tests/" +lint = "pre-commit run --all-files" +doc = "sphinx-build -b html docs {args:docs/_build}" + +[tool.ruff] +line-length = 88 +target-version = "py39" + +[tool.ruff.lint] +extend-select = [ + "I", # isort + "B", # flake8-bugbear + "C4", # flake8-comprehensions + "PGH", # pygrep-hooks + "RUF", # ruff + "W", # pycodestyle + "YTT", # flake8-2020 +] +extend-ignore = ["B018", "B019"] +exclude = ["tests/fixtures"] + +[tool.ruff.lint.mccabe] +max-complexity = 10 + +[tool.ruff.lint.isort] +known-first-party = ["unearth"] + +[tool.pytest.ini_options] +filterwarnings = [ + "ignore::DeprecationWarning" +] diff --git a/tests/fixtures/real-world-locks/uv/abi3audit-0.0.26/LICENSE b/tests/fixtures/real-world-locks/uv/abi3audit-0.0.26/LICENSE new file mode 100644 index 00000000..4fc49162 --- /dev/null +++ b/tests/fixtures/real-world-locks/uv/abi3audit-0.0.26/LICENSE @@ -0,0 +1,21 @@ +The MIT License (MIT) + +Copyright (c) 2022 William Woodruff + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in +all copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN +THE SOFTWARE. diff --git a/tests/fixtures/real-world-locks/uv/abi3audit-0.0.26/pyproject.toml b/tests/fixtures/real-world-locks/uv/abi3audit-0.0.26/pyproject.toml new file mode 100644 index 00000000..b0977b59 --- /dev/null +++ b/tests/fixtures/real-world-locks/uv/abi3audit-0.0.26/pyproject.toml @@ -0,0 +1,105 @@ +[build-system] +requires = ["flit_core >=3.11,<4"] +build-backend = "flit_core.buildapi" + +[project] +name = "abi3audit" +dynamic = ["version"] +description = "Scans Python wheels for abi3 violations and inconsistencies" +readme = "README.md" +license = "MIT" +license-files = ["LICENSE"] +authors = [{ name = "William Woodruff", email = "william@trailofbits.com" }] +classifiers = [ + "Programming Language :: Python :: 3 :: Only", + "Programming Language :: Python :: 3", + "Development Status :: 4 - Beta", + "Intended Audience :: Developers", + "Topic :: Security", +] +dependencies = [ + "abi3info >= 2024.06.19", + "kaitaistruct ~= 0.10", + "packaging >= 21.3", + "pefile >= 2022.5.30", + "pyelftools >= 0.29", + "requests >= 2.32.5", + "requests-cache >= 0.9.6", + "rich >= 12.5.1", +] +requires-python = ">=3.10" + +[project.urls] +Homepage = "https://pypi.org/project/abi3audit/" +Issues = "https://github.com/pypa/abi3audit/issues" +Source = "https://github.com/pypa/abi3audit" + +[dependency-groups] +test = ["pytest", "pytest-cov", "pretend", "coverage[toml]"] +lint = ["interrogate", "mypy", "ruff", "types-requests"] +dev = [{ include-group = "test" }, { include-group = "lint" }] + +[project.scripts] +abi3audit = "abi3audit._cli:main" + +[tool.interrogate] +exclude = ["env", "test", "codegen"] +ignore-semiprivate = true +fail-under = 100 + +[tool.mypy] +allow_redefinition = true +check_untyped_defs = true +disallow_incomplete_defs = true +disallow_untyped_defs = true +exclude = ["_vendor/"] +ignore_missing_imports = true +no_implicit_optional = true +show_error_codes = true +strict_equality = true +warn_no_return = true +warn_redundant_casts = true +warn_return_any = true +warn_unreachable = true +warn_unused_configs = true +warn_unused_ignores = true + +[tool.coverage.run] +omit = ["abi3audit/_vendor/*"] + +[tool.ruff] +line-length = 100 +exclude = ["abi3audit/_vendor"] + +[tool.ruff.lint] +select = [ + "E", + "F", + "I", + "W", + "S", + "B", + "A", + "C4", + "EXE", + "FA", + "ISC", + "ICN", + "LOG", + "PIE", + "PYI", + "SLOT", + "FLY", + "PERF", + "PGH", + "PL", + "FURB", + "UP", +] +ignore = ["PLR1730", "PLR2004"] + +[tool.ruff.lint.flake8-bugbear] +extend-immutable-calls = ["abi3info.models.PyVersion"] + +[tool.ruff.lint.per-file-ignores] +"test/**.py" = ["S101", "S113"] diff --git a/tests/fixtures/real-world-locks/uv/abi3audit-0.0.26/uv.lock b/tests/fixtures/real-world-locks/uv/abi3audit-0.0.26/uv.lock new file mode 100644 index 00000000..751c9f3f --- /dev/null +++ b/tests/fixtures/real-world-locks/uv/abi3audit-0.0.26/uv.lock @@ -0,0 +1,826 @@ +version = 1 +revision = 3 +requires-python = ">=3.10" + +[[package]] +name = "abi3audit" +source = { editable = "." } +dependencies = [ + { name = "abi3info" }, + { name = "kaitaistruct" }, + { name = "packaging" }, + { name = "pefile" }, + { name = "pyelftools" }, + { name = "requests" }, + { name = "requests-cache" }, + { name = "rich" }, +] + +[package.dev-dependencies] +dev = [ + { name = "coverage", extra = ["toml"] }, + { name = "interrogate" }, + { name = "mypy" }, + { name = "pretend" }, + { name = "pytest" }, + { name = "pytest-cov" }, + { name = "ruff" }, + { name = "types-requests" }, +] +lint = [ + { name = "interrogate" }, + { name = "mypy" }, + { name = "ruff" }, + { name = "types-requests" }, +] +test = [ + { name = "coverage", extra = ["toml"] }, + { name = "pretend" }, + { name = "pytest" }, + { name = "pytest-cov" }, +] + +[package.metadata] +requires-dist = [ + { name = "abi3info", specifier = ">=2024.6.19" }, + { name = "kaitaistruct", specifier = "~=0.10" }, + { name = "packaging", specifier = ">=21.3" }, + { name = "pefile", specifier = ">=2022.5.30" }, + { name = "pyelftools", specifier = ">=0.29" }, + { name = "requests", specifier = ">=2.32.5" }, + { name = "requests-cache", specifier = ">=0.9.6" }, + { name = "rich", specifier = ">=12.5.1" }, +] + +[package.metadata.requires-dev] +dev = [ + { name = "coverage", extras = ["toml"] }, + { name = "interrogate" }, + { name = "mypy" }, + { name = "pretend" }, + { name = "pytest" }, + { name = "pytest-cov" }, + { name = "ruff" }, + { name = "types-requests" }, +] +lint = [ + { name = "interrogate" }, + { name = "mypy" }, + { name = "ruff" }, + { name = "types-requests" }, +] +test = [ + { name = "coverage", extras = ["toml"] }, + { name = "pretend" }, + { name = "pytest" }, + { name = "pytest-cov" }, +] + +[[package]] +name = "abi3info" +version = "2025.11.29" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/4e/62/62ba2670d5edf6d658b22ff1dc788bf146c22a73f12627d485f6c290cd6c/abi3info-2025.11.29.tar.gz", hash = "sha256:dd96754872211f96ed6955d47e725bdc579e581dd1705d581923270f825d1318", size = 20437, upload-time = "2025-11-29T19:02:48.656Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/8b/a4/b0c4c553b2ca389e0f135878e70fe9de05d86f4ec978d48c6580aead844a/abi3info-2025.11.29-py3-none-any.whl", hash = "sha256:75343a1d3eb662db4e20fe057fcd7bfc5b66e25aa6abf9cf5e90f70014151ebe", size = 19822, upload-time = "2025-11-29T19:02:46.722Z" }, +] + +[[package]] +name = "attrs" +version = "25.4.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/6b/5c/685e6633917e101e5dcb62b9dd76946cbb57c26e133bae9e0cd36033c0a9/attrs-25.4.0.tar.gz", hash = "sha256:16d5969b87f0859ef33a48b35d55ac1be6e42ae49d5e853b597db70c35c57e11", size = 934251, upload-time = "2025-10-06T13:54:44.725Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/3a/2a/7cc015f5b9f5db42b7d48157e23356022889fc354a2813c15934b7cb5c0e/attrs-25.4.0-py3-none-any.whl", hash = "sha256:adcf7e2a1fb3b36ac48d97835bb6d8ade15b8dcce26aba8bf1d14847b57a3373", size = 67615, upload-time = "2025-10-06T13:54:43.17Z" }, +] + +[[package]] +name = "cattrs" +version = "25.3.0" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "attrs" }, + { name = "exceptiongroup", marker = "python_full_version < '3.11'" }, + { name = "typing-extensions" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/6e/00/2432bb2d445b39b5407f0a90e01b9a271475eea7caf913d7a86bcb956385/cattrs-25.3.0.tar.gz", hash = "sha256:1ac88d9e5eda10436c4517e390a4142d88638fe682c436c93db7ce4a277b884a", size = 509321, upload-time = "2025-10-07T12:26:08.737Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/d8/2b/a40e1488fdfa02d3f9a653a61a5935ea08b3c2225ee818db6a76c7ba9695/cattrs-25.3.0-py3-none-any.whl", hash = "sha256:9896e84e0a5bf723bc7b4b68f4481785367ce07a8a02e7e9ee6eb2819bc306ff", size = 70738, upload-time = "2025-10-07T12:26:06.603Z" }, +] + +[[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 = "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/97/45/4b3a1239bbacd321068ea6e7ac28875b03ab8bc0aa0966452db17cd36714/charset_normalizer-3.4.4-cp313-cp313-macosx_10_13_universal2.whl", hash = "sha256:e1f185f86a6f3403aa2420e815904c67b2f9ebc443f045edd0de921108345794", size = 208091, upload-time = "2025-10-14T04:41:13.346Z" }, + { url = "https://files.pythonhosted.org/packages/7d/62/73a6d7450829655a35bb88a88fca7d736f9882a27eacdca2c6d505b57e2e/charset_normalizer-3.4.4-cp313-cp313-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:6b39f987ae8ccdf0d2642338faf2abb1862340facc796048b604ef14919e55ed", size = 147936, upload-time = "2025-10-14T04:41:14.461Z" }, + { url = "https://files.pythonhosted.org/packages/89/c5/adb8c8b3d6625bef6d88b251bbb0d95f8205831b987631ab0c8bb5d937c2/charset_normalizer-3.4.4-cp313-cp313-manylinux2014_armv7l.manylinux_2_17_armv7l.manylinux_2_31_armv7l.whl", hash = "sha256:3162d5d8ce1bb98dd51af660f2121c55d0fa541b46dff7bb9b9f86ea1d87de72", size = 144180, upload-time = "2025-10-14T04:41:15.588Z" }, + { url = "https://files.pythonhosted.org/packages/91/ed/9706e4070682d1cc219050b6048bfd293ccf67b3d4f5a4f39207453d4b99/charset_normalizer-3.4.4-cp313-cp313-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:81d5eb2a312700f4ecaa977a8235b634ce853200e828fbadf3a9c50bab278328", size = 161346, upload-time = "2025-10-14T04:41:16.738Z" }, + { url = "https://files.pythonhosted.org/packages/d5/0d/031f0d95e4972901a2f6f09ef055751805ff541511dc1252ba3ca1f80cf5/charset_normalizer-3.4.4-cp313-cp313-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:5bd2293095d766545ec1a8f612559f6b40abc0eb18bb2f5d1171872d34036ede", size = 158874, upload-time = "2025-10-14T04:41:17.923Z" }, + { url = "https://files.pythonhosted.org/packages/f5/83/6ab5883f57c9c801ce5e5677242328aa45592be8a00644310a008d04f922/charset_normalizer-3.4.4-cp313-cp313-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:a8a8b89589086a25749f471e6a900d3f662d1d3b6e2e59dcecf787b1cc3a1894", size = 153076, upload-time = "2025-10-14T04:41:19.106Z" }, + { url = "https://files.pythonhosted.org/packages/75/1e/5ff781ddf5260e387d6419959ee89ef13878229732732ee73cdae01800f2/charset_normalizer-3.4.4-cp313-cp313-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:bc7637e2f80d8530ee4a78e878bce464f70087ce73cf7c1caf142416923b98f1", size = 150601, upload-time = "2025-10-14T04:41:20.245Z" }, + { url = "https://files.pythonhosted.org/packages/d7/57/71be810965493d3510a6ca79b90c19e48696fb1ff964da319334b12677f0/charset_normalizer-3.4.4-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:f8bf04158c6b607d747e93949aa60618b61312fe647a6369f88ce2ff16043490", size = 150376, upload-time = "2025-10-14T04:41:21.398Z" }, + { url = "https://files.pythonhosted.org/packages/e5/d5/c3d057a78c181d007014feb7e9f2e65905a6c4ef182c0ddf0de2924edd65/charset_normalizer-3.4.4-cp313-cp313-musllinux_1_2_armv7l.whl", hash = "sha256:554af85e960429cf30784dd47447d5125aaa3b99a6f0683589dbd27e2f45da44", size = 144825, upload-time = "2025-10-14T04:41:22.583Z" }, + { url = "https://files.pythonhosted.org/packages/e6/8c/d0406294828d4976f275ffbe66f00266c4b3136b7506941d87c00cab5272/charset_normalizer-3.4.4-cp313-cp313-musllinux_1_2_ppc64le.whl", hash = "sha256:74018750915ee7ad843a774364e13a3db91682f26142baddf775342c3f5b1133", size = 162583, upload-time = "2025-10-14T04:41:23.754Z" }, + { url = "https://files.pythonhosted.org/packages/d7/24/e2aa1f18c8f15c4c0e932d9287b8609dd30ad56dbe41d926bd846e22fb8d/charset_normalizer-3.4.4-cp313-cp313-musllinux_1_2_riscv64.whl", hash = "sha256:c0463276121fdee9c49b98908b3a89c39be45d86d1dbaa22957e38f6321d4ce3", size = 150366, upload-time = "2025-10-14T04:41:25.27Z" }, + { url = "https://files.pythonhosted.org/packages/e4/5b/1e6160c7739aad1e2df054300cc618b06bf784a7a164b0f238360721ab86/charset_normalizer-3.4.4-cp313-cp313-musllinux_1_2_s390x.whl", hash = "sha256:362d61fd13843997c1c446760ef36f240cf81d3ebf74ac62652aebaf7838561e", size = 160300, upload-time = "2025-10-14T04:41:26.725Z" }, + { url = "https://files.pythonhosted.org/packages/7a/10/f882167cd207fbdd743e55534d5d9620e095089d176d55cb22d5322f2afd/charset_normalizer-3.4.4-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:9a26f18905b8dd5d685d6d07b0cdf98a79f3c7a918906af7cc143ea2e164c8bc", size = 154465, upload-time = "2025-10-14T04:41:28.322Z" }, + { url = "https://files.pythonhosted.org/packages/89/66/c7a9e1b7429be72123441bfdbaf2bc13faab3f90b933f664db506dea5915/charset_normalizer-3.4.4-cp313-cp313-win32.whl", hash = "sha256:9b35f4c90079ff2e2edc5b26c0c77925e5d2d255c42c74fdb70fb49b172726ac", size = 99404, upload-time = "2025-10-14T04:41:29.95Z" }, + { url = "https://files.pythonhosted.org/packages/c4/26/b9924fa27db384bdcd97ab83b4f0a8058d96ad9626ead570674d5e737d90/charset_normalizer-3.4.4-cp313-cp313-win_amd64.whl", hash = "sha256:b435cba5f4f750aa6c0a0d92c541fb79f69a387c91e61f1795227e4ed9cece14", size = 107092, upload-time = "2025-10-14T04:41:31.188Z" }, + { url = "https://files.pythonhosted.org/packages/af/8f/3ed4bfa0c0c72a7ca17f0380cd9e4dd842b09f664e780c13cff1dcf2ef1b/charset_normalizer-3.4.4-cp313-cp313-win_arm64.whl", hash = "sha256:542d2cee80be6f80247095cc36c418f7bddd14f4a6de45af91dfad36d817bba2", size = 100408, upload-time = "2025-10-14T04:41:32.624Z" }, + { url = "https://files.pythonhosted.org/packages/2a/35/7051599bd493e62411d6ede36fd5af83a38f37c4767b92884df7301db25d/charset_normalizer-3.4.4-cp314-cp314-macosx_10_13_universal2.whl", hash = "sha256:da3326d9e65ef63a817ecbcc0df6e94463713b754fe293eaa03da99befb9a5bd", size = 207746, upload-time = "2025-10-14T04:41:33.773Z" }, + { url = "https://files.pythonhosted.org/packages/10/9a/97c8d48ef10d6cd4fcead2415523221624bf58bcf68a802721a6bc807c8f/charset_normalizer-3.4.4-cp314-cp314-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:8af65f14dc14a79b924524b1e7fffe304517b2bff5a58bf64f30b98bbc5079eb", size = 147889, upload-time = "2025-10-14T04:41:34.897Z" }, + { url = "https://files.pythonhosted.org/packages/10/bf/979224a919a1b606c82bd2c5fa49b5c6d5727aa47b4312bb27b1734f53cd/charset_normalizer-3.4.4-cp314-cp314-manylinux2014_armv7l.manylinux_2_17_armv7l.manylinux_2_31_armv7l.whl", hash = "sha256:74664978bb272435107de04e36db5a9735e78232b85b77d45cfb38f758efd33e", size = 143641, upload-time = "2025-10-14T04:41:36.116Z" }, + { url = "https://files.pythonhosted.org/packages/ba/33/0ad65587441fc730dc7bd90e9716b30b4702dc7b617e6ba4997dc8651495/charset_normalizer-3.4.4-cp314-cp314-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:752944c7ffbfdd10c074dc58ec2d5a8a4cd9493b314d367c14d24c17684ddd14", size = 160779, upload-time = "2025-10-14T04:41:37.229Z" }, + { url = "https://files.pythonhosted.org/packages/67/ed/331d6b249259ee71ddea93f6f2f0a56cfebd46938bde6fcc6f7b9a3d0e09/charset_normalizer-3.4.4-cp314-cp314-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:d1f13550535ad8cff21b8d757a3257963e951d96e20ec82ab44bc64aeb62a191", size = 159035, upload-time = "2025-10-14T04:41:38.368Z" }, + { url = "https://files.pythonhosted.org/packages/67/ff/f6b948ca32e4f2a4576aa129d8bed61f2e0543bf9f5f2b7fc3758ed005c9/charset_normalizer-3.4.4-cp314-cp314-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:ecaae4149d99b1c9e7b88bb03e3221956f68fd6d50be2ef061b2381b61d20838", size = 152542, upload-time = "2025-10-14T04:41:39.862Z" }, + { url = "https://files.pythonhosted.org/packages/16/85/276033dcbcc369eb176594de22728541a925b2632f9716428c851b149e83/charset_normalizer-3.4.4-cp314-cp314-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:cb6254dc36b47a990e59e1068afacdcd02958bdcce30bb50cc1700a8b9d624a6", size = 149524, upload-time = "2025-10-14T04:41:41.319Z" }, + { url = "https://files.pythonhosted.org/packages/9e/f2/6a2a1f722b6aba37050e626530a46a68f74e63683947a8acff92569f979a/charset_normalizer-3.4.4-cp314-cp314-musllinux_1_2_aarch64.whl", hash = "sha256:c8ae8a0f02f57a6e61203a31428fa1d677cbe50c93622b4149d5c0f319c1d19e", size = 150395, upload-time = "2025-10-14T04:41:42.539Z" }, + { url = "https://files.pythonhosted.org/packages/60/bb/2186cb2f2bbaea6338cad15ce23a67f9b0672929744381e28b0592676824/charset_normalizer-3.4.4-cp314-cp314-musllinux_1_2_armv7l.whl", hash = "sha256:47cc91b2f4dd2833fddaedd2893006b0106129d4b94fdb6af1f4ce5a9965577c", size = 143680, upload-time = "2025-10-14T04:41:43.661Z" }, + { url = "https://files.pythonhosted.org/packages/7d/a5/bf6f13b772fbb2a90360eb620d52ed8f796f3c5caee8398c3b2eb7b1c60d/charset_normalizer-3.4.4-cp314-cp314-musllinux_1_2_ppc64le.whl", hash = "sha256:82004af6c302b5d3ab2cfc4cc5f29db16123b1a8417f2e25f9066f91d4411090", size = 162045, upload-time = "2025-10-14T04:41:44.821Z" }, + { url = "https://files.pythonhosted.org/packages/df/c5/d1be898bf0dc3ef9030c3825e5d3b83f2c528d207d246cbabe245966808d/charset_normalizer-3.4.4-cp314-cp314-musllinux_1_2_riscv64.whl", hash = "sha256:2b7d8f6c26245217bd2ad053761201e9f9680f8ce52f0fcd8d0755aeae5b2152", size = 149687, upload-time = "2025-10-14T04:41:46.442Z" }, + { url = "https://files.pythonhosted.org/packages/a5/42/90c1f7b9341eef50c8a1cb3f098ac43b0508413f33affd762855f67a410e/charset_normalizer-3.4.4-cp314-cp314-musllinux_1_2_s390x.whl", hash = "sha256:799a7a5e4fb2d5898c60b640fd4981d6a25f1c11790935a44ce38c54e985f828", size = 160014, upload-time = "2025-10-14T04:41:47.631Z" }, + { url = "https://files.pythonhosted.org/packages/76/be/4d3ee471e8145d12795ab655ece37baed0929462a86e72372fd25859047c/charset_normalizer-3.4.4-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:99ae2cffebb06e6c22bdc25801d7b30f503cc87dbd283479e7b606f70aff57ec", size = 154044, upload-time = "2025-10-14T04:41:48.81Z" }, + { url = "https://files.pythonhosted.org/packages/b0/6f/8f7af07237c34a1defe7defc565a9bc1807762f672c0fde711a4b22bf9c0/charset_normalizer-3.4.4-cp314-cp314-win32.whl", hash = "sha256:f9d332f8c2a2fcbffe1378594431458ddbef721c1769d78e2cbc06280d8155f9", size = 99940, upload-time = "2025-10-14T04:41:49.946Z" }, + { url = "https://files.pythonhosted.org/packages/4b/51/8ade005e5ca5b0d80fb4aff72a3775b325bdc3d27408c8113811a7cbe640/charset_normalizer-3.4.4-cp314-cp314-win_amd64.whl", hash = "sha256:8a6562c3700cce886c5be75ade4a5db4214fda19fede41d9792d100288d8f94c", size = 107104, upload-time = "2025-10-14T04:41:51.051Z" }, + { url = "https://files.pythonhosted.org/packages/da/5f/6b8f83a55bb8278772c5ae54a577f3099025f9ade59d0136ac24a0df4bde/charset_normalizer-3.4.4-cp314-cp314-win_arm64.whl", hash = "sha256:de00632ca48df9daf77a2c65a484531649261ec9f25489917f09e455cb09ddb2", size = 100743, upload-time = "2025-10-14T04:41:52.122Z" }, + { 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 = "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.1" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/23/f9/e92df5e07f3fc8d4c7f9a0f146ef75446bf870351cd37b788cf5897f8079/coverage-7.13.1.tar.gz", hash = "sha256:b7593fe7eb5feaa3fbb461ac79aac9f9fc0387a5ca8080b0c6fe2ca27b091afd", size = 825862, upload-time = "2025-12-28T15:42:56.969Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/2d/9a/3742e58fd04b233df95c012ee9f3dfe04708a5e1d32613bd2d47d4e1be0d/coverage-7.13.1-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:e1fa280b3ad78eea5be86f94f461c04943d942697e0dac889fa18fff8f5f9147", size = 218633, upload-time = "2025-12-28T15:40:10.165Z" }, + { url = "https://files.pythonhosted.org/packages/7e/45/7e6bdc94d89cd7c8017ce735cf50478ddfe765d4fbf0c24d71d30ea33d7a/coverage-7.13.1-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:c3d8c679607220979434f494b139dfb00131ebf70bb406553d69c1ff01a5c33d", size = 219147, upload-time = "2025-12-28T15:40:12.069Z" }, + { url = "https://files.pythonhosted.org/packages/f7/38/0d6a258625fd7f10773fe94097dc16937a5f0e3e0cdf3adef67d3ac6baef/coverage-7.13.1-cp310-cp310-manylinux1_i686.manylinux_2_28_i686.manylinux_2_5_i686.whl", hash = "sha256:339dc63b3eba969067b00f41f15ad161bf2946613156fb131266d8debc8e44d0", size = 245894, upload-time = "2025-12-28T15:40:13.556Z" }, + { url = "https://files.pythonhosted.org/packages/27/58/409d15ea487986994cbd4d06376e9860e9b157cfbfd402b1236770ab8dd2/coverage-7.13.1-cp310-cp310-manylinux1_x86_64.manylinux_2_28_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:db622b999ffe49cb891f2fff3b340cdc2f9797d01a0a202a0973ba2562501d90", size = 247721, upload-time = "2025-12-28T15:40:15.37Z" }, + { url = "https://files.pythonhosted.org/packages/da/bf/6e8056a83fd7a96c93341f1ffe10df636dd89f26d5e7b9ca511ce3bcf0df/coverage-7.13.1-cp310-cp310-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:d1443ba9acbb593fa7c1c29e011d7c9761545fe35e7652e85ce7f51a16f7e08d", size = 249585, upload-time = "2025-12-28T15:40:17.226Z" }, + { url = "https://files.pythonhosted.org/packages/f4/15/e1daff723f9f5959acb63cbe35b11203a9df77ee4b95b45fffd38b318390/coverage-7.13.1-cp310-cp310-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:c832ec92c4499ac463186af72f9ed4d8daec15499b16f0a879b0d1c8e5cf4a3b", size = 246597, upload-time = "2025-12-28T15:40:19.028Z" }, + { url = "https://files.pythonhosted.org/packages/74/a6/1efd31c5433743a6ddbc9d37ac30c196bb07c7eab3d74fbb99b924c93174/coverage-7.13.1-cp310-cp310-musllinux_1_2_aarch64.whl", hash = "sha256:562ec27dfa3f311e0db1ba243ec6e5f6ab96b1edfcfc6cf86f28038bc4961ce6", size = 247626, upload-time = "2025-12-28T15:40:20.846Z" }, + { url = "https://files.pythonhosted.org/packages/6d/9f/1609267dd3e749f57fdd66ca6752567d1c13b58a20a809dc409b263d0b5f/coverage-7.13.1-cp310-cp310-musllinux_1_2_i686.whl", hash = "sha256:4de84e71173d4dada2897e5a0e1b7877e5eefbfe0d6a44edee6ce31d9b8ec09e", size = 245629, upload-time = "2025-12-28T15:40:22.397Z" }, + { url = "https://files.pythonhosted.org/packages/e2/f6/6815a220d5ec2466383d7cc36131b9fa6ecbe95c50ec52a631ba733f306a/coverage-7.13.1-cp310-cp310-musllinux_1_2_riscv64.whl", hash = "sha256:a5a68357f686f8c4d527a2dc04f52e669c2fc1cbde38f6f7eb6a0e58cbd17cae", size = 245901, upload-time = "2025-12-28T15:40:23.836Z" }, + { url = "https://files.pythonhosted.org/packages/ac/58/40576554cd12e0872faf6d2c0eb3bc85f71d78427946ddd19ad65201e2c0/coverage-7.13.1-cp310-cp310-musllinux_1_2_x86_64.whl", hash = "sha256:77cc258aeb29a3417062758975521eae60af6f79e930d6993555eeac6a8eac29", size = 246505, upload-time = "2025-12-28T15:40:25.421Z" }, + { url = "https://files.pythonhosted.org/packages/3b/77/9233a90253fba576b0eee81707b5781d0e21d97478e5377b226c5b096c0f/coverage-7.13.1-cp310-cp310-win32.whl", hash = "sha256:bb4f8c3c9a9f34423dba193f241f617b08ffc63e27f67159f60ae6baf2dcfe0f", size = 221257, upload-time = "2025-12-28T15:40:27.217Z" }, + { url = "https://files.pythonhosted.org/packages/e0/43/e842ff30c1a0a623ec80db89befb84a3a7aad7bfe44a6ea77d5a3e61fedd/coverage-7.13.1-cp310-cp310-win_amd64.whl", hash = "sha256:c8e2706ceb622bc63bac98ebb10ef5da80ed70fbd8a7999a5076de3afaef0fb1", size = 222191, upload-time = "2025-12-28T15:40:28.916Z" }, + { url = "https://files.pythonhosted.org/packages/b4/9b/77baf488516e9ced25fc215a6f75d803493fc3f6a1a1227ac35697910c2a/coverage-7.13.1-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:1a55d509a1dc5a5b708b5dad3b5334e07a16ad4c2185e27b40e4dba796ab7f88", size = 218755, upload-time = "2025-12-28T15:40:30.812Z" }, + { url = "https://files.pythonhosted.org/packages/d7/cd/7ab01154e6eb79ee2fab76bf4d89e94c6648116557307ee4ebbb85e5c1bf/coverage-7.13.1-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:4d010d080c4888371033baab27e47c9df7d6fb28d0b7b7adf85a4a49be9298b3", size = 219257, upload-time = "2025-12-28T15:40:32.333Z" }, + { url = "https://files.pythonhosted.org/packages/01/d5/b11ef7863ffbbdb509da0023fad1e9eda1c0eaea61a6d2ea5b17d4ac706e/coverage-7.13.1-cp311-cp311-manylinux1_i686.manylinux_2_28_i686.manylinux_2_5_i686.whl", hash = "sha256:d938b4a840fb1523b9dfbbb454f652967f18e197569c32266d4d13f37244c3d9", size = 249657, upload-time = "2025-12-28T15:40:34.1Z" }, + { url = "https://files.pythonhosted.org/packages/f7/7c/347280982982383621d29b8c544cf497ae07ac41e44b1ca4903024131f55/coverage-7.13.1-cp311-cp311-manylinux1_x86_64.manylinux_2_28_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:bf100a3288f9bb7f919b87eb84f87101e197535b9bd0e2c2b5b3179633324fee", size = 251581, upload-time = "2025-12-28T15:40:36.131Z" }, + { url = "https://files.pythonhosted.org/packages/82/f6/ebcfed11036ade4c0d75fa4453a6282bdd225bc073862766eec184a4c643/coverage-7.13.1-cp311-cp311-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:ef6688db9bf91ba111ae734ba6ef1a063304a881749726e0d3575f5c10a9facf", size = 253691, upload-time = "2025-12-28T15:40:37.626Z" }, + { url = "https://files.pythonhosted.org/packages/02/92/af8f5582787f5d1a8b130b2dcba785fa5e9a7a8e121a0bb2220a6fdbdb8a/coverage-7.13.1-cp311-cp311-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:0b609fc9cdbd1f02e51f67f51e5aee60a841ef58a68d00d5ee2c0faf357481a3", size = 249799, upload-time = "2025-12-28T15:40:39.47Z" }, + { url = "https://files.pythonhosted.org/packages/24/aa/0e39a2a3b16eebf7f193863323edbff38b6daba711abaaf807d4290cf61a/coverage-7.13.1-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:c43257717611ff5e9a1d79dce8e47566235ebda63328718d9b65dd640bc832ef", size = 251389, upload-time = "2025-12-28T15:40:40.954Z" }, + { url = "https://files.pythonhosted.org/packages/73/46/7f0c13111154dc5b978900c0ccee2e2ca239b910890e674a77f1363d483e/coverage-7.13.1-cp311-cp311-musllinux_1_2_i686.whl", hash = "sha256:e09fbecc007f7b6afdfb3b07ce5bd9f8494b6856dd4f577d26c66c391b829851", size = 249450, upload-time = "2025-12-28T15:40:42.489Z" }, + { url = "https://files.pythonhosted.org/packages/ac/ca/e80da6769e8b669ec3695598c58eef7ad98b0e26e66333996aee6316db23/coverage-7.13.1-cp311-cp311-musllinux_1_2_riscv64.whl", hash = "sha256:a03a4f3a19a189919c7055098790285cc5c5b0b3976f8d227aea39dbf9f8bfdb", size = 249170, upload-time = "2025-12-28T15:40:44.279Z" }, + { url = "https://files.pythonhosted.org/packages/af/18/9e29baabdec1a8644157f572541079b4658199cfd372a578f84228e860de/coverage-7.13.1-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:3820778ea1387c2b6a818caec01c63adc5b3750211af6447e8dcfb9b6f08dbba", size = 250081, upload-time = "2025-12-28T15:40:45.748Z" }, + { url = "https://files.pythonhosted.org/packages/00/f8/c3021625a71c3b2f516464d322e41636aea381018319050a8114105872ee/coverage-7.13.1-cp311-cp311-win32.whl", hash = "sha256:ff10896fa55167371960c5908150b434b71c876dfab97b69478f22c8b445ea19", size = 221281, upload-time = "2025-12-28T15:40:47.232Z" }, + { url = "https://files.pythonhosted.org/packages/27/56/c216625f453df6e0559ed666d246fcbaaa93f3aa99eaa5080cea1229aa3d/coverage-7.13.1-cp311-cp311-win_amd64.whl", hash = "sha256:a998cc0aeeea4c6d5622a3754da5a493055d2d95186bad877b0a34ea6e6dbe0a", size = 222215, upload-time = "2025-12-28T15:40:49.19Z" }, + { url = "https://files.pythonhosted.org/packages/5c/9a/be342e76f6e531cae6406dc46af0d350586f24d9b67fdfa6daee02df71af/coverage-7.13.1-cp311-cp311-win_arm64.whl", hash = "sha256:fea07c1a39a22614acb762e3fbbb4011f65eedafcb2948feeef641ac78b4ee5c", size = 220886, upload-time = "2025-12-28T15:40:51.067Z" }, + { url = "https://files.pythonhosted.org/packages/ce/8a/87af46cccdfa78f53db747b09f5f9a21d5fc38d796834adac09b30a8ce74/coverage-7.13.1-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:6f34591000f06e62085b1865c9bc5f7858df748834662a51edadfd2c3bfe0dd3", size = 218927, upload-time = "2025-12-28T15:40:52.814Z" }, + { url = "https://files.pythonhosted.org/packages/82/a8/6e22fdc67242a4a5a153f9438d05944553121c8f4ba70cb072af4c41362e/coverage-7.13.1-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:b67e47c5595b9224599016e333f5ec25392597a89d5744658f837d204e16c63e", size = 219288, upload-time = "2025-12-28T15:40:54.262Z" }, + { url = "https://files.pythonhosted.org/packages/d0/0a/853a76e03b0f7c4375e2ca025df45c918beb367f3e20a0a8e91967f6e96c/coverage-7.13.1-cp312-cp312-manylinux1_i686.manylinux_2_28_i686.manylinux_2_5_i686.whl", hash = "sha256:3e7b8bd70c48ffb28461ebe092c2345536fb18bbbf19d287c8913699735f505c", size = 250786, upload-time = "2025-12-28T15:40:56.059Z" }, + { url = "https://files.pythonhosted.org/packages/ea/b4/694159c15c52b9f7ec7adf49d50e5f8ee71d3e9ef38adb4445d13dd56c20/coverage-7.13.1-cp312-cp312-manylinux1_x86_64.manylinux_2_28_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:c223d078112e90dc0e5c4e35b98b9584164bea9fbbd221c0b21c5241f6d51b62", size = 253543, upload-time = "2025-12-28T15:40:57.585Z" }, + { url = "https://files.pythonhosted.org/packages/96/b2/7f1f0437a5c855f87e17cf5d0dc35920b6440ff2b58b1ba9788c059c26c8/coverage-7.13.1-cp312-cp312-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:794f7c05af0763b1bbd1b9e6eff0e52ad068be3b12cd96c87de037b01390c968", size = 254635, upload-time = "2025-12-28T15:40:59.443Z" }, + { url = "https://files.pythonhosted.org/packages/e9/d1/73c3fdb8d7d3bddd9473c9c6a2e0682f09fc3dfbcb9c3f36412a7368bcab/coverage-7.13.1-cp312-cp312-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:0642eae483cc8c2902e4af7298bf886d605e80f26382124cddc3967c2a3df09e", size = 251202, upload-time = "2025-12-28T15:41:01.328Z" }, + { url = "https://files.pythonhosted.org/packages/66/3c/f0edf75dcc152f145d5598329e864bbbe04ab78660fe3e8e395f9fff010f/coverage-7.13.1-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:9f5e772ed5fef25b3de9f2008fe67b92d46831bd2bc5bdc5dd6bfd06b83b316f", size = 252566, upload-time = "2025-12-28T15:41:03.319Z" }, + { url = "https://files.pythonhosted.org/packages/17/b3/e64206d3c5f7dcbceafd14941345a754d3dbc78a823a6ed526e23b9cdaab/coverage-7.13.1-cp312-cp312-musllinux_1_2_i686.whl", hash = "sha256:45980ea19277dc0a579e432aef6a504fe098ef3a9032ead15e446eb0f1191aee", size = 250711, upload-time = "2025-12-28T15:41:06.411Z" }, + { url = "https://files.pythonhosted.org/packages/dc/ad/28a3eb970a8ef5b479ee7f0c484a19c34e277479a5b70269dc652b730733/coverage-7.13.1-cp312-cp312-musllinux_1_2_riscv64.whl", hash = "sha256:e4f18eca6028ffa62adbd185a8f1e1dd242f2e68164dba5c2b74a5204850b4cf", size = 250278, upload-time = "2025-12-28T15:41:08.285Z" }, + { url = "https://files.pythonhosted.org/packages/54/e3/c8f0f1a93133e3e1291ca76cbb63565bd4b5c5df63b141f539d747fff348/coverage-7.13.1-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:f8dca5590fec7a89ed6826fce625595279e586ead52e9e958d3237821fbc750c", size = 252154, upload-time = "2025-12-28T15:41:09.969Z" }, + { url = "https://files.pythonhosted.org/packages/d0/bf/9939c5d6859c380e405b19e736321f1c7d402728792f4c752ad1adcce005/coverage-7.13.1-cp312-cp312-win32.whl", hash = "sha256:ff86d4e85188bba72cfb876df3e11fa243439882c55957184af44a35bd5880b7", size = 221487, upload-time = "2025-12-28T15:41:11.468Z" }, + { url = "https://files.pythonhosted.org/packages/fa/dc/7282856a407c621c2aad74021680a01b23010bb8ebf427cf5eacda2e876f/coverage-7.13.1-cp312-cp312-win_amd64.whl", hash = "sha256:16cc1da46c04fb0fb128b4dc430b78fa2aba8a6c0c9f8eb391fd5103409a6ac6", size = 222299, upload-time = "2025-12-28T15:41:13.386Z" }, + { url = "https://files.pythonhosted.org/packages/10/79/176a11203412c350b3e9578620013af35bcdb79b651eb976f4a4b32044fa/coverage-7.13.1-cp312-cp312-win_arm64.whl", hash = "sha256:8d9bc218650022a768f3775dd7fdac1886437325d8d295d923ebcfef4892ad5c", size = 220941, upload-time = "2025-12-28T15:41:14.975Z" }, + { url = "https://files.pythonhosted.org/packages/a3/a4/e98e689347a1ff1a7f67932ab535cef82eb5e78f32a9e4132e114bbb3a0a/coverage-7.13.1-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:cb237bfd0ef4d5eb6a19e29f9e528ac67ac3be932ea6b44fb6cc09b9f3ecff78", size = 218951, upload-time = "2025-12-28T15:41:16.653Z" }, + { url = "https://files.pythonhosted.org/packages/32/33/7cbfe2bdc6e2f03d6b240d23dc45fdaf3fd270aaf2d640be77b7f16989ab/coverage-7.13.1-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:1dcb645d7e34dcbcc96cd7c132b1fc55c39263ca62eb961c064eb3928997363b", size = 219325, upload-time = "2025-12-28T15:41:18.609Z" }, + { url = "https://files.pythonhosted.org/packages/59/f6/efdabdb4929487baeb7cb2a9f7dac457d9356f6ad1b255be283d58b16316/coverage-7.13.1-cp313-cp313-manylinux1_i686.manylinux_2_28_i686.manylinux_2_5_i686.whl", hash = "sha256:3d42df8201e00384736f0df9be2ced39324c3907607d17d50d50116c989d84cd", size = 250309, upload-time = "2025-12-28T15:41:20.629Z" }, + { url = "https://files.pythonhosted.org/packages/12/da/91a52516e9d5aea87d32d1523f9cdcf7a35a3b298e6be05d6509ba3cfab2/coverage-7.13.1-cp313-cp313-manylinux1_x86_64.manylinux_2_28_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:fa3edde1aa8807de1d05934982416cb3ec46d1d4d91e280bcce7cca01c507992", size = 252907, upload-time = "2025-12-28T15:41:22.257Z" }, + { url = "https://files.pythonhosted.org/packages/75/38/f1ea837e3dc1231e086db1638947e00d264e7e8c41aa8ecacf6e1e0c05f4/coverage-7.13.1-cp313-cp313-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:9edd0e01a343766add6817bc448408858ba6b489039eaaa2018474e4001651a4", size = 254148, upload-time = "2025-12-28T15:41:23.87Z" }, + { url = "https://files.pythonhosted.org/packages/7f/43/f4f16b881aaa34954ba446318dea6b9ed5405dd725dd8daac2358eda869a/coverage-7.13.1-cp313-cp313-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:985b7836931d033570b94c94713c6dba5f9d3ff26045f72c3e5dbc5fe3361e5a", size = 250515, upload-time = "2025-12-28T15:41:25.437Z" }, + { url = "https://files.pythonhosted.org/packages/84/34/8cba7f00078bd468ea914134e0144263194ce849ec3baad187ffb6203d1c/coverage-7.13.1-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:ffed1e4980889765c84a5d1a566159e363b71d6b6fbaf0bebc9d3c30bc016766", size = 252292, upload-time = "2025-12-28T15:41:28.459Z" }, + { url = "https://files.pythonhosted.org/packages/8c/a4/cffac66c7652d84ee4ac52d3ccb94c015687d3b513f9db04bfcac2ac800d/coverage-7.13.1-cp313-cp313-musllinux_1_2_i686.whl", hash = "sha256:8842af7f175078456b8b17f1b73a0d16a65dcbdc653ecefeb00a56b3c8c298c4", size = 250242, upload-time = "2025-12-28T15:41:30.02Z" }, + { url = "https://files.pythonhosted.org/packages/f4/78/9a64d462263dde416f3c0067efade7b52b52796f489b1037a95b0dc389c9/coverage-7.13.1-cp313-cp313-musllinux_1_2_riscv64.whl", hash = "sha256:ccd7a6fca48ca9c131d9b0a2972a581e28b13416fc313fb98b6d24a03ce9a398", size = 250068, upload-time = "2025-12-28T15:41:32.007Z" }, + { url = "https://files.pythonhosted.org/packages/69/c8/a8994f5fece06db7c4a97c8fc1973684e178599b42e66280dded0524ef00/coverage-7.13.1-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:0403f647055de2609be776965108447deb8e384fe4a553c119e3ff6bfbab4784", size = 251846, upload-time = "2025-12-28T15:41:33.946Z" }, + { url = "https://files.pythonhosted.org/packages/cc/f7/91fa73c4b80305c86598a2d4e54ba22df6bf7d0d97500944af7ef155d9f7/coverage-7.13.1-cp313-cp313-win32.whl", hash = "sha256:549d195116a1ba1e1ae2f5ca143f9777800f6636eab917d4f02b5310d6d73461", size = 221512, upload-time = "2025-12-28T15:41:35.519Z" }, + { url = "https://files.pythonhosted.org/packages/45/0b/0768b4231d5a044da8f75e097a8714ae1041246bb765d6b5563bab456735/coverage-7.13.1-cp313-cp313-win_amd64.whl", hash = "sha256:5899d28b5276f536fcf840b18b61a9fce23cc3aec1d114c44c07fe94ebeaa500", size = 222321, upload-time = "2025-12-28T15:41:37.371Z" }, + { url = "https://files.pythonhosted.org/packages/9b/b8/bdcb7253b7e85157282450262008f1366aa04663f3e3e4c30436f596c3e2/coverage-7.13.1-cp313-cp313-win_arm64.whl", hash = "sha256:868a2fae76dfb06e87291bcbd4dcbcc778a8500510b618d50496e520bd94d9b9", size = 220949, upload-time = "2025-12-28T15:41:39.553Z" }, + { url = "https://files.pythonhosted.org/packages/70/52/f2be52cc445ff75ea8397948c96c1b4ee14f7f9086ea62fc929c5ae7b717/coverage-7.13.1-cp313-cp313t-macosx_10_13_x86_64.whl", hash = "sha256:67170979de0dacac3f3097d02b0ad188d8edcea44ccc44aaa0550af49150c7dc", size = 219643, upload-time = "2025-12-28T15:41:41.567Z" }, + { url = "https://files.pythonhosted.org/packages/47/79/c85e378eaa239e2edec0c5523f71542c7793fe3340954eafb0bc3904d32d/coverage-7.13.1-cp313-cp313t-macosx_11_0_arm64.whl", hash = "sha256:f80e2bb21bfab56ed7405c2d79d34b5dc0bc96c2c1d2a067b643a09fb756c43a", size = 219997, upload-time = "2025-12-28T15:41:43.418Z" }, + { url = "https://files.pythonhosted.org/packages/fe/9b/b1ade8bfb653c0bbce2d6d6e90cc6c254cbb99b7248531cc76253cb4da6d/coverage-7.13.1-cp313-cp313t-manylinux1_i686.manylinux_2_28_i686.manylinux_2_5_i686.whl", hash = "sha256:f83351e0f7dcdb14d7326c3d8d8c4e915fa685cbfdc6281f9470d97a04e9dfe4", size = 261296, upload-time = "2025-12-28T15:41:45.207Z" }, + { url = "https://files.pythonhosted.org/packages/1f/af/ebf91e3e1a2473d523e87e87fd8581e0aa08741b96265730e2d79ce78d8d/coverage-7.13.1-cp313-cp313t-manylinux1_x86_64.manylinux_2_28_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:bb3f6562e89bad0110afbe64e485aac2462efdce6232cdec7862a095dc3412f6", size = 263363, upload-time = "2025-12-28T15:41:47.163Z" }, + { url = "https://files.pythonhosted.org/packages/c4/8b/fb2423526d446596624ac7fde12ea4262e66f86f5120114c3cfd0bb2befa/coverage-7.13.1-cp313-cp313t-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:77545b5dcda13b70f872c3b5974ac64c21d05e65b1590b441c8560115dc3a0d1", size = 265783, upload-time = "2025-12-28T15:41:49.03Z" }, + { url = "https://files.pythonhosted.org/packages/9b/26/ef2adb1e22674913b89f0fe7490ecadcef4a71fa96f5ced90c60ec358789/coverage-7.13.1-cp313-cp313t-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:a4d240d260a1aed814790bbe1f10a5ff31ce6c21bc78f0da4a1e8268d6c80dbd", size = 260508, upload-time = "2025-12-28T15:41:51.035Z" }, + { url = "https://files.pythonhosted.org/packages/ce/7d/f0f59b3404caf662e7b5346247883887687c074ce67ba453ea08c612b1d5/coverage-7.13.1-cp313-cp313t-musllinux_1_2_aarch64.whl", hash = "sha256:d2287ac9360dec3837bfdad969963a5d073a09a85d898bd86bea82aa8876ef3c", size = 263357, upload-time = "2025-12-28T15:41:52.631Z" }, + { url = "https://files.pythonhosted.org/packages/1a/b1/29896492b0b1a047604d35d6fa804f12818fa30cdad660763a5f3159e158/coverage-7.13.1-cp313-cp313t-musllinux_1_2_i686.whl", hash = "sha256:0d2c11f3ea4db66b5cbded23b20185c35066892c67d80ec4be4bab257b9ad1e0", size = 260978, upload-time = "2025-12-28T15:41:54.589Z" }, + { url = "https://files.pythonhosted.org/packages/48/f2/971de1238a62e6f0a4128d37adadc8bb882ee96afbe03ff1570291754629/coverage-7.13.1-cp313-cp313t-musllinux_1_2_riscv64.whl", hash = "sha256:3fc6a169517ca0d7ca6846c3c5392ef2b9e38896f61d615cb75b9e7134d4ee1e", size = 259877, upload-time = "2025-12-28T15:41:56.263Z" }, + { url = "https://files.pythonhosted.org/packages/6a/fc/0474efcbb590ff8628830e9aaec5f1831594874360e3251f1fdec31d07a3/coverage-7.13.1-cp313-cp313t-musllinux_1_2_x86_64.whl", hash = "sha256:d10a2ed46386e850bb3de503a54f9fe8192e5917fcbb143bfef653a9355e9a53", size = 262069, upload-time = "2025-12-28T15:41:58.093Z" }, + { url = "https://files.pythonhosted.org/packages/88/4f/3c159b7953db37a7b44c0eab8a95c37d1aa4257c47b4602c04022d5cb975/coverage-7.13.1-cp313-cp313t-win32.whl", hash = "sha256:75a6f4aa904301dab8022397a22c0039edc1f51e90b83dbd4464b8a38dc87842", size = 222184, upload-time = "2025-12-28T15:41:59.763Z" }, + { url = "https://files.pythonhosted.org/packages/58/a5/6b57d28f81417f9335774f20679d9d13b9a8fb90cd6160957aa3b54a2379/coverage-7.13.1-cp313-cp313t-win_amd64.whl", hash = "sha256:309ef5706e95e62578cda256b97f5e097916a2c26247c287bbe74794e7150df2", size = 223250, upload-time = "2025-12-28T15:42:01.52Z" }, + { url = "https://files.pythonhosted.org/packages/81/7c/160796f3b035acfbb58be80e02e484548595aa67e16a6345e7910ace0a38/coverage-7.13.1-cp313-cp313t-win_arm64.whl", hash = "sha256:92f980729e79b5d16d221038dbf2e8f9a9136afa072f9d5d6ed4cb984b126a09", size = 221521, upload-time = "2025-12-28T15:42:03.275Z" }, + { url = "https://files.pythonhosted.org/packages/aa/8e/ba0e597560c6563fc0adb902fda6526df5d4aa73bb10adf0574d03bd2206/coverage-7.13.1-cp314-cp314-macosx_10_15_x86_64.whl", hash = "sha256:97ab3647280d458a1f9adb85244e81587505a43c0c7cff851f5116cd2814b894", size = 218996, upload-time = "2025-12-28T15:42:04.978Z" }, + { url = "https://files.pythonhosted.org/packages/6b/8e/764c6e116f4221dc7aa26c4061181ff92edb9c799adae6433d18eeba7a14/coverage-7.13.1-cp314-cp314-macosx_11_0_arm64.whl", hash = "sha256:8f572d989142e0908e6acf57ad1b9b86989ff057c006d13b76c146ec6a20216a", size = 219326, upload-time = "2025-12-28T15:42:06.691Z" }, + { url = "https://files.pythonhosted.org/packages/4f/a6/6130dc6d8da28cdcbb0f2bf8865aeca9b157622f7c0031e48c6cf9a0e591/coverage-7.13.1-cp314-cp314-manylinux1_i686.manylinux_2_28_i686.manylinux_2_5_i686.whl", hash = "sha256:d72140ccf8a147e94274024ff6fd8fb7811354cf7ef88b1f0a988ebaa5bc774f", size = 250374, upload-time = "2025-12-28T15:42:08.786Z" }, + { url = "https://files.pythonhosted.org/packages/82/2b/783ded568f7cd6b677762f780ad338bf4b4750205860c17c25f7c708995e/coverage-7.13.1-cp314-cp314-manylinux1_x86_64.manylinux_2_28_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:d3c9f051b028810f5a87c88e5d6e9af3c0ff32ef62763bf15d29f740453ca909", size = 252882, upload-time = "2025-12-28T15:42:10.515Z" }, + { url = "https://files.pythonhosted.org/packages/cd/b2/9808766d082e6a4d59eb0cc881a57fc1600eb2c5882813eefff8254f71b5/coverage-7.13.1-cp314-cp314-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:f398ba4df52d30b1763f62eed9de5620dcde96e6f491f4c62686736b155aa6e4", size = 254218, upload-time = "2025-12-28T15:42:12.208Z" }, + { url = "https://files.pythonhosted.org/packages/44/ea/52a985bb447c871cb4d2e376e401116520991b597c85afdde1ea9ef54f2c/coverage-7.13.1-cp314-cp314-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:132718176cc723026d201e347f800cd1a9e4b62ccd3f82476950834dad501c75", size = 250391, upload-time = "2025-12-28T15:42:14.21Z" }, + { url = "https://files.pythonhosted.org/packages/7f/1d/125b36cc12310718873cfc8209ecfbc1008f14f4f5fa0662aa608e579353/coverage-7.13.1-cp314-cp314-musllinux_1_2_aarch64.whl", hash = "sha256:9e549d642426e3579b3f4b92d0431543b012dcb6e825c91619d4e93b7363c3f9", size = 252239, upload-time = "2025-12-28T15:42:16.292Z" }, + { url = "https://files.pythonhosted.org/packages/6a/16/10c1c164950cade470107f9f14bbac8485f8fb8515f515fca53d337e4a7f/coverage-7.13.1-cp314-cp314-musllinux_1_2_i686.whl", hash = "sha256:90480b2134999301eea795b3a9dbf606c6fbab1b489150c501da84a959442465", size = 250196, upload-time = "2025-12-28T15:42:18.54Z" }, + { url = "https://files.pythonhosted.org/packages/2a/c6/cd860fac08780c6fd659732f6ced1b40b79c35977c1356344e44d72ba6c4/coverage-7.13.1-cp314-cp314-musllinux_1_2_riscv64.whl", hash = "sha256:e825dbb7f84dfa24663dd75835e7257f8882629fc11f03ecf77d84a75134b864", size = 250008, upload-time = "2025-12-28T15:42:20.365Z" }, + { url = "https://files.pythonhosted.org/packages/f0/3a/a8c58d3d38f82a5711e1e0a67268362af48e1a03df27c03072ac30feefcf/coverage-7.13.1-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:623dcc6d7a7ba450bbdbeedbaa0c42b329bdae16491af2282f12a7e809be7eb9", size = 251671, upload-time = "2025-12-28T15:42:22.114Z" }, + { url = "https://files.pythonhosted.org/packages/f0/bc/fd4c1da651d037a1e3d53e8cb3f8182f4b53271ffa9a95a2e211bacc0349/coverage-7.13.1-cp314-cp314-win32.whl", hash = "sha256:6e73ebb44dca5f708dc871fe0b90cf4cff1a13f9956f747cc87b535a840386f5", size = 221777, upload-time = "2025-12-28T15:42:23.919Z" }, + { url = "https://files.pythonhosted.org/packages/4b/50/71acabdc8948464c17e90b5ffd92358579bd0910732c2a1c9537d7536aa6/coverage-7.13.1-cp314-cp314-win_amd64.whl", hash = "sha256:be753b225d159feb397bd0bf91ae86f689bad0da09d3b301478cd39b878ab31a", size = 222592, upload-time = "2025-12-28T15:42:25.619Z" }, + { url = "https://files.pythonhosted.org/packages/f7/c8/a6fb943081bb0cc926499c7907731a6dc9efc2cbdc76d738c0ab752f1a32/coverage-7.13.1-cp314-cp314-win_arm64.whl", hash = "sha256:228b90f613b25ba0019361e4ab81520b343b622fc657daf7e501c4ed6a2366c0", size = 221169, upload-time = "2025-12-28T15:42:27.629Z" }, + { url = "https://files.pythonhosted.org/packages/16/61/d5b7a0a0e0e40d62e59bc8c7aa1afbd86280d82728ba97f0673b746b78e2/coverage-7.13.1-cp314-cp314t-macosx_10_15_x86_64.whl", hash = "sha256:60cfb538fe9ef86e5b2ab0ca8fc8d62524777f6c611dcaf76dc16fbe9b8e698a", size = 219730, upload-time = "2025-12-28T15:42:29.306Z" }, + { url = "https://files.pythonhosted.org/packages/a3/2c/8881326445fd071bb49514d1ce97d18a46a980712b51fee84f9ab42845b4/coverage-7.13.1-cp314-cp314t-macosx_11_0_arm64.whl", hash = "sha256:57dfc8048c72ba48a8c45e188d811e5efd7e49b387effc8fb17e97936dde5bf6", size = 220001, upload-time = "2025-12-28T15:42:31.319Z" }, + { url = "https://files.pythonhosted.org/packages/b5/d7/50de63af51dfa3a7f91cc37ad8fcc1e244b734232fbc8b9ab0f3c834a5cd/coverage-7.13.1-cp314-cp314t-manylinux1_i686.manylinux_2_28_i686.manylinux_2_5_i686.whl", hash = "sha256:3f2f725aa3e909b3c5fdb8192490bdd8e1495e85906af74fe6e34a2a77ba0673", size = 261370, upload-time = "2025-12-28T15:42:32.992Z" }, + { url = "https://files.pythonhosted.org/packages/e1/2c/d31722f0ec918fd7453b2758312729f645978d212b410cd0f7c2aed88a94/coverage-7.13.1-cp314-cp314t-manylinux1_x86_64.manylinux_2_28_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:9ee68b21909686eeb21dfcba2c3b81fee70dcf38b140dcd5aa70680995fa3aa5", size = 263485, upload-time = "2025-12-28T15:42:34.759Z" }, + { url = "https://files.pythonhosted.org/packages/fa/7a/2c114fa5c5fc08ba0777e4aec4c97e0b4a1afcb69c75f1f54cff78b073ab/coverage-7.13.1-cp314-cp314t-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:724b1b270cb13ea2e6503476e34541a0b1f62280bc997eab443f87790202033d", size = 265890, upload-time = "2025-12-28T15:42:36.517Z" }, + { url = "https://files.pythonhosted.org/packages/65/d9/f0794aa1c74ceabc780fe17f6c338456bbc4e96bd950f2e969f48ac6fb20/coverage-7.13.1-cp314-cp314t-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:916abf1ac5cf7eb16bc540a5bf75c71c43a676f5c52fcb9fe75a2bd75fb944e8", size = 260445, upload-time = "2025-12-28T15:42:38.646Z" }, + { url = "https://files.pythonhosted.org/packages/49/23/184b22a00d9bb97488863ced9454068c79e413cb23f472da6cbddc6cfc52/coverage-7.13.1-cp314-cp314t-musllinux_1_2_aarch64.whl", hash = "sha256:776483fd35b58d8afe3acbd9988d5de592ab6da2d2a865edfdbc9fdb43e7c486", size = 263357, upload-time = "2025-12-28T15:42:40.788Z" }, + { url = "https://files.pythonhosted.org/packages/7d/bd/58af54c0c9199ea4190284f389005779d7daf7bf3ce40dcd2d2b2f96da69/coverage-7.13.1-cp314-cp314t-musllinux_1_2_i686.whl", hash = "sha256:b6f3b96617e9852703f5b633ea01315ca45c77e879584f283c44127f0f1ec564", size = 260959, upload-time = "2025-12-28T15:42:42.808Z" }, + { url = "https://files.pythonhosted.org/packages/4b/2a/6839294e8f78a4891bf1df79d69c536880ba2f970d0ff09e7513d6e352e9/coverage-7.13.1-cp314-cp314t-musllinux_1_2_riscv64.whl", hash = "sha256:bd63e7b74661fed317212fab774e2a648bc4bb09b35f25474f8e3325d2945cd7", size = 259792, upload-time = "2025-12-28T15:42:44.818Z" }, + { url = "https://files.pythonhosted.org/packages/ba/c3/528674d4623283310ad676c5af7414b9850ab6d55c2300e8aa4b945ec554/coverage-7.13.1-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:933082f161bbb3e9f90d00990dc956120f608cdbcaeea15c4d897f56ef4fe416", size = 262123, upload-time = "2025-12-28T15:42:47.108Z" }, + { url = "https://files.pythonhosted.org/packages/06/c5/8c0515692fb4c73ac379d8dc09b18eaf0214ecb76ea6e62467ba7a1556ff/coverage-7.13.1-cp314-cp314t-win32.whl", hash = "sha256:18be793c4c87de2965e1c0f060f03d9e5aff66cfeae8e1dbe6e5b88056ec153f", size = 222562, upload-time = "2025-12-28T15:42:49.144Z" }, + { url = "https://files.pythonhosted.org/packages/05/0e/c0a0c4678cb30dac735811db529b321d7e1c9120b79bd728d4f4d6b010e9/coverage-7.13.1-cp314-cp314t-win_amd64.whl", hash = "sha256:0e42e0ec0cd3e0d851cb3c91f770c9301f48647cb2877cb78f74bdaa07639a79", size = 223670, upload-time = "2025-12-28T15:42:51.218Z" }, + { url = "https://files.pythonhosted.org/packages/f5/5f/b177aa0011f354abf03a8f30a85032686d290fdeed4222b27d36b4372a50/coverage-7.13.1-cp314-cp314t-win_arm64.whl", hash = "sha256:eaecf47ef10c72ece9a2a92118257da87e460e113b83cc0d2905cbbe931792b4", size = 221707, upload-time = "2025-12-28T15:42:53.034Z" }, + { url = "https://files.pythonhosted.org/packages/cc/48/d9f421cb8da5afaa1a64570d9989e00fb7955e6acddc5a12979f7666ef60/coverage-7.13.1-py3-none-any.whl", hash = "sha256:2016745cb3ba554469d02819d78958b571792bb68e31302610e898f80dd3a573", size = 210722, upload-time = "2025-12-28T15:42:54.901Z" }, +] + +[package.optional-dependencies] +toml = [ + { name = "tomli", marker = "python_full_version <= '3.11'" }, +] + +[[package]] +name = "exceptiongroup" +version = "1.3.1" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "typing-extensions", marker = "python_full_version < '3.13'" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/50/79/66800aadf48771f6b62f7eb014e352e5d06856655206165d775e675a02c9/exceptiongroup-1.3.1.tar.gz", hash = "sha256:8b412432c6055b0b7d14c310000ae93352ed6754f70fa8f7c34141f91c4e3219", size = 30371, upload-time = "2025-11-21T23:01:54.787Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/8a/0e/97c33bf5009bdbac74fd2beace167cab3f978feb69cc36f1ef79360d6c4e/exceptiongroup-1.3.1-py3-none-any.whl", hash = "sha256:a7a39a3bd276781e98394987d3a5701d0c4edffb633bb7a5144577f82c773598", size = 16740, upload-time = "2025-11-21T23:01:53.443Z" }, +] + +[[package]] +name = "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 = "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 = "interrogate" +version = "1.7.0" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "attrs" }, + { name = "click" }, + { name = "colorama" }, + { name = "py" }, + { name = "tabulate" }, + { name = "tomli", marker = "python_full_version < '3.11'" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/8b/22/74f7fcc96280eea46cf2bcbfa1354ac31de0e60a4be6f7966f12cef20893/interrogate-1.7.0.tar.gz", hash = "sha256:a320d6ec644dfd887cc58247a345054fc4d9f981100c45184470068f4b3719b0", size = 159636, upload-time = "2024-04-07T22:30:46.217Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/12/c9/6869a1dcf4aaf309b9543ec070be3ec3adebee7c9bec9af8c230494134b9/interrogate-1.7.0-py3-none-any.whl", hash = "sha256:b13ff4dd8403369670e2efe684066de9fcb868ad9d7f2b4095d8112142dc9d12", size = 46982, upload-time = "2024-04-07T22:30:44.277Z" }, +] + +[[package]] +name = "kaitaistruct" +version = "0.11" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/27/b8/ca7319556912f68832daa4b81425314857ec08dfccd8dbc8c0f65c992108/kaitaistruct-0.11.tar.gz", hash = "sha256:053ee764288e78b8e53acf748e9733268acbd579b8d82a427b1805453625d74b", size = 11519, upload-time = "2025-09-08T15:46:25.037Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/4a/4a/cf14bf3b1f5ffb13c69cf5f0ea78031247790558ee88984a8bdd22fae60d/kaitaistruct-0.11-py2.py3-none-any.whl", hash = "sha256:5c6ce79177b4e193a577ecd359e26516d1d6d000a0bffd6e1010f2a46a62a561", size = 11372, upload-time = "2025-09-08T15:46:23.635Z" }, +] + +[[package]] +name = "librt" +version = "0.7.8" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/e7/24/5f3646ff414285e0f7708fa4e946b9bf538345a41d1c375c439467721a5e/librt-0.7.8.tar.gz", hash = "sha256:1a4ede613941d9c3470b0368be851df6bb78ab218635512d0370b27a277a0862", size = 148323, upload-time = "2026-01-14T12:56:16.876Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/44/13/57b06758a13550c5f09563893b004f98e9537ee6ec67b7df85c3571c8832/librt-0.7.8-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:b45306a1fc5f53c9330fbee134d8b3227fe5da2ab09813b892790400aa49352d", size = 56521, upload-time = "2026-01-14T12:54:40.066Z" }, + { url = "https://files.pythonhosted.org/packages/c2/24/bbea34d1452a10612fb45ac8356f95351ba40c2517e429602160a49d1fd0/librt-0.7.8-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:864c4b7083eeee250ed55135d2127b260d7eb4b5e953a9e5df09c852e327961b", size = 58456, upload-time = "2026-01-14T12:54:41.471Z" }, + { url = "https://files.pythonhosted.org/packages/04/72/a168808f92253ec3a810beb1eceebc465701197dbc7e865a1c9ceb3c22c7/librt-0.7.8-cp310-cp310-manylinux1_i686.manylinux_2_28_i686.manylinux_2_5_i686.whl", hash = "sha256:6938cc2de153bc927ed8d71c7d2f2ae01b4e96359126c602721340eb7ce1a92d", size = 164392, upload-time = "2026-01-14T12:54:42.843Z" }, + { url = "https://files.pythonhosted.org/packages/14/5c/4c0d406f1b02735c2e7af8ff1ff03a6577b1369b91aa934a9fa2cc42c7ce/librt-0.7.8-cp310-cp310-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:66daa6ac5de4288a5bbfbe55b4caa7bf0cd26b3269c7a476ffe8ce45f837f87d", size = 172959, upload-time = "2026-01-14T12:54:44.602Z" }, + { url = "https://files.pythonhosted.org/packages/82/5f/3e85351c523f73ad8d938989e9a58c7f59fb9c17f761b9981b43f0025ce7/librt-0.7.8-cp310-cp310-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:4864045f49dc9c974dadb942ac56a74cd0479a2aafa51ce272c490a82322ea3c", size = 186717, upload-time = "2026-01-14T12:54:45.986Z" }, + { url = "https://files.pythonhosted.org/packages/08/f8/18bfe092e402d00fe00d33aa1e01dda1bd583ca100b393b4373847eade6d/librt-0.7.8-cp310-cp310-musllinux_1_2_aarch64.whl", hash = "sha256:a36515b1328dc5b3ffce79fe204985ca8572525452eacabee2166f44bb387b2c", size = 184585, upload-time = "2026-01-14T12:54:47.139Z" }, + { url = "https://files.pythonhosted.org/packages/4e/fc/f43972ff56fd790a9fa55028a52ccea1875100edbb856b705bd393b601e3/librt-0.7.8-cp310-cp310-musllinux_1_2_i686.whl", hash = "sha256:b7e7f140c5169798f90b80d6e607ed2ba5059784968a004107c88ad61fb3641d", size = 180497, upload-time = "2026-01-14T12:54:48.946Z" }, + { url = "https://files.pythonhosted.org/packages/e1/3a/25e36030315a410d3ad0b7d0f19f5f188e88d1613d7d3fd8150523ea1093/librt-0.7.8-cp310-cp310-musllinux_1_2_x86_64.whl", hash = "sha256:ff71447cb778a4f772ddc4ce360e6ba9c95527ed84a52096bd1bbf9fee2ec7c0", size = 200052, upload-time = "2026-01-14T12:54:50.382Z" }, + { url = "https://files.pythonhosted.org/packages/fc/b8/f3a5a1931ae2a6ad92bf6893b9ef44325b88641d58723529e2c2935e8abe/librt-0.7.8-cp310-cp310-win32.whl", hash = "sha256:047164e5f68b7a8ebdf9fae91a3c2161d3192418aadd61ddd3a86a56cbe3dc85", size = 43477, upload-time = "2026-01-14T12:54:51.815Z" }, + { url = "https://files.pythonhosted.org/packages/fe/91/c4202779366bc19f871b4ad25db10fcfa1e313c7893feb942f32668e8597/librt-0.7.8-cp310-cp310-win_amd64.whl", hash = "sha256:d6f254d096d84156a46a84861183c183d30734e52383602443292644d895047c", size = 49806, upload-time = "2026-01-14T12:54:53.149Z" }, + { url = "https://files.pythonhosted.org/packages/1b/a3/87ea9c1049f2c781177496ebee29430e4631f439b8553a4969c88747d5d8/librt-0.7.8-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:ff3e9c11aa260c31493d4b3197d1e28dd07768594a4f92bec4506849d736248f", size = 56507, upload-time = "2026-01-14T12:54:54.156Z" }, + { url = "https://files.pythonhosted.org/packages/5e/4a/23bcef149f37f771ad30203d561fcfd45b02bc54947b91f7a9ac34815747/librt-0.7.8-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:ddb52499d0b3ed4aa88746aaf6f36a08314677d5c346234c3987ddc506404eac", size = 58455, upload-time = "2026-01-14T12:54:55.978Z" }, + { url = "https://files.pythonhosted.org/packages/22/6e/46eb9b85c1b9761e0f42b6e6311e1cc544843ac897457062b9d5d0b21df4/librt-0.7.8-cp311-cp311-manylinux1_i686.manylinux_2_28_i686.manylinux_2_5_i686.whl", hash = "sha256:e9c0afebbe6ce177ae8edba0c7c4d626f2a0fc12c33bb993d163817c41a7a05c", size = 164956, upload-time = "2026-01-14T12:54:57.311Z" }, + { url = "https://files.pythonhosted.org/packages/7a/3f/aa7c7f6829fb83989feb7ba9aa11c662b34b4bd4bd5b262f2876ba3db58d/librt-0.7.8-cp311-cp311-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:631599598e2c76ded400c0a8722dec09217c89ff64dc54b060f598ed68e7d2a8", size = 174364, upload-time = "2026-01-14T12:54:59.089Z" }, + { url = "https://files.pythonhosted.org/packages/3f/2d/d57d154b40b11f2cb851c4df0d4c4456bacd9b1ccc4ecb593ddec56c1a8b/librt-0.7.8-cp311-cp311-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:9c1ba843ae20db09b9d5c80475376168feb2640ce91cd9906414f23cc267a1ff", size = 188034, upload-time = "2026-01-14T12:55:00.141Z" }, + { url = "https://files.pythonhosted.org/packages/59/f9/36c4dad00925c16cd69d744b87f7001792691857d3b79187e7a673e812fb/librt-0.7.8-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:b5b007bb22ea4b255d3ee39dfd06d12534de2fcc3438567d9f48cdaf67ae1ae3", size = 186295, upload-time = "2026-01-14T12:55:01.303Z" }, + { url = "https://files.pythonhosted.org/packages/23/9b/8a9889d3df5efb67695a67785028ccd58e661c3018237b73ad081691d0cb/librt-0.7.8-cp311-cp311-musllinux_1_2_i686.whl", hash = "sha256:dbd79caaf77a3f590cbe32dc2447f718772d6eea59656a7dcb9311161b10fa75", size = 181470, upload-time = "2026-01-14T12:55:02.492Z" }, + { url = "https://files.pythonhosted.org/packages/43/64/54d6ef11afca01fef8af78c230726a9394759f2addfbf7afc5e3cc032a45/librt-0.7.8-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:87808a8d1e0bd62a01cafc41f0fd6818b5a5d0ca0d8a55326a81643cdda8f873", size = 201713, upload-time = "2026-01-14T12:55:03.919Z" }, + { url = "https://files.pythonhosted.org/packages/2d/29/73e7ed2991330b28919387656f54109139b49e19cd72902f466bd44415fd/librt-0.7.8-cp311-cp311-win32.whl", hash = "sha256:31724b93baa91512bd0a376e7cf0b59d8b631ee17923b1218a65456fa9bda2e7", size = 43803, upload-time = "2026-01-14T12:55:04.996Z" }, + { url = "https://files.pythonhosted.org/packages/3f/de/66766ff48ed02b4d78deea30392ae200bcbd99ae61ba2418b49fd50a4831/librt-0.7.8-cp311-cp311-win_amd64.whl", hash = "sha256:978e8b5f13e52cf23a9e80f3286d7546baa70bc4ef35b51d97a709d0b28e537c", size = 50080, upload-time = "2026-01-14T12:55:06.489Z" }, + { url = "https://files.pythonhosted.org/packages/6f/e3/33450438ff3a8c581d4ed7f798a70b07c3206d298cf0b87d3806e72e3ed8/librt-0.7.8-cp311-cp311-win_arm64.whl", hash = "sha256:20e3946863d872f7cabf7f77c6c9d370b8b3d74333d3a32471c50d3a86c0a232", size = 43383, upload-time = "2026-01-14T12:55:07.49Z" }, + { url = "https://files.pythonhosted.org/packages/56/04/79d8fcb43cae376c7adbab7b2b9f65e48432c9eced62ac96703bcc16e09b/librt-0.7.8-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:9b6943885b2d49c48d0cff23b16be830ba46b0152d98f62de49e735c6e655a63", size = 57472, upload-time = "2026-01-14T12:55:08.528Z" }, + { url = "https://files.pythonhosted.org/packages/b4/ba/60b96e93043d3d659da91752689023a73981336446ae82078cddf706249e/librt-0.7.8-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:46ef1f4b9b6cc364b11eea0ecc0897314447a66029ee1e55859acb3dd8757c93", size = 58986, upload-time = "2026-01-14T12:55:09.466Z" }, + { url = "https://files.pythonhosted.org/packages/7c/26/5215e4cdcc26e7be7eee21955a7e13cbf1f6d7d7311461a6014544596fac/librt-0.7.8-cp312-cp312-manylinux1_i686.manylinux_2_28_i686.manylinux_2_5_i686.whl", hash = "sha256:907ad09cfab21e3c86e8f1f87858f7049d1097f77196959c033612f532b4e592", size = 168422, upload-time = "2026-01-14T12:55:10.499Z" }, + { url = "https://files.pythonhosted.org/packages/0f/84/e8d1bc86fa0159bfc24f3d798d92cafd3897e84c7fea7fe61b3220915d76/librt-0.7.8-cp312-cp312-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:2991b6c3775383752b3ca0204842743256f3ad3deeb1d0adc227d56b78a9a850", size = 177478, upload-time = "2026-01-14T12:55:11.577Z" }, + { url = "https://files.pythonhosted.org/packages/57/11/d0268c4b94717a18aa91df1100e767b010f87b7ae444dafaa5a2d80f33a6/librt-0.7.8-cp312-cp312-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:03679b9856932b8c8f674e87aa3c55ea11c9274301f76ae8dc4d281bda55cf62", size = 192439, upload-time = "2026-01-14T12:55:12.7Z" }, + { url = "https://files.pythonhosted.org/packages/8d/56/1e8e833b95fe684f80f8894ae4d8b7d36acc9203e60478fcae599120a975/librt-0.7.8-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:3968762fec1b2ad34ce57458b6de25dbb4142713e9ca6279a0d352fa4e9f452b", size = 191483, upload-time = "2026-01-14T12:55:13.838Z" }, + { url = "https://files.pythonhosted.org/packages/17/48/f11cf28a2cb6c31f282009e2208312aa84a5ee2732859f7856ee306176d5/librt-0.7.8-cp312-cp312-musllinux_1_2_i686.whl", hash = "sha256:bb7a7807523a31f03061288cc4ffc065d684c39db7644c676b47d89553c0d714", size = 185376, upload-time = "2026-01-14T12:55:15.017Z" }, + { url = "https://files.pythonhosted.org/packages/b8/6a/d7c116c6da561b9155b184354a60a3d5cdbf08fc7f3678d09c95679d13d9/librt-0.7.8-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:ad64a14b1e56e702e19b24aae108f18ad1bf7777f3af5fcd39f87d0c5a814449", size = 206234, upload-time = "2026-01-14T12:55:16.571Z" }, + { url = "https://files.pythonhosted.org/packages/61/de/1975200bb0285fc921c5981d9978ce6ce11ae6d797df815add94a5a848a3/librt-0.7.8-cp312-cp312-win32.whl", hash = "sha256:0241a6ed65e6666236ea78203a73d800dbed896cf12ae25d026d75dc1fcd1dac", size = 44057, upload-time = "2026-01-14T12:55:18.077Z" }, + { url = "https://files.pythonhosted.org/packages/8e/cd/724f2d0b3461426730d4877754b65d39f06a41ac9d0a92d5c6840f72b9ae/librt-0.7.8-cp312-cp312-win_amd64.whl", hash = "sha256:6db5faf064b5bab9675c32a873436b31e01d66ca6984c6f7f92621656033a708", size = 50293, upload-time = "2026-01-14T12:55:19.179Z" }, + { url = "https://files.pythonhosted.org/packages/bd/cf/7e899acd9ee5727ad8160fdcc9994954e79fab371c66535c60e13b968ffc/librt-0.7.8-cp312-cp312-win_arm64.whl", hash = "sha256:57175aa93f804d2c08d2edb7213e09276bd49097611aefc37e3fa38d1fb99ad0", size = 43574, upload-time = "2026-01-14T12:55:20.185Z" }, + { url = "https://files.pythonhosted.org/packages/a1/fe/b1f9de2829cf7fc7649c1dcd202cfd873837c5cc2fc9e526b0e7f716c3d2/librt-0.7.8-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:4c3995abbbb60b3c129490fa985dfe6cac11d88fc3c36eeb4fb1449efbbb04fc", size = 57500, upload-time = "2026-01-14T12:55:21.219Z" }, + { url = "https://files.pythonhosted.org/packages/eb/d4/4a60fbe2e53b825f5d9a77325071d61cd8af8506255067bf0c8527530745/librt-0.7.8-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:44e0c2cbc9bebd074cf2cdbe472ca185e824be4e74b1c63a8e934cea674bebf2", size = 59019, upload-time = "2026-01-14T12:55:22.256Z" }, + { url = "https://files.pythonhosted.org/packages/6a/37/61ff80341ba5159afa524445f2d984c30e2821f31f7c73cf166dcafa5564/librt-0.7.8-cp313-cp313-manylinux1_i686.manylinux_2_28_i686.manylinux_2_5_i686.whl", hash = "sha256:4d2f1e492cae964b3463a03dc77a7fe8742f7855d7258c7643f0ee32b6651dd3", size = 169015, upload-time = "2026-01-14T12:55:23.24Z" }, + { url = "https://files.pythonhosted.org/packages/1c/86/13d4f2d6a93f181ebf2fc953868826653ede494559da8268023fe567fca3/librt-0.7.8-cp313-cp313-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:451e7ffcef8f785831fdb791bd69211f47e95dc4c6ddff68e589058806f044c6", size = 178161, upload-time = "2026-01-14T12:55:24.826Z" }, + { url = "https://files.pythonhosted.org/packages/88/26/e24ef01305954fc4d771f1f09f3dd682f9eb610e1bec188ffb719374d26e/librt-0.7.8-cp313-cp313-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:3469e1af9f1380e093ae06bedcbdd11e407ac0b303a56bbe9afb1d6824d4982d", size = 193015, upload-time = "2026-01-14T12:55:26.04Z" }, + { url = "https://files.pythonhosted.org/packages/88/a0/92b6bd060e720d7a31ed474d046a69bd55334ec05e9c446d228c4b806ae3/librt-0.7.8-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:f11b300027ce19a34f6d24ebb0a25fd0e24a9d53353225a5c1e6cadbf2916b2e", size = 192038, upload-time = "2026-01-14T12:55:27.208Z" }, + { url = "https://files.pythonhosted.org/packages/06/bb/6f4c650253704279c3a214dad188101d1b5ea23be0606628bc6739456624/librt-0.7.8-cp313-cp313-musllinux_1_2_i686.whl", hash = "sha256:4adc73614f0d3c97874f02f2c7fd2a27854e7e24ad532ea6b965459c5b757eca", size = 186006, upload-time = "2026-01-14T12:55:28.594Z" }, + { url = "https://files.pythonhosted.org/packages/dc/00/1c409618248d43240cadf45f3efb866837fa77e9a12a71481912135eb481/librt-0.7.8-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:60c299e555f87e4c01b2eca085dfccda1dde87f5a604bb45c2906b8305819a93", size = 206888, upload-time = "2026-01-14T12:55:30.214Z" }, + { url = "https://files.pythonhosted.org/packages/d9/83/b2cfe8e76ff5c1c77f8a53da3d5de62d04b5ebf7cf913e37f8bca43b5d07/librt-0.7.8-cp313-cp313-win32.whl", hash = "sha256:b09c52ed43a461994716082ee7d87618096851319bf695d57ec123f2ab708951", size = 44126, upload-time = "2026-01-14T12:55:31.44Z" }, + { url = "https://files.pythonhosted.org/packages/a9/0b/c59d45de56a51bd2d3a401fc63449c0ac163e4ef7f523ea8b0c0dee86ec5/librt-0.7.8-cp313-cp313-win_amd64.whl", hash = "sha256:f8f4a901a3fa28969d6e4519deceab56c55a09d691ea7b12ca830e2fa3461e34", size = 50262, upload-time = "2026-01-14T12:55:33.01Z" }, + { url = "https://files.pythonhosted.org/packages/fc/b9/973455cec0a1ec592395250c474164c4a58ebf3e0651ee920fef1a2623f1/librt-0.7.8-cp313-cp313-win_arm64.whl", hash = "sha256:43d4e71b50763fcdcf64725ac680d8cfa1706c928b844794a7aa0fa9ac8e5f09", size = 43600, upload-time = "2026-01-14T12:55:34.054Z" }, + { url = "https://files.pythonhosted.org/packages/1a/73/fa8814c6ce2d49c3827829cadaa1589b0bf4391660bd4510899393a23ebc/librt-0.7.8-cp314-cp314-macosx_10_13_x86_64.whl", hash = "sha256:be927c3c94c74b05128089a955fba86501c3b544d1d300282cc1b4bd370cb418", size = 57049, upload-time = "2026-01-14T12:55:35.056Z" }, + { url = "https://files.pythonhosted.org/packages/53/fe/f6c70956da23ea235fd2e3cc16f4f0b4ebdfd72252b02d1164dd58b4e6c3/librt-0.7.8-cp314-cp314-macosx_11_0_arm64.whl", hash = "sha256:7b0803e9008c62a7ef79058233db7ff6f37a9933b8f2573c05b07ddafa226611", size = 58689, upload-time = "2026-01-14T12:55:36.078Z" }, + { url = "https://files.pythonhosted.org/packages/1f/4d/7a2481444ac5fba63050d9abe823e6bc16896f575bfc9c1e5068d516cdce/librt-0.7.8-cp314-cp314-manylinux1_i686.manylinux_2_28_i686.manylinux_2_5_i686.whl", hash = "sha256:79feb4d00b2a4e0e05c9c56df707934f41fcb5fe53fd9efb7549068d0495b758", size = 166808, upload-time = "2026-01-14T12:55:37.595Z" }, + { url = "https://files.pythonhosted.org/packages/ac/3c/10901d9e18639f8953f57c8986796cfbf4c1c514844a41c9197cf87cb707/librt-0.7.8-cp314-cp314-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:b9122094e3f24aa759c38f46bd8863433820654927370250f460ae75488b66ea", size = 175614, upload-time = "2026-01-14T12:55:38.756Z" }, + { url = "https://files.pythonhosted.org/packages/db/01/5cbdde0951a5090a80e5ba44e6357d375048123c572a23eecfb9326993a7/librt-0.7.8-cp314-cp314-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:7e03bea66af33c95ce3addf87a9bf1fcad8d33e757bc479957ddbc0e4f7207ac", size = 189955, upload-time = "2026-01-14T12:55:39.939Z" }, + { url = "https://files.pythonhosted.org/packages/6a/b4/e80528d2f4b7eaf1d437fcbd6fc6ba4cbeb3e2a0cb9ed5a79f47c7318706/librt-0.7.8-cp314-cp314-musllinux_1_2_aarch64.whl", hash = "sha256:f1ade7f31675db00b514b98f9ab9a7698c7282dad4be7492589109471852d398", size = 189370, upload-time = "2026-01-14T12:55:41.057Z" }, + { url = "https://files.pythonhosted.org/packages/c1/ab/938368f8ce31a9787ecd4becb1e795954782e4312095daf8fd22420227c8/librt-0.7.8-cp314-cp314-musllinux_1_2_i686.whl", hash = "sha256:a14229ac62adcf1b90a15992f1ab9c69ae8b99ffb23cb64a90878a6e8a2f5b81", size = 183224, upload-time = "2026-01-14T12:55:42.328Z" }, + { url = "https://files.pythonhosted.org/packages/3c/10/559c310e7a6e4014ac44867d359ef8238465fb499e7eb31b6bfe3e3f86f5/librt-0.7.8-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:5bcaaf624fd24e6a0cb14beac37677f90793a96864c67c064a91458611446e83", size = 203541, upload-time = "2026-01-14T12:55:43.501Z" }, + { url = "https://files.pythonhosted.org/packages/f8/db/a0db7acdb6290c215f343835c6efda5b491bb05c3ddc675af558f50fdba3/librt-0.7.8-cp314-cp314-win32.whl", hash = "sha256:7aa7d5457b6c542ecaed79cec4ad98534373c9757383973e638ccced0f11f46d", size = 40657, upload-time = "2026-01-14T12:55:44.668Z" }, + { url = "https://files.pythonhosted.org/packages/72/e0/4f9bdc2a98a798511e81edcd6b54fe82767a715e05d1921115ac70717f6f/librt-0.7.8-cp314-cp314-win_amd64.whl", hash = "sha256:3d1322800771bee4a91f3b4bd4e49abc7d35e65166821086e5afd1e6c0d9be44", size = 46835, upload-time = "2026-01-14T12:55:45.655Z" }, + { url = "https://files.pythonhosted.org/packages/f9/3d/59c6402e3dec2719655a41ad027a7371f8e2334aa794ed11533ad5f34969/librt-0.7.8-cp314-cp314-win_arm64.whl", hash = "sha256:5363427bc6a8c3b1719f8f3845ea53553d301382928a86e8fab7984426949bce", size = 39885, upload-time = "2026-01-14T12:55:47.138Z" }, + { url = "https://files.pythonhosted.org/packages/4e/9c/2481d80950b83085fb14ba3c595db56330d21bbc7d88a19f20165f3538db/librt-0.7.8-cp314-cp314t-macosx_10_13_x86_64.whl", hash = "sha256:ca916919793a77e4a98d4a1701e345d337ce53be4a16620f063191f7322ac80f", size = 59161, upload-time = "2026-01-14T12:55:48.45Z" }, + { url = "https://files.pythonhosted.org/packages/96/79/108df2cfc4e672336765d54e3ff887294c1cc36ea4335c73588875775527/librt-0.7.8-cp314-cp314t-macosx_11_0_arm64.whl", hash = "sha256:54feb7b4f2f6706bb82325e836a01be805770443e2400f706e824e91f6441dde", size = 61008, upload-time = "2026-01-14T12:55:49.527Z" }, + { url = "https://files.pythonhosted.org/packages/46/f2/30179898f9994a5637459d6e169b6abdc982012c0a4b2d4c26f50c06f911/librt-0.7.8-cp314-cp314t-manylinux1_i686.manylinux_2_28_i686.manylinux_2_5_i686.whl", hash = "sha256:39a4c76fee41007070f872b648cc2f711f9abf9a13d0c7162478043377b52c8e", size = 187199, upload-time = "2026-01-14T12:55:50.587Z" }, + { url = "https://files.pythonhosted.org/packages/b4/da/f7563db55cebdc884f518ba3791ad033becc25ff68eb70902b1747dc0d70/librt-0.7.8-cp314-cp314t-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:ac9c8a458245c7de80bc1b9765b177055efff5803f08e548dd4bb9ab9a8d789b", size = 198317, upload-time = "2026-01-14T12:55:51.991Z" }, + { url = "https://files.pythonhosted.org/packages/b3/6c/4289acf076ad371471fa86718c30ae353e690d3de6167f7db36f429272f1/librt-0.7.8-cp314-cp314t-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:95b67aa7eff150f075fda09d11f6bfb26edffd300f6ab1666759547581e8f666", size = 210334, upload-time = "2026-01-14T12:55:53.682Z" }, + { url = "https://files.pythonhosted.org/packages/4a/7f/377521ac25b78ac0a5ff44127a0360ee6d5ddd3ce7327949876a30533daa/librt-0.7.8-cp314-cp314t-musllinux_1_2_aarch64.whl", hash = "sha256:535929b6eff670c593c34ff435d5440c3096f20fa72d63444608a5aef64dd581", size = 211031, upload-time = "2026-01-14T12:55:54.827Z" }, + { url = "https://files.pythonhosted.org/packages/c5/b1/e1e96c3e20b23d00cf90f4aad48f0deb4cdfec2f0ed8380d0d85acf98bbf/librt-0.7.8-cp314-cp314t-musllinux_1_2_i686.whl", hash = "sha256:63937bd0f4d1cb56653dc7ae900d6c52c41f0015e25aaf9902481ee79943b33a", size = 204581, upload-time = "2026-01-14T12:55:56.811Z" }, + { url = "https://files.pythonhosted.org/packages/43/71/0f5d010e92ed9747e14bef35e91b6580533510f1e36a8a09eb79ee70b2f0/librt-0.7.8-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:cf243da9e42d914036fd362ac3fa77d80a41cadcd11ad789b1b5eec4daaf67ca", size = 224731, upload-time = "2026-01-14T12:55:58.175Z" }, + { url = "https://files.pythonhosted.org/packages/22/f0/07fb6ab5c39a4ca9af3e37554f9d42f25c464829254d72e4ebbd81da351c/librt-0.7.8-cp314-cp314t-win32.whl", hash = "sha256:171ca3a0a06c643bd0a2f62a8944e1902c94aa8e5da4db1ea9a8daf872685365", size = 41173, upload-time = "2026-01-14T12:55:59.315Z" }, + { url = "https://files.pythonhosted.org/packages/24/d4/7e4be20993dc6a782639625bd2f97f3c66125c7aa80c82426956811cfccf/librt-0.7.8-cp314-cp314t-win_amd64.whl", hash = "sha256:445b7304145e24c60288a2f172b5ce2ca35c0f81605f5299f3fa567e189d2e32", size = 47668, upload-time = "2026-01-14T12:56:00.261Z" }, + { url = "https://files.pythonhosted.org/packages/fc/85/69f92b2a7b3c0f88ffe107c86b952b397004b5b8ea5a81da3d9c04c04422/librt-0.7.8-cp314-cp314t-win_arm64.whl", hash = "sha256:8766ece9de08527deabcd7cb1b4f1a967a385d26e33e536d6d8913db6ef74f06", size = 40550, upload-time = "2026-01-14T12:56:01.542Z" }, +] + +[[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 = "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 = "mypy" +version = "1.19.1" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "librt", marker = "platform_python_implementation != 'PyPy'" }, + { name = "mypy-extensions" }, + { name = "pathspec" }, + { name = "tomli", marker = "python_full_version < '3.11'" }, + { name = "typing-extensions" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/f5/db/4efed9504bc01309ab9c2da7e352cc223569f05478012b5d9ece38fd44d2/mypy-1.19.1.tar.gz", hash = "sha256:19d88bb05303fe63f71dd2c6270daca27cb9401c4ca8255fe50d1d920e0eb9ba", size = 3582404, upload-time = "2025-12-15T05:03:48.42Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/2f/63/e499890d8e39b1ff2df4c0c6ce5d371b6844ee22b8250687a99fd2f657a8/mypy-1.19.1-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:5f05aa3d375b385734388e844bc01733bd33c644ab48e9684faa54e5389775ec", size = 13101333, upload-time = "2025-12-15T05:03:03.28Z" }, + { url = "https://files.pythonhosted.org/packages/72/4b/095626fc136fba96effc4fd4a82b41d688ab92124f8c4f7564bffe5cf1b0/mypy-1.19.1-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:022ea7279374af1a5d78dfcab853fe6a536eebfda4b59deab53cd21f6cd9f00b", size = 12164102, upload-time = "2025-12-15T05:02:33.611Z" }, + { url = "https://files.pythonhosted.org/packages/0c/5b/952928dd081bf88a83a5ccd49aaecfcd18fd0d2710c7ff07b8fb6f7032b9/mypy-1.19.1-cp310-cp310-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:ee4c11e460685c3e0c64a4c5de82ae143622410950d6be863303a1c4ba0e36d6", size = 12765799, upload-time = "2025-12-15T05:03:28.44Z" }, + { url = "https://files.pythonhosted.org/packages/2a/0d/93c2e4a287f74ef11a66fb6d49c7a9f05e47b0a4399040e6719b57f500d2/mypy-1.19.1-cp310-cp310-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:de759aafbae8763283b2ee5869c7255391fbc4de3ff171f8f030b5ec48381b74", size = 13522149, upload-time = "2025-12-15T05:02:36.011Z" }, + { url = "https://files.pythonhosted.org/packages/7b/0e/33a294b56aaad2b338d203e3a1d8b453637ac36cb278b45005e0901cf148/mypy-1.19.1-cp310-cp310-musllinux_1_2_x86_64.whl", hash = "sha256:ab43590f9cd5108f41aacf9fca31841142c786827a74ab7cc8a2eacb634e09a1", size = 13810105, upload-time = "2025-12-15T05:02:40.327Z" }, + { url = "https://files.pythonhosted.org/packages/0e/fd/3e82603a0cb66b67c5e7abababce6bf1a929ddf67bf445e652684af5c5a0/mypy-1.19.1-cp310-cp310-win_amd64.whl", hash = "sha256:2899753e2f61e571b3971747e302d5f420c3fd09650e1951e99f823bc3089dac", size = 10057200, upload-time = "2025-12-15T05:02:51.012Z" }, + { url = "https://files.pythonhosted.org/packages/ef/47/6b3ebabd5474d9cdc170d1342fbf9dddc1b0ec13ec90bf9004ee6f391c31/mypy-1.19.1-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:d8dfc6ab58ca7dda47d9237349157500468e404b17213d44fc1cb77bce532288", size = 13028539, upload-time = "2025-12-15T05:03:44.129Z" }, + { url = "https://files.pythonhosted.org/packages/5c/a6/ac7c7a88a3c9c54334f53a941b765e6ec6c4ebd65d3fe8cdcfbe0d0fd7db/mypy-1.19.1-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:e3f276d8493c3c97930e354b2595a44a21348b320d859fb4a2b9f66da9ed27ab", size = 12083163, upload-time = "2025-12-15T05:03:37.679Z" }, + { url = "https://files.pythonhosted.org/packages/67/af/3afa9cf880aa4a2c803798ac24f1d11ef72a0c8079689fac5cfd815e2830/mypy-1.19.1-cp311-cp311-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:2abb24cf3f17864770d18d673c85235ba52456b36a06b6afc1e07c1fdcd3d0e6", size = 12687629, upload-time = "2025-12-15T05:02:31.526Z" }, + { url = "https://files.pythonhosted.org/packages/2d/46/20f8a7114a56484ab268b0ab372461cb3a8f7deed31ea96b83a4e4cfcfca/mypy-1.19.1-cp311-cp311-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:a009ffa5a621762d0c926a078c2d639104becab69e79538a494bcccb62cc0331", size = 13436933, upload-time = "2025-12-15T05:03:15.606Z" }, + { url = "https://files.pythonhosted.org/packages/5b/f8/33b291ea85050a21f15da910002460f1f445f8007adb29230f0adea279cb/mypy-1.19.1-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:f7cee03c9a2e2ee26ec07479f38ea9c884e301d42c6d43a19d20fb014e3ba925", size = 13661754, upload-time = "2025-12-15T05:02:26.731Z" }, + { url = "https://files.pythonhosted.org/packages/fd/a3/47cbd4e85bec4335a9cd80cf67dbc02be21b5d4c9c23ad6b95d6c5196bac/mypy-1.19.1-cp311-cp311-win_amd64.whl", hash = "sha256:4b84a7a18f41e167f7995200a1d07a4a6810e89d29859df936f1c3923d263042", size = 10055772, upload-time = "2025-12-15T05:03:26.179Z" }, + { url = "https://files.pythonhosted.org/packages/06/8a/19bfae96f6615aa8a0604915512e0289b1fad33d5909bf7244f02935d33a/mypy-1.19.1-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:a8174a03289288c1f6c46d55cef02379b478bfbc8e358e02047487cad44c6ca1", size = 13206053, upload-time = "2025-12-15T05:03:46.622Z" }, + { url = "https://files.pythonhosted.org/packages/a5/34/3e63879ab041602154ba2a9f99817bb0c85c4df19a23a1443c8986e4d565/mypy-1.19.1-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:ffcebe56eb09ff0c0885e750036a095e23793ba6c2e894e7e63f6d89ad51f22e", size = 12219134, upload-time = "2025-12-15T05:03:24.367Z" }, + { url = "https://files.pythonhosted.org/packages/89/cc/2db6f0e95366b630364e09845672dbee0cbf0bbe753a204b29a944967cd9/mypy-1.19.1-cp312-cp312-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:b64d987153888790bcdb03a6473d321820597ab8dd9243b27a92153c4fa50fd2", size = 12731616, upload-time = "2025-12-15T05:02:44.725Z" }, + { url = "https://files.pythonhosted.org/packages/00/be/dd56c1fd4807bc1eba1cf18b2a850d0de7bacb55e158755eb79f77c41f8e/mypy-1.19.1-cp312-cp312-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:c35d298c2c4bba75feb2195655dfea8124d855dfd7343bf8b8c055421eaf0cf8", size = 13620847, upload-time = "2025-12-15T05:03:39.633Z" }, + { url = "https://files.pythonhosted.org/packages/6d/42/332951aae42b79329f743bf1da088cd75d8d4d9acc18fbcbd84f26c1af4e/mypy-1.19.1-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:34c81968774648ab5ac09c29a375fdede03ba253f8f8287847bd480782f73a6a", size = 13834976, upload-time = "2025-12-15T05:03:08.786Z" }, + { url = "https://files.pythonhosted.org/packages/6f/63/e7493e5f90e1e085c562bb06e2eb32cae27c5057b9653348d38b47daaecc/mypy-1.19.1-cp312-cp312-win_amd64.whl", hash = "sha256:b10e7c2cd7870ba4ad9b2d8a6102eb5ffc1f16ca35e3de6bfa390c1113029d13", size = 10118104, upload-time = "2025-12-15T05:03:10.834Z" }, + { url = "https://files.pythonhosted.org/packages/de/9f/a6abae693f7a0c697dbb435aac52e958dc8da44e92e08ba88d2e42326176/mypy-1.19.1-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:e3157c7594ff2ef1634ee058aafc56a82db665c9438fd41b390f3bde1ab12250", size = 13201927, upload-time = "2025-12-15T05:02:29.138Z" }, + { url = "https://files.pythonhosted.org/packages/9a/a4/45c35ccf6e1c65afc23a069f50e2c66f46bd3798cbe0d680c12d12935caa/mypy-1.19.1-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:bdb12f69bcc02700c2b47e070238f42cb87f18c0bc1fc4cdb4fb2bc5fd7a3b8b", size = 12206730, upload-time = "2025-12-15T05:03:01.325Z" }, + { url = "https://files.pythonhosted.org/packages/05/bb/cdcf89678e26b187650512620eec8368fded4cfd99cfcb431e4cdfd19dec/mypy-1.19.1-cp313-cp313-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:f859fb09d9583a985be9a493d5cfc5515b56b08f7447759a0c5deaf68d80506e", size = 12724581, upload-time = "2025-12-15T05:03:20.087Z" }, + { url = "https://files.pythonhosted.org/packages/d1/32/dd260d52babf67bad8e6770f8e1102021877ce0edea106e72df5626bb0ec/mypy-1.19.1-cp313-cp313-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:c9a6538e0415310aad77cb94004ca6482330fece18036b5f360b62c45814c4ef", size = 13616252, upload-time = "2025-12-15T05:02:49.036Z" }, + { url = "https://files.pythonhosted.org/packages/71/d0/5e60a9d2e3bd48432ae2b454b7ef2b62a960ab51292b1eda2a95edd78198/mypy-1.19.1-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:da4869fc5e7f62a88f3fe0b5c919d1d9f7ea3cef92d3689de2823fd27e40aa75", size = 13840848, upload-time = "2025-12-15T05:02:55.95Z" }, + { url = "https://files.pythonhosted.org/packages/98/76/d32051fa65ecf6cc8c6610956473abdc9b4c43301107476ac03559507843/mypy-1.19.1-cp313-cp313-win_amd64.whl", hash = "sha256:016f2246209095e8eda7538944daa1d60e1e8134d98983b9fc1e92c1fc0cb8dd", size = 10135510, upload-time = "2025-12-15T05:02:58.438Z" }, + { url = "https://files.pythonhosted.org/packages/de/eb/b83e75f4c820c4247a58580ef86fcd35165028f191e7e1ba57128c52782d/mypy-1.19.1-cp314-cp314-macosx_10_15_x86_64.whl", hash = "sha256:06e6170bd5836770e8104c8fdd58e5e725cfeb309f0a6c681a811f557e97eac1", size = 13199744, upload-time = "2025-12-15T05:03:30.823Z" }, + { url = "https://files.pythonhosted.org/packages/94/28/52785ab7bfa165f87fcbb61547a93f98bb20e7f82f90f165a1f69bce7b3d/mypy-1.19.1-cp314-cp314-macosx_11_0_arm64.whl", hash = "sha256:804bd67b8054a85447c8954215a906d6eff9cabeabe493fb6334b24f4bfff718", size = 12215815, upload-time = "2025-12-15T05:02:42.323Z" }, + { url = "https://files.pythonhosted.org/packages/0a/c6/bdd60774a0dbfb05122e3e925f2e9e846c009e479dcec4821dad881f5b52/mypy-1.19.1-cp314-cp314-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:21761006a7f497cb0d4de3d8ef4ca70532256688b0523eee02baf9eec895e27b", size = 12740047, upload-time = "2025-12-15T05:03:33.168Z" }, + { url = "https://files.pythonhosted.org/packages/32/2a/66ba933fe6c76bd40d1fe916a83f04fed253152f451a877520b3c4a5e41e/mypy-1.19.1-cp314-cp314-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:28902ee51f12e0f19e1e16fbe2f8f06b6637f482c459dd393efddd0ec7f82045", size = 13601998, upload-time = "2025-12-15T05:03:13.056Z" }, + { url = "https://files.pythonhosted.org/packages/e3/da/5055c63e377c5c2418760411fd6a63ee2b96cf95397259038756c042574f/mypy-1.19.1-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:481daf36a4c443332e2ae9c137dfee878fcea781a2e3f895d54bd3002a900957", size = 13807476, upload-time = "2025-12-15T05:03:17.977Z" }, + { url = "https://files.pythonhosted.org/packages/cd/09/4ebd873390a063176f06b0dbf1f7783dd87bd120eae7727fa4ae4179b685/mypy-1.19.1-cp314-cp314-win_amd64.whl", hash = "sha256:8bb5c6f6d043655e055be9b542aa5f3bdd30e4f3589163e85f93f3640060509f", size = 10281872, upload-time = "2025-12-15T05:03:05.549Z" }, + { url = "https://files.pythonhosted.org/packages/8d/f4/4ce9a05ce5ded1de3ec1c1d96cf9f9504a04e54ce0ed55cfa38619a32b8d/mypy-1.19.1-py3-none-any.whl", hash = "sha256:f1235f5ea01b7db5468d53ece6aaddf1ad0b88d9e7462b86ef96fe04995d7247", size = 2471239, upload-time = "2025-12-15T05:03:07.248Z" }, +] + +[[package]] +name = "mypy-extensions" +version = "1.1.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/a2/6e/371856a3fb9d31ca8dac321cda606860fa4548858c0cc45d9d1d4ca2628b/mypy_extensions-1.1.0.tar.gz", hash = "sha256:52e68efc3284861e772bbcd66823fde5ae21fd2fdb51c62a211403730b916558", size = 6343, upload-time = "2025-04-22T14:54:24.164Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/79/7b/2c79738432f5c924bef5071f933bcc9efd0473bac3b4aa584a6f7c1c8df8/mypy_extensions-1.1.0-py3-none-any.whl", hash = "sha256:1be4cccdb0f2482337c4743e60421de3a356cd97508abadd57d47403e94f5505", size = 4963, upload-time = "2025-04-22T14:54:22.983Z" }, +] + +[[package]] +name = "packaging" +version = "26.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 = "pathspec" +version = "1.0.3" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/4c/b2/bb8e495d5262bfec41ab5cb18f522f1012933347fb5d9e62452d446baca2/pathspec-1.0.3.tar.gz", hash = "sha256:bac5cf97ae2c2876e2d25ebb15078eb04d76e4b98921ee31c6f85ade8b59444d", size = 130841, upload-time = "2026-01-09T15:46:46.009Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/32/2b/121e912bd60eebd623f873fd090de0e84f322972ab25a7f9044c056804ed/pathspec-1.0.3-py3-none-any.whl", hash = "sha256:e80767021c1cc524aa3fb14bedda9c34406591343cc42797b386ce7b9354fb6c", size = 55021, upload-time = "2026-01-09T15:46:44.652Z" }, +] + +[[package]] +name = "pefile" +version = "2024.8.26" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/03/4f/2750f7f6f025a1507cd3b7218691671eecfd0bbebebe8b39aa0fe1d360b8/pefile-2024.8.26.tar.gz", hash = "sha256:3ff6c5d8b43e8c37bb6e6dd5085658d658a7a0bdcd20b6a07b1fcfc1c4e9d632", size = 76008, upload-time = "2024-08-26T20:58:38.155Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/54/16/12b82f791c7f50ddec566873d5bdd245baa1491bac11d15ffb98aecc8f8b/pefile-2024.8.26-py3-none-any.whl", hash = "sha256:76f8b485dcd3b1bb8166f1128d395fa3d87af26360c2358fb75b80019b957c6f", size = 74766, upload-time = "2024-08-26T21:01:02.632Z" }, +] + +[[package]] +name = "platformdirs" +version = "4.5.1" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/cf/86/0248f086a84f01b37aaec0fa567b397df1a119f73c16f6c7a9aac73ea309/platformdirs-4.5.1.tar.gz", hash = "sha256:61d5cdcc6065745cdd94f0f878977f8de9437be93de97c1c12f853c9c0cdcbda", size = 21715, upload-time = "2025-12-05T13:52:58.638Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/cb/28/3bfe2fa5a7b9c46fe7e13c97bda14c895fb10fa2ebf1d0abb90e0cea7ee1/platformdirs-4.5.1-py3-none-any.whl", hash = "sha256:d03afa3963c806a9bed9d5125c8f4cb2fdaf74a55ab60e5d59b3fde758104d31", size = 18731, upload-time = "2025-12-05T13:52:56.823Z" }, +] + +[[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 = "pretend" +version = "1.0.9" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/3c/f8/7c86fd40c9e83deb10891a60d2dcb1af0b3b38064d72ebdb12486acc824f/pretend-1.0.9.tar.gz", hash = "sha256:c90eb810cde8ebb06dafcb8796f9a95228ce796531bc806e794c2f4649aa1b10", size = 4848, upload-time = "2018-04-14T14:31:08.493Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/49/1f/3d4f0579913edd3ad5b23ad52fcc42531cb736ad52af2ba6c057da8785b6/pretend-1.0.9-py2.py3-none-any.whl", hash = "sha256:e389b12b7073604be67845dbe32bf8297360ad9a609b24846fe15d86e0b7dc01", size = 3848, upload-time = "2018-04-14T14:31:04.213Z" }, +] + +[[package]] +name = "py" +version = "1.11.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/98/ff/fec109ceb715d2a6b4c4a85a61af3b40c723a961e8828319fbcb15b868dc/py-1.11.0.tar.gz", hash = "sha256:51c75c4126074b472f746a24399ad32f6053d1b34b68d2fa41e558e6f4a98719", size = 207796, upload-time = "2021-11-04T17:17:01.377Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/f6/f0/10642828a8dfb741e5f3fbaac830550a518a775c7fff6f04a007259b0548/py-1.11.0-py2.py3-none-any.whl", hash = "sha256:607c53218732647dff4acdfcd50cb62615cedf612e72d1724fb1a0cc6405b378", size = 98708, upload-time = "2021-11-04T17:17:00.152Z" }, +] + +[[package]] +name = "pyelftools" +version = "0.32" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/b9/ab/33968940b2deb3d92f5b146bc6d4009a5f95d1d06c148ea2f9ee965071af/pyelftools-0.32.tar.gz", hash = "sha256:6de90ee7b8263e740c8715a925382d4099b354f29ac48ea40d840cf7aa14ace5", size = 15047199, upload-time = "2025-02-19T14:20:05.549Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/af/43/700932c4f0638c3421177144a2e86448c0d75dbaee2c7936bda3f9fd0878/pyelftools-0.32-py3-none-any.whl", hash = "sha256:013df952a006db5e138b1edf6d8a68ecc50630adbd0d83a2d41e7f846163d738", size = 188525, upload-time = "2025-02-19T14:19:59.919Z" }, +] + +[[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 = "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 = "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-cache" +version = "1.2.1" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "attrs" }, + { name = "cattrs" }, + { name = "platformdirs" }, + { name = "requests" }, + { name = "url-normalize" }, + { name = "urllib3" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/1a/be/7b2a95a9e7a7c3e774e43d067c51244e61dea8b120ae2deff7089a93fb2b/requests_cache-1.2.1.tar.gz", hash = "sha256:68abc986fdc5b8d0911318fbb5f7c80eebcd4d01bfacc6685ecf8876052511d1", size = 3018209, upload-time = "2024-06-18T17:18:03.774Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/4e/2e/8f4051119f460cfc786aa91f212165bb6e643283b533db572d7b33952bd2/requests_cache-1.2.1-py3-none-any.whl", hash = "sha256:1285151cddf5331067baa82598afe2d47c7495a1334bfe7a7d329b43e9fd3603", size = 61425, upload-time = "2024-06-18T17:17:45Z" }, +] + +[[package]] +name = "rich" +version = "14.2.0" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "markdown-it-py" }, + { name = "pygments" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/fb/d2/8920e102050a0de7bfabeb4c4614a49248cf8d5d7a8d01885fbb24dc767a/rich-14.2.0.tar.gz", hash = "sha256:73ff50c7c0c1c77c8243079283f4edb376f0f6442433aecb8ce7e6d0b92d1fe4", size = 219990, upload-time = "2025-10-09T14:16:53.064Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/25/7a/b0178788f8dc6cafce37a212c99565fa1fe7872c70c6c9c1e1a372d9d88f/rich-14.2.0-py3-none-any.whl", hash = "sha256:76bc51fe2e57d2b1be1f96c524b890b816e334ab4c1e45888799bfaab0021edd", size = 243393, upload-time = "2025-10-09T14:16:51.245Z" }, +] + +[[package]] +name = "ruff" +version = "0.14.14" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/2e/06/f71e3a86b2df0dfa2d2f72195941cd09b44f87711cb7fa5193732cb9a5fc/ruff-0.14.14.tar.gz", hash = "sha256:2d0f819c9a90205f3a867dbbd0be083bee9912e170fd7d9704cc8ae45824896b", size = 4515732, upload-time = "2026-01-22T22:30:17.527Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/d2/89/20a12e97bc6b9f9f68343952da08a8099c57237aef953a56b82711d55edd/ruff-0.14.14-py3-none-linux_armv6l.whl", hash = "sha256:7cfe36b56e8489dee8fbc777c61959f60ec0f1f11817e8f2415f429552846aed", size = 10467650, upload-time = "2026-01-22T22:30:08.578Z" }, + { url = "https://files.pythonhosted.org/packages/a3/b1/c5de3fd2d5a831fcae21beda5e3589c0ba67eec8202e992388e4b17a6040/ruff-0.14.14-py3-none-macosx_10_12_x86_64.whl", hash = "sha256:6006a0082336e7920b9573ef8a7f52eec837add1265cc74e04ea8a4368cd704c", size = 10883245, upload-time = "2026-01-22T22:30:04.155Z" }, + { url = "https://files.pythonhosted.org/packages/b8/7c/3c1db59a10e7490f8f6f8559d1db8636cbb13dccebf18686f4e3c9d7c772/ruff-0.14.14-py3-none-macosx_11_0_arm64.whl", hash = "sha256:026c1d25996818f0bf498636686199d9bd0d9d6341c9c2c3b62e2a0198b758de", size = 10231273, upload-time = "2026-01-22T22:30:34.642Z" }, + { url = "https://files.pythonhosted.org/packages/a1/6e/5e0e0d9674be0f8581d1f5e0f0a04761203affce3232c1a1189d0e3b4dad/ruff-0.14.14-py3-none-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:f666445819d31210b71e0a6d1c01e24447a20b85458eea25a25fe8142210ae0e", size = 10585753, upload-time = "2026-01-22T22:30:31.781Z" }, + { url = "https://files.pythonhosted.org/packages/23/09/754ab09f46ff1884d422dc26d59ba18b4e5d355be147721bb2518aa2a014/ruff-0.14.14-py3-none-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:3c0f18b922c6d2ff9a5e6c3ee16259adc513ca775bcf82c67ebab7cbd9da5bc8", size = 10286052, upload-time = "2026-01-22T22:30:24.827Z" }, + { url = "https://files.pythonhosted.org/packages/c8/cc/e71f88dd2a12afb5f50733851729d6b571a7c3a35bfdb16c3035132675a0/ruff-0.14.14-py3-none-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:1629e67489c2dea43e8658c3dba659edbfd87361624b4040d1df04c9740ae906", size = 11043637, upload-time = "2026-01-22T22:30:13.239Z" }, + { url = "https://files.pythonhosted.org/packages/67/b2/397245026352494497dac935d7f00f1468c03a23a0c5db6ad8fc49ca3fb2/ruff-0.14.14-py3-none-manylinux_2_17_ppc64.manylinux2014_ppc64.whl", hash = "sha256:27493a2131ea0f899057d49d303e4292b2cae2bb57253c1ed1f256fbcd1da480", size = 12194761, upload-time = "2026-01-22T22:30:22.542Z" }, + { url = "https://files.pythonhosted.org/packages/5b/06/06ef271459f778323112c51b7587ce85230785cd64e91772034ddb88f200/ruff-0.14.14-py3-none-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:01ff589aab3f5b539e35db38425da31a57521efd1e4ad1ae08fc34dbe30bd7df", size = 12005701, upload-time = "2026-01-22T22:30:20.499Z" }, + { url = "https://files.pythonhosted.org/packages/41/d6/99364514541cf811ccc5ac44362f88df66373e9fec1b9d1c4cc830593fe7/ruff-0.14.14-py3-none-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:1cc12d74eef0f29f51775f5b755913eb523546b88e2d733e1d701fe65144e89b", size = 11282455, upload-time = "2026-01-22T22:29:59.679Z" }, + { url = "https://files.pythonhosted.org/packages/ca/71/37daa46f89475f8582b7762ecd2722492df26421714a33e72ccc9a84d7a5/ruff-0.14.14-py3-none-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:bb8481604b7a9e75eff53772496201690ce2687067e038b3cc31aaf16aa0b974", size = 11215882, upload-time = "2026-01-22T22:29:57.032Z" }, + { url = "https://files.pythonhosted.org/packages/2c/10/a31f86169ec91c0705e618443ee74ede0bdd94da0a57b28e72db68b2dbac/ruff-0.14.14-py3-none-manylinux_2_31_riscv64.whl", hash = "sha256:14649acb1cf7b5d2d283ebd2f58d56b75836ed8c6f329664fa91cdea19e76e66", size = 11180549, upload-time = "2026-01-22T22:30:27.175Z" }, + { url = "https://files.pythonhosted.org/packages/fd/1e/c723f20536b5163adf79bdd10c5f093414293cdf567eed9bdb7b83940f3f/ruff-0.14.14-py3-none-musllinux_1_2_aarch64.whl", hash = "sha256:e8058d2145566510790eab4e2fad186002e288dec5e0d343a92fe7b0bc1b3e13", size = 10543416, upload-time = "2026-01-22T22:30:01.964Z" }, + { url = "https://files.pythonhosted.org/packages/3e/34/8a84cea7e42c2d94ba5bde1d7a4fae164d6318f13f933d92da6d7c2041ff/ruff-0.14.14-py3-none-musllinux_1_2_armv7l.whl", hash = "sha256:e651e977a79e4c758eb807f0481d673a67ffe53cfa92209781dfa3a996cf8412", size = 10285491, upload-time = "2026-01-22T22:30:29.51Z" }, + { url = "https://files.pythonhosted.org/packages/55/ef/b7c5ea0be82518906c978e365e56a77f8de7678c8bb6651ccfbdc178c29f/ruff-0.14.14-py3-none-musllinux_1_2_i686.whl", hash = "sha256:cc8b22da8d9d6fdd844a68ae937e2a0adf9b16514e9a97cc60355e2d4b219fc3", size = 10733525, upload-time = "2026-01-22T22:30:06.499Z" }, + { url = "https://files.pythonhosted.org/packages/6a/5b/aaf1dfbcc53a2811f6cc0a1759de24e4b03e02ba8762daabd9b6bd8c59e3/ruff-0.14.14-py3-none-musllinux_1_2_x86_64.whl", hash = "sha256:16bc890fb4cc9781bb05beb5ab4cd51be9e7cb376bf1dd3580512b24eb3fda2b", size = 11315626, upload-time = "2026-01-22T22:30:36.848Z" }, + { url = "https://files.pythonhosted.org/packages/2c/aa/9f89c719c467dfaf8ad799b9bae0df494513fb21d31a6059cb5870e57e74/ruff-0.14.14-py3-none-win32.whl", hash = "sha256:b530c191970b143375b6a68e6f743800b2b786bbcf03a7965b06c4bf04568167", size = 10502442, upload-time = "2026-01-22T22:30:38.93Z" }, + { url = "https://files.pythonhosted.org/packages/87/44/90fa543014c45560cae1fffc63ea059fb3575ee6e1cb654562197e5d16fb/ruff-0.14.14-py3-none-win_amd64.whl", hash = "sha256:3dde1435e6b6fe5b66506c1dff67a421d0b7f6488d466f651c07f4cab3bf20fd", size = 11630486, upload-time = "2026-01-22T22:30:10.852Z" }, + { url = "https://files.pythonhosted.org/packages/9e/6a/40fee331a52339926a92e17ae748827270b288a35ef4a15c9c8f2ec54715/ruff-0.14.14-py3-none-win_arm64.whl", hash = "sha256:56e6981a98b13a32236a72a8da421d7839221fa308b223b9283312312e5ac76c", size = 10920448, upload-time = "2026-01-22T22:30:15.417Z" }, +] + +[[package]] +name = "tabulate" +version = "0.9.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/ec/fe/802052aecb21e3797b8f7902564ab6ea0d60ff8ca23952079064155d1ae1/tabulate-0.9.0.tar.gz", hash = "sha256:0095b12bf5966de529c0feb1fa08671671b3368eec77d7ef7ab114be2c068b3c", size = 81090, upload-time = "2022-10-06T17:21:48.54Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/40/44/4a5f08c96eb108af5cb50b41f76142f0afa346dfa99d5296fe7202a11854/tabulate-0.9.0-py3-none-any.whl", hash = "sha256:024ca478df22e9340661486f85298cff5f6dcdba14f3813e8830015b9ed1948f", size = 35252, upload-time = "2022-10-06T17:21:44.262Z" }, +] + +[[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/34/91/7f65f9809f2936e1f4ce6268ae1903074563603b2a2bd969ebbda802744f/tomli-2.4.0-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:84d081fbc252d1b6a982e1870660e7330fb8f90f676f6e78b052ad4e64714bf0", size = 154915, upload-time = "2026-01-11T11:22:06.703Z" }, + { url = "https://files.pythonhosted.org/packages/20/aa/64dd73a5a849c2e8f216b755599c511badde80e91e9bc2271baa7b2cdbb1/tomli-2.4.0-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:9a08144fa4cba33db5255f9b74f0b89888622109bd2776148f2597447f92a94e", size = 149038, upload-time = "2026-01-11T11:22:07.56Z" }, + { url = "https://files.pythonhosted.org/packages/9e/8a/6d38870bd3d52c8d1505ce054469a73f73a0fe62c0eaf5dddf61447e32fa/tomli-2.4.0-cp313-cp313-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:c73add4bb52a206fd0c0723432db123c0c75c280cbd67174dd9d2db228ebb1b4", size = 242245, upload-time = "2026-01-11T11:22:08.344Z" }, + { url = "https://files.pythonhosted.org/packages/59/bb/8002fadefb64ab2669e5b977df3f5e444febea60e717e755b38bb7c41029/tomli-2.4.0-cp313-cp313-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:1fb2945cbe303b1419e2706e711b7113da57b7db31ee378d08712d678a34e51e", size = 250335, upload-time = "2026-01-11T11:22:09.951Z" }, + { url = "https://files.pythonhosted.org/packages/a5/3d/4cdb6f791682b2ea916af2de96121b3cb1284d7c203d97d92d6003e91c8d/tomli-2.4.0-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:bbb1b10aa643d973366dc2cb1ad94f99c1726a02343d43cbc011edbfac579e7c", size = 245962, upload-time = "2026-01-11T11:22:11.27Z" }, + { url = "https://files.pythonhosted.org/packages/f2/4a/5f25789f9a460bd858ba9756ff52d0830d825b458e13f754952dd15fb7bb/tomli-2.4.0-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:4cbcb367d44a1f0c2be408758b43e1ffb5308abe0ea222897d6bfc8e8281ef2f", size = 250396, upload-time = "2026-01-11T11:22:12.325Z" }, + { url = "https://files.pythonhosted.org/packages/aa/2f/b73a36fea58dfa08e8b3a268750e6853a6aac2a349241a905ebd86f3047a/tomli-2.4.0-cp313-cp313-win32.whl", hash = "sha256:7d49c66a7d5e56ac959cb6fc583aff0651094ec071ba9ad43df785abc2320d86", size = 97530, upload-time = "2026-01-11T11:22:13.865Z" }, + { url = "https://files.pythonhosted.org/packages/3b/af/ca18c134b5d75de7e8dc551c5234eaba2e8e951f6b30139599b53de9c187/tomli-2.4.0-cp313-cp313-win_amd64.whl", hash = "sha256:3cf226acb51d8f1c394c1b310e0e0e61fecdd7adcb78d01e294ac297dd2e7f87", size = 108227, upload-time = "2026-01-11T11:22:15.224Z" }, + { url = "https://files.pythonhosted.org/packages/22/c3/b386b832f209fee8073c8138ec50f27b4460db2fdae9ffe022df89a57f9b/tomli-2.4.0-cp313-cp313-win_arm64.whl", hash = "sha256:d20b797a5c1ad80c516e41bc1fb0443ddb5006e9aaa7bda2d71978346aeb9132", size = 94748, upload-time = "2026-01-11T11:22:16.009Z" }, + { url = "https://files.pythonhosted.org/packages/f3/c4/84047a97eb1004418bc10bdbcfebda209fca6338002eba2dc27cc6d13563/tomli-2.4.0-cp314-cp314-macosx_10_15_x86_64.whl", hash = "sha256:26ab906a1eb794cd4e103691daa23d95c6919cc2fa9160000ac02370cc9dd3f6", size = 154725, upload-time = "2026-01-11T11:22:17.269Z" }, + { url = "https://files.pythonhosted.org/packages/a8/5d/d39038e646060b9d76274078cddf146ced86dc2b9e8bbf737ad5983609a0/tomli-2.4.0-cp314-cp314-macosx_11_0_arm64.whl", hash = "sha256:20cedb4ee43278bc4f2fee6cb50daec836959aadaf948db5172e776dd3d993fc", size = 148901, upload-time = "2026-01-11T11:22:18.287Z" }, + { url = "https://files.pythonhosted.org/packages/73/e5/383be1724cb30f4ce44983d249645684a48c435e1cd4f8b5cded8a816d3c/tomli-2.4.0-cp314-cp314-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:39b0b5d1b6dd03684b3fb276407ebed7090bbec989fa55838c98560c01113b66", size = 243375, upload-time = "2026-01-11T11:22:19.154Z" }, + { url = "https://files.pythonhosted.org/packages/31/f0/bea80c17971c8d16d3cc109dc3585b0f2ce1036b5f4a8a183789023574f2/tomli-2.4.0-cp314-cp314-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:a26d7ff68dfdb9f87a016ecfd1e1c2bacbe3108f4e0f8bcd2228ef9a766c787d", size = 250639, upload-time = "2026-01-11T11:22:20.168Z" }, + { url = "https://files.pythonhosted.org/packages/2c/8f/2853c36abbb7608e3f945d8a74e32ed3a74ee3a1f468f1ffc7d1cb3abba6/tomli-2.4.0-cp314-cp314-musllinux_1_2_aarch64.whl", hash = "sha256:20ffd184fb1df76a66e34bd1b36b4a4641bd2b82954befa32fe8163e79f1a702", size = 246897, upload-time = "2026-01-11T11:22:21.544Z" }, + { url = "https://files.pythonhosted.org/packages/49/f0/6c05e3196ed5337b9fe7ea003e95fd3819a840b7a0f2bf5a408ef1dad8ed/tomli-2.4.0-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:75c2f8bbddf170e8effc98f5e9084a8751f8174ea6ccf4fca5398436e0320bc8", size = 254697, upload-time = "2026-01-11T11:22:23.058Z" }, + { url = "https://files.pythonhosted.org/packages/f3/f5/2922ef29c9f2951883525def7429967fc4d8208494e5ab524234f06b688b/tomli-2.4.0-cp314-cp314-win32.whl", hash = "sha256:31d556d079d72db7c584c0627ff3a24c5d3fb4f730221d3444f3efb1b2514776", size = 98567, upload-time = "2026-01-11T11:22:24.033Z" }, + { url = "https://files.pythonhosted.org/packages/7b/31/22b52e2e06dd2a5fdbc3ee73226d763b184ff21fc24e20316a44ccc4d96b/tomli-2.4.0-cp314-cp314-win_amd64.whl", hash = "sha256:43e685b9b2341681907759cf3a04e14d7104b3580f808cfde1dfdb60ada85475", size = 108556, upload-time = "2026-01-11T11:22:25.378Z" }, + { url = "https://files.pythonhosted.org/packages/48/3d/5058dff3255a3d01b705413f64f4306a141a8fd7a251e5a495e3f192a998/tomli-2.4.0-cp314-cp314-win_arm64.whl", hash = "sha256:3d895d56bd3f82ddd6faaff993c275efc2ff38e52322ea264122d72729dca2b2", size = 96014, upload-time = "2026-01-11T11:22:26.138Z" }, + { url = "https://files.pythonhosted.org/packages/b8/4e/75dab8586e268424202d3a1997ef6014919c941b50642a1682df43204c22/tomli-2.4.0-cp314-cp314t-macosx_10_15_x86_64.whl", hash = "sha256:5b5807f3999fb66776dbce568cc9a828544244a8eb84b84b9bafc080c99597b9", size = 163339, upload-time = "2026-01-11T11:22:27.143Z" }, + { url = "https://files.pythonhosted.org/packages/06/e3/b904d9ab1016829a776d97f163f183a48be6a4deb87304d1e0116a349519/tomli-2.4.0-cp314-cp314t-macosx_11_0_arm64.whl", hash = "sha256:c084ad935abe686bd9c898e62a02a19abfc9760b5a79bc29644463eaf2840cb0", size = 159490, upload-time = "2026-01-11T11:22:28.399Z" }, + { url = "https://files.pythonhosted.org/packages/e3/5a/fc3622c8b1ad823e8ea98a35e3c632ee316d48f66f80f9708ceb4f2a0322/tomli-2.4.0-cp314-cp314t-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:0f2e3955efea4d1cfbcb87bc321e00dc08d2bcb737fd1d5e398af111d86db5df", size = 269398, upload-time = "2026-01-11T11:22:29.345Z" }, + { url = "https://files.pythonhosted.org/packages/fd/33/62bd6152c8bdd4c305ad9faca48f51d3acb2df1f8791b1477d46ff86e7f8/tomli-2.4.0-cp314-cp314t-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:0e0fe8a0b8312acf3a88077a0802565cb09ee34107813bba1c7cd591fa6cfc8d", size = 276515, upload-time = "2026-01-11T11:22:30.327Z" }, + { url = "https://files.pythonhosted.org/packages/4b/ff/ae53619499f5235ee4211e62a8d7982ba9e439a0fb4f2f351a93d67c1dd2/tomli-2.4.0-cp314-cp314t-musllinux_1_2_aarch64.whl", hash = "sha256:413540dce94673591859c4c6f794dfeaa845e98bf35d72ed59636f869ef9f86f", size = 273806, upload-time = "2026-01-11T11:22:32.56Z" }, + { url = "https://files.pythonhosted.org/packages/47/71/cbca7787fa68d4d0a9f7072821980b39fbb1b6faeb5f5cf02f4a5559fa28/tomli-2.4.0-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:0dc56fef0e2c1c470aeac5b6ca8cc7b640bb93e92d9803ddaf9ea03e198f5b0b", size = 281340, upload-time = "2026-01-11T11:22:33.505Z" }, + { url = "https://files.pythonhosted.org/packages/f5/00/d595c120963ad42474cf6ee7771ad0d0e8a49d0f01e29576ee9195d9ecdf/tomli-2.4.0-cp314-cp314t-win32.whl", hash = "sha256:d878f2a6707cc9d53a1be1414bbb419e629c3d6e67f69230217bb663e76b5087", size = 108106, upload-time = "2026-01-11T11:22:34.451Z" }, + { url = "https://files.pythonhosted.org/packages/de/69/9aa0c6a505c2f80e519b43764f8b4ba93b5a0bbd2d9a9de6e2b24271b9a5/tomli-2.4.0-cp314-cp314t-win_amd64.whl", hash = "sha256:2add28aacc7425117ff6364fe9e06a183bb0251b03f986df0e78e974047571fd", size = 120504, upload-time = "2026-01-11T11:22:35.764Z" }, + { url = "https://files.pythonhosted.org/packages/b3/9f/f1668c281c58cfae01482f7114a4b88d345e4c140386241a1a24dcc9e7bc/tomli-2.4.0-cp314-cp314t-win_arm64.whl", hash = "sha256:2b1e3b80e1d5e52e40e9b924ec43d81570f0e7d09d11081b797bc4692765a3d4", size = 99561, upload-time = "2026-01-11T11:22:36.624Z" }, + { 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 = "types-requests" +version = "2.32.4.20260107" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "urllib3" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/0f/f3/a0663907082280664d745929205a89d41dffb29e89a50f753af7d57d0a96/types_requests-2.32.4.20260107.tar.gz", hash = "sha256:018a11ac158f801bfa84857ddec1650750e393df8a004a8a9ae2a9bec6fcb24f", size = 23165, upload-time = "2026-01-07T03:20:54.091Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/1c/12/709ea261f2bf91ef0a26a9eed20f2623227a8ed85610c1e54c5805692ecb/types_requests-2.32.4.20260107-py3-none-any.whl", hash = "sha256:b703fe72f8ce5b31ef031264fe9395cac8f46a04661a79f7ed31a80fb308730d", size = 20676, upload-time = "2026-01-07T03:20:52.929Z" }, +] + +[[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 = "url-normalize" +version = "2.2.1" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "idna" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/80/31/febb777441e5fcdaacb4522316bf2a527c44551430a4873b052d545e3279/url_normalize-2.2.1.tar.gz", hash = "sha256:74a540a3b6eba1d95bdc610c24f2c0141639f3ba903501e61a52a8730247ff37", size = 18846, upload-time = "2025-04-26T20:37:58.553Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/bc/d9/5ec15501b675f7bc07c5d16aa70d8d778b12375686b6efd47656efdc67cd/url_normalize-2.2.1-py3-none-any.whl", hash = "sha256:3deb687587dc91f7b25c9ae5162ffc0f057ae85d22b1e15cf5698311247f567b", size = 14728, upload-time = "2025-04-26T20:37:57.217Z" }, +] + +[[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" }, +] diff --git a/tests/fixtures/real-world-locks/uv/fastapi-cli-0.0.32/LICENSE b/tests/fixtures/real-world-locks/uv/fastapi-cli-0.0.32/LICENSE new file mode 100644 index 00000000..ef706eae --- /dev/null +++ b/tests/fixtures/real-world-locks/uv/fastapi-cli-0.0.32/LICENSE @@ -0,0 +1,21 @@ +The MIT License (MIT) + +Copyright (c) 2024 Sebastián Ramírez + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in +all copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN +THE SOFTWARE. diff --git a/tests/fixtures/real-world-locks/uv/fastapi-cli-0.0.32/pyproject.toml b/tests/fixtures/real-world-locks/uv/fastapi-cli-0.0.32/pyproject.toml new file mode 100644 index 00000000..2f2fb86e --- /dev/null +++ b/tests/fixtures/real-world-locks/uv/fastapi-cli-0.0.32/pyproject.toml @@ -0,0 +1,180 @@ +[project] +name = "fastapi-cli" +dynamic = ["version"] +description = "Run and manage FastAPI apps from the command line with FastAPI CLI. 🚀" +authors = [ + {name = "Sebastián Ramírez", email = "tiangolo@gmail.com"}, +] +requires-python = ">=3.10" +readme = "README.md" +license = "MIT" +license-files = ["LICENSE"] +classifiers = [ + "Intended Audience :: Information Technology", + "Intended Audience :: System Administrators", + "Operating System :: OS Independent", + "Programming Language :: Python :: 3", + "Programming Language :: Python", + "Topic :: Software Development :: Libraries :: Application Frameworks", + "Topic :: Software Development :: Libraries :: Python Modules", + "Topic :: Software Development :: Libraries", + "Topic :: Software Development", + "Typing :: Typed", + "Development Status :: 4 - Beta", + "Framework :: FastAPI", + "Intended Audience :: Developers", + "Programming Language :: Python :: 3 :: Only", + "Programming Language :: Python :: 3.10", + "Programming Language :: Python :: 3.11", + "Programming Language :: Python :: 3.12", + "Programming Language :: Python :: 3.13", + "Programming Language :: Python :: 3.14", +] +dependencies = [ + "typer >= 0.16.0", + "uvicorn[standard] >= 0.15.0", + "rich-toolkit >= 0.14.8", + "tomli >= 2.0.0; python_version < '3.11'" +] + +[project.optional-dependencies] +standard = [ + "uvicorn[standard] >= 0.15.0", + "fastapi-cloud-cli >= 0.1.1", +] +standard-no-fastapi-cloud-cli = [ + "uvicorn[standard] >= 0.15.0", +] +new = [ + "fastapi-new >= 0.0.2", +] + +[project.urls] +Homepage = "https://github.com/fastapi/fastapi-cli" +Documentation = "https://fastapi.tiangolo.com/fastapi-cli/" +Repository = "https://github.com/fastapi/fastapi-cli" +Issues = "https://github.com/fastapi/fastapi-cli/issues" +Changelog = "https://github.com/fastapi/fastapi-cli/blob/main/release-notes.md" + +[dependency-groups] +dev = [ + { include-group = "tests" }, + "prek>=0.2.24,<1.0.0", + "zizmor>=1.24.1", +] +github-actions = [ + "smokeshow>=0.5.0", +] +tests = [ + "coverage[toml]>=6.2,<8.0", + "fastapi>=0.128.0", + "mypy==2.1.0", + "pytest>=7.4.0,<10.0.0", + "ruff>=0.15.15", + "uvicorn>=0.39.0", + "ty>=0.0.25", +] + +[build-system] +requires = ["pdm-backend"] +build-backend = "pdm.backend" + + +[tool.pdm] +version = { source = "file", path = "src/fastapi_cli/__init__.py" } +distribution = true + +[tool.pdm.build] +source-includes = [ + "tests/", + "scripts/", +] + +[tool.pytest.ini_options] +addopts = [ + "--strict-config", + "--strict-markers", +] +xfail_strict = true +junit_family = "xunit2" + +[tool.coverage.run] +parallel = true +data_file = "coverage/.coverage" +source = [ + "src", + "tests", +] +relative_files = true +context = '${CONTEXT}' +dynamic_context = "test_function" +omit = [ + "tests/assets/*", +] + +[tool.coverage.report] +show_missing = true +sort = "-Cover" +exclude_lines = [ + "pragma: no cover", + "@overload", + 'if __name__ == "__main__":', + "if TYPE_CHECKING:", +] + +[tool.coverage.html] +show_contexts = true + +[tool.mypy] +strict = true +exclude = [ + "tests/assets/*", +] + +[tool.ruff.lint] +select = [ + "E", # pycodestyle errors + "W", # pycodestyle warnings + "F", # pyflakes + "I", # isort + "B", # flake8-bugbear + "C4", # flake8-comprehensions + "UP", # pyupgrade +] +ignore = [ + "E501", # line too long, handled by black + "B008", # do not perform function calls in argument defaults + "C901", # too complex + "W191", # indentation contains tabs +] + +# [tool.ruff.lint.per-file-ignores] +# "__init__.py" = ["F401"] + + +[tool.ruff.lint.isort] +known-third-party = ["typer", "fastapi"] + +[tool.ruff.lint.pyupgrade] +# Preserve types, even if a file imports `from __future__ import annotations`. +keep-runtime-typing = true + +[tool.ty.terminal] +error-on-warning = true + +[tool.typos.files] +extend-exclude = [ + "coverage/", + "dist/", + "htmlcov/", + "uv.lock", +] + +[tool.typos.default] +extend-ignore-re = [ + # GitHub usernames in @mentions + "@[a-zA-Z0-9](?:-?[a-zA-Z0-9])*", +] + +[tool.typos.default.extend-identifiers] +alls = "alls" diff --git a/tests/fixtures/real-world-locks/uv/fastapi-cli-0.0.32/uv.lock b/tests/fixtures/real-world-locks/uv/fastapi-cli-0.0.32/uv.lock new file mode 100644 index 00000000..340c5047 --- /dev/null +++ b/tests/fixtures/real-world-locks/uv/fastapi-cli-0.0.32/uv.lock @@ -0,0 +1,1663 @@ +version = 1 +revision = 3 +requires-python = ">=3.10" +resolution-markers = [ + "python_full_version >= '3.15'", + "python_full_version == '3.14.*'", + "python_full_version == '3.13.*'", + "python_full_version < '3.13'", +] + +[[package]] +name = "annotated-doc" +version = "0.0.4" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/57/ba/046ceea27344560984e26a590f90bc7f4a75b06701f653222458922b558c/annotated_doc-0.0.4.tar.gz", hash = "sha256:fbcda96e87e9c92ad167c2e53839e57503ecfda18804ea28102353485033faa4", size = 7288, upload-time = "2025-11-10T22:07:42.062Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/1e/d3/26bf1008eb3d2daa8ef4cacc7f3bfdc11818d111f7e2d0201bc6e3b49d45/annotated_doc-0.0.4-py3-none-any.whl", hash = "sha256:571ac1dc6991c450b25a9c2d84a3705e2ae7a53467b5d111c24fa8baabbed320", size = 5303, upload-time = "2025-11-10T22:07:40.673Z" }, +] + +[[package]] +name = "annotated-types" +version = "0.7.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/ee/67/531ea369ba64dcff5ec9c3402f9f51bf748cec26dde048a2f973a4eea7f5/annotated_types-0.7.0.tar.gz", hash = "sha256:aff07c09a53a08bc8cfccb9c85b05f1aa9a2a6f23728d790723543408344ce89", size = 16081, upload-time = "2024-05-20T21:33:25.928Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/78/b6/6307fbef88d9b5ee7421e68d78a9f162e0da4900bc5f5793f6d3d0e34fb8/annotated_types-0.7.0-py3-none-any.whl", hash = "sha256:1f02e8b43a8fbbc3f3e0d4f0f4bfc8131bcb4eebe8849b8e5c773f3a1c582a53", size = 13643, upload-time = "2024-05-20T21:33:24.1Z" }, +] + +[[package]] +name = "anyio" +version = "4.12.1" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "exceptiongroup", marker = "python_full_version < '3.11'" }, + { name = "idna" }, + { name = "typing-extensions", marker = "python_full_version < '3.13'" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/96/f0/5eb65b2bb0d09ac6776f2eb54adee6abe8228ea05b20a5ad0e4945de8aac/anyio-4.12.1.tar.gz", hash = "sha256:41cfcc3a4c85d3f05c932da7c26d0201ac36f72abd4435ba90d0464a3ffed703", size = 228685, upload-time = "2026-01-06T11:45:21.246Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/38/0e/27be9fdef66e72d64c0cdc3cc2823101b80585f8119b5c112c2e8f5f7dab/anyio-4.12.1-py3-none-any.whl", hash = "sha256:d405828884fc140aa80a3c667b8beed277f1dfedec42ba031bd6ac3db606ab6c", size = 113592, upload-time = "2026-01-06T11:45:19.497Z" }, +] + +[[package]] +name = "ast-serialize" +version = "0.3.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/a9/9d/912fefab0e30aee6a3af8a62bbea4a81b29afa4ba2c973d31170620a26de/ast_serialize-0.3.0.tar.gz", hash = "sha256:1bc3ca09a63a021376527c4e938deedd11d11d675ce850e6f9c7487f5889992b", size = 60689, upload-time = "2026-04-30T23:24:48.104Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/6f/57/a54d4de491d6cdd7a4e4b0952cc3ca9f60dcefa7b5fb48d6d492debe1649/ast_serialize-0.3.0-cp314-cp314t-macosx_10_12_x86_64.whl", hash = "sha256:3a867927df59f76a18dc1d874a0b2c079b42c58972dca637905576deb0912e14", size = 1182966, upload-time = "2026-04-30T23:23:57.376Z" }, + { url = "https://files.pythonhosted.org/packages/ee/9e/a5db014bb0f91b209236b57c429389e31290c0093532b8436d577699b2fa/ast_serialize-0.3.0-cp314-cp314t-macosx_11_0_arm64.whl", hash = "sha256:a6fb063bf040abf8321e7b8113a0554eda445ffc508aa51287f8808886a5ae22", size = 1171316, upload-time = "2026-04-30T23:23:59.63Z" }, + { url = "https://files.pythonhosted.org/packages/15/59/fd55133e478c4326f60a11df02573bf7ccb2ac685810b50f1803d0f68053/ast_serialize-0.3.0-cp314-cp314t-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:5075cd8482573d743586779e5f9b652a015e37d4e95132d7e5a9bc5c8f483d8f", size = 1232234, upload-time = "2026-04-30T23:24:01.168Z" }, + { url = "https://files.pythonhosted.org/packages/cc/79/0ca1d26357ecb4a697d74d00b73ef3137f24c140424125393a0de820eb09/ast_serialize-0.3.0-cp314-cp314t-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:41560b27794f4553b0f77811e9fb325b77db4a2b39018d437e09932275306e66", size = 1233437, upload-time = "2026-04-30T23:24:03.151Z" }, + { url = "https://files.pythonhosted.org/packages/53/3e/7078ec94dd6e124b8e028ac77016a4f13c83fa1c145790f2e68f3816998b/ast_serialize-0.3.0-cp314-cp314t-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:b967c01ca74909c5d90e0fe4393401e2cc5da5ebd9a6262a19e45ffd3757dec8", size = 1440188, upload-time = "2026-04-30T23:24:04.717Z" }, + { url = "https://files.pythonhosted.org/packages/21/16/cca7195ef55a012f8013c3442afa91d287a0a36dcf88b480b262475135b3/ast_serialize-0.3.0-cp314-cp314t-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:424ebb8f46cd993f7cec4009d119312d8433dd90e6b0df0499cd2c91bdcc5af9", size = 1254211, upload-time = "2026-04-30T23:24:06.18Z" }, + { url = "https://files.pythonhosted.org/packages/a0/0f/f3d4dfae67dee6580534361a6343367d34217e7d25cff858bd1d8f03b8ed/ast_serialize-0.3.0-cp314-cp314t-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:d14b1d566b56e2ee70b11fec1de7e0b94ec7cd83717ec7d189967841a361190e", size = 1255973, upload-time = "2026-04-30T23:24:07.772Z" }, + { url = "https://files.pythonhosted.org/packages/14/41/55fbfe02c42f40fbe3e74eda167d977d555ff720ce1abfa08515236efd88/ast_serialize-0.3.0-cp314-cp314t-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:7ba30b18735f047ec11103d1ab92f4789cf1fea1e0dc89b04a2f5a0632fd79de", size = 1298629, upload-time = "2026-04-30T23:24:09.4Z" }, + { url = "https://files.pythonhosted.org/packages/28/36/7d2501cacc7989fb8504aa9da2a2022a174200a59d4e6639de4367a57fdd/ast_serialize-0.3.0-cp314-cp314t-musllinux_1_2_aarch64.whl", hash = "sha256:e6ea0754cb7b0f682ebb005ffb0d18f8d17993490d9c289863cd69cacc4ab8df", size = 1408435, upload-time = "2026-04-30T23:24:11.013Z" }, + { url = "https://files.pythonhosted.org/packages/03/e7/54e3b469c3fa0bf9cd532fa643d1d33b73303f8d70beac3e366b68dd64b7/ast_serialize-0.3.0-cp314-cp314t-musllinux_1_2_armv7l.whl", hash = "sha256:a0c5aa1073a5ba7b2abaa4b54abe8b8d75c4d1e2d54a2ff70b0ca6222fea5728", size = 1508174, upload-time = "2026-04-30T23:24:12.635Z" }, + { url = "https://files.pythonhosted.org/packages/b5/2a/9b9621865b02c60539e26d9b114a312b4fa46aa703e33e79317174bfea21/ast_serialize-0.3.0-cp314-cp314t-musllinux_1_2_i686.whl", hash = "sha256:4e52650d834c1ea7791969a361de2c54c13b2fb4c519ec79445fa8b9021a147d", size = 1502354, upload-time = "2026-04-30T23:24:14.186Z" }, + { url = "https://files.pythonhosted.org/packages/34/dd/f138bc5c43b0c414fdd12eefe15677839323078b6e75301ad7f96cd26d45/ast_serialize-0.3.0-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:15bd6af3f136c61dae27805eb6b8f3269e85a545c4c27ffe9e530ead78d2b36d", size = 1450504, upload-time = "2026-04-30T23:24:16.076Z" }, + { url = "https://files.pythonhosted.org/packages/68/cf/97ef9e1c315601db74365955c8edd3292e3055500d6317602815dbdf08ae/ast_serialize-0.3.0-cp314-cp314t-win32.whl", hash = "sha256:d188bfe37b674b49708497683051d4b571366a668799c9b8e8a94513694969d9", size = 1058662, upload-time = "2026-04-30T23:24:17.535Z" }, + { url = "https://files.pythonhosted.org/packages/f8/d6/e2c3483c31580fdb623f92ad38d2f856cde4b9205a3e6bd84760f3de7d82/ast_serialize-0.3.0-cp314-cp314t-win_amd64.whl", hash = "sha256:5832c2fdf8f8a6cf682b4cfcf677f5eaf39b4ddbc490f5480cfccdd1e7ce8fa1", size = 1100349, upload-time = "2026-04-30T23:24:18.992Z" }, + { url = "https://files.pythonhosted.org/packages/ab/89/29abcb1fe18a429cda60c6e0bbd1d6e90499339842a2f548d7567542357e/ast_serialize-0.3.0-cp314-cp314t-win_arm64.whl", hash = "sha256:670f177188d128fb7f9f15b5ad0e1b553d22c34e3f584dcb83eb8077600437f0", size = 1072895, upload-time = "2026-04-30T23:24:20.706Z" }, + { url = "https://files.pythonhosted.org/packages/bc/93/72abad83966ed6235647c9f956417dc1e17e997696388521910e3d1fa3f4/ast_serialize-0.3.0-cp39-abi3-macosx_10_12_x86_64.whl", hash = "sha256:2ec2fafa5e4313cc8feed96e436ebe19ac7bc6fa41fbc2827e826c48b9e4c3a9", size = 1190024, upload-time = "2026-04-30T23:24:22.486Z" }, + { url = "https://files.pythonhosted.org/packages/85/4f/eb88584b2f0234e581762011208ca203252bf6c98e59b4769daa571f3576/ast_serialize-0.3.0-cp39-abi3-macosx_11_0_arm64.whl", hash = "sha256:ef6d3c08b7b4cd29b48410338e134764a00e76d25841eb02c1084e868c888ecc", size = 1178633, upload-time = "2026-04-30T23:24:24.35Z" }, + { url = "https://files.pythonhosted.org/packages/56/51/cf1ec1ff3e616373d0dcbd5fad502e0029dc541f13ab642259762a7d127f/ast_serialize-0.3.0-cp39-abi3-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:3d841424f41b886e98044abc80769c14a956e6e5ccd5fb5b0d9f5ead72be18a4", size = 1241351, upload-time = "2026-04-30T23:24:25.987Z" }, + { url = "https://files.pythonhosted.org/packages/0d/44/68fcf50478cf1093f2d423f034ae06453122c8b415d8e21a44668eca485d/ast_serialize-0.3.0-cp39-abi3-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:d21453734ad39367ede5d37efe4f59f830ce1c09f432fc72a90e368f77a4a3e7", size = 1239582, upload-time = "2026-04-30T23:24:27.808Z" }, + { url = "https://files.pythonhosted.org/packages/9d/c1/a6c9fa284eceb5fc6f21347e968445a051d7ca2c4d34e6a04314646dbcee/ast_serialize-0.3.0-cp39-abi3-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:f5e110cdce2a347e1dd987529c88ef54d26f67848dce3eba1b3b2cc2cf085c94", size = 1448853, upload-time = "2026-04-30T23:24:29.534Z" }, + { url = "https://files.pythonhosted.org/packages/23/5f/8ad3829a09e4e8c5328a53ce7d4711d660944e3e164c5f6abcc2c8f27167/ast_serialize-0.3.0-cp39-abi3-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:3b6e23a98e57560a055f5c4b68700a0fd5ce483d2814c23140b3638c7f5d1e61", size = 1262204, upload-time = "2026-04-30T23:24:31.482Z" }, + { url = "https://files.pythonhosted.org/packages/25/13/44aa28d97f10e25247e8576b5f6b2795d4fa1a80acc88acc942c508d06f7/ast_serialize-0.3.0-cp39-abi3-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:c1c9e763d70293d65ce1e1ea8c943140c68d0953f0268c7ee0998f2e07f77dd0", size = 1266458, upload-time = "2026-04-30T23:24:33.088Z" }, + { url = "https://files.pythonhosted.org/packages/d8/58/b3a8be3777cd3744324fd5cec0d80d37cd96fc7cbb0fb010e03dff1e870f/ast_serialize-0.3.0-cp39-abi3-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:4388a1796c228f1ce5c391426f7d21a0003ad3b47f677dbeded9bd1a85c7209f", size = 1308700, upload-time = "2026-04-30T23:24:34.657Z" }, + { url = "https://files.pythonhosted.org/packages/13/03/f8312d6b57f5471a9dc7946f22b8798a1fc296d38c25766223aacadec42c/ast_serialize-0.3.0-cp39-abi3-musllinux_1_2_aarch64.whl", hash = "sha256:5283cdcc0c64c3d8b9b688dc6aaa012d9c0cf1380a7f774a6bae6a1c01b3205a", size = 1416724, upload-time = "2026-04-30T23:24:36.562Z" }, + { url = "https://files.pythonhosted.org/packages/50/5d/13fc3789a7abac00559da2e2e9f386db4612aa1f84fc53d09bf714c37545/ast_serialize-0.3.0-cp39-abi3-musllinux_1_2_armv7l.whl", hash = "sha256:f5ef88cc5842a5d7a6ac09dc0d5fc2c98f5d276c1f076f866d55047ce886785b", size = 1515441, upload-time = "2026-04-30T23:24:38.018Z" }, + { url = "https://files.pythonhosted.org/packages/eb/b9/7ab43fc7a23b1f970281093228f5f79bed6edeed7a3e672bde6d7a832a58/ast_serialize-0.3.0-cp39-abi3-musllinux_1_2_i686.whl", hash = "sha256:cc14bf402bdc0978594ecce783793de2c7470cd4f5cd7eb286ca97ed8ff7cba9", size = 1510522, upload-time = "2026-04-30T23:24:39.798Z" }, + { url = "https://files.pythonhosted.org/packages/56/ec/d75fc2b788d319f1fad77c14156896f31afdfc68af85b505e5bdebcb9592/ast_serialize-0.3.0-cp39-abi3-musllinux_1_2_x86_64.whl", hash = "sha256:11eae0cf1b7b3e0678133cc2daa974ea972caf02eb4b3aa062af6fa9acd52c57", size = 1460917, upload-time = "2026-04-30T23:24:41.305Z" }, + { url = "https://files.pythonhosted.org/packages/95/74/f99c81193a2725911e1911ae567ed27c2f2419332c7f3537366f9d238cac/ast_serialize-0.3.0-cp39-abi3-win32.whl", hash = "sha256:2db3dd99de5e6a5a11d7dda73de8750eb6e5baaf25245adf7bdcfe64b6108ae2", size = 1067804, upload-time = "2026-04-30T23:24:43.091Z" }, + { url = "https://files.pythonhosted.org/packages/16/81/76af00c47daa151e89f98ae21fbbcb2840aaa9f5766579c4da76a3c57188/ast_serialize-0.3.0-cp39-abi3-win_amd64.whl", hash = "sha256:a2cd125adccf7969470621905d302750cd25951f22ea430d9a25b7be031e5549", size = 1105561, upload-time = "2026-04-30T23:24:44.578Z" }, + { url = "https://files.pythonhosted.org/packages/bd/46/d3ec57ad500f598d1554bd14ce4df615960549ab2844961bc4e1f5fbd174/ast_serialize-0.3.0-cp39-abi3-win_arm64.whl", hash = "sha256:0dd00da29985f15f50dc35728b7e1e7c84507bccfea1d9914738530f1c72238a", size = 1077165, upload-time = "2026-04-30T23:24:46.377Z" }, +] + +[[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 = "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 = "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.1" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/23/f9/e92df5e07f3fc8d4c7f9a0f146ef75446bf870351cd37b788cf5897f8079/coverage-7.13.1.tar.gz", hash = "sha256:b7593fe7eb5feaa3fbb461ac79aac9f9fc0387a5ca8080b0c6fe2ca27b091afd", size = 825862, upload-time = "2025-12-28T15:42:56.969Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/2d/9a/3742e58fd04b233df95c012ee9f3dfe04708a5e1d32613bd2d47d4e1be0d/coverage-7.13.1-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:e1fa280b3ad78eea5be86f94f461c04943d942697e0dac889fa18fff8f5f9147", size = 218633, upload-time = "2025-12-28T15:40:10.165Z" }, + { url = "https://files.pythonhosted.org/packages/7e/45/7e6bdc94d89cd7c8017ce735cf50478ddfe765d4fbf0c24d71d30ea33d7a/coverage-7.13.1-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:c3d8c679607220979434f494b139dfb00131ebf70bb406553d69c1ff01a5c33d", size = 219147, upload-time = "2025-12-28T15:40:12.069Z" }, + { url = "https://files.pythonhosted.org/packages/f7/38/0d6a258625fd7f10773fe94097dc16937a5f0e3e0cdf3adef67d3ac6baef/coverage-7.13.1-cp310-cp310-manylinux1_i686.manylinux_2_28_i686.manylinux_2_5_i686.whl", hash = "sha256:339dc63b3eba969067b00f41f15ad161bf2946613156fb131266d8debc8e44d0", size = 245894, upload-time = "2025-12-28T15:40:13.556Z" }, + { url = "https://files.pythonhosted.org/packages/27/58/409d15ea487986994cbd4d06376e9860e9b157cfbfd402b1236770ab8dd2/coverage-7.13.1-cp310-cp310-manylinux1_x86_64.manylinux_2_28_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:db622b999ffe49cb891f2fff3b340cdc2f9797d01a0a202a0973ba2562501d90", size = 247721, upload-time = "2025-12-28T15:40:15.37Z" }, + { url = "https://files.pythonhosted.org/packages/da/bf/6e8056a83fd7a96c93341f1ffe10df636dd89f26d5e7b9ca511ce3bcf0df/coverage-7.13.1-cp310-cp310-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:d1443ba9acbb593fa7c1c29e011d7c9761545fe35e7652e85ce7f51a16f7e08d", size = 249585, upload-time = "2025-12-28T15:40:17.226Z" }, + { url = "https://files.pythonhosted.org/packages/f4/15/e1daff723f9f5959acb63cbe35b11203a9df77ee4b95b45fffd38b318390/coverage-7.13.1-cp310-cp310-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:c832ec92c4499ac463186af72f9ed4d8daec15499b16f0a879b0d1c8e5cf4a3b", size = 246597, upload-time = "2025-12-28T15:40:19.028Z" }, + { url = "https://files.pythonhosted.org/packages/74/a6/1efd31c5433743a6ddbc9d37ac30c196bb07c7eab3d74fbb99b924c93174/coverage-7.13.1-cp310-cp310-musllinux_1_2_aarch64.whl", hash = "sha256:562ec27dfa3f311e0db1ba243ec6e5f6ab96b1edfcfc6cf86f28038bc4961ce6", size = 247626, upload-time = "2025-12-28T15:40:20.846Z" }, + { url = "https://files.pythonhosted.org/packages/6d/9f/1609267dd3e749f57fdd66ca6752567d1c13b58a20a809dc409b263d0b5f/coverage-7.13.1-cp310-cp310-musllinux_1_2_i686.whl", hash = "sha256:4de84e71173d4dada2897e5a0e1b7877e5eefbfe0d6a44edee6ce31d9b8ec09e", size = 245629, upload-time = "2025-12-28T15:40:22.397Z" }, + { url = "https://files.pythonhosted.org/packages/e2/f6/6815a220d5ec2466383d7cc36131b9fa6ecbe95c50ec52a631ba733f306a/coverage-7.13.1-cp310-cp310-musllinux_1_2_riscv64.whl", hash = "sha256:a5a68357f686f8c4d527a2dc04f52e669c2fc1cbde38f6f7eb6a0e58cbd17cae", size = 245901, upload-time = "2025-12-28T15:40:23.836Z" }, + { url = "https://files.pythonhosted.org/packages/ac/58/40576554cd12e0872faf6d2c0eb3bc85f71d78427946ddd19ad65201e2c0/coverage-7.13.1-cp310-cp310-musllinux_1_2_x86_64.whl", hash = "sha256:77cc258aeb29a3417062758975521eae60af6f79e930d6993555eeac6a8eac29", size = 246505, upload-time = "2025-12-28T15:40:25.421Z" }, + { url = "https://files.pythonhosted.org/packages/3b/77/9233a90253fba576b0eee81707b5781d0e21d97478e5377b226c5b096c0f/coverage-7.13.1-cp310-cp310-win32.whl", hash = "sha256:bb4f8c3c9a9f34423dba193f241f617b08ffc63e27f67159f60ae6baf2dcfe0f", size = 221257, upload-time = "2025-12-28T15:40:27.217Z" }, + { url = "https://files.pythonhosted.org/packages/e0/43/e842ff30c1a0a623ec80db89befb84a3a7aad7bfe44a6ea77d5a3e61fedd/coverage-7.13.1-cp310-cp310-win_amd64.whl", hash = "sha256:c8e2706ceb622bc63bac98ebb10ef5da80ed70fbd8a7999a5076de3afaef0fb1", size = 222191, upload-time = "2025-12-28T15:40:28.916Z" }, + { url = "https://files.pythonhosted.org/packages/b4/9b/77baf488516e9ced25fc215a6f75d803493fc3f6a1a1227ac35697910c2a/coverage-7.13.1-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:1a55d509a1dc5a5b708b5dad3b5334e07a16ad4c2185e27b40e4dba796ab7f88", size = 218755, upload-time = "2025-12-28T15:40:30.812Z" }, + { url = "https://files.pythonhosted.org/packages/d7/cd/7ab01154e6eb79ee2fab76bf4d89e94c6648116557307ee4ebbb85e5c1bf/coverage-7.13.1-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:4d010d080c4888371033baab27e47c9df7d6fb28d0b7b7adf85a4a49be9298b3", size = 219257, upload-time = "2025-12-28T15:40:32.333Z" }, + { url = "https://files.pythonhosted.org/packages/01/d5/b11ef7863ffbbdb509da0023fad1e9eda1c0eaea61a6d2ea5b17d4ac706e/coverage-7.13.1-cp311-cp311-manylinux1_i686.manylinux_2_28_i686.manylinux_2_5_i686.whl", hash = "sha256:d938b4a840fb1523b9dfbbb454f652967f18e197569c32266d4d13f37244c3d9", size = 249657, upload-time = "2025-12-28T15:40:34.1Z" }, + { url = "https://files.pythonhosted.org/packages/f7/7c/347280982982383621d29b8c544cf497ae07ac41e44b1ca4903024131f55/coverage-7.13.1-cp311-cp311-manylinux1_x86_64.manylinux_2_28_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:bf100a3288f9bb7f919b87eb84f87101e197535b9bd0e2c2b5b3179633324fee", size = 251581, upload-time = "2025-12-28T15:40:36.131Z" }, + { url = "https://files.pythonhosted.org/packages/82/f6/ebcfed11036ade4c0d75fa4453a6282bdd225bc073862766eec184a4c643/coverage-7.13.1-cp311-cp311-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:ef6688db9bf91ba111ae734ba6ef1a063304a881749726e0d3575f5c10a9facf", size = 253691, upload-time = "2025-12-28T15:40:37.626Z" }, + { url = "https://files.pythonhosted.org/packages/02/92/af8f5582787f5d1a8b130b2dcba785fa5e9a7a8e121a0bb2220a6fdbdb8a/coverage-7.13.1-cp311-cp311-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:0b609fc9cdbd1f02e51f67f51e5aee60a841ef58a68d00d5ee2c0faf357481a3", size = 249799, upload-time = "2025-12-28T15:40:39.47Z" }, + { url = "https://files.pythonhosted.org/packages/24/aa/0e39a2a3b16eebf7f193863323edbff38b6daba711abaaf807d4290cf61a/coverage-7.13.1-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:c43257717611ff5e9a1d79dce8e47566235ebda63328718d9b65dd640bc832ef", size = 251389, upload-time = "2025-12-28T15:40:40.954Z" }, + { url = "https://files.pythonhosted.org/packages/73/46/7f0c13111154dc5b978900c0ccee2e2ca239b910890e674a77f1363d483e/coverage-7.13.1-cp311-cp311-musllinux_1_2_i686.whl", hash = "sha256:e09fbecc007f7b6afdfb3b07ce5bd9f8494b6856dd4f577d26c66c391b829851", size = 249450, upload-time = "2025-12-28T15:40:42.489Z" }, + { url = "https://files.pythonhosted.org/packages/ac/ca/e80da6769e8b669ec3695598c58eef7ad98b0e26e66333996aee6316db23/coverage-7.13.1-cp311-cp311-musllinux_1_2_riscv64.whl", hash = "sha256:a03a4f3a19a189919c7055098790285cc5c5b0b3976f8d227aea39dbf9f8bfdb", size = 249170, upload-time = "2025-12-28T15:40:44.279Z" }, + { url = "https://files.pythonhosted.org/packages/af/18/9e29baabdec1a8644157f572541079b4658199cfd372a578f84228e860de/coverage-7.13.1-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:3820778ea1387c2b6a818caec01c63adc5b3750211af6447e8dcfb9b6f08dbba", size = 250081, upload-time = "2025-12-28T15:40:45.748Z" }, + { url = "https://files.pythonhosted.org/packages/00/f8/c3021625a71c3b2f516464d322e41636aea381018319050a8114105872ee/coverage-7.13.1-cp311-cp311-win32.whl", hash = "sha256:ff10896fa55167371960c5908150b434b71c876dfab97b69478f22c8b445ea19", size = 221281, upload-time = "2025-12-28T15:40:47.232Z" }, + { url = "https://files.pythonhosted.org/packages/27/56/c216625f453df6e0559ed666d246fcbaaa93f3aa99eaa5080cea1229aa3d/coverage-7.13.1-cp311-cp311-win_amd64.whl", hash = "sha256:a998cc0aeeea4c6d5622a3754da5a493055d2d95186bad877b0a34ea6e6dbe0a", size = 222215, upload-time = "2025-12-28T15:40:49.19Z" }, + { url = "https://files.pythonhosted.org/packages/5c/9a/be342e76f6e531cae6406dc46af0d350586f24d9b67fdfa6daee02df71af/coverage-7.13.1-cp311-cp311-win_arm64.whl", hash = "sha256:fea07c1a39a22614acb762e3fbbb4011f65eedafcb2948feeef641ac78b4ee5c", size = 220886, upload-time = "2025-12-28T15:40:51.067Z" }, + { url = "https://files.pythonhosted.org/packages/ce/8a/87af46cccdfa78f53db747b09f5f9a21d5fc38d796834adac09b30a8ce74/coverage-7.13.1-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:6f34591000f06e62085b1865c9bc5f7858df748834662a51edadfd2c3bfe0dd3", size = 218927, upload-time = "2025-12-28T15:40:52.814Z" }, + { url = "https://files.pythonhosted.org/packages/82/a8/6e22fdc67242a4a5a153f9438d05944553121c8f4ba70cb072af4c41362e/coverage-7.13.1-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:b67e47c5595b9224599016e333f5ec25392597a89d5744658f837d204e16c63e", size = 219288, upload-time = "2025-12-28T15:40:54.262Z" }, + { url = "https://files.pythonhosted.org/packages/d0/0a/853a76e03b0f7c4375e2ca025df45c918beb367f3e20a0a8e91967f6e96c/coverage-7.13.1-cp312-cp312-manylinux1_i686.manylinux_2_28_i686.manylinux_2_5_i686.whl", hash = "sha256:3e7b8bd70c48ffb28461ebe092c2345536fb18bbbf19d287c8913699735f505c", size = 250786, upload-time = "2025-12-28T15:40:56.059Z" }, + { url = "https://files.pythonhosted.org/packages/ea/b4/694159c15c52b9f7ec7adf49d50e5f8ee71d3e9ef38adb4445d13dd56c20/coverage-7.13.1-cp312-cp312-manylinux1_x86_64.manylinux_2_28_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:c223d078112e90dc0e5c4e35b98b9584164bea9fbbd221c0b21c5241f6d51b62", size = 253543, upload-time = "2025-12-28T15:40:57.585Z" }, + { url = "https://files.pythonhosted.org/packages/96/b2/7f1f0437a5c855f87e17cf5d0dc35920b6440ff2b58b1ba9788c059c26c8/coverage-7.13.1-cp312-cp312-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:794f7c05af0763b1bbd1b9e6eff0e52ad068be3b12cd96c87de037b01390c968", size = 254635, upload-time = "2025-12-28T15:40:59.443Z" }, + { url = "https://files.pythonhosted.org/packages/e9/d1/73c3fdb8d7d3bddd9473c9c6a2e0682f09fc3dfbcb9c3f36412a7368bcab/coverage-7.13.1-cp312-cp312-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:0642eae483cc8c2902e4af7298bf886d605e80f26382124cddc3967c2a3df09e", size = 251202, upload-time = "2025-12-28T15:41:01.328Z" }, + { url = "https://files.pythonhosted.org/packages/66/3c/f0edf75dcc152f145d5598329e864bbbe04ab78660fe3e8e395f9fff010f/coverage-7.13.1-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:9f5e772ed5fef25b3de9f2008fe67b92d46831bd2bc5bdc5dd6bfd06b83b316f", size = 252566, upload-time = "2025-12-28T15:41:03.319Z" }, + { url = "https://files.pythonhosted.org/packages/17/b3/e64206d3c5f7dcbceafd14941345a754d3dbc78a823a6ed526e23b9cdaab/coverage-7.13.1-cp312-cp312-musllinux_1_2_i686.whl", hash = "sha256:45980ea19277dc0a579e432aef6a504fe098ef3a9032ead15e446eb0f1191aee", size = 250711, upload-time = "2025-12-28T15:41:06.411Z" }, + { url = "https://files.pythonhosted.org/packages/dc/ad/28a3eb970a8ef5b479ee7f0c484a19c34e277479a5b70269dc652b730733/coverage-7.13.1-cp312-cp312-musllinux_1_2_riscv64.whl", hash = "sha256:e4f18eca6028ffa62adbd185a8f1e1dd242f2e68164dba5c2b74a5204850b4cf", size = 250278, upload-time = "2025-12-28T15:41:08.285Z" }, + { url = "https://files.pythonhosted.org/packages/54/e3/c8f0f1a93133e3e1291ca76cbb63565bd4b5c5df63b141f539d747fff348/coverage-7.13.1-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:f8dca5590fec7a89ed6826fce625595279e586ead52e9e958d3237821fbc750c", size = 252154, upload-time = "2025-12-28T15:41:09.969Z" }, + { url = "https://files.pythonhosted.org/packages/d0/bf/9939c5d6859c380e405b19e736321f1c7d402728792f4c752ad1adcce005/coverage-7.13.1-cp312-cp312-win32.whl", hash = "sha256:ff86d4e85188bba72cfb876df3e11fa243439882c55957184af44a35bd5880b7", size = 221487, upload-time = "2025-12-28T15:41:11.468Z" }, + { url = "https://files.pythonhosted.org/packages/fa/dc/7282856a407c621c2aad74021680a01b23010bb8ebf427cf5eacda2e876f/coverage-7.13.1-cp312-cp312-win_amd64.whl", hash = "sha256:16cc1da46c04fb0fb128b4dc430b78fa2aba8a6c0c9f8eb391fd5103409a6ac6", size = 222299, upload-time = "2025-12-28T15:41:13.386Z" }, + { url = "https://files.pythonhosted.org/packages/10/79/176a11203412c350b3e9578620013af35bcdb79b651eb976f4a4b32044fa/coverage-7.13.1-cp312-cp312-win_arm64.whl", hash = "sha256:8d9bc218650022a768f3775dd7fdac1886437325d8d295d923ebcfef4892ad5c", size = 220941, upload-time = "2025-12-28T15:41:14.975Z" }, + { url = "https://files.pythonhosted.org/packages/a3/a4/e98e689347a1ff1a7f67932ab535cef82eb5e78f32a9e4132e114bbb3a0a/coverage-7.13.1-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:cb237bfd0ef4d5eb6a19e29f9e528ac67ac3be932ea6b44fb6cc09b9f3ecff78", size = 218951, upload-time = "2025-12-28T15:41:16.653Z" }, + { url = "https://files.pythonhosted.org/packages/32/33/7cbfe2bdc6e2f03d6b240d23dc45fdaf3fd270aaf2d640be77b7f16989ab/coverage-7.13.1-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:1dcb645d7e34dcbcc96cd7c132b1fc55c39263ca62eb961c064eb3928997363b", size = 219325, upload-time = "2025-12-28T15:41:18.609Z" }, + { url = "https://files.pythonhosted.org/packages/59/f6/efdabdb4929487baeb7cb2a9f7dac457d9356f6ad1b255be283d58b16316/coverage-7.13.1-cp313-cp313-manylinux1_i686.manylinux_2_28_i686.manylinux_2_5_i686.whl", hash = "sha256:3d42df8201e00384736f0df9be2ced39324c3907607d17d50d50116c989d84cd", size = 250309, upload-time = "2025-12-28T15:41:20.629Z" }, + { url = "https://files.pythonhosted.org/packages/12/da/91a52516e9d5aea87d32d1523f9cdcf7a35a3b298e6be05d6509ba3cfab2/coverage-7.13.1-cp313-cp313-manylinux1_x86_64.manylinux_2_28_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:fa3edde1aa8807de1d05934982416cb3ec46d1d4d91e280bcce7cca01c507992", size = 252907, upload-time = "2025-12-28T15:41:22.257Z" }, + { url = "https://files.pythonhosted.org/packages/75/38/f1ea837e3dc1231e086db1638947e00d264e7e8c41aa8ecacf6e1e0c05f4/coverage-7.13.1-cp313-cp313-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:9edd0e01a343766add6817bc448408858ba6b489039eaaa2018474e4001651a4", size = 254148, upload-time = "2025-12-28T15:41:23.87Z" }, + { url = "https://files.pythonhosted.org/packages/7f/43/f4f16b881aaa34954ba446318dea6b9ed5405dd725dd8daac2358eda869a/coverage-7.13.1-cp313-cp313-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:985b7836931d033570b94c94713c6dba5f9d3ff26045f72c3e5dbc5fe3361e5a", size = 250515, upload-time = "2025-12-28T15:41:25.437Z" }, + { url = "https://files.pythonhosted.org/packages/84/34/8cba7f00078bd468ea914134e0144263194ce849ec3baad187ffb6203d1c/coverage-7.13.1-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:ffed1e4980889765c84a5d1a566159e363b71d6b6fbaf0bebc9d3c30bc016766", size = 252292, upload-time = "2025-12-28T15:41:28.459Z" }, + { url = "https://files.pythonhosted.org/packages/8c/a4/cffac66c7652d84ee4ac52d3ccb94c015687d3b513f9db04bfcac2ac800d/coverage-7.13.1-cp313-cp313-musllinux_1_2_i686.whl", hash = "sha256:8842af7f175078456b8b17f1b73a0d16a65dcbdc653ecefeb00a56b3c8c298c4", size = 250242, upload-time = "2025-12-28T15:41:30.02Z" }, + { url = "https://files.pythonhosted.org/packages/f4/78/9a64d462263dde416f3c0067efade7b52b52796f489b1037a95b0dc389c9/coverage-7.13.1-cp313-cp313-musllinux_1_2_riscv64.whl", hash = "sha256:ccd7a6fca48ca9c131d9b0a2972a581e28b13416fc313fb98b6d24a03ce9a398", size = 250068, upload-time = "2025-12-28T15:41:32.007Z" }, + { url = "https://files.pythonhosted.org/packages/69/c8/a8994f5fece06db7c4a97c8fc1973684e178599b42e66280dded0524ef00/coverage-7.13.1-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:0403f647055de2609be776965108447deb8e384fe4a553c119e3ff6bfbab4784", size = 251846, upload-time = "2025-12-28T15:41:33.946Z" }, + { url = "https://files.pythonhosted.org/packages/cc/f7/91fa73c4b80305c86598a2d4e54ba22df6bf7d0d97500944af7ef155d9f7/coverage-7.13.1-cp313-cp313-win32.whl", hash = "sha256:549d195116a1ba1e1ae2f5ca143f9777800f6636eab917d4f02b5310d6d73461", size = 221512, upload-time = "2025-12-28T15:41:35.519Z" }, + { url = "https://files.pythonhosted.org/packages/45/0b/0768b4231d5a044da8f75e097a8714ae1041246bb765d6b5563bab456735/coverage-7.13.1-cp313-cp313-win_amd64.whl", hash = "sha256:5899d28b5276f536fcf840b18b61a9fce23cc3aec1d114c44c07fe94ebeaa500", size = 222321, upload-time = "2025-12-28T15:41:37.371Z" }, + { url = "https://files.pythonhosted.org/packages/9b/b8/bdcb7253b7e85157282450262008f1366aa04663f3e3e4c30436f596c3e2/coverage-7.13.1-cp313-cp313-win_arm64.whl", hash = "sha256:868a2fae76dfb06e87291bcbd4dcbcc778a8500510b618d50496e520bd94d9b9", size = 220949, upload-time = "2025-12-28T15:41:39.553Z" }, + { url = "https://files.pythonhosted.org/packages/70/52/f2be52cc445ff75ea8397948c96c1b4ee14f7f9086ea62fc929c5ae7b717/coverage-7.13.1-cp313-cp313t-macosx_10_13_x86_64.whl", hash = "sha256:67170979de0dacac3f3097d02b0ad188d8edcea44ccc44aaa0550af49150c7dc", size = 219643, upload-time = "2025-12-28T15:41:41.567Z" }, + { url = "https://files.pythonhosted.org/packages/47/79/c85e378eaa239e2edec0c5523f71542c7793fe3340954eafb0bc3904d32d/coverage-7.13.1-cp313-cp313t-macosx_11_0_arm64.whl", hash = "sha256:f80e2bb21bfab56ed7405c2d79d34b5dc0bc96c2c1d2a067b643a09fb756c43a", size = 219997, upload-time = "2025-12-28T15:41:43.418Z" }, + { url = "https://files.pythonhosted.org/packages/fe/9b/b1ade8bfb653c0bbce2d6d6e90cc6c254cbb99b7248531cc76253cb4da6d/coverage-7.13.1-cp313-cp313t-manylinux1_i686.manylinux_2_28_i686.manylinux_2_5_i686.whl", hash = "sha256:f83351e0f7dcdb14d7326c3d8d8c4e915fa685cbfdc6281f9470d97a04e9dfe4", size = 261296, upload-time = "2025-12-28T15:41:45.207Z" }, + { url = "https://files.pythonhosted.org/packages/1f/af/ebf91e3e1a2473d523e87e87fd8581e0aa08741b96265730e2d79ce78d8d/coverage-7.13.1-cp313-cp313t-manylinux1_x86_64.manylinux_2_28_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:bb3f6562e89bad0110afbe64e485aac2462efdce6232cdec7862a095dc3412f6", size = 263363, upload-time = "2025-12-28T15:41:47.163Z" }, + { url = "https://files.pythonhosted.org/packages/c4/8b/fb2423526d446596624ac7fde12ea4262e66f86f5120114c3cfd0bb2befa/coverage-7.13.1-cp313-cp313t-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:77545b5dcda13b70f872c3b5974ac64c21d05e65b1590b441c8560115dc3a0d1", size = 265783, upload-time = "2025-12-28T15:41:49.03Z" }, + { url = "https://files.pythonhosted.org/packages/9b/26/ef2adb1e22674913b89f0fe7490ecadcef4a71fa96f5ced90c60ec358789/coverage-7.13.1-cp313-cp313t-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:a4d240d260a1aed814790bbe1f10a5ff31ce6c21bc78f0da4a1e8268d6c80dbd", size = 260508, upload-time = "2025-12-28T15:41:51.035Z" }, + { url = "https://files.pythonhosted.org/packages/ce/7d/f0f59b3404caf662e7b5346247883887687c074ce67ba453ea08c612b1d5/coverage-7.13.1-cp313-cp313t-musllinux_1_2_aarch64.whl", hash = "sha256:d2287ac9360dec3837bfdad969963a5d073a09a85d898bd86bea82aa8876ef3c", size = 263357, upload-time = "2025-12-28T15:41:52.631Z" }, + { url = "https://files.pythonhosted.org/packages/1a/b1/29896492b0b1a047604d35d6fa804f12818fa30cdad660763a5f3159e158/coverage-7.13.1-cp313-cp313t-musllinux_1_2_i686.whl", hash = "sha256:0d2c11f3ea4db66b5cbded23b20185c35066892c67d80ec4be4bab257b9ad1e0", size = 260978, upload-time = "2025-12-28T15:41:54.589Z" }, + { url = "https://files.pythonhosted.org/packages/48/f2/971de1238a62e6f0a4128d37adadc8bb882ee96afbe03ff1570291754629/coverage-7.13.1-cp313-cp313t-musllinux_1_2_riscv64.whl", hash = "sha256:3fc6a169517ca0d7ca6846c3c5392ef2b9e38896f61d615cb75b9e7134d4ee1e", size = 259877, upload-time = "2025-12-28T15:41:56.263Z" }, + { url = "https://files.pythonhosted.org/packages/6a/fc/0474efcbb590ff8628830e9aaec5f1831594874360e3251f1fdec31d07a3/coverage-7.13.1-cp313-cp313t-musllinux_1_2_x86_64.whl", hash = "sha256:d10a2ed46386e850bb3de503a54f9fe8192e5917fcbb143bfef653a9355e9a53", size = 262069, upload-time = "2025-12-28T15:41:58.093Z" }, + { url = "https://files.pythonhosted.org/packages/88/4f/3c159b7953db37a7b44c0eab8a95c37d1aa4257c47b4602c04022d5cb975/coverage-7.13.1-cp313-cp313t-win32.whl", hash = "sha256:75a6f4aa904301dab8022397a22c0039edc1f51e90b83dbd4464b8a38dc87842", size = 222184, upload-time = "2025-12-28T15:41:59.763Z" }, + { url = "https://files.pythonhosted.org/packages/58/a5/6b57d28f81417f9335774f20679d9d13b9a8fb90cd6160957aa3b54a2379/coverage-7.13.1-cp313-cp313t-win_amd64.whl", hash = "sha256:309ef5706e95e62578cda256b97f5e097916a2c26247c287bbe74794e7150df2", size = 223250, upload-time = "2025-12-28T15:42:01.52Z" }, + { url = "https://files.pythonhosted.org/packages/81/7c/160796f3b035acfbb58be80e02e484548595aa67e16a6345e7910ace0a38/coverage-7.13.1-cp313-cp313t-win_arm64.whl", hash = "sha256:92f980729e79b5d16d221038dbf2e8f9a9136afa072f9d5d6ed4cb984b126a09", size = 221521, upload-time = "2025-12-28T15:42:03.275Z" }, + { url = "https://files.pythonhosted.org/packages/aa/8e/ba0e597560c6563fc0adb902fda6526df5d4aa73bb10adf0574d03bd2206/coverage-7.13.1-cp314-cp314-macosx_10_15_x86_64.whl", hash = "sha256:97ab3647280d458a1f9adb85244e81587505a43c0c7cff851f5116cd2814b894", size = 218996, upload-time = "2025-12-28T15:42:04.978Z" }, + { url = "https://files.pythonhosted.org/packages/6b/8e/764c6e116f4221dc7aa26c4061181ff92edb9c799adae6433d18eeba7a14/coverage-7.13.1-cp314-cp314-macosx_11_0_arm64.whl", hash = "sha256:8f572d989142e0908e6acf57ad1b9b86989ff057c006d13b76c146ec6a20216a", size = 219326, upload-time = "2025-12-28T15:42:06.691Z" }, + { url = "https://files.pythonhosted.org/packages/4f/a6/6130dc6d8da28cdcbb0f2bf8865aeca9b157622f7c0031e48c6cf9a0e591/coverage-7.13.1-cp314-cp314-manylinux1_i686.manylinux_2_28_i686.manylinux_2_5_i686.whl", hash = "sha256:d72140ccf8a147e94274024ff6fd8fb7811354cf7ef88b1f0a988ebaa5bc774f", size = 250374, upload-time = "2025-12-28T15:42:08.786Z" }, + { url = "https://files.pythonhosted.org/packages/82/2b/783ded568f7cd6b677762f780ad338bf4b4750205860c17c25f7c708995e/coverage-7.13.1-cp314-cp314-manylinux1_x86_64.manylinux_2_28_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:d3c9f051b028810f5a87c88e5d6e9af3c0ff32ef62763bf15d29f740453ca909", size = 252882, upload-time = "2025-12-28T15:42:10.515Z" }, + { url = "https://files.pythonhosted.org/packages/cd/b2/9808766d082e6a4d59eb0cc881a57fc1600eb2c5882813eefff8254f71b5/coverage-7.13.1-cp314-cp314-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:f398ba4df52d30b1763f62eed9de5620dcde96e6f491f4c62686736b155aa6e4", size = 254218, upload-time = "2025-12-28T15:42:12.208Z" }, + { url = "https://files.pythonhosted.org/packages/44/ea/52a985bb447c871cb4d2e376e401116520991b597c85afdde1ea9ef54f2c/coverage-7.13.1-cp314-cp314-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:132718176cc723026d201e347f800cd1a9e4b62ccd3f82476950834dad501c75", size = 250391, upload-time = "2025-12-28T15:42:14.21Z" }, + { url = "https://files.pythonhosted.org/packages/7f/1d/125b36cc12310718873cfc8209ecfbc1008f14f4f5fa0662aa608e579353/coverage-7.13.1-cp314-cp314-musllinux_1_2_aarch64.whl", hash = "sha256:9e549d642426e3579b3f4b92d0431543b012dcb6e825c91619d4e93b7363c3f9", size = 252239, upload-time = "2025-12-28T15:42:16.292Z" }, + { url = "https://files.pythonhosted.org/packages/6a/16/10c1c164950cade470107f9f14bbac8485f8fb8515f515fca53d337e4a7f/coverage-7.13.1-cp314-cp314-musllinux_1_2_i686.whl", hash = "sha256:90480b2134999301eea795b3a9dbf606c6fbab1b489150c501da84a959442465", size = 250196, upload-time = "2025-12-28T15:42:18.54Z" }, + { url = "https://files.pythonhosted.org/packages/2a/c6/cd860fac08780c6fd659732f6ced1b40b79c35977c1356344e44d72ba6c4/coverage-7.13.1-cp314-cp314-musllinux_1_2_riscv64.whl", hash = "sha256:e825dbb7f84dfa24663dd75835e7257f8882629fc11f03ecf77d84a75134b864", size = 250008, upload-time = "2025-12-28T15:42:20.365Z" }, + { url = "https://files.pythonhosted.org/packages/f0/3a/a8c58d3d38f82a5711e1e0a67268362af48e1a03df27c03072ac30feefcf/coverage-7.13.1-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:623dcc6d7a7ba450bbdbeedbaa0c42b329bdae16491af2282f12a7e809be7eb9", size = 251671, upload-time = "2025-12-28T15:42:22.114Z" }, + { url = "https://files.pythonhosted.org/packages/f0/bc/fd4c1da651d037a1e3d53e8cb3f8182f4b53271ffa9a95a2e211bacc0349/coverage-7.13.1-cp314-cp314-win32.whl", hash = "sha256:6e73ebb44dca5f708dc871fe0b90cf4cff1a13f9956f747cc87b535a840386f5", size = 221777, upload-time = "2025-12-28T15:42:23.919Z" }, + { url = "https://files.pythonhosted.org/packages/4b/50/71acabdc8948464c17e90b5ffd92358579bd0910732c2a1c9537d7536aa6/coverage-7.13.1-cp314-cp314-win_amd64.whl", hash = "sha256:be753b225d159feb397bd0bf91ae86f689bad0da09d3b301478cd39b878ab31a", size = 222592, upload-time = "2025-12-28T15:42:25.619Z" }, + { url = "https://files.pythonhosted.org/packages/f7/c8/a6fb943081bb0cc926499c7907731a6dc9efc2cbdc76d738c0ab752f1a32/coverage-7.13.1-cp314-cp314-win_arm64.whl", hash = "sha256:228b90f613b25ba0019361e4ab81520b343b622fc657daf7e501c4ed6a2366c0", size = 221169, upload-time = "2025-12-28T15:42:27.629Z" }, + { url = "https://files.pythonhosted.org/packages/16/61/d5b7a0a0e0e40d62e59bc8c7aa1afbd86280d82728ba97f0673b746b78e2/coverage-7.13.1-cp314-cp314t-macosx_10_15_x86_64.whl", hash = "sha256:60cfb538fe9ef86e5b2ab0ca8fc8d62524777f6c611dcaf76dc16fbe9b8e698a", size = 219730, upload-time = "2025-12-28T15:42:29.306Z" }, + { url = "https://files.pythonhosted.org/packages/a3/2c/8881326445fd071bb49514d1ce97d18a46a980712b51fee84f9ab42845b4/coverage-7.13.1-cp314-cp314t-macosx_11_0_arm64.whl", hash = "sha256:57dfc8048c72ba48a8c45e188d811e5efd7e49b387effc8fb17e97936dde5bf6", size = 220001, upload-time = "2025-12-28T15:42:31.319Z" }, + { url = "https://files.pythonhosted.org/packages/b5/d7/50de63af51dfa3a7f91cc37ad8fcc1e244b734232fbc8b9ab0f3c834a5cd/coverage-7.13.1-cp314-cp314t-manylinux1_i686.manylinux_2_28_i686.manylinux_2_5_i686.whl", hash = "sha256:3f2f725aa3e909b3c5fdb8192490bdd8e1495e85906af74fe6e34a2a77ba0673", size = 261370, upload-time = "2025-12-28T15:42:32.992Z" }, + { url = "https://files.pythonhosted.org/packages/e1/2c/d31722f0ec918fd7453b2758312729f645978d212b410cd0f7c2aed88a94/coverage-7.13.1-cp314-cp314t-manylinux1_x86_64.manylinux_2_28_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:9ee68b21909686eeb21dfcba2c3b81fee70dcf38b140dcd5aa70680995fa3aa5", size = 263485, upload-time = "2025-12-28T15:42:34.759Z" }, + { url = "https://files.pythonhosted.org/packages/fa/7a/2c114fa5c5fc08ba0777e4aec4c97e0b4a1afcb69c75f1f54cff78b073ab/coverage-7.13.1-cp314-cp314t-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:724b1b270cb13ea2e6503476e34541a0b1f62280bc997eab443f87790202033d", size = 265890, upload-time = "2025-12-28T15:42:36.517Z" }, + { url = "https://files.pythonhosted.org/packages/65/d9/f0794aa1c74ceabc780fe17f6c338456bbc4e96bd950f2e969f48ac6fb20/coverage-7.13.1-cp314-cp314t-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:916abf1ac5cf7eb16bc540a5bf75c71c43a676f5c52fcb9fe75a2bd75fb944e8", size = 260445, upload-time = "2025-12-28T15:42:38.646Z" }, + { url = "https://files.pythonhosted.org/packages/49/23/184b22a00d9bb97488863ced9454068c79e413cb23f472da6cbddc6cfc52/coverage-7.13.1-cp314-cp314t-musllinux_1_2_aarch64.whl", hash = "sha256:776483fd35b58d8afe3acbd9988d5de592ab6da2d2a865edfdbc9fdb43e7c486", size = 263357, upload-time = "2025-12-28T15:42:40.788Z" }, + { url = "https://files.pythonhosted.org/packages/7d/bd/58af54c0c9199ea4190284f389005779d7daf7bf3ce40dcd2d2b2f96da69/coverage-7.13.1-cp314-cp314t-musllinux_1_2_i686.whl", hash = "sha256:b6f3b96617e9852703f5b633ea01315ca45c77e879584f283c44127f0f1ec564", size = 260959, upload-time = "2025-12-28T15:42:42.808Z" }, + { url = "https://files.pythonhosted.org/packages/4b/2a/6839294e8f78a4891bf1df79d69c536880ba2f970d0ff09e7513d6e352e9/coverage-7.13.1-cp314-cp314t-musllinux_1_2_riscv64.whl", hash = "sha256:bd63e7b74661fed317212fab774e2a648bc4bb09b35f25474f8e3325d2945cd7", size = 259792, upload-time = "2025-12-28T15:42:44.818Z" }, + { url = "https://files.pythonhosted.org/packages/ba/c3/528674d4623283310ad676c5af7414b9850ab6d55c2300e8aa4b945ec554/coverage-7.13.1-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:933082f161bbb3e9f90d00990dc956120f608cdbcaeea15c4d897f56ef4fe416", size = 262123, upload-time = "2025-12-28T15:42:47.108Z" }, + { url = "https://files.pythonhosted.org/packages/06/c5/8c0515692fb4c73ac379d8dc09b18eaf0214ecb76ea6e62467ba7a1556ff/coverage-7.13.1-cp314-cp314t-win32.whl", hash = "sha256:18be793c4c87de2965e1c0f060f03d9e5aff66cfeae8e1dbe6e5b88056ec153f", size = 222562, upload-time = "2025-12-28T15:42:49.144Z" }, + { url = "https://files.pythonhosted.org/packages/05/0e/c0a0c4678cb30dac735811db529b321d7e1c9120b79bd728d4f4d6b010e9/coverage-7.13.1-cp314-cp314t-win_amd64.whl", hash = "sha256:0e42e0ec0cd3e0d851cb3c91f770c9301f48647cb2877cb78f74bdaa07639a79", size = 223670, upload-time = "2025-12-28T15:42:51.218Z" }, + { url = "https://files.pythonhosted.org/packages/f5/5f/b177aa0011f354abf03a8f30a85032686d290fdeed4222b27d36b4372a50/coverage-7.13.1-cp314-cp314t-win_arm64.whl", hash = "sha256:eaecf47ef10c72ece9a2a92118257da87e460e113b83cc0d2905cbbe931792b4", size = 221707, upload-time = "2025-12-28T15:42:53.034Z" }, + { url = "https://files.pythonhosted.org/packages/cc/48/d9f421cb8da5afaa1a64570d9989e00fb7955e6acddc5a12979f7666ef60/coverage-7.13.1-py3-none-any.whl", hash = "sha256:2016745cb3ba554469d02819d78958b571792bb68e31302610e898f80dd3a573", size = 210722, upload-time = "2025-12-28T15:42:54.901Z" }, +] + +[package.optional-dependencies] +toml = [ + { name = "tomli", marker = "python_full_version <= '3.11'" }, +] + +[[package]] +name = "detect-installer" +version = "0.1.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/5f/ce/6897d812825e9d4c53e3c7112726e800cc5231b013b2223bf64f653ff362/detect_installer-0.1.0.tar.gz", hash = "sha256:00ad7ba0a36e3cf7d08a40d3643011746dbc112597c7d475cc91c416710ca4e7", size = 3049, upload-time = "2026-02-23T10:40:22.567Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/cc/34/8cc73273414405086c58852916e4031812a6a30fe04c057e37ad99397b7f/detect_installer-0.1.0-py3-none-any.whl", hash = "sha256:034fb20fd665c36e6ba52b8821525ea07fb4f7f938cac459df889fb33801528a", size = 4539, upload-time = "2026-02-23T10:40:23.807Z" }, +] + +[[package]] +name = "dnspython" +version = "2.8.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/8c/8b/57666417c0f90f08bcafa776861060426765fdb422eb10212086fb811d26/dnspython-2.8.0.tar.gz", hash = "sha256:181d3c6996452cb1189c4046c61599b84a5a86e099562ffde77d26984ff26d0f", size = 368251, upload-time = "2025-09-07T18:58:00.022Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/ba/5a/18ad964b0086c6e62e2e7500f7edc89e3faa45033c71c1893d34eed2b2de/dnspython-2.8.0-py3-none-any.whl", hash = "sha256:01d9bbc4a2d76bf0db7c1f729812ded6d912bd318d3b1cf81d30c0f845dbf3af", size = 331094, upload-time = "2025-09-07T18:57:58.071Z" }, +] + +[[package]] +name = "email-validator" +version = "2.3.0" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "dnspython" }, + { name = "idna" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/f5/22/900cb125c76b7aaa450ce02fd727f452243f2e91a61af068b40adba60ea9/email_validator-2.3.0.tar.gz", hash = "sha256:9fc05c37f2f6cf439ff414f8fc46d917929974a82244c20eb10231ba60c54426", size = 51238, upload-time = "2025-08-26T13:09:06.831Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/de/15/545e2b6cf2e3be84bc1ed85613edd75b8aea69807a71c26f4ca6a9258e82/email_validator-2.3.0-py3-none-any.whl", hash = "sha256:80f13f623413e6b197ae73bb10bf4eb0908faf509ad8362c5edeb0be7fd450b4", size = 35604, upload-time = "2025-08-26T13:09:05.858Z" }, +] + +[[package]] +name = "exceptiongroup" +version = "1.3.1" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "typing-extensions", marker = "python_full_version < '3.13'" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/50/79/66800aadf48771f6b62f7eb014e352e5d06856655206165d775e675a02c9/exceptiongroup-1.3.1.tar.gz", hash = "sha256:8b412432c6055b0b7d14c310000ae93352ed6754f70fa8f7c34141f91c4e3219", size = 30371, upload-time = "2025-11-21T23:01:54.787Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/8a/0e/97c33bf5009bdbac74fd2beace167cab3f978feb69cc36f1ef79360d6c4e/exceptiongroup-1.3.1-py3-none-any.whl", hash = "sha256:a7a39a3bd276781e98394987d3a5701d0c4edffb633bb7a5144577f82c773598", size = 16740, upload-time = "2025-11-21T23:01:53.443Z" }, +] + +[[package]] +name = "fastapi" +version = "0.139.0" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "annotated-doc" }, + { name = "pydantic" }, + { name = "starlette" }, + { name = "typing-extensions" }, + { name = "typing-inspection" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/d3/af/a5f50ccfa659ec1802cb4ca842c23f06d906a8cc9aef6016a2caeea3d4ed/fastapi-0.139.0.tar.gz", hash = "sha256:99ab7b2d92223c76d6cf10757ab3f89d45b38267fc20b2a136cf02f6beac3145", size = 423016, upload-time = "2026-07-01T16:35:33.436Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/9e/7c/8e3c6ad324ea5cb36604fc3f968554887891c316d9dfde57761611d907ad/fastapi-0.139.0-py3-none-any.whl", hash = "sha256:cf15e1e9e667ddb0ad63811e60bd11390d1aac838ca4a7a23f421807b2308189", size = 130339, upload-time = "2026-07-01T16:35:32.19Z" }, +] + +[[package]] +name = "fastapi-cli" +source = { editable = "." } +dependencies = [ + { name = "rich-toolkit" }, + { name = "tomli", marker = "python_full_version < '3.11'" }, + { name = "typer" }, + { name = "uvicorn", extra = ["standard"] }, +] + +[package.optional-dependencies] +new = [ + { name = "fastapi-new" }, +] +standard = [ + { name = "fastapi-cloud-cli" }, + { name = "uvicorn", extra = ["standard"] }, +] +standard-no-fastapi-cloud-cli = [ + { name = "uvicorn", extra = ["standard"] }, +] + +[package.dev-dependencies] +dev = [ + { name = "coverage", extra = ["toml"] }, + { name = "fastapi" }, + { name = "mypy" }, + { name = "prek" }, + { name = "pytest" }, + { name = "ruff" }, + { name = "ty" }, + { name = "uvicorn" }, + { name = "zizmor" }, +] +github-actions = [ + { name = "smokeshow" }, +] +tests = [ + { name = "coverage", extra = ["toml"] }, + { name = "fastapi" }, + { name = "mypy" }, + { name = "pytest" }, + { name = "ruff" }, + { name = "ty" }, + { name = "uvicorn" }, +] + +[package.metadata] +requires-dist = [ + { name = "fastapi-cloud-cli", marker = "extra == 'standard'", specifier = ">=0.1.1" }, + { name = "fastapi-new", marker = "extra == 'new'", specifier = ">=0.0.2" }, + { name = "rich-toolkit", specifier = ">=0.14.8" }, + { name = "tomli", marker = "python_full_version < '3.11'", specifier = ">=2.0.0" }, + { name = "typer", specifier = ">=0.16.0" }, + { name = "uvicorn", extras = ["standard"], specifier = ">=0.15.0" }, + { name = "uvicorn", extras = ["standard"], marker = "extra == 'standard'", specifier = ">=0.15.0" }, + { name = "uvicorn", extras = ["standard"], marker = "extra == 'standard-no-fastapi-cloud-cli'", specifier = ">=0.15.0" }, +] +provides-extras = ["standard", "standard-no-fastapi-cloud-cli", "new"] + +[package.metadata.requires-dev] +dev = [ + { name = "coverage", extras = ["toml"], specifier = ">=6.2,<8.0" }, + { name = "fastapi", specifier = ">=0.128.0" }, + { name = "mypy", specifier = "==2.1.0" }, + { name = "prek", specifier = ">=0.2.24,<1.0.0" }, + { name = "pytest", specifier = ">=7.4.0,<10.0.0" }, + { name = "ruff", specifier = ">=0.15.15" }, + { name = "ty", specifier = ">=0.0.25" }, + { name = "uvicorn", specifier = ">=0.39.0" }, + { name = "zizmor", specifier = ">=1.24.1" }, +] +github-actions = [{ name = "smokeshow", specifier = ">=0.5.0" }] +tests = [ + { name = "coverage", extras = ["toml"], specifier = ">=6.2,<8.0" }, + { name = "fastapi", specifier = ">=0.128.0" }, + { name = "mypy", specifier = "==2.1.0" }, + { name = "pytest", specifier = ">=7.4.0,<10.0.0" }, + { name = "ruff", specifier = ">=0.15.15" }, + { name = "ty", specifier = ">=0.0.25" }, + { name = "uvicorn", specifier = ">=0.39.0" }, +] + +[[package]] +name = "fastapi-cloud-cli" +version = "0.22.1" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "detect-installer" }, + { name = "fastar" }, + { name = "httpx" }, + { name = "pydantic", extra = ["email"] }, + { name = "rich-toolkit" }, + { name = "rignore" }, + { name = "sentry-sdk" }, + { name = "typer" }, + { name = "uvicorn", extra = ["standard"] }, +] +sdist = { url = "https://files.pythonhosted.org/packages/0b/e5/bee77aa542ec66bcc55b458d606f3356a58f5bb9f2c59006f6ff53a3869b/fastapi_cloud_cli-0.22.1.tar.gz", hash = "sha256:50d80de6ce397a4959e6f3509574edac65d0a6998655215c95d077b18ec2f4b1", size = 94501, upload-time = "2026-07-01T22:09:03.358Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/75/0c/9e3069ff571142e571d68156aa4c1ab1d51c0b87f21bbe0f44c10029b19b/fastapi_cloud_cli-0.22.1-py3-none-any.whl", hash = "sha256:4ba307b97b08282d1efb2daef5f1e88af23e02fe2286c25273c1794e399df509", size = 77739, upload-time = "2026-07-01T22:09:02.359Z" }, +] + +[[package]] +name = "fastapi-new" +version = "0.0.7" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "rich" }, + { name = "rich-toolkit" }, + { name = "typer" }, + { name = "typing-extensions" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/6f/a9/eecf3d1fa3bb6626b938d2636d3bd85cad422a81e16e24bcf41e38e55598/fastapi_new-0.0.7.tar.gz", hash = "sha256:d0966ec765158afad64468a9cd719721b9671a04adff6ff28fcc3206bfdb79ce", size = 11427, upload-time = "2026-06-19T17:56:23.023Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/ca/91/97041e368745274e2e2c2facb3aec8f291acda8bb1fae71282bb08f63a10/fastapi_new-0.0.7-py3-none-any.whl", hash = "sha256:475e6035a20874b7d877644aef04daa4c2312006af2173fd4929b84809e34cc1", size = 6792, upload-time = "2026-06-19T17:56:22.106Z" }, +] + +[[package]] +name = "fastar" +version = "0.10.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/5a/8a/841a8fea5d704ed19836a1f7f83fe2b2d95624a14e9ddf45823ffb518c98/fastar-0.10.0.tar.gz", hash = "sha256:cba4452d6a33894faf5b0b9d55342a1259ad5c94cbdb16af09346084e0787680", size = 70357, upload-time = "2026-04-08T01:02:01.507Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/9e/f0/2f5150407fdb68f5939222a68cca72feb4d377f6981b26375af93fb39cd2/fastar-0.10.0-cp310-cp310-macosx_10_12_x86_64.whl", hash = "sha256:8a3be1e0157e1a7f3905c479e709d51f991e2c8735eeb5fe04a6d74905838dd7", size = 710441, upload-time = "2026-04-08T01:00:57.5Z" }, + { url = "https://files.pythonhosted.org/packages/3a/09/60acfcf97cc55440b7471e12e7014fe50eb7fec12e4a64ab442d477bac8e/fastar-0.10.0-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:cc498ae63d7251d7d1b22c4b4b6912cd5da8424a0d14c8f2592db9586b890f9a", size = 630694, upload-time = "2026-04-08T01:00:45.281Z" }, + { url = "https://files.pythonhosted.org/packages/fe/96/ad4999288f4e6b6aa9132f73a34b31b5890eb2f9b5992cd88bd7a419508b/fastar-0.10.0-cp310-cp310-manylinux_2_12_i686.manylinux2010_i686.whl", hash = "sha256:c86ca02ed5841df9361c3c7d08cd7d53535b63a380ac03e2990c330bae327814", size = 870982, upload-time = "2026-04-08T01:00:20.053Z" }, + { url = "https://files.pythonhosted.org/packages/37/b1/4e7c5693900539ee855590baf644a93b256f09a45fbf411ce224d3c5aef4/fastar-0.10.0-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:e5f866cd59cec022ef083da91e6e5e81190b298852a5e59af91744a29c562397", size = 763022, upload-time = "2026-04-08T00:59:17.195Z" }, + { url = "https://files.pythonhosted.org/packages/e8/29/68fce7b00077b32c5a1f3ce23823aeaf8ac359f6b534ed71a64286a78c4b/fastar-0.10.0-cp310-cp310-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:b4730e7a7b1dbdb6287b4a1c96c4e2bbb93a3d89adb2aafd406611b38d359565", size = 760858, upload-time = "2026-04-08T00:59:29.51Z" }, + { url = "https://files.pythonhosted.org/packages/a6/d6/6ede1c241b0f7d51ac5b12666f8567765ad193a4ee1282d55ae3c294d2e2/fastar-0.10.0-cp310-cp310-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:87fb62d380bbc8fd366158dc42261ddb688ac118dea9baca31e87af2d7be6199", size = 926489, upload-time = "2026-04-08T00:59:41.655Z" }, + { url = "https://files.pythonhosted.org/packages/7c/19/b7b4c4e9feb717af28e573930aefd041e6405a4a9fe8db0a23505c8174f6/fastar-0.10.0-cp310-cp310-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:35c167456bab6e07e3ba1f9760e3c6e86429eb0c3c1726a25b48b37b35be67b7", size = 819332, upload-time = "2026-04-08T01:00:06.248Z" }, + { url = "https://files.pythonhosted.org/packages/42/78/79d86ccbcb269ce23334df74b4c60147df06bf2bac62e01087f34c7b86e4/fastar-0.10.0-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:807f94b12a90b0f4083ef99a2e879e21c011e165e7bff226095c9c95b93a11dd", size = 823076, upload-time = "2026-04-08T01:00:32.074Z" }, + { url = "https://files.pythonhosted.org/packages/0d/39/6382b996a1ab71fb1ae5b6967e83c49d47300c8bc754ab224cccce61d2c1/fastar-0.10.0-cp310-cp310-manylinux_2_31_riscv64.whl", hash = "sha256:12681e50ec1b903c43e96f4fa895dce6a13c0c2b2cf53d7ff31f8a95e85dfbb2", size = 887427, upload-time = "2026-04-08T00:59:53.568Z" }, + { url = "https://files.pythonhosted.org/packages/13/21/3080446c3167319c49bb6ea052d319116be79688242b9dc9b5fac569ab66/fastar-0.10.0-cp310-cp310-musllinux_1_2_aarch64.whl", hash = "sha256:589962dda82edf899344e9c11c3cd3a5ee89a45e65774a132e55342bf73c6daf", size = 971036, upload-time = "2026-04-08T01:01:09.925Z" }, + { url = "https://files.pythonhosted.org/packages/1a/53/cda958a555597af3babc64bfb7abbd55b9533eb0e05cc18f7bfcb883e2a8/fastar-0.10.0-cp310-cp310-musllinux_1_2_armv7l.whl", hash = "sha256:efcb19e54b0fe3ad9bf3d510b60fa7ba743004c650db95ad0780f2a21932de39", size = 1037917, upload-time = "2026-04-08T01:01:22.758Z" }, + { url = "https://files.pythonhosted.org/packages/bf/7e/ef12a153800a30e927a646270a2b0d22d6970b463c631e9bc1d83ae29056/fastar-0.10.0-cp310-cp310-musllinux_1_2_i686.whl", hash = "sha256:a0213bf114fac483764055cfae401b9649868a77cd12ee2d0e55942ccee95c2f", size = 1078973, upload-time = "2026-04-08T01:01:35.896Z" }, + { url = "https://files.pythonhosted.org/packages/aa/c9/8ede5750e2504a9aef6a5cb359824df668e43e3523a3893ad902e766a99f/fastar-0.10.0-cp310-cp310-musllinux_1_2_x86_64.whl", hash = "sha256:dae7717bdbdd84f2ec86aa4d27e549f969dda9c3aa4e7142c49d621e06929831", size = 1029808, upload-time = "2026-04-08T01:01:48.824Z" }, + { url = "https://files.pythonhosted.org/packages/03/83/7ac50407608dee7513adc455aa68c01185344943919c61f2b3616502c752/fastar-0.10.0-cp310-cp310-win32.whl", hash = "sha256:00d8b7ed32a027bcdaa2de65028f733cca8fe13260203fb083cfa3471dc3b20f", size = 458540, upload-time = "2026-04-08T01:02:21.967Z" }, + { url = "https://files.pythonhosted.org/packages/60/5d/bdffe38aead14b2800cbe5d534082c4115cebabb07d83ac14e6ea4d1bd26/fastar-0.10.0-cp310-cp310-win_amd64.whl", hash = "sha256:95de318566bddb7bde5f8f5975d54bbb7e0ea4c6c459f4b6cf6e33cda76ff852", size = 488254, upload-time = "2026-04-08T01:02:09.639Z" }, + { url = "https://files.pythonhosted.org/packages/58/19/d55ec2c1772970783e56bba92356bb167f51bb8a926f41c90d48fdd9834d/fastar-0.10.0-cp311-cp311-macosx_10_12_x86_64.whl", hash = "sha256:6e0a3c95b301f4b3f3ed36f1c856f097c7f7efb7f940361903da6304a7fa32a5", size = 710429, upload-time = "2026-04-08T01:00:58.794Z" }, + { url = "https://files.pythonhosted.org/packages/84/20/ad0162167a9af1fbeff4a87f98ae5e626cd005247e30461a8a16a6206f79/fastar-0.10.0-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:de5929174e285fc97e6caae6d14e49871404df70d3a96e55994bf4e63df9dc33", size = 630135, upload-time = "2026-04-08T01:00:46.974Z" }, + { url = "https://files.pythonhosted.org/packages/fe/05/2ac36459dfefda8377448a0fbaa6153d43aba7e910ef8ea4b1c783b9c6b2/fastar-0.10.0-cp311-cp311-manylinux_2_12_i686.manylinux2010_i686.whl", hash = "sha256:fe6e816634e2c76fdc759c07398958a061d3b43db3953c0077d444a631788830", size = 870975, upload-time = "2026-04-08T01:00:21.567Z" }, + { url = "https://files.pythonhosted.org/packages/8a/d9/16cded9c396c2f2444c018ba8629b71eb34ef0efde316da7a40b60d03e1d/fastar-0.10.0-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:a1201487ddc0e3b7ac2db2bee69faaf1eee0240085b0b951b4f008b62e26bcef", size = 762608, upload-time = "2026-04-08T00:59:19.084Z" }, + { url = "https://files.pythonhosted.org/packages/3e/58/2739d815ad2d16166662c8b0bb1bad43876a112171c956630c48934c3728/fastar-0.10.0-cp311-cp311-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:e96fae564de42e7b0ef7aefb6d237f262b3efd600dc8c3849c11a4eb12951239", size = 760715, upload-time = "2026-04-08T00:59:31.232Z" }, + { url = "https://files.pythonhosted.org/packages/dc/bd/70bb27c29c995b6db1dad47cc12e70106f12cf9d95c78b1415e1773736b5/fastar-0.10.0-cp311-cp311-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:605abd4096422930127e686e4a4a6baae60d62690b6b75e6158fb2b811649c53", size = 926704, upload-time = "2026-04-08T00:59:42.952Z" }, + { url = "https://files.pythonhosted.org/packages/a4/aa/6b08f4d29ca05a3f48369923a6197fe2a72c9380f8189175519543c44cd0/fastar-0.10.0-cp311-cp311-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:fa547adf0917089560ca7e4639eb8b506ed3b7c8dad0540481531e1b3c90e2b3", size = 819010, upload-time = "2026-04-08T01:00:07.601Z" }, + { url = "https://files.pythonhosted.org/packages/be/cf/0469d047c241b7f86581522e9306f0841dd37a581242f03646f4686ba526/fastar-0.10.0-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:fae04deb3b0ae1f44d594895da21b1a6c68b5dff9baa3f2a4f9d05f0621bf595", size = 823096, upload-time = "2026-04-08T01:00:33.523Z" }, + { url = "https://files.pythonhosted.org/packages/a3/0d/d8fd5e78a6f9248b4613472263adebf2bc6dda783321923f1be373c5d046/fastar-0.10.0-cp311-cp311-manylinux_2_31_riscv64.whl", hash = "sha256:250d34c8c187de6bbacd30568c560ce9139284b10fde43f6a46897f2d4877f10", size = 887433, upload-time = "2026-04-08T00:59:54.68Z" }, + { url = "https://files.pythonhosted.org/packages/41/1a/ba60f85371bd8bc720c0c27272682e7dd4321e8110e414a5013229f0f7ac/fastar-0.10.0-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:9f4c7e59c9da206951f27e5fcbbf06bc2f403af0a4d57eca62df0b01fdfdd83f", size = 970681, upload-time = "2026-04-08T01:01:11.261Z" }, + { url = "https://files.pythonhosted.org/packages/68/28/1847c5ee218d376e7af5e4cc1839b4c60047acd55980b1ea636d9be484d2/fastar-0.10.0-cp311-cp311-musllinux_1_2_armv7l.whl", hash = "sha256:f2b8ab7ce9e16e139715b232a50123061707c7ef4257048bf6be218d9558dcb9", size = 1037729, upload-time = "2026-04-08T01:01:24.085Z" }, + { url = "https://files.pythonhosted.org/packages/06/a9/c453e387254ecacabc00889fa21a885e9f97ef8c2678d0b3a479b176718f/fastar-0.10.0-cp311-cp311-musllinux_1_2_i686.whl", hash = "sha256:c579af39ae48f67a7c021eaaead03a1a2bfe9549afaed1ada8e605bc439c3262", size = 1078884, upload-time = "2026-04-08T01:01:37.213Z" }, + { url = "https://files.pythonhosted.org/packages/a8/96/f0d1a53a78b7adce62a86ef624d96f6dd3904530cf3f2dbe725d0ec4b50d/fastar-0.10.0-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:eb3d4d1975f486ddcbcd820f94d686e74937ddf4805a8d7dce5de45eb476a7c6", size = 1029822, upload-time = "2026-04-08T01:01:50.197Z" }, + { url = "https://files.pythonhosted.org/packages/d0/42/3a4e121bd804bd6d8c1620e543dff711738165c2ff312f729135aa162a0c/fastar-0.10.0-cp311-cp311-win32.whl", hash = "sha256:3d6972a216ef121a2897a4248f21cf07786680ba6b1f1ed0550c6f8bb0686771", size = 456962, upload-time = "2026-04-08T01:02:23.278Z" }, + { url = "https://files.pythonhosted.org/packages/11/0f/c5d27e7cb8131c49986b41600882b07a8b2a9eb71c3d0b9a3c70dd7f4137/fastar-0.10.0-cp311-cp311-win_amd64.whl", hash = "sha256:fe5247416625f6c60e5e8200478b71629e4037d0deffdd922e55f11c7101d6f3", size = 488375, upload-time = "2026-04-08T01:02:10.978Z" }, + { url = "https://files.pythonhosted.org/packages/fa/9d/3b78ea6d973eb8c73f349b2c5f7211c60baf50f91df4115a832fb29f15b4/fastar-0.10.0-cp311-cp311-win_arm64.whl", hash = "sha256:a5a74d9eeef1a163bd360bdf24150df94aaa361234178730aa9a13fe2ab58224", size = 464156, upload-time = "2026-04-08T01:02:02.523Z" }, + { url = "https://files.pythonhosted.org/packages/a0/01/59c22fe38edc439bea9256f368eb367f252dcd943ef7178db3c4cfe8d99e/fastar-0.10.0-cp312-cp312-macosx_10_12_x86_64.whl", hash = "sha256:c3f42416208280e3c74ecdcc81f97bdab8729aeee46d1cb8f591e0c30de1d4c8", size = 708604, upload-time = "2026-04-08T01:01:00.067Z" }, + { url = "https://files.pythonhosted.org/packages/d9/90/9a654b29515d85446df6db23b7cb26a6ae05ccbdcb9bf469f312578958cf/fastar-0.10.0-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:5698f70e46ef7bc86bb414865e832631e2d7d0543c93461c785a52775a13808c", size = 627857, upload-time = "2026-04-08T01:00:48.282Z" }, + { url = "https://files.pythonhosted.org/packages/6e/dd/bc0deb3c8fc1966f074725e4f44bf6573a4f1de8e3b7d77e08371ebeb0ea/fastar-0.10.0-cp312-cp312-manylinux_2_12_i686.manylinux2010_i686.whl", hash = "sha256:e0df3df848fe78657f9f9b40a811606cae34aa45ad79cd51f26d6f048f0d4ae1", size = 866216, upload-time = "2026-04-08T01:00:23.092Z" }, + { url = "https://files.pythonhosted.org/packages/97/3c/45023b3538b0eb34d0ac04b6bd4dc707c1480a48e88af5365d7be7448334/fastar-0.10.0-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:a453abf99af0f42bb03db90f9bd4aa69b5a7b88d50841577d428ec51f206856f", size = 761054, upload-time = "2026-04-08T00:59:20.36Z" }, + { url = "https://files.pythonhosted.org/packages/69/07/23294498fceda38c3472f2c24a6aee1478991f1fd1982392bca6345af3ae/fastar-0.10.0-cp312-cp312-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:c6a3e7acc58377de02ff3e8937d4b7e09b1270c294a0d5a0d3c2614aee69058e", size = 758885, upload-time = "2026-04-08T00:59:32.486Z" }, + { url = "https://files.pythonhosted.org/packages/19/89/1e0b3b5ef774deb0937bfeb93d2d21147a1db7a8d741ea63903b1f5d7cd6/fastar-0.10.0-cp312-cp312-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:50a4a5fcd001f289fe66cbcff0aaf9e081532253cd7427270734988b22db6136", size = 924750, upload-time = "2026-04-08T00:59:44.41Z" }, + { url = "https://files.pythonhosted.org/packages/b1/85/486c640b768f9f6524d9cebd32e84808070136fea5696884b946bf63ecbb/fastar-0.10.0-cp312-cp312-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:54f60b5a87a2884efa8fc51978989e58cb1dc0ec1f645629491cd12f1dd5bb77", size = 817365, upload-time = "2026-04-08T01:00:09.616Z" }, + { url = "https://files.pythonhosted.org/packages/f3/4b/271ac7f9067ab39cffe95f2349604ac2248906be6fd86a70abb3c9f3d8bb/fastar-0.10.0-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:edaa085c8555620ec24aac1663251d62bdece619fcf6a4ad9dc2389a5fa13220", size = 819348, upload-time = "2026-04-08T01:00:35.083Z" }, + { url = "https://files.pythonhosted.org/packages/4b/fc/ca87c6fee7eaad484711f8dca44c792e4dc0f2d3f4548c93939b06bdc7eb/fastar-0.10.0-cp312-cp312-manylinux_2_31_riscv64.whl", hash = "sha256:4110f5a357ea88fa35f27021cf30c26d863a5b589d6ac9e4e854ed02b34c9f35", size = 885868, upload-time = "2026-04-08T00:59:56.124Z" }, + { url = "https://files.pythonhosted.org/packages/2f/00/588f0960ab1b36978d75a91bd44d9be9072c05211b04f224adcff9e83285/fastar-0.10.0-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:efa48b89ca2c8496f7fa0d36162e12d7476c597d0bae4d8fc42f86b958bd8fea", size = 968860, upload-time = "2026-04-08T01:01:12.557Z" }, + { url = "https://files.pythonhosted.org/packages/f4/4f/e07b9d82a58c27a8018d098b3ed51f561732c17fa6643c317bfba2907bdc/fastar-0.10.0-cp312-cp312-musllinux_1_2_armv7l.whl", hash = "sha256:2637a20a69ea34455aa53cca8340273166bba8bd5c06727ea64ec151ba56abe0", size = 1036445, upload-time = "2026-04-08T01:01:25.512Z" }, + { url = "https://files.pythonhosted.org/packages/19/6e/de7934cea77c9938ecad2443b114cfee13a760534bb88279a0701b12fac3/fastar-0.10.0-cp312-cp312-musllinux_1_2_i686.whl", hash = "sha256:e9ea5e45a1dd85c3104273b4b1628112f6a09115ed95dc0d31595097ce278fb2", size = 1074104, upload-time = "2026-04-08T01:01:38.464Z" }, + { url = "https://files.pythonhosted.org/packages/7e/8d/54d56acbe2bbab3efbf2c1b93ea709e0cd78b7ff9d42b4038f520a580009/fastar-0.10.0-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:68d70adc24b9f4cf4520ed60dbd9fb60a6eb22bb96fd6756bcb387616cb2a979", size = 1026288, upload-time = "2026-04-08T01:01:51.658Z" }, + { url = "https://files.pythonhosted.org/packages/94/6f/593bc59ec9306859c1481b5ebbda563f13366211490aa1a553861968c33f/fastar-0.10.0-cp312-cp312-win32.whl", hash = "sha256:eb87010b1cb84674feffcc588b4febbf9def4008213346ae2630eda14611deb9", size = 455195, upload-time = "2026-04-08T01:02:24.597Z" }, + { url = "https://files.pythonhosted.org/packages/df/fc/5f6c85db7a59ae9742dec30ea3ec0c4f6522890420e7fea60e8db471aadf/fastar-0.10.0-cp312-cp312-win_amd64.whl", hash = "sha256:3b70d6a8c641bc658cf3d9f6f406841e43f571c8a4fd97ca3b3df98464af6217", size = 486724, upload-time = "2026-04-08T01:02:12.654Z" }, + { url = "https://files.pythonhosted.org/packages/aa/56/f6ef9a47e7008457bdf2718fbae20f692f1f936e58ead6f61e355d3d0714/fastar-0.10.0-cp312-cp312-win_arm64.whl", hash = "sha256:70d7de8e9fd117db28f6fc6334f53786bf5f144e6eefdb86ca56098eb321608e", size = 462462, upload-time = "2026-04-08T01:02:04.21Z" }, + { url = "https://files.pythonhosted.org/packages/89/56/92d0cf82a87431854957ce678de3552f0eb4073a9c1a64a7d4e719915a11/fastar-0.10.0-cp313-cp313-macosx_10_12_x86_64.whl", hash = "sha256:ecbfbe096b40a45395c36e59100d24d26cd5b8b4193a2b4d232963196ec90670", size = 708160, upload-time = "2026-04-08T01:01:01.411Z" }, + { url = "https://files.pythonhosted.org/packages/78/1f/af0dde5242bb2cdc08081f364a9589b8f7ddb2bd5a8c5697e4cbab7c06cc/fastar-0.10.0-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:1ddfa1f9d7d7cc0cf6d86b94c0281e6b5ac1d61fdcab3c86c1bdb7204a114b10", size = 627884, upload-time = "2026-04-08T01:00:49.56Z" }, + { url = "https://files.pythonhosted.org/packages/ea/e1/1ad761f48331593eabe7ce10b0f68a09a2b5f55beace3057cf8fe3f0fafa/fastar-0.10.0-cp313-cp313-manylinux_2_12_i686.manylinux2010_i686.whl", hash = "sha256:0d81b83e42fc97b8e75bfd8df2be1878199c482a5b5633b80bce80cb740eb3f9", size = 865599, upload-time = "2026-04-08T01:00:24.384Z" }, + { url = "https://files.pythonhosted.org/packages/ec/fb/75bffcaa81da72e7e12e656a69c564dfb87ea8ca6fa9ab9c6f5c396ebaeb/fastar-0.10.0-cp313-cp313-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:3ec47f63e53ee3a9e117eeb18cbf4a14b3052e64bdc7ed4cdb812da741557547", size = 760975, upload-time = "2026-04-08T00:59:21.504Z" }, + { url = "https://files.pythonhosted.org/packages/66/36/3f22fc6c248b80676c1d230159313192dbcdf7fb45c3ad167036465733fe/fastar-0.10.0-cp313-cp313-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:3a6abbd746ce3f6012c7e5d25a1193edb437dba3793337a9d5cdf7eafdc9d6e6", size = 757834, upload-time = "2026-04-08T00:59:34.034Z" }, + { url = "https://files.pythonhosted.org/packages/d3/25/76cb9ba8392a00b81c27b85f87cc9d61d713b2ac96981507ca01bba80b9f/fastar-0.10.0-cp313-cp313-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:26efe8b1d4c3c343befd10514216953d47f4e5d69274f2af2e38c22149728717", size = 923080, upload-time = "2026-04-08T00:59:45.592Z" }, + { url = "https://files.pythonhosted.org/packages/90/5e/4f1526deb1c2baa6f7e7973e354562d91da8159da445709c19a277447e4a/fastar-0.10.0-cp313-cp313-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:bb21af50dcaed47350f2299627f350999b672a971ae17a963c10b5754425a645", size = 816582, upload-time = "2026-04-08T01:00:11.464Z" }, + { url = "https://files.pythonhosted.org/packages/88/2b/475e09dc60824baefd55ee752f8b5b4faf2be9b9f2d3309f9a85529d5ab3/fastar-0.10.0-cp313-cp313-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:2dc9e8453af9f36bb7a56bd666020e9539dbda715192543373c2edc3cc16f0a3", size = 819304, upload-time = "2026-04-08T01:00:36.383Z" }, + { url = "https://files.pythonhosted.org/packages/f6/5c/221659f40c819e995fb5d8c823ee9890790b705b2d37701fd0a6cb9dee16/fastar-0.10.0-cp313-cp313-manylinux_2_31_riscv64.whl", hash = "sha256:b3cb3b95106aa355e6a97665c3e97d3886ab36aa8165aeb7d4812964af79ed0a", size = 885014, upload-time = "2026-04-08T00:59:57.614Z" }, + { url = "https://files.pythonhosted.org/packages/b7/58/0e62784e9383ac940dfd31df8d2982a95e9fbd0d2c511fbd6ec9d402b97d/fastar-0.10.0-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:4afa2628ef97316ad00b54a2d09042b0c0944d269d7006fc26dfef951a7f23a1", size = 968599, upload-time = "2026-04-08T01:01:13.884Z" }, + { url = "https://files.pythonhosted.org/packages/3f/fb/2abfd1aed679534ef99929e851c6ca83d88783d22d941fd41ce02707ea92/fastar-0.10.0-cp313-cp313-musllinux_1_2_armv7l.whl", hash = "sha256:1627e03e17b51e59c4f242a5600e850d35707edf6f82a048dd34bf9578d9fbb8", size = 1035271, upload-time = "2026-04-08T01:01:26.954Z" }, + { url = "https://files.pythonhosted.org/packages/94/34/2f0a8f89a240a763d0cb6104df5d44013754a58150b201303c5135a4ce02/fastar-0.10.0-cp313-cp313-musllinux_1_2_i686.whl", hash = "sha256:17b7dbb8b8b563569794ebd79e3058ffd6d1cec1e187c7af0cf5947c189fc50b", size = 1073373, upload-time = "2026-04-08T01:01:39.838Z" }, + { url = "https://files.pythonhosted.org/packages/75/9a/44b9b1a9dec721d229a57646d7c5c160dbb1975972c2d3935ddd93cd8a12/fastar-0.10.0-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:1762dcf52a145b9e6f7a4b5b1b17dd36af2607416a3f26c4632983fc5ae84526", size = 1026086, upload-time = "2026-04-08T01:01:53.298Z" }, + { url = "https://files.pythonhosted.org/packages/5f/79/f024615d2aeefa2d1bd97f09f9ba07e8d8c16702741d8ae621a1dc47b36c/fastar-0.10.0-cp313-cp313-win32.whl", hash = "sha256:85b1f6d78a9baf4995ce27d03b1614061c5abf483043152e9af7d8ddbd86d166", size = 454752, upload-time = "2026-04-08T01:02:25.826Z" }, + { url = "https://files.pythonhosted.org/packages/a3/e1/d74fa86ded5032c8649acf78a7079c6ec26929adb78659c730dc00fa8c41/fastar-0.10.0-cp313-cp313-win_amd64.whl", hash = "sha256:fd700ecfc4429f8e9ba968e752c098a34d585f735d7d182da49779ac81ef269e", size = 486221, upload-time = "2026-04-08T01:02:14.195Z" }, + { url = "https://files.pythonhosted.org/packages/48/40/35f8bc84ae59c0d7dbe5ef3d229b6bb4d97848ed83e44dafff35f5a45fe8/fastar-0.10.0-cp313-cp313-win_arm64.whl", hash = "sha256:af0394cbb692b3232b484ffd1c16fec01630ccd6a68c2d05cb74c726764ff40e", size = 461878, upload-time = "2026-04-08T01:02:05.51Z" }, + { url = "https://files.pythonhosted.org/packages/11/c1/b50ccf451639648cc51321a9d0f06f43c87a83f209c517ed5a951043bd43/fastar-0.10.0-cp314-cp314-macosx_10_12_x86_64.whl", hash = "sha256:62d7c4e6cdb30474fec30d58db809c8ec0521bf8dca778b5b4998797cc49604a", size = 707858, upload-time = "2026-04-08T01:01:02.828Z" }, + { url = "https://files.pythonhosted.org/packages/6c/5d/db6406631bb8eea0e3fc75d02ea96112f99011cdcc465122f30253daecaa/fastar-0.10.0-cp314-cp314-macosx_11_0_arm64.whl", hash = "sha256:38c1c8da874ef5601dc38a8e83a4cc9016c6960a95d11be8fe69861d619907f6", size = 627773, upload-time = "2026-04-08T01:00:50.779Z" }, + { url = "https://files.pythonhosted.org/packages/7d/2f/fed5365dda5edc600af7a02d09cd961c4d6fc59edf1664e27088531c6f9d/fastar-0.10.0-cp314-cp314-manylinux_2_12_i686.manylinux2010_i686.whl", hash = "sha256:05551a40043b7fef387f1a320e2836692aee012b7a0cdbb37f4d3cfeed3f69d3", size = 866110, upload-time = "2026-04-08T01:00:25.808Z" }, + { url = "https://files.pythonhosted.org/packages/81/38/9bc6f5e105b94a1c46f859581ea86f57822e563f97dc95cf0c585442d146/fastar-0.10.0-cp314-cp314-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:9200167f5b7586f887fbbe7195db415ba7bda268ade345d22f1ccf195557dec5", size = 761146, upload-time = "2026-04-08T00:59:22.988Z" }, + { url = "https://files.pythonhosted.org/packages/7e/26/becf11edea8765f3e193ced940191cd1e4e2b6da96bde7eaf1f04cb449dc/fastar-0.10.0-cp314-cp314-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:deb7eb3fd1a420ec65517547a34241151e626d5cc366cf01db02886f9bae97e5", size = 758134, upload-time = "2026-04-08T00:59:35.188Z" }, + { url = "https://files.pythonhosted.org/packages/49/ea/b3927b8c0bc475ac8f92b1487c7b30e9df3145d12724f68b4fb96b9e3bb3/fastar-0.10.0-cp314-cp314-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:82aec9a3e2a466591e1bdd76aee79366dc10f519199b476faf90cc94a91fbf51", size = 925510, upload-time = "2026-04-08T00:59:46.921Z" }, + { url = "https://files.pythonhosted.org/packages/ce/5a/8e8f2a43256d23afb28116e8265d6895a71c59b6a9d98a7779d18a350bbe/fastar-0.10.0-cp314-cp314-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:65eff4e31058114c3929141f3dbd78420b3a35d58da288f21042ab2d0951db53", size = 817052, upload-time = "2026-04-08T01:00:13.017Z" }, + { url = "https://files.pythonhosted.org/packages/fb/a2/7447832868d4b4c2a9c4236121a7a3a145489e2e1ecd1a9ee4eb394aca12/fastar-0.10.0-cp314-cp314-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:c9f99153e458dfa655b604824319027c59faa82ba8096bee22093f3126d381a2", size = 819386, upload-time = "2026-04-08T01:00:37.955Z" }, + { url = "https://files.pythonhosted.org/packages/85/1c/407f36f19b2cd0f0754d9805810195d9afe9c2a325acb52064bae906e96a/fastar-0.10.0-cp314-cp314-manylinux_2_31_riscv64.whl", hash = "sha256:89b3cf8e88c2810b10200e350a9aa1a371db0513527dde1b353191a871ade380", size = 885601, upload-time = "2026-04-08T00:59:59.24Z" }, + { url = "https://files.pythonhosted.org/packages/07/fc/b61aaefb25bdac2847372bfc181dd7a41063f0b051e0dc4400bc2356b37b/fastar-0.10.0-cp314-cp314-musllinux_1_2_aarch64.whl", hash = "sha256:e09e420cc182df4db27f95cfd4ca656f290e560f7716cc2223bb7c4869b655ef", size = 968719, upload-time = "2026-04-08T01:01:15.36Z" }, + { url = "https://files.pythonhosted.org/packages/8e/23/3b45734447d280b152c6bf078240f958427e81daa84254302cbae7e27564/fastar-0.10.0-cp314-cp314-musllinux_1_2_armv7l.whl", hash = "sha256:2916f644b8263847356e4c4c22f6b00561538a608766650e66f7b17aebaa518d", size = 1035661, upload-time = "2026-04-08T01:01:28.228Z" }, + { url = "https://files.pythonhosted.org/packages/cb/56/0bf7902476f4cff2c90d34b3ebce594a3867a56bd672076ba312a99cc237/fastar-0.10.0-cp314-cp314-musllinux_1_2_i686.whl", hash = "sha256:71af0d37d9198af4a71690789b2f36c80aac9a84f0273956c5bfcc9de9e80170", size = 1073882, upload-time = "2026-04-08T01:01:41.795Z" }, + { url = "https://files.pythonhosted.org/packages/0c/51/3b8a126cad02936388a1533edac7d53675f904a9e63efbff6207ac92ee17/fastar-0.10.0-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:5b1e0942f0396bf2c14ce0bfd508f1a6100e76471f40d352dbff7e458213c0dd", size = 1026025, upload-time = "2026-04-08T01:01:54.621Z" }, + { url = "https://files.pythonhosted.org/packages/cd/82/1bf5192a652db703be2660eb76865677fa91d1d1c273b5ae8507f2451479/fastar-0.10.0-cp314-cp314-win32.whl", hash = "sha256:1d52bcebaf7b43be2e0e94471d326df68c1ef548d67e1d46e085d777bd401a85", size = 454870, upload-time = "2026-04-08T01:02:27.38Z" }, + { url = "https://files.pythonhosted.org/packages/df/ca/21c82444ea5f940e8832c35fcd74dc5fc5b0050a278598f6c615c9323a25/fastar-0.10.0-cp314-cp314-win_amd64.whl", hash = "sha256:e389212c62e6e0fa3f67af936fb9e706947678f751cc25b9b6f384165d4555d3", size = 486153, upload-time = "2026-04-08T01:02:15.6Z" }, + { url = "https://files.pythonhosted.org/packages/da/3f/1e0bfc91c2d76e1a0e75f34c33bad9f055d805626e1a22dd0545e784ec0c/fastar-0.10.0-cp314-cp314-win_arm64.whl", hash = "sha256:ee6058122e87a283d23fea27d4a07800490db874f4057785b399f9fbcf444660", size = 461668, upload-time = "2026-04-08T01:02:07.154Z" }, + { url = "https://files.pythonhosted.org/packages/fa/08/94f3bfcd239d6c2467b50fef7010672f46f7c1f42b9a6f741941815a39ec/fastar-0.10.0-cp314-cp314t-macosx_10_12_x86_64.whl", hash = "sha256:076c761a42aeabdac9ab7985e79810b24e3176d2c822b7d48d3caaaf8c9ac191", size = 707405, upload-time = "2026-04-08T01:01:04.312Z" }, + { url = "https://files.pythonhosted.org/packages/f7/ce/68ea8bfa2ec35723d14ad8cabae8b141373d7193ac6b5bc01e9273debc53/fastar-0.10.0-cp314-cp314t-macosx_11_0_arm64.whl", hash = "sha256:8556912f0bcda9bf430053e3a6fc3fb3706bd2ed7a478cad071236cff2d07cb8", size = 627023, upload-time = "2026-04-08T01:00:52.153Z" }, + { url = "https://files.pythonhosted.org/packages/1a/61/b46501f669fda46be25c1e91ea5132eac563bc6ec2fcb04059137f5b83bf/fastar-0.10.0-cp314-cp314t-manylinux_2_12_i686.manylinux2010_i686.whl", hash = "sha256:13ff7db59cb86b8fb59b14327d8f7a9357d26576987096be6dce4169cff70e50", size = 865500, upload-time = "2026-04-08T01:00:27.016Z" }, + { url = "https://files.pythonhosted.org/packages/7c/ce/7dd6d1c67a3538bc75345e1604a0d5a63450f2f78e1db4967ac20393daa4/fastar-0.10.0-cp314-cp314t-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:8f4c81a8c13463bbb5c2533b786ba5162c49af487707b2854d8bc223bbae033a", size = 759477, upload-time = "2026-04-08T00:59:24.248Z" }, + { url = "https://files.pythonhosted.org/packages/3c/f8/e2aa5425e11e7e562f75d280122735b8e374159a7a6a43693bee594eb1da/fastar-0.10.0-cp314-cp314t-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:128cda8d35d9acb962da45c060b1cc3dfeaf0174d8c576fd294151c92b4edd63", size = 757352, upload-time = "2026-04-08T00:59:36.275Z" }, + { url = "https://files.pythonhosted.org/packages/23/7d/6674cfc89fe07079ff577c0bbbb57d4b0f20fc71520f25d6379c5be23e04/fastar-0.10.0-cp314-cp314t-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:9400058e458876dfdfbec1e2164254833fac8c6ed9d0570f476f2a2723315b10", size = 922930, upload-time = "2026-04-08T00:59:48.38Z" }, + { url = "https://files.pythonhosted.org/packages/85/9b/a948ae0a331601c99d07a6143274821a371f5f56669b970483e724df895c/fastar-0.10.0-cp314-cp314t-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:6a69e0f260e17e99d3701cc9bbdfe7896df2fd8d74f34c09efc6427cc2e1c4fd", size = 816039, upload-time = "2026-04-08T01:00:14.63Z" }, + { url = "https://files.pythonhosted.org/packages/7d/0e/1e15e3769185bd28a6f32e28d79940f670a6495e0c939b306d7f57a43cb8/fastar-0.10.0-cp314-cp314t-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:802fbfc4a1b6e87eccc1c8e7310599dcb9200f63d5cc230a19abf505993bff00", size = 819246, upload-time = "2026-04-08T01:00:39.26Z" }, + { url = "https://files.pythonhosted.org/packages/fe/de/cbbd6eeaed1c5013a93bc5c81d6a288e1b5900dfb118020d57e4e8b4aa67/fastar-0.10.0-cp314-cp314t-manylinux_2_31_riscv64.whl", hash = "sha256:9af06eab447b555073b927a5bd8fd02cad792470f930ee653768bf892640523b", size = 884282, upload-time = "2026-04-08T01:00:00.854Z" }, + { url = "https://files.pythonhosted.org/packages/cd/7e/f5dd560e01efaf701689a7961d149d488d575827768d77d2d52464b14af3/fastar-0.10.0-cp314-cp314t-musllinux_1_2_aarch64.whl", hash = "sha256:eeeef8ce05c196125e29cc6529f95ff7d52d96dc31b371369af777542082c4cb", size = 966791, upload-time = "2026-04-08T01:01:16.772Z" }, + { url = "https://files.pythonhosted.org/packages/b2/26/ad2e20836dda41a1c01ca15b5e63a388c1424a3d04ed02c96d3074ed7df1/fastar-0.10.0-cp314-cp314t-musllinux_1_2_armv7l.whl", hash = "sha256:6eee2382c1a8c1f5008365e469358ce1162c9cd8fc55780acaa4cb55af09c0f4", size = 1034710, upload-time = "2026-04-08T01:01:29.979Z" }, + { url = "https://files.pythonhosted.org/packages/ac/07/a6753d70d7d25e73a38b5ab229b4e00f9790fe7db6f022a3b087ed2702a3/fastar-0.10.0-cp314-cp314t-musllinux_1_2_i686.whl", hash = "sha256:961f3f4ad805f40d7003c2041f0f85f1a3ba3d67b9508e9ea6225146d2c8147b", size = 1074017, upload-time = "2026-04-08T01:01:43.107Z" }, + { url = "https://files.pythonhosted.org/packages/ef/b4/f0b121a2300b629d09766aa3ffc2e755d8d72f31fe2bcf0b1055dbda1cbd/fastar-0.10.0-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:86a1805316324eeb98b05f6b1db921bc3a9d9c9c6f535b2204b2e039a29048c4", size = 1025819, upload-time = "2026-04-08T01:01:56.008Z" }, + { url = "https://files.pythonhosted.org/packages/12/71/1e82b796ccee8d3318bf9e7638a5d8d13a862681d26dedd0e807fa17f2c6/fastar-0.10.0-cp314-cp314t-win32.whl", hash = "sha256:c35f812bc59dab63ce7b130ed9e2258478ec9a07338b55670513fd985c244fac", size = 454307, upload-time = "2026-04-08T01:02:32.01Z" }, + { url = "https://files.pythonhosted.org/packages/27/5d/9ace4277834d59d5c89c0ffa0fc9805db4e4bcba1da9e54356ae18c9583b/fastar-0.10.0-cp314-cp314t-win_amd64.whl", hash = "sha256:1df45800337eb8f70f256c5139953e1910189fc3578adc81d135818529a1df6f", size = 486365, upload-time = "2026-04-08T01:02:20.244Z" }, + { url = "https://files.pythonhosted.org/packages/71/4f/a5b3cfc3e1779c756c4fef790facef12d63088402a718b167b780d4f2ccc/fastar-0.10.0-cp314-cp314t-win_arm64.whl", hash = "sha256:3152a80835ef11cbfaf153037dd694e0014f25d074792b9785421679acdbe179", size = 461150, upload-time = "2026-04-08T01:02:08.47Z" }, + { url = "https://files.pythonhosted.org/packages/3c/48/996569aec94cf77d1fc55e530959a1650c348af5450becba61126841dc3b/fastar-0.10.0-pp311-pypy311_pp73-macosx_10_12_x86_64.whl", hash = "sha256:22a584420387bc17c003ff058987c3e6f3d8a1b447034a9ddcdfc7ca1c4eb56e", size = 712160, upload-time = "2026-04-08T01:01:08.597Z" }, + { url = "https://files.pythonhosted.org/packages/a4/8d/64dd6f1f26079e9bb78d6d6dc866c8d6cba0b1f18b71b9a6cbe3702014fd/fastar-0.10.0-pp311-pypy311_pp73-macosx_11_0_arm64.whl", hash = "sha256:7f0a076a01834fc166e00a1cf4d78c02bdc2f62e1d90a4f3270f84d861996b87", size = 633356, upload-time = "2026-04-08T01:00:56.258Z" }, + { url = "https://files.pythonhosted.org/packages/e9/2b/8fc2aba7053297716b5e84ac48147a1d21bcb5f971ac9cf626f155386a78/fastar-0.10.0-pp311-pypy311_pp73-manylinux_2_12_i686.manylinux2010_i686.whl", hash = "sha256:b61f9fd39cb27bb78cc790e92db59c12031eff2900dcbd66e6355109723599b6", size = 872526, upload-time = "2026-04-08T01:00:30.843Z" }, + { url = "https://files.pythonhosted.org/packages/42/bc/004c028abfe21b6794bfea5176a51408360a8aa06317fb68cc8052185257/fastar-0.10.0-pp311-pypy311_pp73-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:9ab60ecec2c8cd08006ec1a81157918905fe0037049cb3bf3ae68577b2c2c482", size = 764974, upload-time = "2026-04-08T00:59:28.173Z" }, + { url = "https://files.pythonhosted.org/packages/6e/a5/2a0aca15f0407452051a370aa60a56b1a34800a36ecb77fe88a35b69d7a6/fastar-0.10.0-pp311-pypy311_pp73-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:0b561cf1f314a7fd4ffee3ae03dcdc03cab50ab0f63f35417eb389fc38773792", size = 763895, upload-time = "2026-04-08T00:59:40.531Z" }, + { url = "https://files.pythonhosted.org/packages/e7/ba/73f562d53d88f652e6ac2748809e4ed732a22bcedde5d1ec502eed666e4d/fastar-0.10.0-pp311-pypy311_pp73-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:d6b26757f5de13d58ed474898c52f5a958e76925672b2350f5163628572c9509", size = 927715, upload-time = "2026-04-08T00:59:52.356Z" }, + { url = "https://files.pythonhosted.org/packages/ca/4a/89190cb3a98e2bf9da083fc1fab8d128a4875d5c4de9d50aa027d48bbe24/fastar-0.10.0-pp311-pypy311_pp73-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:78f4964f03cfd497f450926b1ed2d383841dbb01c148169f2c9458b25708f119", size = 821305, upload-time = "2026-04-08T01:00:18.746Z" }, + { url = "https://files.pythonhosted.org/packages/65/f6/592ae14e4cc248824c653ae946ceb1491c16f8fc83b2c768bb56088c2abc/fastar-0.10.0-pp311-pypy311_pp73-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:b43aeed18dd1d78aa615ae9486db8d5c366aaf8baa3c0585ce3fc52429081add", size = 824243, upload-time = "2026-04-08T01:00:43.704Z" }, + { url = "https://files.pythonhosted.org/packages/92/52/56e7c94a01eb7ce8ecefb370af5e0411a927c44baef8e59ec46c5b49079c/fastar-0.10.0-pp311-pypy311_pp73-manylinux_2_31_riscv64.whl", hash = "sha256:e2566bf172b566b688bd00beebbaae4f9df5794b688c02382bb1e11425ac8680", size = 889530, upload-time = "2026-04-08T01:00:04.703Z" }, + { url = "https://files.pythonhosted.org/packages/d6/d4/b6b20cf5503a72e02c38cdf94d0a89faea061f5bc6a3674467a29b3536f8/fastar-0.10.0-pp311-pypy311_pp73-musllinux_1_2_aarch64.whl", hash = "sha256:04e0ef65dc853c459c8c1fbc00ba16dd32c0d7765bfa04ad0d844002d59b70fd", size = 973117, upload-time = "2026-04-08T01:01:21.405Z" }, + { url = "https://files.pythonhosted.org/packages/d9/9b/f16465be678a2d4fe26782122088f0347be6ad6d022c1b4793bbc09fed56/fastar-0.10.0-pp311-pypy311_pp73-musllinux_1_2_armv7l.whl", hash = "sha256:910194438a11cd803e1d63f166dfb1bd352054e66bc675af196b7fcf382f69f8", size = 1039524, upload-time = "2026-04-08T01:01:34.227Z" }, + { url = "https://files.pythonhosted.org/packages/24/ba/6e44ba81378c8f06670d1c905ad99e19a5856f890ee81b0c8112839dbc9e/fastar-0.10.0-pp311-pypy311_pp73-musllinux_1_2_i686.whl", hash = "sha256:9585543641f669ca1a741b64e1d5ae23f62b7d76e8dcf1fd0a7dd247330fb23d", size = 1080892, upload-time = "2026-04-08T01:01:47.585Z" }, + { url = "https://files.pythonhosted.org/packages/e9/cc/9f87149da2d84876a2913f198849acbb6b0c6de1b8cab3d32993bbaccbde/fastar-0.10.0-pp311-pypy311_pp73-musllinux_1_2_x86_64.whl", hash = "sha256:c55f18520e7e392e27067bf51727a4ad30dc5f4064876781b03939dfab65cd48", size = 1032033, upload-time = "2026-04-08T01:02:00.149Z" }, +] + +[[package]] +name = "h11" +version = "0.16.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/01/ee/02a2c011bdab74c6fb3c75474d40b3052059d95df7e73351460c8588d963/h11-0.16.0.tar.gz", hash = "sha256:4e35b956cf45792e4caa5885e69fba00bdbc6ffafbfa020300e549b208ee5ff1", size = 101250, upload-time = "2025-04-24T03:35:25.427Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/04/4b/29cac41a4d98d144bf5f6d33995617b185d14b22401f75ca86f384e87ff1/h11-0.16.0-py3-none-any.whl", hash = "sha256:63cf8bbe7522de3bf65932fda1d9c2772064ffb3dae62d55932da54b31cb6c86", size = 37515, upload-time = "2025-04-24T03:35:24.344Z" }, +] + +[[package]] +name = "httpcore" +version = "1.0.9" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "certifi" }, + { name = "h11" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/06/94/82699a10bca87a5556c9c59b5963f2d039dbd239f25bc2a63907a05a14cb/httpcore-1.0.9.tar.gz", hash = "sha256:6e34463af53fd2ab5d807f399a9b45ea31c3dfa2276f15a2c3f00afff6e176e8", size = 85484, upload-time = "2025-04-24T22:06:22.219Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/7e/f5/f66802a942d491edb555dd61e3a9961140fd64c90bce1eafd741609d334d/httpcore-1.0.9-py3-none-any.whl", hash = "sha256:2d400746a40668fc9dec9810239072b40b4484b640a8c38fd654a024c7a1bf55", size = 78784, upload-time = "2025-04-24T22:06:20.566Z" }, +] + +[[package]] +name = "httptools" +version = "0.8.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/43/e5/d471fcb0e14523fe1c3f4ba58ca52480e7bd70ad7109a3846bc75892f7fb/httptools-0.8.0.tar.gz", hash = "sha256:6b2a32f18d97e16e90827d7a819ffa8dbd8cc245fc4e1fa9d1095b54ef4bd999", size = 271342, upload-time = "2026-05-25T22:17:48.841Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/40/b9/be66eb0decd730d89b9c94f930e4b8d87787b05724bb84af98bfd825f72c/httptools-0.8.0-cp310-cp310-macosx_10_9_universal2.whl", hash = "sha256:bf3b6f807c8541503cecfbb8a8dffb385640d0d96102f3d112aa8740f9b7c826", size = 208805, upload-time = "2026-05-25T22:16:50.434Z" }, + { url = "https://files.pythonhosted.org/packages/9d/f7/b4d41eaae2869d31356bc4bbf546f44fae83ff298af0a043ca0625b06773/httptools-0.8.0-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:da684f2e1aa2ee9bdcb083f3f3a68c5956750b375bc5df864d3a5f0c42a40b77", size = 113527, upload-time = "2026-05-25T22:16:51.672Z" }, + { url = "https://files.pythonhosted.org/packages/e6/e4/77487e14fc7be47180fd0eb4267c7486d0cc59b74031839a3daf8650136b/httptools-0.8.0-cp310-cp310-manylinux1_x86_64.manylinux_2_28_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:a6f21e2a3b0067bbe7f67e34cfd16276af556e5e52f4c7503be0cb5f90e905e4", size = 450035, upload-time = "2026-05-25T22:16:53.313Z" }, + { url = "https://files.pythonhosted.org/packages/da/72/5a8f787e323f56fbd86c32a4be92a86776e4cfe8b4317db999f452028362/httptools-0.8.0-cp310-cp310-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:0ea897f0c729581ebf72131a438a7932d9b14efef72d75ada966700cac3caaeb", size = 451101, upload-time = "2026-05-25T22:16:54.696Z" }, + { url = "https://files.pythonhosted.org/packages/ed/41/b44a25560955197674b6744cb903664300e239235a5eaa69df0890d87054/httptools-0.8.0-cp310-cp310-musllinux_1_2_aarch64.whl", hash = "sha256:c0d726cc107fceb7d45f978483b4b70dd8caa836f5914d3434bb18628eb73813", size = 436140, upload-time = "2026-05-25T22:16:56.239Z" }, + { url = "https://files.pythonhosted.org/packages/74/b0/054aac84c03d7e097bf4c605fb7e74eec3d65c0276adf64ee97f3a103ff5/httptools-0.8.0-cp310-cp310-musllinux_1_2_x86_64.whl", hash = "sha256:9878eb2785ba5eb70631ad269b37976f73d647955e26c91d490eb8a4edfda4ba", size = 437041, upload-time = "2026-05-25T22:16:57.716Z" }, + { url = "https://files.pythonhosted.org/packages/bb/e8/86b85bbc0ac7892232f1a99ab96a9aa71936984fa06adfc0afc83ca7789e/httptools-0.8.0-cp310-cp310-win_amd64.whl", hash = "sha256:b205e5f5523fa039679da0dfe5a10132b2a4abeae6a86fdd1ddc035f7f836557", size = 90454, upload-time = "2026-05-25T22:16:58.871Z" }, + { url = "https://files.pythonhosted.org/packages/f8/d2/c3eedaef57de65c3cc5f8dc244cf12d09c84ad258a479055aad6db23206c/httptools-0.8.0-cp311-cp311-macosx_10_9_universal2.whl", hash = "sha256:ed377e64805bdba4943c82717333f8f8603a13b09aff9cead2717c6c817fb168", size = 208428, upload-time = "2026-05-25T22:16:59.717Z" }, + { url = "https://files.pythonhosted.org/packages/f1/94/dfe435d90d0ef61ec0f2cc3d480eef78c59727c6c2ce039f433882f6131a/httptools-0.8.0-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:9518c406d7b310f05adb1a37f80acabac40504a575d7c0da6d3e365c695ac20d", size = 113366, upload-time = "2026-05-25T22:17:00.795Z" }, + { url = "https://files.pythonhosted.org/packages/cc/d4/13025f1a56e615dcb331e0bbe2d9a1143212b58c263385fc5d2e558f5bac/httptools-0.8.0-cp311-cp311-manylinux1_x86_64.manylinux_2_28_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:57278e6fa0424c42a8a3e454828ab4f0aff27b40cddf9679579b98c6dce6a376", size = 464676, upload-time = "2026-05-25T22:17:02.014Z" }, + { url = "https://files.pythonhosted.org/packages/bf/95/4c1c26c0b985f8a3331682d802598f14e32dc41bf7509266eb2c04ad4801/httptools-0.8.0-cp311-cp311-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:bbb8caadb2b742d293169d2b458b5c001ef70e3158704aa3d3ef9597624c5d1d", size = 464235, upload-time = "2026-05-25T22:17:03.109Z" }, + { url = "https://files.pythonhosted.org/packages/a2/82/6735be2b0ca527718c431cdb8e5f70c3862c0844a687df0f572c51e11497/httptools-0.8.0-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:52dd695b865fe96d9d2b16b64a895f3f57bf3cb064e8383cd3b5713a069e8085", size = 449809, upload-time = "2026-05-25T22:17:04.443Z" }, + { url = "https://files.pythonhosted.org/packages/b5/f9/5811c74f37a758c8a4aa3dc430375119d335947e883efc4664d8f3559a41/httptools-0.8.0-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:20b4aac66ff65f7db06a375808b78f42a94970aa22e826b3cb2b43eb09174124", size = 452174, upload-time = "2026-05-25T22:17:05.476Z" }, + { url = "https://files.pythonhosted.org/packages/cc/94/97b75870dea07b71e3ec535cebe525b08d723152e4c7d13fa887e51f4de2/httptools-0.8.0-cp311-cp311-win_amd64.whl", hash = "sha256:a1b4c8e7a489a0d750d91894e9a8cdc295838f1924c0ca903ae993456fddec07", size = 90991, upload-time = "2026-05-25T22:17:06.75Z" }, + { url = "https://files.pythonhosted.org/packages/14/88/1d21a36da8f5cb0fa49eafd4b169eba5608d57e75bbcf61845cbc6243216/httptools-0.8.0-cp312-cp312-macosx_10_13_universal2.whl", hash = "sha256:880490234c10f70a9830743097e8958d6e4b9f5a0ffc24515023afeef984054d", size = 208247, upload-time = "2026-05-25T22:17:07.843Z" }, + { url = "https://files.pythonhosted.org/packages/a5/42/cc4feea2945cb3051038f090c9b36bd5b8a9d7f5a894a506a8983e33fd1c/httptools-0.8.0-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:5931891fb7b441b8a3853cf1b85c82c903defce084dd5f6771ca46e31bf862c5", size = 113064, upload-time = "2026-05-25T22:17:09.136Z" }, + { url = "https://files.pythonhosted.org/packages/e3/a6/febbb8b8db0f58b38e44ad6cb946e6a255ae49b55f2e8543408fb7501ccd/httptools-0.8.0-cp312-cp312-manylinux1_x86_64.manylinux_2_28_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:b15fc622b0f869d19207c4089a501d9bcc63ca5e071ffdd2f03f922df882dcb2", size = 523851, upload-time = "2026-05-25T22:17:10.106Z" }, + { url = "https://files.pythonhosted.org/packages/b7/e4/f90a0df0b83beff265b7e3b65f2a4cefd95792d4be0ac3e16049f2acd3c2/httptools-0.8.0-cp312-cp312-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:425f83884fd6343828d8c565f046cb72b6d19063f6924093e11bcd8e1548cd09", size = 518842, upload-time = "2026-05-25T22:17:11.218Z" }, + { url = "https://files.pythonhosted.org/packages/9e/2d/0c9ac76dd2c893841fbf6498d6acec4f2442e1b7067f6e3e316a80e494e8/httptools-0.8.0-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:ef7c3c97f4311c7be57e2986629df89d49cb434dbff78eafcd48c2bff986b15a", size = 501238, upload-time = "2026-05-25T22:17:12.728Z" }, + { url = "https://files.pythonhosted.org/packages/ca/42/906adc91ae3a5fa9c59c0a2f21c139725bd7e5b41ae6acd485cd14123ebf/httptools-0.8.0-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:a1afd7c9fbff0d9f5d489c4ce2768bd09c84a46ddefc7161e6aa82ae35c85745", size = 509567, upload-time = "2026-05-25T22:17:13.842Z" }, + { url = "https://files.pythonhosted.org/packages/05/0b/4240efeb672751ee5b9b380cb0e3fdc050bc05f68adc7a8aefc4fcd9a69a/httptools-0.8.0-cp312-cp312-win_amd64.whl", hash = "sha256:cd96f29b4bab1d42fa6e3d008711c75e0f79e94e06827330160e3a304227f150", size = 90918, upload-time = "2026-05-25T22:17:15.155Z" }, + { url = "https://files.pythonhosted.org/packages/5e/e5/8cfcabc5546e8022f168be28bcdaa128a240a0befdd03b59d558b4f18bd6/httptools-0.8.0-cp313-cp313-macosx_10_13_universal2.whl", hash = "sha256:614ceea8ea606848bece2338ac03b3ce5324bcb4be8dc7d377ed708012fa4db8", size = 205148, upload-time = "2026-05-25T22:17:16.333Z" }, + { url = "https://files.pythonhosted.org/packages/2a/0e/0fb14848c19a686c8062ff9067c1a48793e3224b47bc5b201535b6036fce/httptools-0.8.0-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:2d689918c15a013c65ef52d9fd495d766893ab831a2c8d89f2ac5940a5df847c", size = 111368, upload-time = "2026-05-25T22:17:17.586Z" }, + { url = "https://files.pythonhosted.org/packages/2e/1b/46f1cecf06b9bbde8e4b8c88034ac7908989e5ff7a3a388ef38392949c1f/httptools-0.8.0-cp313-cp313-manylinux1_x86_64.manylinux_2_28_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:eb3028cca2fc0a6d720e52ef61d8ebb62fcbfeb1de56874546d858d3f25a26b7", size = 486447, upload-time = "2026-05-25T22:17:18.564Z" }, + { url = "https://files.pythonhosted.org/packages/77/00/258bfc0837221f81d9725c45f9b948a6a6b2994a147a4fb66e85100c668f/httptools-0.8.0-cp313-cp313-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:88bdd940f2b5d487b4d032c6afa5489a7dc4694410d43de3c38c4fb3af0dc45d", size = 482448, upload-time = "2026-05-25T22:17:19.912Z" }, + { url = "https://files.pythonhosted.org/packages/04/ab/d1cef3b5523f4d272a70f42a776c3169a2dddfe3a54de4b2ce4a36341528/httptools-0.8.0-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:6a43c9dd399758ccc0531acb0a3c4a6c299ee893ee9400e9c893b7bdcfae0681", size = 464460, upload-time = "2026-05-25T22:17:20.882Z" }, + { url = "https://files.pythonhosted.org/packages/ce/48/5d1d072442277bb2b3434e0e60690b8e8c23840ef7de8b6ea54040a536d3/httptools-0.8.0-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:0770728beb05094c809b98e814edff5fef69d26ad7d21185f2f6d5884a0ba683", size = 471312, upload-time = "2026-05-25T22:17:22.085Z" }, + { url = "https://files.pythonhosted.org/packages/0d/66/b96623b27e51a68199ef4efdda0613cced9233fe3062ac74e50749c5ad37/httptools-0.8.0-cp313-cp313-win_amd64.whl", hash = "sha256:7685df791fad561384bfb139e77fde27a1ffd93134e016f95a0db424ffbf77b1", size = 90117, upload-time = "2026-05-25T22:17:23.074Z" }, + { url = "https://files.pythonhosted.org/packages/1a/12/fa3fbf5f9517b273edea2dc982aa82a8c634091e67c590792b729017bc6f/httptools-0.8.0-cp314-cp314-macosx_10_13_universal2.whl", hash = "sha256:de242a49b5d18e0a8776e654e9f6bf6d89f3875a5c35b425a0e7ce940feb3fd6", size = 206183, upload-time = "2026-05-25T22:17:24.004Z" }, + { url = "https://files.pythonhosted.org/packages/30/fc/5e7c4cb443370f2090a3aba0453a07384d29ff66b7435bb90e77e1037599/httptools-0.8.0-cp314-cp314-macosx_11_0_arm64.whl", hash = "sha256:159e9ab5f701ccd42e555a12f1ad8ff69702910fc1c996cf2bb66e5fcb7a231b", size = 112079, upload-time = "2026-05-25T22:17:25.216Z" }, + { url = "https://files.pythonhosted.org/packages/ba/53/771bd891eb0f236f32145d6a1775777ec85745f3cc983a1f23d1a3b8ddfe/httptools-0.8.0-cp314-cp314-manylinux1_x86_64.manylinux_2_28_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:c4a9f1707e4823d54dfec6c33fa3697d302aed536ed352a7ebb5a061ddb869d0", size = 481596, upload-time = "2026-05-25T22:17:26.186Z" }, + { url = "https://files.pythonhosted.org/packages/62/42/94e15bc68ce3d423243c45d7f1b0c7561f13844f97dc52ae23182fb65628/httptools-0.8.0-cp314-cp314-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:d76ad7b951387e3632c8716a9bb03ac5b45c5f16119aa409db0459520887944e", size = 480865, upload-time = "2026-05-25T22:17:27.542Z" }, + { url = "https://files.pythonhosted.org/packages/1c/7c/fe2980fc03723272e30f135b62360b075f513dfe7cc73aef36c7f04012bd/httptools-0.8.0-cp314-cp314-musllinux_1_2_aarch64.whl", hash = "sha256:a3b7387147361c3fd47a0bde763c5c91b5b4cd4dc9989b8ece84ff436c99843b", size = 463189, upload-time = "2026-05-25T22:17:28.546Z" }, + { url = "https://files.pythonhosted.org/packages/15/1b/47fc5fff68acd1bfa20b4734059c9a06cadb88119dcd5258b5b0d21d91c8/httptools-0.8.0-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:f256d6ce930c52ca1cb2a960b7da03548c454e7d28b06059ad41bfe789036ce0", size = 466610, upload-time = "2026-05-25T22:17:29.816Z" }, + { url = "https://files.pythonhosted.org/packages/60/bd/07b13c93ffd9bec9546e0d43f8e19378dd696dbd278511406bc07371ef1f/httptools-0.8.0-cp314-cp314-win_amd64.whl", hash = "sha256:19d1ee275bb59ba2643ba9a3a1e51cc0c788caf2b8df506368e03f56fdd08527", size = 92705, upload-time = "2026-05-25T22:17:31.133Z" }, + { url = "https://files.pythonhosted.org/packages/fd/c4/121648f68ce066d7bd762d6b6d97e620847642d38d54f3d90ff11d947629/httptools-0.8.0-cp314-cp314t-macosx_10_13_universal2.whl", hash = "sha256:de1ed58a974e75d56560acc7e7fed01a454994429456f65209789992e41f2568", size = 215023, upload-time = "2026-05-25T22:17:32.401Z" }, + { url = "https://files.pythonhosted.org/packages/b9/b0/312a062ae741ae3e8baa8c8bf20be81b2e67337b259ab4349bebc7b6142e/httptools-0.8.0-cp314-cp314t-macosx_11_0_arm64.whl", hash = "sha256:e93c227b595c6926c1acee96891dd9da4be338cfbe82e5cd3bb9d8dd7dc4ac0b", size = 117405, upload-time = "2026-05-25T22:17:33.742Z" }, + { url = "https://files.pythonhosted.org/packages/fc/37/fccd705f795386bb05bf413012fecff2a33e5aa8c2f069096de3e9fd8702/httptools-0.8.0-cp314-cp314t-manylinux1_x86_64.manylinux_2_28_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:2a021c3a8e65cc125390d72f59b968afca3bdcaff25bd67965e0a055a14946ca", size = 558497, upload-time = "2026-05-25T22:17:34.732Z" }, + { url = "https://files.pythonhosted.org/packages/bd/39/f172e8003576de35f5ba77ff417cf0e34429d35dc014deef15afa337a72c/httptools-0.8.0-cp314-cp314t-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:48774d39cbb70e2b1f71f88852a3087ae1d3a1eb80482bb48c13067ab080c14f", size = 571585, upload-time = "2026-05-25T22:17:35.813Z" }, + { url = "https://files.pythonhosted.org/packages/3e/b9/f5564760af99f3dbbf3f9104dc00e5da27e96cf433c6bdcf77617f70bf3f/httptools-0.8.0-cp314-cp314t-musllinux_1_2_aarch64.whl", hash = "sha256:88eead8ec8680a9f146c655bc88445a325bd7921cfd8194c7337e9467282427d", size = 543297, upload-time = "2026-05-25T22:17:37.08Z" }, + { url = "https://files.pythonhosted.org/packages/99/67/8d9f2c313618e161b82f3873188e7196126da1d6e29688df40eb3997c77a/httptools-0.8.0-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:2c032fa028f46871ec7e1fc59fc15e8023eab3e6bbe6ece786a1611719a5d081", size = 539535, upload-time = "2026-05-25T22:17:38.032Z" }, + { url = "https://files.pythonhosted.org/packages/48/63/b906c01e53f50d432c0defe43ce52764a111dc1bdd028bafbeb54dcfd008/httptools-0.8.0-cp314-cp314t-win_amd64.whl", hash = "sha256:384c17174464c8e873398b7af24f0b1f44d992c820328413951a625323155d77", size = 108209, upload-time = "2026-05-25T22:17:39.473Z" }, +] + +[[package]] +name = "httpx" +version = "0.28.1" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "anyio" }, + { name = "certifi" }, + { name = "httpcore" }, + { name = "idna" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/b1/df/48c586a5fe32a0f01324ee087459e112ebb7224f646c0b5023f5e79e9956/httpx-0.28.1.tar.gz", hash = "sha256:75e98c5f16b0f35b567856f597f06ff2270a374470a5c2392242528e3e3e42fc", size = 141406, upload-time = "2024-12-06T15:37:23.222Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/2a/39/e50c7c3a983047577ee07d2a9e53faf5a69493943ec3f6a384bdc792deb2/httpx-0.28.1-py3-none-any.whl", hash = "sha256:d909fcccc110f8c7faf814ca82a9a4d816bc5a6dbfea25d6591d6985b8ba59ad", size = 73517, upload-time = "2024-12-06T15:37:21.509Z" }, +] + +[[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 = "iniconfig" +version = "2.3.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/72/34/14ca021ce8e5dfedc35312d08ba8bf51fdd999c576889fc2c24cb97f4f10/iniconfig-2.3.0.tar.gz", hash = "sha256:c76315c77db068650d49c5b56314774a7804df16fee4402c1f19d6d15d8c4730", size = 20503, upload-time = "2025-10-18T21:55:43.219Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/cb/b1/3846dd7f199d53cb17f49cba7e651e9ce294d8497c8c150530ed11865bb8/iniconfig-2.3.0-py3-none-any.whl", hash = "sha256:f631c04d2c48c52b84d0d0549c99ff3859c98df65b3101406327ecc7d53fbf12", size = 7484, upload-time = "2025-10-18T21:55:41.639Z" }, +] + +[[package]] +name = "librt" +version = "0.11.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/40/08/9e7f6b5d2b5bed6ad055cdd5925f192bb403a51280f86b56554d9d0699a2/librt-0.11.0.tar.gz", hash = "sha256:075dc3ef4458a278e0195cbf6ac9d38808d9b906c5a6c7f7f79c3888276a3fb1", size = 200139, upload-time = "2026-05-10T18:17:25.138Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/83/10/37fd9e9ba96cb0bd742dfb20fc3d082e54bdbec759d7300df927f360ef07/librt-0.11.0-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:6e94ebfcfa2d5e9926d6c3b9aa4617ffc42a845b4321fb84021b872358c82a0f", size = 141706, upload-time = "2026-05-10T18:15:16.129Z" }, + { url = "https://files.pythonhosted.org/packages/cf/72/1b1466f358e4a0b728051f69bc27e67b432c6eaa2e05b88db49d3785ae0d/librt-0.11.0-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:ae627397a2f351560440d872d6f7c8dbb4072e57868e7b2fc5b8b430fe489d45", size = 142605, upload-time = "2026-05-10T18:15:18.148Z" }, + { url = "https://files.pythonhosted.org/packages/ca/85/ed26dd2f6bc9a0baf48306433e579e8d354d70b2bcb78134ed950a5d0e1e/librt-0.11.0-cp310-cp310-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:dc329359321b67d24efdf4bc69012b0597001649544db662c001db5a0184794c", size = 476555, upload-time = "2026-05-10T18:15:19.569Z" }, + { url = "https://files.pythonhosted.org/packages/66/fe/11891191c0e0a3fd617724e891f6e67a71a7658974a892b9a9a97fdb2977/librt-0.11.0-cp310-cp310-manylinux2014_i686.manylinux_2_17_i686.manylinux_2_28_i686.whl", hash = "sha256:7e82e642ab0f7608ce2fe53d76ca2280a9ee33a1b06556142c7c6fe80a86fc33", size = 468434, upload-time = "2026-05-10T18:15:20.87Z" }, + { url = "https://files.pythonhosted.org/packages/6f/50/5ec949d7f9ce1a07af903aa3e13abb98b717923bdead6e719b2f824ccc07/librt-0.11.0-cp310-cp310-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:88145c15c67731d54283d135b03244028c750cc9edc334a96a4f5950ebdb2884", size = 496918, upload-time = "2026-05-10T18:15:22.616Z" }, + { url = "https://files.pythonhosted.org/packages/ea/c4/177336c7524e34875a38bf668e88b193a6723a4eb4045d07f74df6e1506c/librt-0.11.0-cp310-cp310-manylinux_2_34_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:9d36a51b3d93320b686588e27123f4995804dbf1bce81df78c02fc3c6eea9280", size = 490334, upload-time = "2026-05-10T18:15:24.2Z" }, + { url = "https://files.pythonhosted.org/packages/13/1f/da3112f7569eda3b49f9a2629bae1fe059812b6085df16c885f6454dff49/librt-0.11.0-cp310-cp310-musllinux_1_2_aarch64.whl", hash = "sha256:d00f3ac06a2a8b246327f11e186a53a100a4d5c7ed52346367e5ec751d51586c", size = 511287, upload-time = "2026-05-10T18:15:26.226Z" }, + { url = "https://files.pythonhosted.org/packages/fa/94/03fec301522e172d105581431223be56b27594ff46440ebfbb658a3735d5/librt-0.11.0-cp310-cp310-musllinux_1_2_i686.whl", hash = "sha256:461bbceede621f1ffb8839755f8663e886087ee7af16294cab7fb4d782c62eeb", size = 517202, upload-time = "2026-05-10T18:15:27.965Z" }, + { url = "https://files.pythonhosted.org/packages/b7/6e/339f6e5a7b413ce014f1917a756dae630fe59cc99f34153205b1cb540901/librt-0.11.0-cp310-cp310-musllinux_1_2_riscv64.whl", hash = "sha256:0cad8a4d6a8ff03c9b76f9414caccd78e7cfbc8a2e12fa334d8e1d9932753783", size = 497517, upload-time = "2026-05-10T18:15:29.614Z" }, + { url = "https://files.pythonhosted.org/packages/cd/43/acdd5ce317cb46e8253ca9bfbdb8b12e68a24d745949336a7f3d5fb79ba0/librt-0.11.0-cp310-cp310-musllinux_1_2_x86_64.whl", hash = "sha256:f37aa505b3cf60701562eddb32df74b12a9e380c207fd8b06dd157a943ac7ea0", size = 538878, upload-time = "2026-05-10T18:15:30.928Z" }, + { url = "https://files.pythonhosted.org/packages/29/b5/7a25bb12e3172839f647f196b3e988318b7bb1ca7501732a225c4dce2ec0/librt-0.11.0-cp310-cp310-win32.whl", hash = "sha256:94663a21534637f0e787ec2a2a756022df6e5b7b2335a5cdd7d8e33d68a2af89", size = 100070, upload-time = "2026-05-10T18:15:32.551Z" }, + { url = "https://files.pythonhosted.org/packages/c6/0d/ebbcf4d77999c02c937b05d2b90ff4cd4dcc7e9a365ba132329ac1fe7a0f/librt-0.11.0-cp310-cp310-win_amd64.whl", hash = "sha256:dec7db73758c2b54953fd8b7fe348c45188fe26b39ee18446196edd08453a5d4", size = 117918, upload-time = "2026-05-10T18:15:33.678Z" }, + { url = "https://files.pythonhosted.org/packages/fe/87/2bf31fe17587b29e3f93ec31421e2b1e1c3e349b8bf6c7c313dbad1d5340/librt-0.11.0-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:93d95bd45b7d58343d8b90d904450a545144eec19a002511163426f8ab1fae29", size = 141092, upload-time = "2026-05-10T18:15:34.795Z" }, + { url = "https://files.pythonhosted.org/packages/cf/08/5c5bf772920b7ebac6e32bc91a643e0ab3870199c0b542356d3baa83970a/librt-0.11.0-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:4ee278c769a713638cdacd4c0436d72156e75df3ebc0166ab2b9dc43acc386c9", size = 142035, upload-time = "2026-05-10T18:15:36.242Z" }, + { url = "https://files.pythonhosted.org/packages/06/20/662a03d254e5b000d838e8b345d83303ddb768c080fd488e40634c0fa66b/librt-0.11.0-cp311-cp311-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:f230cb1cbc9faaa616f9a678f530ebcf186e414b6bcbd88b960e4ba1b92428d5", size = 475022, upload-time = "2026-05-10T18:15:37.56Z" }, + { url = "https://files.pythonhosted.org/packages/de/f3/aa81523e45184c6ec23dc7f63263362ec55f80a09d424c012359ecbe7e35/librt-0.11.0-cp311-cp311-manylinux2014_i686.manylinux_2_17_i686.manylinux_2_28_i686.whl", hash = "sha256:5d63c855d86938d9de93e265c9bd8c705b51ec494de5738340ee93767a686e4b", size = 467273, upload-time = "2026-05-10T18:15:39.182Z" }, + { url = "https://files.pythonhosted.org/packages/6b/6f/59c74b560ca8853834d5501d589c8a2519f4184f273a085ffd0f37a1cc47/librt-0.11.0-cp311-cp311-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:993f028be9e96a08d31df3479ac80d99be374d17f3b78e4796b3fd3c913d4e89", size = 497083, upload-time = "2026-05-10T18:15:40.634Z" }, + { url = "https://files.pythonhosted.org/packages/fe/7b/5aa4d2c9600a719401160bf7055417df0b2a47439b9d88286ce45e56b65f/librt-0.11.0-cp311-cp311-manylinux_2_34_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:258d73a0aa66a055e65b2e4d1b8cdb23b9d132c5bb915d9547d804fcaed116cc", size = 489139, upload-time = "2026-05-10T18:15:41.934Z" }, + { url = "https://files.pythonhosted.org/packages/d6/31/9143803d7da6856a69153785768c4936864430eec0fd9461c3ea527d9922/librt-0.11.0-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:0827efe7854718f04aaddf6496e96960a956e676fe1d0f04eb41511fd8ad06d5", size = 508442, upload-time = "2026-05-10T18:15:43.206Z" }, + { url = "https://files.pythonhosted.org/packages/2f/5a/bce08184488426bda4ccc2c4964ac048c8f68ae89bd7120082eef4233cfd/librt-0.11.0-cp311-cp311-musllinux_1_2_i686.whl", hash = "sha256:7753e57d6e12d019c0d8786f1c09c709f4c3fcc57c3887b24e36e6c06ec938b7", size = 514230, upload-time = "2026-05-10T18:15:44.761Z" }, + { url = "https://files.pythonhosted.org/packages/89/8c/bb5e213d254b7505a0e658da199d8ab719086632ce09eef311ab27976523/librt-0.11.0-cp311-cp311-musllinux_1_2_riscv64.whl", hash = "sha256:11bd19822431cc21af9f27374e7ae2e58103c7d98bda823536a6c47f6bb2bb3d", size = 494231, upload-time = "2026-05-10T18:15:46.308Z" }, + { url = "https://files.pythonhosted.org/packages/9d/fb/541cdad5b1ab1300398c74c4c9a497b88e5074c21b1244c8f49731d3a284/librt-0.11.0-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:22bdf239b219d3993761a148ffa134b19e52e9989c84f845d5d7b71d70a17412", size = 537585, upload-time = "2026-05-10T18:15:47.629Z" }, + { url = "https://files.pythonhosted.org/packages/8f/f2/464bb69295c320cb06bddb4f14a4ec67934ee14b2bffb12b19fb7ab287ba/librt-0.11.0-cp311-cp311-win32.whl", hash = "sha256:46c60b61e308eb535fbd6fa622b1ee1bb2815691c1ad9c98bf7b84952ec3bc8d", size = 100509, upload-time = "2026-05-10T18:15:49.157Z" }, + { url = "https://files.pythonhosted.org/packages/6d/e7/a17ee1788f9e4fbf548c19f4afa07c92089b9e24fef6cb2410863781ef4c/librt-0.11.0-cp311-cp311-win_amd64.whl", hash = "sha256:902e546ff044f579ff1c953ff5fce97b636fe9e3943996b2177710c6ef076f73", size = 118628, upload-time = "2026-05-10T18:15:50.345Z" }, + { url = "https://files.pythonhosted.org/packages/cc/c7/6c766214f9f9903bcfcfbef97d807af8d8f5aa3502d247858ab17582d212/librt-0.11.0-cp311-cp311-win_arm64.whl", hash = "sha256:65ac3bc20f78aa0ee5ae84baa68917f89fef4af63e941084dd019a0d0e749f0c", size = 103122, upload-time = "2026-05-10T18:15:52.068Z" }, + { url = "https://files.pythonhosted.org/packages/8b/d0/07c77e067f0838949b43bd89232c29d72efebb9d2801a9750184eb706b71/librt-0.11.0-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:b87504f1690a23b9a2cca841191a04f83895d4fc2dd04df91d82b1a04ca2ad46", size = 144147, upload-time = "2026-05-10T18:15:53.227Z" }, + { url = "https://files.pythonhosted.org/packages/7a/24/8493538fa4f62f982686398a5b8f68008138a75086abdea19ade64bf4255/librt-0.11.0-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:40071fc5fe0ce8daa6de616702314a01e1250711682b0523d6ab8d4525910cb3", size = 143614, upload-time = "2026-05-10T18:15:54.657Z" }, + { url = "https://files.pythonhosted.org/packages/ff/1e/f8bad050810d9171f34a1648ed910e56814c2ba61639f2bd53c6377ae24b/librt-0.11.0-cp312-cp312-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:137e79445c896a0ea7b265f52d23954e05b64222ee1af69e2cb34219067cbb67", size = 485538, upload-time = "2026-05-10T18:15:56.117Z" }, + { url = "https://files.pythonhosted.org/packages/c0/fe/3594ebfbaf03084ba4b120c9ba5c3183fd938a48725e9bbe6ff0a5159ad8/librt-0.11.0-cp312-cp312-manylinux2014_i686.manylinux_2_17_i686.manylinux_2_28_i686.whl", hash = "sha256:cca6644054e78746d8d4ef238681f9c34ff8b584fe6b988ecebb8db3b15e622a", size = 479623, upload-time = "2026-05-10T18:15:57.544Z" }, + { url = "https://files.pythonhosted.org/packages/b0/da/5d1876984b3746c85dbd219dbfcb73c85f54ee263fd32e5b2a632ec14571/librt-0.11.0-cp312-cp312-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:d5b0eea49f5562861ee8d757a32ef7d559c1d35be2aaaa1ec28941d74c9ffc8a", size = 513082, upload-time = "2026-05-10T18:15:58.805Z" }, + { url = "https://files.pythonhosted.org/packages/19/6e/55bdf5d5ca00c3e18430690bf2c953d8d3ffd3c337418173d33dec985dc9/librt-0.11.0-cp312-cp312-manylinux_2_34_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:0d1029d7e1ae1a7e647ed6fb5df8c4ce2dffefb7a9f5fd1376a4554d96dac09f", size = 508105, upload-time = "2026-05-10T18:16:00.2Z" }, + { url = "https://files.pythonhosted.org/packages/07/10/f1f23a7c595ee90ece4d35c851e5d104b1311a887ed1b4ac4c35bbd13da8/librt-0.11.0-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:bc3ce6b33c5828d9e80592011a5c584cb2ce86edbc4088405f70da47dc1d1b3b", size = 522268, upload-time = "2026-05-10T18:16:01.708Z" }, + { url = "https://files.pythonhosted.org/packages/b6/02/5720f5697a7f54b78b3aefbe20df3a48cedcff1276618c4aa481177942ed/librt-0.11.0-cp312-cp312-musllinux_1_2_i686.whl", hash = "sha256:936c5995f3514a42111f20099397d8177c79b4d7e70961e396c6f5a0a3566766", size = 527348, upload-time = "2026-05-10T18:16:03.496Z" }, + { url = "https://files.pythonhosted.org/packages/50/db/b4a47c6f91db4ff76348a0b3dd0cc65e090a078b765a810a62ff9434c3d3/librt-0.11.0-cp312-cp312-musllinux_1_2_riscv64.whl", hash = "sha256:9bc0ca6ad9381cbe8e4aa6e5726e4c80c78115a6e9723c599ed1d73e092bc49d", size = 516294, upload-time = "2026-05-10T18:16:05.173Z" }, + { url = "https://files.pythonhosted.org/packages/9e/58/9384b2f4eb1ed1d273d40948a7c5c4b2360213b402ef3be4641c06299f9c/librt-0.11.0-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:070aa8c26c0a74774317a72df8851facc7f0f012a5b406557ac56992d92e1ec8", size = 553608, upload-time = "2026-05-10T18:16:06.839Z" }, + { url = "https://files.pythonhosted.org/packages/21/7b/5aa8848a7c6a9278c79375146da1812e695754ceec5f005e6043461a7315/librt-0.11.0-cp312-cp312-win32.whl", hash = "sha256:6bf14feb84b05ae945277395451998c89c54d0def4070eb5c08de544930b245a", size = 101879, upload-time = "2026-05-10T18:16:08.103Z" }, + { url = "https://files.pythonhosted.org/packages/37/33/8a745436944947575b584231750a41417de1a38cf6a2e9251d1065651c09/librt-0.11.0-cp312-cp312-win_amd64.whl", hash = "sha256:75672f0bc524ede266287d532d7923dbce94c7514ad07627bac3d0c6d92cc4d9", size = 119831, upload-time = "2026-05-10T18:16:09.174Z" }, + { url = "https://files.pythonhosted.org/packages/59/67/a6739ac96e28b7855808bdb0370e250606104a859750d209e5a0716fe7ab/librt-0.11.0-cp312-cp312-win_arm64.whl", hash = "sha256:2f10cf143e4a9bb0f4f5af568a00df94a2d69ef41c2579584454bb0fe5cc642c", size = 103470, upload-time = "2026-05-10T18:16:10.369Z" }, + { url = "https://files.pythonhosted.org/packages/82/61/e59168d4d0bf2bf90f4f0caf7a001bfc60254c3af4586013b04dc3ef517b/librt-0.11.0-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:78dc31f7fdfe9c9d0eb0e8f42d139db230e826415bbcabd9f0e9faaaee909894", size = 144119, upload-time = "2026-05-10T18:16:11.771Z" }, + { url = "https://files.pythonhosted.org/packages/61/fd/caa1d60b12f7dd79ccea23054e06eeaebe266a5f52c40a6b651069200ce5/librt-0.11.0-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:fa475675db22290c3158e1d42326d0f5a65f04f44a0e68c3630a25b53560fb9c", size = 143565, upload-time = "2026-05-10T18:16:13.334Z" }, + { url = "https://files.pythonhosted.org/packages/b8/a9/dc744f5c2b4978d48db970be29f22716d3413d28b14ad99740817315cf2c/librt-0.11.0-cp313-cp313-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:621db29691044bdeda22e789e482e1b0f3a985d90e3426c9c6d17606416205ea", size = 485395, upload-time = "2026-05-10T18:16:14.729Z" }, + { url = "https://files.pythonhosted.org/packages/8f/21/7f8e97a1e4dae952a5a95948f6f8507a173bc1e669f54340bba6ca1ca31b/librt-0.11.0-cp313-cp313-manylinux2014_i686.manylinux_2_17_i686.manylinux_2_28_i686.whl", hash = "sha256:a9010e2ed5b3a9e158c5fd966b3ab7e834bb3d3aacc8f66c91dd4b57a3799230", size = 479383, upload-time = "2026-05-10T18:16:16.321Z" }, + { url = "https://files.pythonhosted.org/packages/a6/6d/d8ee9c114bebf2c50e29ec2aa940826fccb62a645c3e4c18760987d0e16d/librt-0.11.0-cp313-cp313-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:7c39513d8b7477a2e1ed8c43fc21c524e8d5a0f8d4e8b7b074dbdbe7820a08e2", size = 513010, upload-time = "2026-05-10T18:16:17.647Z" }, + { url = "https://files.pythonhosted.org/packages/f0/43/0b5708af2bd30a46400e72ba6bdaa8f066f15fb9a688527e34220e8d6c06/librt-0.11.0-cp313-cp313-manylinux_2_34_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:7aef3cf1d5af86e770ab04bfd993dfc4ae8b8c17f66fb77dd4a7d50de7bbb1a3", size = 508433, upload-time = "2026-05-10T18:16:19.309Z" }, + { url = "https://files.pythonhosted.org/packages/4a/50/356187247d09013490481033183b3532b58acf8028bcb34b2b56a375c9b2/librt-0.11.0-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:557183ddc36babe46b27dd60facbd5adb4492181a5be887587d57cda6e092f21", size = 522595, upload-time = "2026-05-10T18:16:20.642Z" }, + { url = "https://files.pythonhosted.org/packages/40/e7/c6ac4240899c7f3248079d5a9900debe0dadb3fdeaf856684c987105ba47/librt-0.11.0-cp313-cp313-musllinux_1_2_i686.whl", hash = "sha256:83d3e1f72bd42f6c5c0b7daec530c3f829bd02db42c70b8ddf0c2d90a2459930", size = 527255, upload-time = "2026-05-10T18:16:22.352Z" }, + { url = "https://files.pythonhosted.org/packages/eb/b5/a81322dbeedeeaf9c1ee6f001734d28a09d8383ac9e6779bc24bbd0743c6/librt-0.11.0-cp313-cp313-musllinux_1_2_riscv64.whl", hash = "sha256:4ce1f21fbe589bc1afd7872dece84fb0e1144f794a288e58a10d2c54a55c43be", size = 516847, upload-time = "2026-05-10T18:16:23.627Z" }, + { url = "https://files.pythonhosted.org/packages/ae/66/6e6323787d592b55204a42595ff1102da5115601b53a7e9ddebc889a6da5/librt-0.11.0-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:970b09f7044ea2b64c9da42fd3d335666518cfd1c6e8a182c95da73d0214b41e", size = 553920, upload-time = "2026-05-10T18:16:25.025Z" }, + { url = "https://files.pythonhosted.org/packages/9c/21/623f8ca230857102066d9ca8c6c1734995908c4d0d1bee7bb2ef0021cb33/librt-0.11.0-cp313-cp313-win32.whl", hash = "sha256:78fddc31cd4d3caa897ad5d31f856b1faadc9474021ad6cb182b9018793e254e", size = 101898, upload-time = "2026-05-10T18:16:26.649Z" }, + { url = "https://files.pythonhosted.org/packages/b3/1d/b4ebd44dd723f768469007515cb92251e0ae286c94c140f374801140fa74/librt-0.11.0-cp313-cp313-win_amd64.whl", hash = "sha256:8ca8aa88751a775870b764e93bad5135385f563cb8dcee399abf034ea4d3cb47", size = 119812, upload-time = "2026-05-10T18:16:27.859Z" }, + { url = "https://files.pythonhosted.org/packages/3b/e4/b2f4ca7965ca373b491cdb4bc25cdb30c1649ca81a8782056a83850292a9/librt-0.11.0-cp313-cp313-win_arm64.whl", hash = "sha256:96f044bb325fd9cf1a723015638c219e9143f0dfbc0ca54c565df2b7fc748b44", size = 103448, upload-time = "2026-05-10T18:16:29.066Z" }, + { url = "https://files.pythonhosted.org/packages/29/eb/dbce197da4e227779e56b5735f2decc3eb36e55a1cdbf1bd65d6639d76c1/librt-0.11.0-cp314-cp314-macosx_10_13_x86_64.whl", hash = "sha256:4a017a95e5837dc15a8c5661d60e05daa96b90908b1aa6b7acdf443cd25c8ebd", size = 143345, upload-time = "2026-05-10T18:16:30.674Z" }, + { url = "https://files.pythonhosted.org/packages/76/a3/254bebd0c11c8ba684018efb8006ff22e466abce445215cca6c778e7d9de/librt-0.11.0-cp314-cp314-macosx_11_0_arm64.whl", hash = "sha256:b1ecbd9819deccc39b7542bf4d2a740d8a620694d39989e58661d3763458f8d4", size = 143131, upload-time = "2026-05-10T18:16:32.037Z" }, + { url = "https://files.pythonhosted.org/packages/f1/3f/f77d6122d21ac7bf6ae8a7dfced1bd2a7ac545d3273ebdcaf8042f6d619f/librt-0.11.0-cp314-cp314-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:7da327dacd7be8f8ec36547373550744a3cc0e536d54665cd83f8bcd961200e8", size = 477024, upload-time = "2026-05-10T18:16:33.493Z" }, + { url = "https://files.pythonhosted.org/packages/ac/0a/2c996dadebaa7d9bbbd43ef2d4f3e66b6da545f838a41694ef6172cebec8/librt-0.11.0-cp314-cp314-manylinux2014_i686.manylinux_2_17_i686.manylinux_2_28_i686.whl", hash = "sha256:0dc56b1f8d06e60db362cc3fdae206681817f86ce4725d34511473487f12a34b", size = 474221, upload-time = "2026-05-10T18:16:34.864Z" }, + { url = "https://files.pythonhosted.org/packages/0a/7e/f5d92af8486b8272c23b3e686b46ff72d89c8169585eb61eef01a2ac7147/librt-0.11.0-cp314-cp314-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:05fb8fb2ab90e21c8d12ea240d744ad514da9baf381ebfa70d91d20d21713175", size = 505174, upload-time = "2026-05-10T18:16:36.705Z" }, + { url = "https://files.pythonhosted.org/packages/af/1a/cb0734fe86398eb33193ab753b7326255c74cac5eb09e76b9b16536e7adb/librt-0.11.0-cp314-cp314-manylinux_2_34_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:cae74872be221df4374d10fec61f93ed1513b9546ea84f2c0bf73ab3e9bd0b03", size = 497216, upload-time = "2026-05-10T18:16:38.418Z" }, + { url = "https://files.pythonhosted.org/packages/18/06/094820f91558b66e29943c0ec41c9914f460f48dd51fc503c3101e10842d/librt-0.11.0-cp314-cp314-musllinux_1_2_aarch64.whl", hash = "sha256:32bcc918c0148eb7e3d57385125bac7e5f9e4359d05f07448b09f6f778c2f31c", size = 513921, upload-time = "2026-05-10T18:16:39.848Z" }, + { url = "https://files.pythonhosted.org/packages/0b/c2/00de9018871a282f530cacb457d5ec0428f6ac7e6fedde9aff7468d9fb04/librt-0.11.0-cp314-cp314-musllinux_1_2_i686.whl", hash = "sha256:f9743fc99135d5f78d2454435615f6dec0473ca507c26ce9d92b10b562a280d3", size = 520850, upload-time = "2026-05-10T18:16:41.471Z" }, + { url = "https://files.pythonhosted.org/packages/51/9d/64631832348fd1834fb3a61b996434edddaaf25a31d03b0a76273159d2cf/librt-0.11.0-cp314-cp314-musllinux_1_2_riscv64.whl", hash = "sha256:5ba067f4aadae8fda802d91d2124c90c42195ff32d9161d3549e6d05cfe26f96", size = 504237, upload-time = "2026-05-10T18:16:43.15Z" }, + { url = "https://files.pythonhosted.org/packages/a5/ec/ae5525eb16edc827a044e7bb8777a455ff95d4bca9379e7e6bddd7383647/librt-0.11.0-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:de3bf945454d032f9e390b85c4072e0a0570bf825421c8be0e71209fa65e1abe", size = 546261, upload-time = "2026-05-10T18:16:44.408Z" }, + { url = "https://files.pythonhosted.org/packages/5a/09/adce371f27ca039411da9659f7430fcc2ba6cd0c7b3e4467a0f091be7fa9/librt-0.11.0-cp314-cp314-win32.whl", hash = "sha256:d2277a05f6dcb9fd13db9566aac4fabd68c3ea1ea46ee5567d4eef8efa495a2f", size = 96965, upload-time = "2026-05-10T18:16:46.039Z" }, + { url = "https://files.pythonhosted.org/packages/d6/ee/8ac720d98548f173c7ce2e632a7ca94673f74cacd5c8162a84af5b35958a/librt-0.11.0-cp314-cp314-win_amd64.whl", hash = "sha256:ab73e8db5e3f564d812c1f5c3a175930a5f9bc96ccb5e3b22a34d7858b401cf7", size = 115151, upload-time = "2026-05-10T18:16:47.133Z" }, + { url = "https://files.pythonhosted.org/packages/94/20/c900cf14efeb09b6bef2b2dff20779f73464b97fd58d1c6bccc379588ae3/librt-0.11.0-cp314-cp314-win_arm64.whl", hash = "sha256:aea3caa317752e3a466fa8af45d91ee0ea8c7fdd96e42b0a8dd9b76a7931eba1", size = 98850, upload-time = "2026-05-10T18:16:48.597Z" }, + { url = "https://files.pythonhosted.org/packages/0c/71/944bfe4b64e12abffcd3c15e1cce07f72f3d55655083786285f4dedeb532/librt-0.11.0-cp314-cp314t-macosx_10_13_x86_64.whl", hash = "sha256:d1b36540d7aaf9b9101b3a6f376c8d8e9f7a9aec93ed05918f2c69d493ffef72", size = 151138, upload-time = "2026-05-10T18:16:49.839Z" }, + { url = "https://files.pythonhosted.org/packages/b6/10/99e64a5c86989357fda078c8143c533389585f6473b7439172dd8f3b3b2d/librt-0.11.0-cp314-cp314t-macosx_11_0_arm64.whl", hash = "sha256:efbb343ab2ce3540f4ecbe6315d677ed70f37cd9a72b1e58066c918ca83acbaa", size = 151976, upload-time = "2026-05-10T18:16:51.062Z" }, + { url = "https://files.pythonhosted.org/packages/21/31/5072ad880946d83e5ea4147d6d018c78eefce85b77819b19bdd0ee229435/librt-0.11.0-cp314-cp314t-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:aa0dd688aab3f7914d3e6e5e3554978e0383312fb8e771d84be008a35b9ee548", size = 557927, upload-time = "2026-05-10T18:16:52.632Z" }, + { url = "https://files.pythonhosted.org/packages/5e/8d/70b5fb7cfbab60edbe7381614ab985da58e144fbf465c86d44c95f43cdca/librt-0.11.0-cp314-cp314t-manylinux2014_i686.manylinux_2_17_i686.manylinux_2_28_i686.whl", hash = "sha256:f5fb36b8c6c63fdcbb1d526d94c0d1331610d43f4118cc1beb4efef4f3faacb2", size = 539698, upload-time = "2026-05-10T18:16:53.934Z" }, + { url = "https://files.pythonhosted.org/packages/fa/a3/ba3495a0b3edbd24a4cae0d1d3c64f39a9fc45d06e812101289b50c1a619/librt-0.11.0-cp314-cp314t-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:4a9a237d13addb93715b6fee74023d5ee3469b53fce527626c0e088aa585805f", size = 577162, upload-time = "2026-05-10T18:16:55.589Z" }, + { url = "https://files.pythonhosted.org/packages/f7/db/36e25fb81f99937ff1b96612a1dc9fd66f039cb9cc3aee12c01fac31aab9/librt-0.11.0-cp314-cp314t-manylinux_2_34_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:5ddd17bd87b2c56ddd60e546a7984a2e64c4e8eab92fb4cf3830a48ad5469d51", size = 566494, upload-time = "2026-05-10T18:16:56.975Z" }, + { url = "https://files.pythonhosted.org/packages/33/0d/3f622b47f0b013eeb9cf4cc07ae9bfe378d832a4eec998b2b209fe84244d/librt-0.11.0-cp314-cp314t-musllinux_1_2_aarch64.whl", hash = "sha256:bd43992b4473d42f12ff9e68326079f0696d9d4e6000e8f39a0238d482ba6ee2", size = 596858, upload-time = "2026-05-10T18:16:58.374Z" }, + { url = "https://files.pythonhosted.org/packages/a9/02/71b90bc93039c46a2000651f6ad60122b114c8f54c4ad306e0e96f5b75ad/librt-0.11.0-cp314-cp314t-musllinux_1_2_i686.whl", hash = "sha256:f8e3e8056dd674e279741485e2e512d6e9a751c7455809d0114e6ebf8d781085", size = 590318, upload-time = "2026-05-10T18:16:59.676Z" }, + { url = "https://files.pythonhosted.org/packages/04/04/418cb3f75621e2b761fb1ab0f017f4d70a1a72a6e7c74ee4f7e8d198c2f3/librt-0.11.0-cp314-cp314t-musllinux_1_2_riscv64.whl", hash = "sha256:c1f708d8ae9c56cf38a903c44297243d2ec83fd82b396b977e0144a3e76217e3", size = 575115, upload-time = "2026-05-10T18:17:01.007Z" }, + { url = "https://files.pythonhosted.org/packages/cc/2c/5a2183ac58dd911f26b5d7e7d7d8f1d87fcecdddd99d6c12169a258ff62c/librt-0.11.0-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:0add982e0e7b9fc14cf4b33789d5f13f66581889b88c2f58099f6ce8f92617bd", size = 617918, upload-time = "2026-05-10T18:17:02.682Z" }, + { url = "https://files.pythonhosted.org/packages/15/1f/dc6771a52592a4451be6effa200cbfc9cec61e4393d3033d81a9d307961d/librt-0.11.0-cp314-cp314t-win32.whl", hash = "sha256:2b481d846ac894c4e8403c5fd0e87c5d11d6499e404b474602508a224ff531c8", size = 103562, upload-time = "2026-05-10T18:17:03.99Z" }, + { url = "https://files.pythonhosted.org/packages/62/4a/7d1415567027286a75ba1093ec4aca11f073e0f559c530cf3e0a757ad55c/librt-0.11.0-cp314-cp314t-win_amd64.whl", hash = "sha256:28edb433edde181112a908c78907af28f964eabc15f4dd16c9d66c834302677c", size = 124327, upload-time = "2026-05-10T18:17:05.465Z" }, + { url = "https://files.pythonhosted.org/packages/ce/62/b40b382fa0c66fee1478073eb8db352a4a6beda4a1adccf1df911d8c289c/librt-0.11.0-cp314-cp314t-win_arm64.whl", hash = "sha256:dee008f20b542e3cd162ba338a7f9ec0f6d23d395f66fe8aeeec3c9d067ea253", size = 102572, upload-time = "2026-05-10T18:17:06.809Z" }, +] + +[[package]] +name = "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 = "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 = "mypy" +version = "2.1.0" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "ast-serialize" }, + { name = "librt", marker = "platform_python_implementation != 'PyPy'" }, + { name = "mypy-extensions" }, + { name = "pathspec" }, + { name = "tomli", marker = "python_full_version < '3.11'" }, + { name = "typing-extensions" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/82/15/cca9d88503549ed6fedeaa1d448cdddd542ee8a490232d732e278036fbf2/mypy-2.1.0.tar.gz", hash = "sha256:81e76ad12c2d804512e9b13240d1588316531bfba07558286078bfbce9613633", size = 3898359, upload-time = "2026-05-11T18:37:36.237Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/a4/71/d351dca3e9b30da2328ee9d445c88b8388072808ebfbc49eb69d30b67749/mypy-2.1.0-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:11a6beb180257a805961aea9ec591bbd0bd17f1e18d35b8456d57aee5bedfedc", size = 14778792, upload-time = "2026-05-11T18:36:23.605Z" }, + { url = "https://files.pythonhosted.org/packages/2f/45/7d51594b644c17c0bcf74ed8cd5fc33b324276d708e8506f220b70dab9d9/mypy-2.1.0-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:8ef78c1d306bbf9a8a12f526c44902c9c28dffd6c52c52bf6a72641ce18d3849", size = 13645739, upload-time = "2026-05-11T18:37:22.752Z" }, + { url = "https://files.pythonhosted.org/packages/65/01/455c31b170e9468265074840bf18863a8482a24103fdaabe4e199392aa5f/mypy-2.1.0-cp310-cp310-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:c209a90853081ff01d01ee895cafe10f7db1474e0d95beaeef0f6c1db9119bbd", size = 14074199, upload-time = "2026-05-11T18:35:09.292Z" }, + { url = "https://files.pythonhosted.org/packages/41/5a/93093f0b29a9e982deafde698f740a2eb2e05886e79ccf0594c7fd5413a3/mypy-2.1.0-cp310-cp310-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:47cebf61abde7c088a4e27718a8b13a81655686b2e9c251f5c0915a802248166", size = 14953128, upload-time = "2026-05-11T18:31:57.678Z" }, + { url = "https://files.pythonhosted.org/packages/7f/2f/a196f5331d96170ad3d28f144d2aba690d4b2911381f68d51e489c7ab82a/mypy-2.1.0-cp310-cp310-musllinux_1_2_x86_64.whl", hash = "sha256:d57a90ae5e872138a425ec328edbc9b235d1934c4377881a33ec05b341acc9a8", size = 15249378, upload-time = "2026-05-11T18:33:00.101Z" }, + { url = "https://files.pythonhosted.org/packages/54/de/94d321cc12da9f71341ac0c270efbed5c725750c7b4c334d957de9a087d9/mypy-2.1.0-cp310-cp310-win_amd64.whl", hash = "sha256:aea7f7a8a55b459c34275fc468ada6ca7c173a5e43a68f5dbe588a563d8a06b8", size = 11060994, upload-time = "2026-05-11T18:33:18.848Z" }, + { url = "https://files.pythonhosted.org/packages/e1/62/0c27ca55219a7c764a7fb88c7bb2b7b2f9780ade8bbf16bc8ed8400eef6b/mypy-2.1.0-cp310-cp310-win_arm64.whl", hash = "sha256:c989640253f0d76843e9c6c1bbf4bd48c5e85ada61bde4beb37cb3eca035685e", size = 9976743, upload-time = "2026-05-11T18:31:25.554Z" }, + { url = "https://files.pythonhosted.org/packages/0a/a1/639f3024794a2a15899cb90707fe02e044c4412794c39c5769fd3df2e2ef/mypy-2.1.0-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:a683016b16fe2f572dc04c72be7ee0504ac1605a265d0200f5cea695fb788f41", size = 14691685, upload-time = "2026-05-11T18:33:27.973Z" }, + { url = "https://files.pythonhosted.org/packages/3b/08/9a585dea4325f20d8b80dc78623fa50d1fd2173b710f6237afd6ba6ab39b/mypy-2.1.0-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:1a293c534adb55271fef24a26da04b855540a8c13cc07bc5917b9fd2c394f2ca", size = 13555165, upload-time = "2026-05-11T18:32:16.107Z" }, + { url = "https://files.pythonhosted.org/packages/81/dc/7c42cc9c6cb01e8eb09961f1f738741d3e9c7e9d5c5b30ec69222625cd5f/mypy-2.1.0-cp311-cp311-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:7406f4d048e71e576f5356d317e5b0a9e666dfd966bd99f9d14ca06e1a341538", size = 13994376, upload-time = "2026-05-11T18:32:39.256Z" }, + { url = "https://files.pythonhosted.org/packages/d4/fa/285946c33bce716e082c11dfeee9ee196eaf1f5042efb3581a31f9f205e4/mypy-2.1.0-cp311-cp311-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:e0210d626fc8b31ccc90233754c7bc90e1f43205e85d96387f7db1285b55c398", size = 14864618, upload-time = "2026-05-11T18:34:49.765Z" }, + { url = "https://files.pythonhosted.org/packages/2b/83/82397f48af6c27e295d57979ded8490c9829040152cf7571b2f026aeb9a0/mypy-2.1.0-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:3712c20deed54e814eaaa825603bada8ea1c390670a397c95b98405347acc563", size = 15102063, upload-time = "2026-05-11T18:34:05.855Z" }, + { url = "https://files.pythonhosted.org/packages/40/68/b02dec39057b88eb03dc0aa854732e26e8361f34f9d0e20c7614967d1eba/mypy-2.1.0-cp311-cp311-win_amd64.whl", hash = "sha256:fcaa0e479066e31f7cceb6a3bea39cb22b2ff51a6b2f24f193d19179ba17c389", size = 11060564, upload-time = "2026-05-11T18:35:36.494Z" }, + { url = "https://files.pythonhosted.org/packages/cf/a8/ea3dcbef31f99b634f2ee23bb0321cbc8c1b388b76a861eb849f13c347dc/mypy-2.1.0-cp311-cp311-win_arm64.whl", hash = "sha256:0b1a5260c95aa443083f9ed3592662941951bca3d4ca224a5dc517c38b7cf666", size = 9966983, upload-time = "2026-05-11T18:37:14.139Z" }, + { url = "https://files.pythonhosted.org/packages/95/b1/55861beb5c339b44f9a2ba92df9e2cb1eeb4ae1eee674cdf7772c797778b/mypy-2.1.0-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:244358bf1c0da7722230bce60683d52e8e9fd030554926f15b747a84efb5b3af", size = 14874381, upload-time = "2026-05-11T18:37:31.784Z" }, + { url = "https://files.pythonhosted.org/packages/0b/b3/b7f770114b7d0ac92d0f76e8d93c2780844a70488a90e91821927850da86/mypy-2.1.0-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:4ec7c57657493c7a75534df2751c8ae2cda383c16ecc55d2106c54476b1b16f6", size = 13665501, upload-time = "2026-05-11T18:34:23.063Z" }, + { url = "https://files.pythonhosted.org/packages/b6/f3/8ae2037967e2126689a0c11d99e2b707134a565191e92c60ca2572aec60a/mypy-2.1.0-cp312-cp312-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:d8161b6ff4392410023224f0969d17db93e1e154bc3e4ba62598e720723ae211", size = 14045750, upload-time = "2026-05-11T18:31:48.151Z" }, + { url = "https://files.pythonhosted.org/packages/a0/32/615eb5911859e43d054941b0d0a7d06cfa2870eba86529cf385b052b111c/mypy-2.1.0-cp312-cp312-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:bf03e12003084a67395184d3eb8cbd6a489dc3655b5664b28c210a9e2403ab0b", size = 15061630, upload-time = "2026-05-11T18:37:06.898Z" }, + { url = "https://files.pythonhosted.org/packages/d4/03/4eafbfff8bfab1b87082741eae6e6a624028c984e6708b73bce2a8570c9d/mypy-2.1.0-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:20509760fd791c51579d573153407d226385ec1f8bcce55d730b354f3336bc22", size = 15288831, upload-time = "2026-05-11T18:31:18.07Z" }, + { url = "https://files.pythonhosted.org/packages/99/ee/919661478e5891a3c96e549c036e467e64563ab85995b10c53c8358e16a3/mypy-2.1.0-cp312-cp312-win_amd64.whl", hash = "sha256:6753d0c1fdd6b1a23b9e4f283ce80b2153b724adcb2653b20b85a8a28ac6436b", size = 11135228, upload-time = "2026-05-11T18:34:31.23Z" }, + { url = "https://files.pythonhosted.org/packages/24/0a/6a12b9782ca0831a553192f351679f4548abc9d19a7cc93bb7feb02084c7/mypy-2.1.0-cp312-cp312-win_arm64.whl", hash = "sha256:98ebb6589bb3b6d0c6f0c459d53ca55b8091fbc13d277c4041c885392e8195e8", size = 10040684, upload-time = "2026-05-11T18:36:48.199Z" }, + { url = "https://files.pythonhosted.org/packages/6e/dd/c7191469c777f07689c032a8f7326e393ea34c92d6d76eb7ce5ba57ea66d/mypy-2.1.0-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:35aac3bb114e03888f535d5eb51b8bafbb3266586b599da1940f9b1be3ec5bd5", size = 14852174, upload-time = "2026-05-11T18:31:38.929Z" }, + { url = "https://files.pythonhosted.org/packages/55/8c/aed55408879043d72bb9135f4d0d19a02b886dd569631e113e3d2706cb8d/mypy-2.1.0-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:8de55a8c861f2a49331f807be98d90caeceeef520bde13d43a160207f8af613e", size = 13651542, upload-time = "2026-05-11T18:36:04.636Z" }, + { url = "https://files.pythonhosted.org/packages/3a/8e/f371a824b1f1fa8ea6e3dbb8703d232977d572be2329554a3bc4d960302f/mypy-2.1.0-cp313-cp313-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:5fdf2941a07434af755837d9880f7d7d25f1dacb1af9dcd4b9b66f2220a3024e", size = 14033929, upload-time = "2026-05-11T18:35:55.742Z" }, + { url = "https://files.pythonhosted.org/packages/94/21/f54be870d6dd53a82c674407e0f8eed7174b05ec78d42e5abd7b42e84fd5/mypy-2.1.0-cp313-cp313-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:e195b817c13f02352a9c124301f9f30f078405444679b6753c1b96b6eed37285", size = 15039200, upload-time = "2026-05-11T18:33:10.281Z" }, + { url = "https://files.pythonhosted.org/packages/17/99/bf21748626a40ce59fd29a39386ab46afec88b7bd2f0fa6c3a97c995523f/mypy-2.1.0-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:5431d42af987ebd92ba2f71d45c85ed41d8e6ca9f5fd209a69f68f707d2469e5", size = 15272690, upload-time = "2026-05-11T18:32:07.205Z" }, + { url = "https://files.pythonhosted.org/packages/d6/d7/9e90d2cf47100bea550ed2bc7b0d4de3a62181d84d5e37da0003e8462637/mypy-2.1.0-cp313-cp313-win_amd64.whl", hash = "sha256:767fe8c66dc3e01e19e1737d4c38ebefead16125e1b8e58ad421903b376f5c65", size = 11147435, upload-time = "2026-05-11T18:33:56.477Z" }, + { url = "https://files.pythonhosted.org/packages/ec/46/e5c449e858798e35ffc90946282a27c62a77be743fe17480e4977374eb91/mypy-2.1.0-cp313-cp313-win_arm64.whl", hash = "sha256:ecfe70d43775ab99562ab128ce49854a362044c9f894961f68f898c23cb7429d", size = 10035052, upload-time = "2026-05-11T18:32:30.049Z" }, + { url = "https://files.pythonhosted.org/packages/b0/ca/b279a672e874aedd5498ae25f722dacc8aa86bbffb939b3f97cbb1cf6686/mypy-2.1.0-cp314-cp314-macosx_10_15_x86_64.whl", hash = "sha256:7354c5a7f69d9345c3d6e69921d57088eea3ddeeb6b20d34c1b3855b02c36ec2", size = 14848422, upload-time = "2026-05-11T18:35:45.984Z" }, + { url = "https://files.pythonhosted.org/packages/27/e6/3efe56c631d959b9b4454e208b0ac4b7f4f58b404c89f8bec7b49efdfc21/mypy-2.1.0-cp314-cp314-macosx_11_0_arm64.whl", hash = "sha256:49890d4f76ac9e06ec117f9e09f3174da70a620a0c300953d8595c926e80947f", size = 13677374, upload-time = "2026-05-11T18:36:57.188Z" }, + { url = "https://files.pythonhosted.org/packages/84/7f/8107ea87a44fd1f1b59882442f033c9c3488c127201b1d1d15f1cbd6022e/mypy-2.1.0-cp314-cp314-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:761be68e023ef5d94678772396a8af1220030f80837a3afd8d0aef3b419666f4", size = 14055743, upload-time = "2026-05-11T18:35:18.361Z" }, + { url = "https://files.pythonhosted.org/packages/51/4d/b6d34db183133b83761b9199a82d31557cdbb70a380d8c3b3438e11882a3/mypy-2.1.0-cp314-cp314-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:c90345fc182dc363b891350457ec69c35140858538f38b4540845afcc32b1aef", size = 15020937, upload-time = "2026-05-11T18:34:59.618Z" }, + { url = "https://files.pythonhosted.org/packages/ff/d7/f08360c691d758acb02f45022c34d98b92892f4ea756644e1000d4b9f3d8/mypy-2.1.0-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:b84802e7b5a6daf1f5e15bc9fcd7ddae77be13981ffab037f1c67bb84d67d135", size = 15253371, upload-time = "2026-05-11T18:36:41.081Z" }, + { url = "https://files.pythonhosted.org/packages/67/1b/09460a13719530a19bce27bd3bc8449e83569dd2ba7faf51c9c3c30c0b61/mypy-2.1.0-cp314-cp314-win_amd64.whl", hash = "sha256:022c771234936ceac541ebaf836fe9e2abeb3f5e09aff21588fe543ff006fe21", size = 11326429, upload-time = "2026-05-11T18:34:13.526Z" }, + { url = "https://files.pythonhosted.org/packages/40/62/75dbf0f82f7b6680340efc614af29dd0b3c17b8a4f1cd09b8bd2fd6bc814/mypy-2.1.0-cp314-cp314-win_arm64.whl", hash = "sha256:498207db725cec88829a6a5c2fc771205fd043719ef98bc49aba8fb9fc4e6d57", size = 10218799, upload-time = "2026-05-11T18:32:23.491Z" }, + { url = "https://files.pythonhosted.org/packages/b2/66/caca04ed7d972fb6eb6dd1ccd6df1de5c38fae8c5b3dc1c4e8e0d85ee6b9/mypy-2.1.0-cp314-cp314t-macosx_10_15_x86_64.whl", hash = "sha256:7d5e5cad0efeba72b93cd17490cc0d69c5ac9ca132994fe3fb0314808aeeb83e", size = 15923458, upload-time = "2026-05-11T18:35:28.64Z" }, + { url = "https://files.pythonhosted.org/packages/ed/52/2d90cbe49d014b13ed7ff337930c30bad35893fe38a1e4641e756bb62191/mypy-2.1.0-cp314-cp314t-macosx_11_0_arm64.whl", hash = "sha256:ff715050c127d724fd260a2e666e7747fdd83511c0c47d449d98238970aef780", size = 14757697, upload-time = "2026-05-11T18:36:14.208Z" }, + { url = "https://files.pythonhosted.org/packages/ac/37/d98f4a14e081b238992d0ed96b6d39c7cc0148c9699eb71eaa68629665ea/mypy-2.1.0-cp314-cp314t-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:82208da9e09414d520e912d3e462d454854bed0810b71540bb016dcbca7308fd", size = 15405638, upload-time = "2026-05-11T18:33:48.249Z" }, + { url = "https://files.pythonhosted.org/packages/a3/c2/15c46613b24a84fad2aea1248bf9619b99c2767ae9071fe224c179a0b7d4/mypy-2.1.0-cp314-cp314t-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:e79ebc1b904b84f0310dff7469655a9c36c7a68bddb37bdd42b67a332df61d08", size = 16215852, upload-time = "2026-05-11T18:32:50.296Z" }, + { url = "https://files.pythonhosted.org/packages/5c/90/9c16a57f482c76d25f6379762b56bbf65c711d8158cf271fb2802cfb0640/mypy-2.1.0-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:e583edc957cfb0deb142079162ae826f58449b116c1d442f2d91c69d9fced081", size = 16452695, upload-time = "2026-05-11T18:33:38.182Z" }, + { url = "https://files.pythonhosted.org/packages/0f/4c/215a4eeb63cacc5f17f516691ea7285d11e249802b942476bff15922a314/mypy-2.1.0-cp314-cp314t-win_amd64.whl", hash = "sha256:b33b6cd332695bba180d55e717a79d3038e479a2c49cc5eb3d53603409b9a5d7", size = 12866622, upload-time = "2026-05-11T18:34:39.945Z" }, + { url = "https://files.pythonhosted.org/packages/4b/50/1043e1db5f455ffe4c9ab22747cd8ca2bc492b1e4f4e21b130a44ee2b217/mypy-2.1.0-cp314-cp314t-win_arm64.whl", hash = "sha256:4f910fe825376a7b66ef7ca8c98e5a149e8cd64c19ae71d84047a74ee060d4e6", size = 10610798, upload-time = "2026-05-11T18:36:31.444Z" }, + { url = "https://files.pythonhosted.org/packages/0d/2a/13ca1f292f6db1b98ff495ef3467736b331621c5917cad984b7043e7348d/mypy-2.1.0-py3-none-any.whl", hash = "sha256:a663814603a5c563fb87a4f96fb473eeb30d1f5a4885afcf44f9db000a366289", size = 2693302, upload-time = "2026-05-11T18:31:29.246Z" }, +] + +[[package]] +name = "mypy-extensions" +version = "1.1.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/a2/6e/371856a3fb9d31ca8dac321cda606860fa4548858c0cc45d9d1d4ca2628b/mypy_extensions-1.1.0.tar.gz", hash = "sha256:52e68efc3284861e772bbcd66823fde5ae21fd2fdb51c62a211403730b916558", size = 6343, upload-time = "2025-04-22T14:54:24.164Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/79/7b/2c79738432f5c924bef5071f933bcc9efd0473bac3b4aa584a6f7c1c8df8/mypy_extensions-1.1.0-py3-none-any.whl", hash = "sha256:1be4cccdb0f2482337c4743e60421de3a356cd97508abadd57d47403e94f5505", size = 4963, upload-time = "2025-04-22T14:54:22.983Z" }, +] + +[[package]] +name = "packaging" +version = "25.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/a1/d4/1fc4078c65507b51b96ca8f8c3ba19e6a61c8253c72794544580a7b6c24d/packaging-25.0.tar.gz", hash = "sha256:d443872c98d677bf60f6a1f2f8c1cb748e8fe762d2bf9d3148b5599295b0fc4f", size = 165727, upload-time = "2025-04-19T11:48:59.673Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/20/12/38679034af332785aac8774540895e234f4d07f7545804097de4b666afd8/packaging-25.0-py3-none-any.whl", hash = "sha256:29572ef2b1f17581046b3a2227d5c611fb25ec70ca1ba8554b24b0e69331a484", size = 66469, upload-time = "2025-04-19T11:48:57.875Z" }, +] + +[[package]] +name = "pathspec" +version = "1.0.2" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/41/b9/6eb731b52f132181a9144bbe77ff82117f6b2d2fbfba49aaab2c014c4760/pathspec-1.0.2.tar.gz", hash = "sha256:fa32b1eb775ed9ba8d599b22c5f906dc098113989da2c00bf8b210078ca7fb92", size = 130502, upload-time = "2026-01-08T04:33:27.613Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/78/6b/14fc9049d78435fd29e82846c777bd7ed9c470013dc8d0260fff3ff1c11e/pathspec-1.0.2-py3-none-any.whl", hash = "sha256:62f8558917908d237d399b9b338ef455a814801a4688bc41074b25feefd93472", size = 54844, upload-time = "2026-01-08T04:33:26.4Z" }, +] + +[[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 = "prek" +version = "0.4.8" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/8e/46/e436a6eb9fdb4d3fd08d0ab7fdba19fe03a9e994ec810de57869b853bd8e/prek-0.4.8.tar.gz", hash = "sha256:d15d8bef72ab7b02c7dc01458ac9e05b3131534492b5ce9bb11c4f6f636fa868", size = 494570, upload-time = "2026-07-04T12:05:10.941Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/5c/78/b4149c8913ced2e42debb49e261c4788a1ce431e84226921c2e1a7ea8545/prek-0.4.8-py3-none-linux_armv6l.whl", hash = "sha256:1f8f8cdc65836b571824c965daebb81b449f7e4a43894c58621f5708d5a185ed", size = 5668955, upload-time = "2026-07-04T12:04:41.588Z" }, + { url = "https://files.pythonhosted.org/packages/76/5f/7f54a0087b6b2f1751aeb41266d9c15e66fd0055492814798ab818cd0414/prek-0.4.8-py3-none-macosx_10_12_x86_64.whl", hash = "sha256:bce1798e96d9e3a6e6abf435da7107e81452f69edb3ca7c6f90a457355ea46e2", size = 6030947, upload-time = "2026-07-04T12:04:43.8Z" }, + { url = "https://files.pythonhosted.org/packages/6c/d6/f2829fc3902920c36b764a386fa303e71a8219dac25cb3827c575e84199a/prek-0.4.8-py3-none-macosx_11_0_arm64.whl", hash = "sha256:ab3a52db17254d701c3cebb7eea58c8230aa7c1959aacfd5b5f25de18edb15d1", size = 5572593, upload-time = "2026-07-04T12:04:45.763Z" }, + { url = "https://files.pythonhosted.org/packages/74/8c/c5589955bcd5e3e33b67d8bc3110818cecac82a38fd6bc8b5dfdc5de421c/prek-0.4.8-py3-none-manylinux_2_17_aarch64.manylinux2014_aarch64.musllinux_1_1_aarch64.whl", hash = "sha256:b3fcfd620523bbc3f51a21d7cd63449f659b9e2cf3582de12dd5949e23227b8f", size = 5847150, upload-time = "2026-07-04T12:04:47.419Z" }, + { url = "https://files.pythonhosted.org/packages/2d/9d/1f2dc91bdb79d2c4714b27eac9477a51490fba5b4731330dbbebc76bd345/prek-0.4.8-py3-none-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:42e65bc8425e9d7f1691a13ca1da2e07807d1ba76c35740833354b945131689e", size = 5573738, upload-time = "2026-07-04T12:04:49.125Z" }, + { url = "https://files.pythonhosted.org/packages/81/29/69a7b58e16ecbc5f3989bf4b028018d11a82dcdd320b93d6588d72f32aa7/prek-0.4.8-py3-none-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:f578492a8e0c9bc6b4bf6dfbba8716f647d4cd0769bf10ad6cf336e3096fd392", size = 5981054, upload-time = "2026-07-04T12:04:50.842Z" }, + { url = "https://files.pythonhosted.org/packages/63/cc/9b9850a60c22ed18c7755ebd2d72c6eefb37fac58149d09f6adc4691c2cf/prek-0.4.8-py3-none-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:d4335f9d5beb123a3884a7fe34f57c9f0828f4fbb7666beab4298833459b104f", size = 6751350, upload-time = "2026-07-04T12:04:52.529Z" }, + { url = "https://files.pythonhosted.org/packages/01/e5/c425aa7272b430630119e6757def3a2007555ba8cbeb2630e0448e7a8b7f/prek-0.4.8-py3-none-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:18a8747df9c602e052881d3efb14dd7f7d62a59bd7277ae5171c9e7661d59d84", size = 6243881, upload-time = "2026-07-04T12:04:54.703Z" }, + { url = "https://files.pythonhosted.org/packages/1c/da/accd3ad07fd2891d3c2777eb42435439fdf11982c51d60f087c0b6b6e102/prek-0.4.8-py3-none-manylinux_2_28_aarch64.whl", hash = "sha256:4db639db481d5f854eff9b3d2108889e613b8c15868bcf6bdd777c7cee577436", size = 5848846, upload-time = "2026-07-04T12:04:56.402Z" }, + { url = "https://files.pythonhosted.org/packages/15/00/3477704635249f21f5f98ce444cd7690c2aa9dc8d146a045db88ef2cd8c5/prek-0.4.8-py3-none-manylinux_2_31_riscv64.whl", hash = "sha256:c3890a6f92316d2cf44eb50584e8d2b23a596dd70487022e61186a71a2ac0900", size = 5713942, upload-time = "2026-07-04T12:04:58.311Z" }, + { url = "https://files.pythonhosted.org/packages/fb/e6/3ca4fabaebeadc976d9a92d1d9130674265355ea3b728418bad61583b097/prek-0.4.8-py3-none-musllinux_1_1_armv7l.whl", hash = "sha256:fc7e15c24c591a37c6ffce5b25a021b16c299ac2649f183d812b67d665cd6551", size = 5554725, upload-time = "2026-07-04T12:04:59.96Z" }, + { url = "https://files.pythonhosted.org/packages/a5/46/2ab6aaaeff0cedb8955b2e4032071c8712382bdd423bb849718c3720180d/prek-0.4.8-py3-none-musllinux_1_1_i686.whl", hash = "sha256:36fe721704ff0c7624c1167639e23a5fe658bfd38c314f487219c9afd1eeb733", size = 5838595, upload-time = "2026-07-04T12:05:01.861Z" }, + { url = "https://files.pythonhosted.org/packages/ae/8b/91398f2b6cd1629d5d8ca8c85b08eca500814a374313b0193f4aaf6ab6c4/prek-0.4.8-py3-none-musllinux_1_1_x86_64.whl", hash = "sha256:162e544abc394a8124f3a4ad68efee116bad09440e679dbd1675177335c2a432", size = 6357222, upload-time = "2026-07-04T12:05:03.845Z" }, + { url = "https://files.pythonhosted.org/packages/b2/2a/ce5cbfaad36866134a21754640a05ecdba641fcd7ad15aa74cf3443f34f6/prek-0.4.8-py3-none-win32.whl", hash = "sha256:2602e46c8c5da7dfa69f60fcf88c2b57132ac623f49fb08bfb3094298c5f07e3", size = 5354388, upload-time = "2026-07-04T12:05:05.587Z" }, + { url = "https://files.pythonhosted.org/packages/df/03/3bc908bc5f7e430315553e47dfa055f19923a3888f9afe4da19f244b5cbf/prek-0.4.8-py3-none-win_amd64.whl", hash = "sha256:7cb22da60bee41b89c4978c0bea7126a3c0ccc003dae6748cf29b53947815edc", size = 5748221, upload-time = "2026-07-04T12:05:07.559Z" }, + { url = "https://files.pythonhosted.org/packages/dd/a7/4295e6d5f5028171dfeb115ad38ab76bf3fe0c8df91b70d73c79aa760a94/prek-0.4.8-py3-none-win_arm64.whl", hash = "sha256:da70057f577b15d4bd121bf9dd29ee205fd4b4d75a0cafba062e84d7e8b4378b", size = 5574425, upload-time = "2026-07-04T12:05:09.595Z" }, +] + +[[package]] +name = "pydantic" +version = "2.12.5" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "annotated-types" }, + { name = "pydantic-core" }, + { name = "typing-extensions" }, + { name = "typing-inspection" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/69/44/36f1a6e523abc58ae5f928898e4aca2e0ea509b5aa6f6f392a5d882be928/pydantic-2.12.5.tar.gz", hash = "sha256:4d351024c75c0f085a9febbb665ce8c0c6ec5d30e903bdb6394b7ede26aebb49", size = 821591, upload-time = "2025-11-26T15:11:46.471Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/5a/87/b70ad306ebb6f9b585f114d0ac2137d792b48be34d732d60e597c2f8465a/pydantic-2.12.5-py3-none-any.whl", hash = "sha256:e561593fccf61e8a20fc46dfc2dfe075b8be7d0188df33f221ad1f0139180f9d", size = 463580, upload-time = "2025-11-26T15:11:44.605Z" }, +] + +[package.optional-dependencies] +email = [ + { name = "email-validator" }, +] + +[[package]] +name = "pydantic-core" +version = "2.41.5" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "typing-extensions" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/71/70/23b021c950c2addd24ec408e9ab05d59b035b39d97cdc1130e1bce647bb6/pydantic_core-2.41.5.tar.gz", hash = "sha256:08daa51ea16ad373ffd5e7606252cc32f07bc72b28284b6bc9c6df804816476e", size = 460952, upload-time = "2025-11-04T13:43:49.098Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/c6/90/32c9941e728d564b411d574d8ee0cf09b12ec978cb22b294995bae5549a5/pydantic_core-2.41.5-cp310-cp310-macosx_10_12_x86_64.whl", hash = "sha256:77b63866ca88d804225eaa4af3e664c5faf3568cea95360d21f4725ab6e07146", size = 2107298, upload-time = "2025-11-04T13:39:04.116Z" }, + { url = "https://files.pythonhosted.org/packages/fb/a8/61c96a77fe28993d9a6fb0f4127e05430a267b235a124545d79fea46dd65/pydantic_core-2.41.5-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:dfa8a0c812ac681395907e71e1274819dec685fec28273a28905df579ef137e2", size = 1901475, upload-time = "2025-11-04T13:39:06.055Z" }, + { url = "https://files.pythonhosted.org/packages/5d/b6/338abf60225acc18cdc08b4faef592d0310923d19a87fba1faf05af5346e/pydantic_core-2.41.5-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:5921a4d3ca3aee735d9fd163808f5e8dd6c6972101e4adbda9a4667908849b97", size = 1918815, upload-time = "2025-11-04T13:39:10.41Z" }, + { url = "https://files.pythonhosted.org/packages/d1/1c/2ed0433e682983d8e8cba9c8d8ef274d4791ec6a6f24c58935b90e780e0a/pydantic_core-2.41.5-cp310-cp310-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:e25c479382d26a2a41b7ebea1043564a937db462816ea07afa8a44c0866d52f9", size = 2065567, upload-time = "2025-11-04T13:39:12.244Z" }, + { url = "https://files.pythonhosted.org/packages/b3/24/cf84974ee7d6eae06b9e63289b7b8f6549d416b5c199ca2d7ce13bbcf619/pydantic_core-2.41.5-cp310-cp310-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:f547144f2966e1e16ae626d8ce72b4cfa0caedc7fa28052001c94fb2fcaa1c52", size = 2230442, upload-time = "2025-11-04T13:39:13.962Z" }, + { url = "https://files.pythonhosted.org/packages/fd/21/4e287865504b3edc0136c89c9c09431be326168b1eb7841911cbc877a995/pydantic_core-2.41.5-cp310-cp310-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:6f52298fbd394f9ed112d56f3d11aabd0d5bd27beb3084cc3d8ad069483b8941", size = 2350956, upload-time = "2025-11-04T13:39:15.889Z" }, + { url = "https://files.pythonhosted.org/packages/a8/76/7727ef2ffa4b62fcab916686a68a0426b9b790139720e1934e8ba797e238/pydantic_core-2.41.5-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:100baa204bb412b74fe285fb0f3a385256dad1d1879f0a5cb1499ed2e83d132a", size = 2068253, upload-time = "2025-11-04T13:39:17.403Z" }, + { url = "https://files.pythonhosted.org/packages/d5/8c/a4abfc79604bcb4c748e18975c44f94f756f08fb04218d5cb87eb0d3a63e/pydantic_core-2.41.5-cp310-cp310-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:05a2c8852530ad2812cb7914dc61a1125dc4e06252ee98e5638a12da6cc6fb6c", size = 2177050, upload-time = "2025-11-04T13:39:19.351Z" }, + { url = "https://files.pythonhosted.org/packages/67/b1/de2e9a9a79b480f9cb0b6e8b6ba4c50b18d4e89852426364c66aa82bb7b3/pydantic_core-2.41.5-cp310-cp310-musllinux_1_1_aarch64.whl", hash = "sha256:29452c56df2ed968d18d7e21f4ab0ac55e71dc59524872f6fc57dcf4a3249ed2", size = 2147178, upload-time = "2025-11-04T13:39:21Z" }, + { url = "https://files.pythonhosted.org/packages/16/c1/dfb33f837a47b20417500efaa0378adc6635b3c79e8369ff7a03c494b4ac/pydantic_core-2.41.5-cp310-cp310-musllinux_1_1_armv7l.whl", hash = "sha256:d5160812ea7a8a2ffbe233d8da666880cad0cbaf5d4de74ae15c313213d62556", size = 2341833, upload-time = "2025-11-04T13:39:22.606Z" }, + { url = "https://files.pythonhosted.org/packages/47/36/00f398642a0f4b815a9a558c4f1dca1b4020a7d49562807d7bc9ff279a6c/pydantic_core-2.41.5-cp310-cp310-musllinux_1_1_x86_64.whl", hash = "sha256:df3959765b553b9440adfd3c795617c352154e497a4eaf3752555cfb5da8fc49", size = 2321156, upload-time = "2025-11-04T13:39:25.843Z" }, + { url = "https://files.pythonhosted.org/packages/7e/70/cad3acd89fde2010807354d978725ae111ddf6d0ea46d1ea1775b5c1bd0c/pydantic_core-2.41.5-cp310-cp310-win32.whl", hash = "sha256:1f8d33a7f4d5a7889e60dc39856d76d09333d8a6ed0f5f1190635cbec70ec4ba", size = 1989378, upload-time = "2025-11-04T13:39:27.92Z" }, + { url = "https://files.pythonhosted.org/packages/76/92/d338652464c6c367e5608e4488201702cd1cbb0f33f7b6a85a60fe5f3720/pydantic_core-2.41.5-cp310-cp310-win_amd64.whl", hash = "sha256:62de39db01b8d593e45871af2af9e497295db8d73b085f6bfd0b18c83c70a8f9", size = 2013622, upload-time = "2025-11-04T13:39:29.848Z" }, + { url = "https://files.pythonhosted.org/packages/e8/72/74a989dd9f2084b3d9530b0915fdda64ac48831c30dbf7c72a41a5232db8/pydantic_core-2.41.5-cp311-cp311-macosx_10_12_x86_64.whl", hash = "sha256:a3a52f6156e73e7ccb0f8cced536adccb7042be67cb45f9562e12b319c119da6", size = 2105873, upload-time = "2025-11-04T13:39:31.373Z" }, + { url = "https://files.pythonhosted.org/packages/12/44/37e403fd9455708b3b942949e1d7febc02167662bf1a7da5b78ee1ea2842/pydantic_core-2.41.5-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:7f3bf998340c6d4b0c9a2f02d6a400e51f123b59565d74dc60d252ce888c260b", size = 1899826, upload-time = "2025-11-04T13:39:32.897Z" }, + { url = "https://files.pythonhosted.org/packages/33/7f/1d5cab3ccf44c1935a359d51a8a2a9e1a654b744b5e7f80d41b88d501eec/pydantic_core-2.41.5-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:378bec5c66998815d224c9ca994f1e14c0c21cb95d2f52b6021cc0b2a58f2a5a", size = 1917869, upload-time = "2025-11-04T13:39:34.469Z" }, + { url = "https://files.pythonhosted.org/packages/6e/6a/30d94a9674a7fe4f4744052ed6c5e083424510be1e93da5bc47569d11810/pydantic_core-2.41.5-cp311-cp311-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:e7b576130c69225432866fe2f4a469a85a54ade141d96fd396dffcf607b558f8", size = 2063890, upload-time = "2025-11-04T13:39:36.053Z" }, + { url = "https://files.pythonhosted.org/packages/50/be/76e5d46203fcb2750e542f32e6c371ffa9b8ad17364cf94bb0818dbfb50c/pydantic_core-2.41.5-cp311-cp311-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:6cb58b9c66f7e4179a2d5e0f849c48eff5c1fca560994d6eb6543abf955a149e", size = 2229740, upload-time = "2025-11-04T13:39:37.753Z" }, + { url = "https://files.pythonhosted.org/packages/d3/ee/fed784df0144793489f87db310a6bbf8118d7b630ed07aa180d6067e653a/pydantic_core-2.41.5-cp311-cp311-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:88942d3a3dff3afc8288c21e565e476fc278902ae4d6d134f1eeda118cc830b1", size = 2350021, upload-time = "2025-11-04T13:39:40.94Z" }, + { url = "https://files.pythonhosted.org/packages/c8/be/8fed28dd0a180dca19e72c233cbf58efa36df055e5b9d90d64fd1740b828/pydantic_core-2.41.5-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:f31d95a179f8d64d90f6831d71fa93290893a33148d890ba15de25642c5d075b", size = 2066378, upload-time = "2025-11-04T13:39:42.523Z" }, + { url = "https://files.pythonhosted.org/packages/b0/3b/698cf8ae1d536a010e05121b4958b1257f0b5522085e335360e53a6b1c8b/pydantic_core-2.41.5-cp311-cp311-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:c1df3d34aced70add6f867a8cf413e299177e0c22660cc767218373d0779487b", size = 2175761, upload-time = "2025-11-04T13:39:44.553Z" }, + { url = "https://files.pythonhosted.org/packages/b8/ba/15d537423939553116dea94ce02f9c31be0fa9d0b806d427e0308ec17145/pydantic_core-2.41.5-cp311-cp311-musllinux_1_1_aarch64.whl", hash = "sha256:4009935984bd36bd2c774e13f9a09563ce8de4abaa7226f5108262fa3e637284", size = 2146303, upload-time = "2025-11-04T13:39:46.238Z" }, + { url = "https://files.pythonhosted.org/packages/58/7f/0de669bf37d206723795f9c90c82966726a2ab06c336deba4735b55af431/pydantic_core-2.41.5-cp311-cp311-musllinux_1_1_armv7l.whl", hash = "sha256:34a64bc3441dc1213096a20fe27e8e128bd3ff89921706e83c0b1ac971276594", size = 2340355, upload-time = "2025-11-04T13:39:48.002Z" }, + { url = "https://files.pythonhosted.org/packages/e5/de/e7482c435b83d7e3c3ee5ee4451f6e8973cff0eb6007d2872ce6383f6398/pydantic_core-2.41.5-cp311-cp311-musllinux_1_1_x86_64.whl", hash = "sha256:c9e19dd6e28fdcaa5a1de679aec4141f691023916427ef9bae8584f9c2fb3b0e", size = 2319875, upload-time = "2025-11-04T13:39:49.705Z" }, + { url = "https://files.pythonhosted.org/packages/fe/e6/8c9e81bb6dd7560e33b9053351c29f30c8194b72f2d6932888581f503482/pydantic_core-2.41.5-cp311-cp311-win32.whl", hash = "sha256:2c010c6ded393148374c0f6f0bf89d206bf3217f201faa0635dcd56bd1520f6b", size = 1987549, upload-time = "2025-11-04T13:39:51.842Z" }, + { url = "https://files.pythonhosted.org/packages/11/66/f14d1d978ea94d1bc21fc98fcf570f9542fe55bfcc40269d4e1a21c19bf7/pydantic_core-2.41.5-cp311-cp311-win_amd64.whl", hash = "sha256:76ee27c6e9c7f16f47db7a94157112a2f3a00e958bc626e2f4ee8bec5c328fbe", size = 2011305, upload-time = "2025-11-04T13:39:53.485Z" }, + { url = "https://files.pythonhosted.org/packages/56/d8/0e271434e8efd03186c5386671328154ee349ff0354d83c74f5caaf096ed/pydantic_core-2.41.5-cp311-cp311-win_arm64.whl", hash = "sha256:4bc36bbc0b7584de96561184ad7f012478987882ebf9f9c389b23f432ea3d90f", size = 1972902, upload-time = "2025-11-04T13:39:56.488Z" }, + { url = "https://files.pythonhosted.org/packages/5f/5d/5f6c63eebb5afee93bcaae4ce9a898f3373ca23df3ccaef086d0233a35a7/pydantic_core-2.41.5-cp312-cp312-macosx_10_12_x86_64.whl", hash = "sha256:f41a7489d32336dbf2199c8c0a215390a751c5b014c2c1c5366e817202e9cdf7", size = 2110990, upload-time = "2025-11-04T13:39:58.079Z" }, + { url = "https://files.pythonhosted.org/packages/aa/32/9c2e8ccb57c01111e0fd091f236c7b371c1bccea0fa85247ac55b1e2b6b6/pydantic_core-2.41.5-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:070259a8818988b9a84a449a2a7337c7f430a22acc0859c6b110aa7212a6d9c0", size = 1896003, upload-time = "2025-11-04T13:39:59.956Z" }, + { url = "https://files.pythonhosted.org/packages/68/b8/a01b53cb0e59139fbc9e4fda3e9724ede8de279097179be4ff31f1abb65a/pydantic_core-2.41.5-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:e96cea19e34778f8d59fe40775a7a574d95816eb150850a85a7a4c8f4b94ac69", size = 1919200, upload-time = "2025-11-04T13:40:02.241Z" }, + { url = "https://files.pythonhosted.org/packages/38/de/8c36b5198a29bdaade07b5985e80a233a5ac27137846f3bc2d3b40a47360/pydantic_core-2.41.5-cp312-cp312-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:ed2e99c456e3fadd05c991f8f437ef902e00eedf34320ba2b0842bd1c3ca3a75", size = 2052578, upload-time = "2025-11-04T13:40:04.401Z" }, + { url = "https://files.pythonhosted.org/packages/00/b5/0e8e4b5b081eac6cb3dbb7e60a65907549a1ce035a724368c330112adfdd/pydantic_core-2.41.5-cp312-cp312-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:65840751b72fbfd82c3c640cff9284545342a4f1eb1586ad0636955b261b0b05", size = 2208504, upload-time = "2025-11-04T13:40:06.072Z" }, + { url = "https://files.pythonhosted.org/packages/77/56/87a61aad59c7c5b9dc8caad5a41a5545cba3810c3e828708b3d7404f6cef/pydantic_core-2.41.5-cp312-cp312-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:e536c98a7626a98feb2d3eaf75944ef6f3dbee447e1f841eae16f2f0a72d8ddc", size = 2335816, upload-time = "2025-11-04T13:40:07.835Z" }, + { url = "https://files.pythonhosted.org/packages/0d/76/941cc9f73529988688a665a5c0ecff1112b3d95ab48f81db5f7606f522d3/pydantic_core-2.41.5-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:eceb81a8d74f9267ef4081e246ffd6d129da5d87e37a77c9bde550cb04870c1c", size = 2075366, upload-time = "2025-11-04T13:40:09.804Z" }, + { url = "https://files.pythonhosted.org/packages/d3/43/ebef01f69baa07a482844faaa0a591bad1ef129253ffd0cdaa9d8a7f72d3/pydantic_core-2.41.5-cp312-cp312-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:d38548150c39b74aeeb0ce8ee1d8e82696f4a4e16ddc6de7b1d8823f7de4b9b5", size = 2171698, upload-time = "2025-11-04T13:40:12.004Z" }, + { url = "https://files.pythonhosted.org/packages/b1/87/41f3202e4193e3bacfc2c065fab7706ebe81af46a83d3e27605029c1f5a6/pydantic_core-2.41.5-cp312-cp312-musllinux_1_1_aarch64.whl", hash = "sha256:c23e27686783f60290e36827f9c626e63154b82b116d7fe9adba1fda36da706c", size = 2132603, upload-time = "2025-11-04T13:40:13.868Z" }, + { url = "https://files.pythonhosted.org/packages/49/7d/4c00df99cb12070b6bccdef4a195255e6020a550d572768d92cc54dba91a/pydantic_core-2.41.5-cp312-cp312-musllinux_1_1_armv7l.whl", hash = "sha256:482c982f814460eabe1d3bb0adfdc583387bd4691ef00b90575ca0d2b6fe2294", size = 2329591, upload-time = "2025-11-04T13:40:15.672Z" }, + { url = "https://files.pythonhosted.org/packages/cc/6a/ebf4b1d65d458f3cda6a7335d141305dfa19bdc61140a884d165a8a1bbc7/pydantic_core-2.41.5-cp312-cp312-musllinux_1_1_x86_64.whl", hash = "sha256:bfea2a5f0b4d8d43adf9d7b8bf019fb46fdd10a2e5cde477fbcb9d1fa08c68e1", size = 2319068, upload-time = "2025-11-04T13:40:17.532Z" }, + { url = "https://files.pythonhosted.org/packages/49/3b/774f2b5cd4192d5ab75870ce4381fd89cf218af999515baf07e7206753f0/pydantic_core-2.41.5-cp312-cp312-win32.whl", hash = "sha256:b74557b16e390ec12dca509bce9264c3bbd128f8a2c376eaa68003d7f327276d", size = 1985908, upload-time = "2025-11-04T13:40:19.309Z" }, + { url = "https://files.pythonhosted.org/packages/86/45/00173a033c801cacf67c190fef088789394feaf88a98a7035b0e40d53dc9/pydantic_core-2.41.5-cp312-cp312-win_amd64.whl", hash = "sha256:1962293292865bca8e54702b08a4f26da73adc83dd1fcf26fbc875b35d81c815", size = 2020145, upload-time = "2025-11-04T13:40:21.548Z" }, + { url = "https://files.pythonhosted.org/packages/f9/22/91fbc821fa6d261b376a3f73809f907cec5ca6025642c463d3488aad22fb/pydantic_core-2.41.5-cp312-cp312-win_arm64.whl", hash = "sha256:1746d4a3d9a794cacae06a5eaaccb4b8643a131d45fbc9af23e353dc0a5ba5c3", size = 1976179, upload-time = "2025-11-04T13:40:23.393Z" }, + { url = "https://files.pythonhosted.org/packages/87/06/8806241ff1f70d9939f9af039c6c35f2360cf16e93c2ca76f184e76b1564/pydantic_core-2.41.5-cp313-cp313-macosx_10_12_x86_64.whl", hash = "sha256:941103c9be18ac8daf7b7adca8228f8ed6bb7a1849020f643b3a14d15b1924d9", size = 2120403, upload-time = "2025-11-04T13:40:25.248Z" }, + { url = "https://files.pythonhosted.org/packages/94/02/abfa0e0bda67faa65fef1c84971c7e45928e108fe24333c81f3bfe35d5f5/pydantic_core-2.41.5-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:112e305c3314f40c93998e567879e887a3160bb8689ef3d2c04b6cc62c33ac34", size = 1896206, upload-time = "2025-11-04T13:40:27.099Z" }, + { url = "https://files.pythonhosted.org/packages/15/df/a4c740c0943e93e6500f9eb23f4ca7ec9bf71b19e608ae5b579678c8d02f/pydantic_core-2.41.5-cp313-cp313-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:0cbaad15cb0c90aa221d43c00e77bb33c93e8d36e0bf74760cd00e732d10a6a0", size = 1919307, upload-time = "2025-11-04T13:40:29.806Z" }, + { url = "https://files.pythonhosted.org/packages/9a/e3/6324802931ae1d123528988e0e86587c2072ac2e5394b4bc2bc34b61ff6e/pydantic_core-2.41.5-cp313-cp313-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:03ca43e12fab6023fc79d28ca6b39b05f794ad08ec2feccc59a339b02f2b3d33", size = 2063258, upload-time = "2025-11-04T13:40:33.544Z" }, + { url = "https://files.pythonhosted.org/packages/c9/d4/2230d7151d4957dd79c3044ea26346c148c98fbf0ee6ebd41056f2d62ab5/pydantic_core-2.41.5-cp313-cp313-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:dc799088c08fa04e43144b164feb0c13f9a0bc40503f8df3e9fde58a3c0c101e", size = 2214917, upload-time = "2025-11-04T13:40:35.479Z" }, + { url = "https://files.pythonhosted.org/packages/e6/9f/eaac5df17a3672fef0081b6c1bb0b82b33ee89aa5cec0d7b05f52fd4a1fa/pydantic_core-2.41.5-cp313-cp313-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:97aeba56665b4c3235a0e52b2c2f5ae9cd071b8a8310ad27bddb3f7fb30e9aa2", size = 2332186, upload-time = "2025-11-04T13:40:37.436Z" }, + { url = "https://files.pythonhosted.org/packages/cf/4e/35a80cae583a37cf15604b44240e45c05e04e86f9cfd766623149297e971/pydantic_core-2.41.5-cp313-cp313-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:406bf18d345822d6c21366031003612b9c77b3e29ffdb0f612367352aab7d586", size = 2073164, upload-time = "2025-11-04T13:40:40.289Z" }, + { url = "https://files.pythonhosted.org/packages/bf/e3/f6e262673c6140dd3305d144d032f7bd5f7497d3871c1428521f19f9efa2/pydantic_core-2.41.5-cp313-cp313-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:b93590ae81f7010dbe380cdeab6f515902ebcbefe0b9327cc4804d74e93ae69d", size = 2179146, upload-time = "2025-11-04T13:40:42.809Z" }, + { url = "https://files.pythonhosted.org/packages/75/c7/20bd7fc05f0c6ea2056a4565c6f36f8968c0924f19b7d97bbfea55780e73/pydantic_core-2.41.5-cp313-cp313-musllinux_1_1_aarch64.whl", hash = "sha256:01a3d0ab748ee531f4ea6c3e48ad9dac84ddba4b0d82291f87248f2f9de8d740", size = 2137788, upload-time = "2025-11-04T13:40:44.752Z" }, + { url = "https://files.pythonhosted.org/packages/3a/8d/34318ef985c45196e004bc46c6eab2eda437e744c124ef0dbe1ff2c9d06b/pydantic_core-2.41.5-cp313-cp313-musllinux_1_1_armv7l.whl", hash = "sha256:6561e94ba9dacc9c61bce40e2d6bdc3bfaa0259d3ff36ace3b1e6901936d2e3e", size = 2340133, upload-time = "2025-11-04T13:40:46.66Z" }, + { url = "https://files.pythonhosted.org/packages/9c/59/013626bf8c78a5a5d9350d12e7697d3d4de951a75565496abd40ccd46bee/pydantic_core-2.41.5-cp313-cp313-musllinux_1_1_x86_64.whl", hash = "sha256:915c3d10f81bec3a74fbd4faebe8391013ba61e5a1a8d48c4455b923bdda7858", size = 2324852, upload-time = "2025-11-04T13:40:48.575Z" }, + { url = "https://files.pythonhosted.org/packages/1a/d9/c248c103856f807ef70c18a4f986693a46a8ffe1602e5d361485da502d20/pydantic_core-2.41.5-cp313-cp313-win32.whl", hash = "sha256:650ae77860b45cfa6e2cdafc42618ceafab3a2d9a3811fcfbd3bbf8ac3c40d36", size = 1994679, upload-time = "2025-11-04T13:40:50.619Z" }, + { url = "https://files.pythonhosted.org/packages/9e/8b/341991b158ddab181cff136acd2552c9f35bd30380422a639c0671e99a91/pydantic_core-2.41.5-cp313-cp313-win_amd64.whl", hash = "sha256:79ec52ec461e99e13791ec6508c722742ad745571f234ea6255bed38c6480f11", size = 2019766, upload-time = "2025-11-04T13:40:52.631Z" }, + { url = "https://files.pythonhosted.org/packages/73/7d/f2f9db34af103bea3e09735bb40b021788a5e834c81eedb541991badf8f5/pydantic_core-2.41.5-cp313-cp313-win_arm64.whl", hash = "sha256:3f84d5c1b4ab906093bdc1ff10484838aca54ef08de4afa9de0f5f14d69639cd", size = 1981005, upload-time = "2025-11-04T13:40:54.734Z" }, + { url = "https://files.pythonhosted.org/packages/ea/28/46b7c5c9635ae96ea0fbb779e271a38129df2550f763937659ee6c5dbc65/pydantic_core-2.41.5-cp314-cp314-macosx_10_12_x86_64.whl", hash = "sha256:3f37a19d7ebcdd20b96485056ba9e8b304e27d9904d233d7b1015db320e51f0a", size = 2119622, upload-time = "2025-11-04T13:40:56.68Z" }, + { url = "https://files.pythonhosted.org/packages/74/1a/145646e5687e8d9a1e8d09acb278c8535ebe9e972e1f162ed338a622f193/pydantic_core-2.41.5-cp314-cp314-macosx_11_0_arm64.whl", hash = "sha256:1d1d9764366c73f996edd17abb6d9d7649a7eb690006ab6adbda117717099b14", size = 1891725, upload-time = "2025-11-04T13:40:58.807Z" }, + { url = "https://files.pythonhosted.org/packages/23/04/e89c29e267b8060b40dca97bfc64a19b2a3cf99018167ea1677d96368273/pydantic_core-2.41.5-cp314-cp314-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:25e1c2af0fce638d5f1988b686f3b3ea8cd7de5f244ca147c777769e798a9cd1", size = 1915040, upload-time = "2025-11-04T13:41:00.853Z" }, + { url = "https://files.pythonhosted.org/packages/84/a3/15a82ac7bd97992a82257f777b3583d3e84bdb06ba6858f745daa2ec8a85/pydantic_core-2.41.5-cp314-cp314-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:506d766a8727beef16b7adaeb8ee6217c64fc813646b424d0804d67c16eddb66", size = 2063691, upload-time = "2025-11-04T13:41:03.504Z" }, + { url = "https://files.pythonhosted.org/packages/74/9b/0046701313c6ef08c0c1cf0e028c67c770a4e1275ca73131563c5f2a310a/pydantic_core-2.41.5-cp314-cp314-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:4819fa52133c9aa3c387b3328f25c1facc356491e6135b459f1de698ff64d869", size = 2213897, upload-time = "2025-11-04T13:41:05.804Z" }, + { url = "https://files.pythonhosted.org/packages/8a/cd/6bac76ecd1b27e75a95ca3a9a559c643b3afcd2dd62086d4b7a32a18b169/pydantic_core-2.41.5-cp314-cp314-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:2b761d210c9ea91feda40d25b4efe82a1707da2ef62901466a42492c028553a2", size = 2333302, upload-time = "2025-11-04T13:41:07.809Z" }, + { url = "https://files.pythonhosted.org/packages/4c/d2/ef2074dc020dd6e109611a8be4449b98cd25e1b9b8a303c2f0fca2f2bcf7/pydantic_core-2.41.5-cp314-cp314-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:22f0fb8c1c583a3b6f24df2470833b40207e907b90c928cc8d3594b76f874375", size = 2064877, upload-time = "2025-11-04T13:41:09.827Z" }, + { url = "https://files.pythonhosted.org/packages/18/66/e9db17a9a763d72f03de903883c057b2592c09509ccfe468187f2a2eef29/pydantic_core-2.41.5-cp314-cp314-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:2782c870e99878c634505236d81e5443092fba820f0373997ff75f90f68cd553", size = 2180680, upload-time = "2025-11-04T13:41:12.379Z" }, + { url = "https://files.pythonhosted.org/packages/d3/9e/3ce66cebb929f3ced22be85d4c2399b8e85b622db77dad36b73c5387f8f8/pydantic_core-2.41.5-cp314-cp314-musllinux_1_1_aarch64.whl", hash = "sha256:0177272f88ab8312479336e1d777f6b124537d47f2123f89cb37e0accea97f90", size = 2138960, upload-time = "2025-11-04T13:41:14.627Z" }, + { url = "https://files.pythonhosted.org/packages/a6/62/205a998f4327d2079326b01abee48e502ea739d174f0a89295c481a2272e/pydantic_core-2.41.5-cp314-cp314-musllinux_1_1_armv7l.whl", hash = "sha256:63510af5e38f8955b8ee5687740d6ebf7c2a0886d15a6d65c32814613681bc07", size = 2339102, upload-time = "2025-11-04T13:41:16.868Z" }, + { url = "https://files.pythonhosted.org/packages/3c/0d/f05e79471e889d74d3d88f5bd20d0ed189ad94c2423d81ff8d0000aab4ff/pydantic_core-2.41.5-cp314-cp314-musllinux_1_1_x86_64.whl", hash = "sha256:e56ba91f47764cc14f1daacd723e3e82d1a89d783f0f5afe9c364b8bb491ccdb", size = 2326039, upload-time = "2025-11-04T13:41:18.934Z" }, + { url = "https://files.pythonhosted.org/packages/ec/e1/e08a6208bb100da7e0c4b288eed624a703f4d129bde2da475721a80cab32/pydantic_core-2.41.5-cp314-cp314-win32.whl", hash = "sha256:aec5cf2fd867b4ff45b9959f8b20ea3993fc93e63c7363fe6851424c8a7e7c23", size = 1995126, upload-time = "2025-11-04T13:41:21.418Z" }, + { url = "https://files.pythonhosted.org/packages/48/5d/56ba7b24e9557f99c9237e29f5c09913c81eeb2f3217e40e922353668092/pydantic_core-2.41.5-cp314-cp314-win_amd64.whl", hash = "sha256:8e7c86f27c585ef37c35e56a96363ab8de4e549a95512445b85c96d3e2f7c1bf", size = 2015489, upload-time = "2025-11-04T13:41:24.076Z" }, + { url = "https://files.pythonhosted.org/packages/4e/bb/f7a190991ec9e3e0ba22e4993d8755bbc4a32925c0b5b42775c03e8148f9/pydantic_core-2.41.5-cp314-cp314-win_arm64.whl", hash = "sha256:e672ba74fbc2dc8eea59fb6d4aed6845e6905fc2a8afe93175d94a83ba2a01a0", size = 1977288, upload-time = "2025-11-04T13:41:26.33Z" }, + { url = "https://files.pythonhosted.org/packages/92/ed/77542d0c51538e32e15afe7899d79efce4b81eee631d99850edc2f5e9349/pydantic_core-2.41.5-cp314-cp314t-macosx_10_12_x86_64.whl", hash = "sha256:8566def80554c3faa0e65ac30ab0932b9e3a5cd7f8323764303d468e5c37595a", size = 2120255, upload-time = "2025-11-04T13:41:28.569Z" }, + { url = "https://files.pythonhosted.org/packages/bb/3d/6913dde84d5be21e284439676168b28d8bbba5600d838b9dca99de0fad71/pydantic_core-2.41.5-cp314-cp314t-macosx_11_0_arm64.whl", hash = "sha256:b80aa5095cd3109962a298ce14110ae16b8c1aece8b72f9dafe81cf597ad80b3", size = 1863760, upload-time = "2025-11-04T13:41:31.055Z" }, + { url = "https://files.pythonhosted.org/packages/5a/f0/e5e6b99d4191da102f2b0eb9687aaa7f5bea5d9964071a84effc3e40f997/pydantic_core-2.41.5-cp314-cp314t-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:3006c3dd9ba34b0c094c544c6006cc79e87d8612999f1a5d43b769b89181f23c", size = 1878092, upload-time = "2025-11-04T13:41:33.21Z" }, + { url = "https://files.pythonhosted.org/packages/71/48/36fb760642d568925953bcc8116455513d6e34c4beaa37544118c36aba6d/pydantic_core-2.41.5-cp314-cp314t-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:72f6c8b11857a856bcfa48c86f5368439f74453563f951e473514579d44aa612", size = 2053385, upload-time = "2025-11-04T13:41:35.508Z" }, + { url = "https://files.pythonhosted.org/packages/20/25/92dc684dd8eb75a234bc1c764b4210cf2646479d54b47bf46061657292a8/pydantic_core-2.41.5-cp314-cp314t-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:5cb1b2f9742240e4bb26b652a5aeb840aa4b417c7748b6f8387927bc6e45e40d", size = 2218832, upload-time = "2025-11-04T13:41:37.732Z" }, + { url = "https://files.pythonhosted.org/packages/e2/09/f53e0b05023d3e30357d82eb35835d0f6340ca344720a4599cd663dca599/pydantic_core-2.41.5-cp314-cp314t-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:bd3d54f38609ff308209bd43acea66061494157703364ae40c951f83ba99a1a9", size = 2327585, upload-time = "2025-11-04T13:41:40Z" }, + { url = "https://files.pythonhosted.org/packages/aa/4e/2ae1aa85d6af35a39b236b1b1641de73f5a6ac4d5a7509f77b814885760c/pydantic_core-2.41.5-cp314-cp314t-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:2ff4321e56e879ee8d2a879501c8e469414d948f4aba74a2d4593184eb326660", size = 2041078, upload-time = "2025-11-04T13:41:42.323Z" }, + { url = "https://files.pythonhosted.org/packages/cd/13/2e215f17f0ef326fc72afe94776edb77525142c693767fc347ed6288728d/pydantic_core-2.41.5-cp314-cp314t-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:d0d2568a8c11bf8225044aa94409e21da0cb09dcdafe9ecd10250b2baad531a9", size = 2173914, upload-time = "2025-11-04T13:41:45.221Z" }, + { url = "https://files.pythonhosted.org/packages/02/7a/f999a6dcbcd0e5660bc348a3991c8915ce6599f4f2c6ac22f01d7a10816c/pydantic_core-2.41.5-cp314-cp314t-musllinux_1_1_aarch64.whl", hash = "sha256:a39455728aabd58ceabb03c90e12f71fd30fa69615760a075b9fec596456ccc3", size = 2129560, upload-time = "2025-11-04T13:41:47.474Z" }, + { url = "https://files.pythonhosted.org/packages/3a/b1/6c990ac65e3b4c079a4fb9f5b05f5b013afa0f4ed6780a3dd236d2cbdc64/pydantic_core-2.41.5-cp314-cp314t-musllinux_1_1_armv7l.whl", hash = "sha256:239edca560d05757817c13dc17c50766136d21f7cd0fac50295499ae24f90fdf", size = 2329244, upload-time = "2025-11-04T13:41:49.992Z" }, + { url = "https://files.pythonhosted.org/packages/d9/02/3c562f3a51afd4d88fff8dffb1771b30cfdfd79befd9883ee094f5b6c0d8/pydantic_core-2.41.5-cp314-cp314t-musllinux_1_1_x86_64.whl", hash = "sha256:2a5e06546e19f24c6a96a129142a75cee553cc018ffee48a460059b1185f4470", size = 2331955, upload-time = "2025-11-04T13:41:54.079Z" }, + { url = "https://files.pythonhosted.org/packages/5c/96/5fb7d8c3c17bc8c62fdb031c47d77a1af698f1d7a406b0f79aaa1338f9ad/pydantic_core-2.41.5-cp314-cp314t-win32.whl", hash = "sha256:b4ececa40ac28afa90871c2cc2b9ffd2ff0bf749380fbdf57d165fd23da353aa", size = 1988906, upload-time = "2025-11-04T13:41:56.606Z" }, + { url = "https://files.pythonhosted.org/packages/22/ed/182129d83032702912c2e2d8bbe33c036f342cc735737064668585dac28f/pydantic_core-2.41.5-cp314-cp314t-win_amd64.whl", hash = "sha256:80aa89cad80b32a912a65332f64a4450ed00966111b6615ca6816153d3585a8c", size = 1981607, upload-time = "2025-11-04T13:41:58.889Z" }, + { url = "https://files.pythonhosted.org/packages/9f/ed/068e41660b832bb0b1aa5b58011dea2a3fe0ba7861ff38c4d4904c1c1a99/pydantic_core-2.41.5-cp314-cp314t-win_arm64.whl", hash = "sha256:35b44f37a3199f771c3eaa53051bc8a70cd7b54f333531c59e29fd4db5d15008", size = 1974769, upload-time = "2025-11-04T13:42:01.186Z" }, + { url = "https://files.pythonhosted.org/packages/11/72/90fda5ee3b97e51c494938a4a44c3a35a9c96c19bba12372fb9c634d6f57/pydantic_core-2.41.5-graalpy311-graalpy242_311_native-macosx_10_12_x86_64.whl", hash = "sha256:b96d5f26b05d03cc60f11a7761a5ded1741da411e7fe0909e27a5e6a0cb7b034", size = 2115441, upload-time = "2025-11-04T13:42:39.557Z" }, + { url = "https://files.pythonhosted.org/packages/1f/53/8942f884fa33f50794f119012dc6a1a02ac43a56407adaac20463df8e98f/pydantic_core-2.41.5-graalpy311-graalpy242_311_native-macosx_11_0_arm64.whl", hash = "sha256:634e8609e89ceecea15e2d61bc9ac3718caaaa71963717bf3c8f38bfde64242c", size = 1930291, upload-time = "2025-11-04T13:42:42.169Z" }, + { url = "https://files.pythonhosted.org/packages/79/c8/ecb9ed9cd942bce09fc888ee960b52654fbdbede4ba6c2d6e0d3b1d8b49c/pydantic_core-2.41.5-graalpy311-graalpy242_311_native-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:93e8740d7503eb008aa2df04d3b9735f845d43ae845e6dcd2be0b55a2da43cd2", size = 1948632, upload-time = "2025-11-04T13:42:44.564Z" }, + { url = "https://files.pythonhosted.org/packages/2e/1b/687711069de7efa6af934e74f601e2a4307365e8fdc404703afc453eab26/pydantic_core-2.41.5-graalpy311-graalpy242_311_native-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:f15489ba13d61f670dcc96772e733aad1a6f9c429cc27574c6cdaed82d0146ad", size = 2138905, upload-time = "2025-11-04T13:42:47.156Z" }, + { url = "https://files.pythonhosted.org/packages/09/32/59b0c7e63e277fa7911c2fc70ccfb45ce4b98991e7ef37110663437005af/pydantic_core-2.41.5-graalpy312-graalpy250_312_native-macosx_10_12_x86_64.whl", hash = "sha256:7da7087d756b19037bc2c06edc6c170eeef3c3bafcb8f532ff17d64dc427adfd", size = 2110495, upload-time = "2025-11-04T13:42:49.689Z" }, + { url = "https://files.pythonhosted.org/packages/aa/81/05e400037eaf55ad400bcd318c05bb345b57e708887f07ddb2d20e3f0e98/pydantic_core-2.41.5-graalpy312-graalpy250_312_native-macosx_11_0_arm64.whl", hash = "sha256:aabf5777b5c8ca26f7824cb4a120a740c9588ed58df9b2d196ce92fba42ff8dc", size = 1915388, upload-time = "2025-11-04T13:42:52.215Z" }, + { url = "https://files.pythonhosted.org/packages/6e/0d/e3549b2399f71d56476b77dbf3cf8937cec5cd70536bdc0e374a421d0599/pydantic_core-2.41.5-graalpy312-graalpy250_312_native-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:c007fe8a43d43b3969e8469004e9845944f1a80e6acd47c150856bb87f230c56", size = 1942879, upload-time = "2025-11-04T13:42:56.483Z" }, + { url = "https://files.pythonhosted.org/packages/f7/07/34573da085946b6a313d7c42f82f16e8920bfd730665de2d11c0c37a74b5/pydantic_core-2.41.5-graalpy312-graalpy250_312_native-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:76d0819de158cd855d1cbb8fcafdf6f5cf1eb8e470abe056d5d161106e38062b", size = 2139017, upload-time = "2025-11-04T13:42:59.471Z" }, + { url = "https://files.pythonhosted.org/packages/e6/b0/1a2aa41e3b5a4ba11420aba2d091b2d17959c8d1519ece3627c371951e73/pydantic_core-2.41.5-pp310-pypy310_pp73-macosx_10_12_x86_64.whl", hash = "sha256:b5819cd790dbf0c5eb9f82c73c16b39a65dd6dd4d1439dcdea7816ec9adddab8", size = 2103351, upload-time = "2025-11-04T13:43:02.058Z" }, + { url = "https://files.pythonhosted.org/packages/a4/ee/31b1f0020baaf6d091c87900ae05c6aeae101fa4e188e1613c80e4f1ea31/pydantic_core-2.41.5-pp310-pypy310_pp73-macosx_11_0_arm64.whl", hash = "sha256:5a4e67afbc95fa5c34cf27d9089bca7fcab4e51e57278d710320a70b956d1b9a", size = 1925363, upload-time = "2025-11-04T13:43:05.159Z" }, + { url = "https://files.pythonhosted.org/packages/e1/89/ab8e86208467e467a80deaca4e434adac37b10a9d134cd2f99b28a01e483/pydantic_core-2.41.5-pp310-pypy310_pp73-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:ece5c59f0ce7d001e017643d8d24da587ea1f74f6993467d85ae8a5ef9d4f42b", size = 2135615, upload-time = "2025-11-04T13:43:08.116Z" }, + { url = "https://files.pythonhosted.org/packages/99/0a/99a53d06dd0348b2008f2f30884b34719c323f16c3be4e6cc1203b74a91d/pydantic_core-2.41.5-pp310-pypy310_pp73-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:16f80f7abe3351f8ea6858914ddc8c77e02578544a0ebc15b4c2e1a0e813b0b2", size = 2175369, upload-time = "2025-11-04T13:43:12.49Z" }, + { url = "https://files.pythonhosted.org/packages/6d/94/30ca3b73c6d485b9bb0bc66e611cff4a7138ff9736b7e66bcf0852151636/pydantic_core-2.41.5-pp310-pypy310_pp73-musllinux_1_1_aarch64.whl", hash = "sha256:33cb885e759a705b426baada1fe68cbb0a2e68e34c5d0d0289a364cf01709093", size = 2144218, upload-time = "2025-11-04T13:43:15.431Z" }, + { url = "https://files.pythonhosted.org/packages/87/57/31b4f8e12680b739a91f472b5671294236b82586889ef764b5fbc6669238/pydantic_core-2.41.5-pp310-pypy310_pp73-musllinux_1_1_armv7l.whl", hash = "sha256:c8d8b4eb992936023be7dee581270af5c6e0697a8559895f527f5b7105ecd36a", size = 2329951, upload-time = "2025-11-04T13:43:18.062Z" }, + { url = "https://files.pythonhosted.org/packages/7d/73/3c2c8edef77b8f7310e6fb012dbc4b8551386ed575b9eb6fb2506e28a7eb/pydantic_core-2.41.5-pp310-pypy310_pp73-musllinux_1_1_x86_64.whl", hash = "sha256:242a206cd0318f95cd21bdacff3fcc3aab23e79bba5cac3db5a841c9ef9c6963", size = 2318428, upload-time = "2025-11-04T13:43:20.679Z" }, + { url = "https://files.pythonhosted.org/packages/2f/02/8559b1f26ee0d502c74f9cca5c0d2fd97e967e083e006bbbb4e97f3a043a/pydantic_core-2.41.5-pp310-pypy310_pp73-win_amd64.whl", hash = "sha256:d3a978c4f57a597908b7e697229d996d77a6d3c94901e9edee593adada95ce1a", size = 2147009, upload-time = "2025-11-04T13:43:23.286Z" }, + { url = "https://files.pythonhosted.org/packages/5f/9b/1b3f0e9f9305839d7e84912f9e8bfbd191ed1b1ef48083609f0dabde978c/pydantic_core-2.41.5-pp311-pypy311_pp73-macosx_10_12_x86_64.whl", hash = "sha256:b2379fa7ed44ddecb5bfe4e48577d752db9fc10be00a6b7446e9663ba143de26", size = 2101980, upload-time = "2025-11-04T13:43:25.97Z" }, + { url = "https://files.pythonhosted.org/packages/a4/ed/d71fefcb4263df0da6a85b5d8a7508360f2f2e9b3bf5814be9c8bccdccc1/pydantic_core-2.41.5-pp311-pypy311_pp73-macosx_11_0_arm64.whl", hash = "sha256:266fb4cbf5e3cbd0b53669a6d1b039c45e3ce651fd5442eff4d07c2cc8d66808", size = 1923865, upload-time = "2025-11-04T13:43:28.763Z" }, + { url = "https://files.pythonhosted.org/packages/ce/3a/626b38db460d675f873e4444b4bb030453bbe7b4ba55df821d026a0493c4/pydantic_core-2.41.5-pp311-pypy311_pp73-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:58133647260ea01e4d0500089a8c4f07bd7aa6ce109682b1426394988d8aaacc", size = 2134256, upload-time = "2025-11-04T13:43:31.71Z" }, + { url = "https://files.pythonhosted.org/packages/83/d9/8412d7f06f616bbc053d30cb4e5f76786af3221462ad5eee1f202021eb4e/pydantic_core-2.41.5-pp311-pypy311_pp73-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:287dad91cfb551c363dc62899a80e9e14da1f0e2b6ebde82c806612ca2a13ef1", size = 2174762, upload-time = "2025-11-04T13:43:34.744Z" }, + { url = "https://files.pythonhosted.org/packages/55/4c/162d906b8e3ba3a99354e20faa1b49a85206c47de97a639510a0e673f5da/pydantic_core-2.41.5-pp311-pypy311_pp73-musllinux_1_1_aarch64.whl", hash = "sha256:03b77d184b9eb40240ae9fd676ca364ce1085f203e1b1256f8ab9984dca80a84", size = 2143141, upload-time = "2025-11-04T13:43:37.701Z" }, + { url = "https://files.pythonhosted.org/packages/1f/f2/f11dd73284122713f5f89fc940f370d035fa8e1e078d446b3313955157fe/pydantic_core-2.41.5-pp311-pypy311_pp73-musllinux_1_1_armv7l.whl", hash = "sha256:a668ce24de96165bb239160b3d854943128f4334822900534f2fe947930e5770", size = 2330317, upload-time = "2025-11-04T13:43:40.406Z" }, + { url = "https://files.pythonhosted.org/packages/88/9d/b06ca6acfe4abb296110fb1273a4d848a0bfb2ff65f3ee92127b3244e16b/pydantic_core-2.41.5-pp311-pypy311_pp73-musllinux_1_1_x86_64.whl", hash = "sha256:f14f8f046c14563f8eb3f45f499cc658ab8d10072961e07225e507adb700e93f", size = 2316992, upload-time = "2025-11-04T13:43:43.602Z" }, + { url = "https://files.pythonhosted.org/packages/36/c7/cfc8e811f061c841d7990b0201912c3556bfeb99cdcb7ed24adc8d6f8704/pydantic_core-2.41.5-pp311-pypy311_pp73-win_amd64.whl", hash = "sha256:56121965f7a4dc965bff783d70b907ddf3d57f6eba29b6d2e5dabfaf07799c51", size = 2145302, upload-time = "2025-11-04T13:43:46.64Z" }, +] + +[[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 = "pytest" +version = "9.1.1" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "colorama", marker = "sys_platform == 'win32'" }, + { name = "exceptiongroup", marker = "python_full_version < '3.11'" }, + { name = "iniconfig" }, + { name = "packaging" }, + { name = "pluggy" }, + { name = "pygments" }, + { name = "tomli", marker = "python_full_version < '3.11'" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/e4/47/b9efed96c114afcfa3c9d3fe98a76a1d14c74a9e266d397cf6eb64be5e01/pytest-9.1.1.tar.gz", hash = "sha256:1088fbde8f2b49d95a549a195707afa7a76a3ce9bcadc26b6d71f0ffda5fe313", size = 1636369, upload-time = "2026-06-19T10:58:32.857Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/24/25/1de2678b631f5a49215c6c96fff41ba892b0a34df68d6d80292b1b48aa7f/pytest-9.1.1-py3-none-any.whl", hash = "sha256:37a86b45efb9a47a61a36449063e8e18d0cab3161329fc099eb21783169c4f0c", size = 386536, upload-time = "2026-06-19T10:58:31.347Z" }, +] + +[[package]] +name = "python-dotenv" +version = "1.2.1" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/f0/26/19cadc79a718c5edbec86fd4919a6b6d3f681039a2f6d66d14be94e75fb9/python_dotenv-1.2.1.tar.gz", hash = "sha256:42667e897e16ab0d66954af0e60a9caa94f0fd4ecf3aaf6d2d260eec1aa36ad6", size = 44221, upload-time = "2025-10-26T15:12:10.434Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/14/1b/a298b06749107c305e1fe0f814c6c74aea7b2f1e10989cb30f544a1b3253/python_dotenv-1.2.1-py3-none-any.whl", hash = "sha256:b81ee9561e9ca4004139c6cbba3a238c32b03e4894671e181b671e8cb8425d61", size = 21230, upload-time = "2025-10-26T15:12:09.109Z" }, +] + +[[package]] +name = "pyyaml" +version = "6.0.3" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/05/8e/961c0007c59b8dd7729d542c61a4d537767a59645b82a0b521206e1e25c2/pyyaml-6.0.3.tar.gz", hash = "sha256:d76623373421df22fb4cf8817020cbb7ef15c725b9d5e45f17e189bfc384190f", size = 130960, upload-time = "2025-09-25T21:33:16.546Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/f4/a0/39350dd17dd6d6c6507025c0e53aef67a9293a6d37d3511f23ea510d5800/pyyaml-6.0.3-cp310-cp310-macosx_10_13_x86_64.whl", hash = "sha256:214ed4befebe12df36bcc8bc2b64b396ca31be9304b8f59e25c11cf94a4c033b", size = 184227, upload-time = "2025-09-25T21:31:46.04Z" }, + { url = "https://files.pythonhosted.org/packages/05/14/52d505b5c59ce73244f59c7a50ecf47093ce4765f116cdb98286a71eeca2/pyyaml-6.0.3-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:02ea2dfa234451bbb8772601d7b8e426c2bfa197136796224e50e35a78777956", size = 174019, upload-time = "2025-09-25T21:31:47.706Z" }, + { url = "https://files.pythonhosted.org/packages/43/f7/0e6a5ae5599c838c696adb4e6330a59f463265bfa1e116cfd1fbb0abaaae/pyyaml-6.0.3-cp310-cp310-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:b30236e45cf30d2b8e7b3e85881719e98507abed1011bf463a8fa23e9c3e98a8", size = 740646, upload-time = "2025-09-25T21:31:49.21Z" }, + { url = "https://files.pythonhosted.org/packages/2f/3a/61b9db1d28f00f8fd0ae760459a5c4bf1b941baf714e207b6eb0657d2578/pyyaml-6.0.3-cp310-cp310-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:66291b10affd76d76f54fad28e22e51719ef9ba22b29e1d7d03d6777a9174198", size = 840793, upload-time = "2025-09-25T21:31:50.735Z" }, + { url = "https://files.pythonhosted.org/packages/7a/1e/7acc4f0e74c4b3d9531e24739e0ab832a5edf40e64fbae1a9c01941cabd7/pyyaml-6.0.3-cp310-cp310-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:9c7708761fccb9397fe64bbc0395abcae8c4bf7b0eac081e12b809bf47700d0b", size = 770293, upload-time = "2025-09-25T21:31:51.828Z" }, + { url = "https://files.pythonhosted.org/packages/8b/ef/abd085f06853af0cd59fa5f913d61a8eab65d7639ff2a658d18a25d6a89d/pyyaml-6.0.3-cp310-cp310-musllinux_1_2_aarch64.whl", hash = "sha256:418cf3f2111bc80e0933b2cd8cd04f286338bb88bdc7bc8e6dd775ebde60b5e0", size = 732872, upload-time = "2025-09-25T21:31:53.282Z" }, + { url = "https://files.pythonhosted.org/packages/1f/15/2bc9c8faf6450a8b3c9fc5448ed869c599c0a74ba2669772b1f3a0040180/pyyaml-6.0.3-cp310-cp310-musllinux_1_2_x86_64.whl", hash = "sha256:5e0b74767e5f8c593e8c9b5912019159ed0533c70051e9cce3e8b6aa699fcd69", size = 758828, upload-time = "2025-09-25T21:31:54.807Z" }, + { url = "https://files.pythonhosted.org/packages/a3/00/531e92e88c00f4333ce359e50c19b8d1de9fe8d581b1534e35ccfbc5f393/pyyaml-6.0.3-cp310-cp310-win32.whl", hash = "sha256:28c8d926f98f432f88adc23edf2e6d4921ac26fb084b028c733d01868d19007e", size = 142415, upload-time = "2025-09-25T21:31:55.885Z" }, + { url = "https://files.pythonhosted.org/packages/2a/fa/926c003379b19fca39dd4634818b00dec6c62d87faf628d1394e137354d4/pyyaml-6.0.3-cp310-cp310-win_amd64.whl", hash = "sha256:bdb2c67c6c1390b63c6ff89f210c8fd09d9a1217a465701eac7316313c915e4c", size = 158561, upload-time = "2025-09-25T21:31:57.406Z" }, + { url = "https://files.pythonhosted.org/packages/6d/16/a95b6757765b7b031c9374925bb718d55e0a9ba8a1b6a12d25962ea44347/pyyaml-6.0.3-cp311-cp311-macosx_10_13_x86_64.whl", hash = "sha256:44edc647873928551a01e7a563d7452ccdebee747728c1080d881d68af7b997e", size = 185826, upload-time = "2025-09-25T21:31:58.655Z" }, + { url = "https://files.pythonhosted.org/packages/16/19/13de8e4377ed53079ee996e1ab0a9c33ec2faf808a4647b7b4c0d46dd239/pyyaml-6.0.3-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:652cb6edd41e718550aad172851962662ff2681490a8a711af6a4d288dd96824", size = 175577, upload-time = "2025-09-25T21:32:00.088Z" }, + { url = "https://files.pythonhosted.org/packages/0c/62/d2eb46264d4b157dae1275b573017abec435397aa59cbcdab6fc978a8af4/pyyaml-6.0.3-cp311-cp311-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:10892704fc220243f5305762e276552a0395f7beb4dbf9b14ec8fd43b57f126c", size = 775556, upload-time = "2025-09-25T21:32:01.31Z" }, + { url = "https://files.pythonhosted.org/packages/10/cb/16c3f2cf3266edd25aaa00d6c4350381c8b012ed6f5276675b9eba8d9ff4/pyyaml-6.0.3-cp311-cp311-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:850774a7879607d3a6f50d36d04f00ee69e7fc816450e5f7e58d7f17f1ae5c00", size = 882114, upload-time = "2025-09-25T21:32:03.376Z" }, + { url = "https://files.pythonhosted.org/packages/71/60/917329f640924b18ff085ab889a11c763e0b573da888e8404ff486657602/pyyaml-6.0.3-cp311-cp311-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:b8bb0864c5a28024fac8a632c443c87c5aa6f215c0b126c449ae1a150412f31d", size = 806638, upload-time = "2025-09-25T21:32:04.553Z" }, + { url = "https://files.pythonhosted.org/packages/dd/6f/529b0f316a9fd167281a6c3826b5583e6192dba792dd55e3203d3f8e655a/pyyaml-6.0.3-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:1d37d57ad971609cf3c53ba6a7e365e40660e3be0e5175fa9f2365a379d6095a", size = 767463, upload-time = "2025-09-25T21:32:06.152Z" }, + { url = "https://files.pythonhosted.org/packages/f2/6a/b627b4e0c1dd03718543519ffb2f1deea4a1e6d42fbab8021936a4d22589/pyyaml-6.0.3-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:37503bfbfc9d2c40b344d06b2199cf0e96e97957ab1c1b546fd4f87e53e5d3e4", size = 794986, upload-time = "2025-09-25T21:32:07.367Z" }, + { url = "https://files.pythonhosted.org/packages/45/91/47a6e1c42d9ee337c4839208f30d9f09caa9f720ec7582917b264defc875/pyyaml-6.0.3-cp311-cp311-win32.whl", hash = "sha256:8098f252adfa6c80ab48096053f512f2321f0b998f98150cea9bd23d83e1467b", size = 142543, upload-time = "2025-09-25T21:32:08.95Z" }, + { url = "https://files.pythonhosted.org/packages/da/e3/ea007450a105ae919a72393cb06f122f288ef60bba2dc64b26e2646fa315/pyyaml-6.0.3-cp311-cp311-win_amd64.whl", hash = "sha256:9f3bfb4965eb874431221a3ff3fdcddc7e74e3b07799e0e84ca4a0f867d449bf", size = 158763, upload-time = "2025-09-25T21:32:09.96Z" }, + { url = "https://files.pythonhosted.org/packages/d1/33/422b98d2195232ca1826284a76852ad5a86fe23e31b009c9886b2d0fb8b2/pyyaml-6.0.3-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:7f047e29dcae44602496db43be01ad42fc6f1cc0d8cd6c83d342306c32270196", size = 182063, upload-time = "2025-09-25T21:32:11.445Z" }, + { url = "https://files.pythonhosted.org/packages/89/a0/6cf41a19a1f2f3feab0e9c0b74134aa2ce6849093d5517a0c550fe37a648/pyyaml-6.0.3-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:fc09d0aa354569bc501d4e787133afc08552722d3ab34836a80547331bb5d4a0", size = 173973, upload-time = "2025-09-25T21:32:12.492Z" }, + { url = "https://files.pythonhosted.org/packages/ed/23/7a778b6bd0b9a8039df8b1b1d80e2e2ad78aa04171592c8a5c43a56a6af4/pyyaml-6.0.3-cp312-cp312-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:9149cad251584d5fb4981be1ecde53a1ca46c891a79788c0df828d2f166bda28", size = 775116, upload-time = "2025-09-25T21:32:13.652Z" }, + { url = "https://files.pythonhosted.org/packages/65/30/d7353c338e12baef4ecc1b09e877c1970bd3382789c159b4f89d6a70dc09/pyyaml-6.0.3-cp312-cp312-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:5fdec68f91a0c6739b380c83b951e2c72ac0197ace422360e6d5a959d8d97b2c", size = 844011, upload-time = "2025-09-25T21:32:15.21Z" }, + { url = "https://files.pythonhosted.org/packages/8b/9d/b3589d3877982d4f2329302ef98a8026e7f4443c765c46cfecc8858c6b4b/pyyaml-6.0.3-cp312-cp312-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:ba1cc08a7ccde2d2ec775841541641e4548226580ab850948cbfda66a1befcdc", size = 807870, upload-time = "2025-09-25T21:32:16.431Z" }, + { url = "https://files.pythonhosted.org/packages/05/c0/b3be26a015601b822b97d9149ff8cb5ead58c66f981e04fedf4e762f4bd4/pyyaml-6.0.3-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:8dc52c23056b9ddd46818a57b78404882310fb473d63f17b07d5c40421e47f8e", size = 761089, upload-time = "2025-09-25T21:32:17.56Z" }, + { url = "https://files.pythonhosted.org/packages/be/8e/98435a21d1d4b46590d5459a22d88128103f8da4c2d4cb8f14f2a96504e1/pyyaml-6.0.3-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:41715c910c881bc081f1e8872880d3c650acf13dfa8214bad49ed4cede7c34ea", size = 790181, upload-time = "2025-09-25T21:32:18.834Z" }, + { url = "https://files.pythonhosted.org/packages/74/93/7baea19427dcfbe1e5a372d81473250b379f04b1bd3c4c5ff825e2327202/pyyaml-6.0.3-cp312-cp312-win32.whl", hash = "sha256:96b533f0e99f6579b3d4d4995707cf36df9100d67e0c8303a0c55b27b5f99bc5", size = 137658, upload-time = "2025-09-25T21:32:20.209Z" }, + { url = "https://files.pythonhosted.org/packages/86/bf/899e81e4cce32febab4fb42bb97dcdf66bc135272882d1987881a4b519e9/pyyaml-6.0.3-cp312-cp312-win_amd64.whl", hash = "sha256:5fcd34e47f6e0b794d17de1b4ff496c00986e1c83f7ab2fb8fcfe9616ff7477b", size = 154003, upload-time = "2025-09-25T21:32:21.167Z" }, + { url = "https://files.pythonhosted.org/packages/1a/08/67bd04656199bbb51dbed1439b7f27601dfb576fb864099c7ef0c3e55531/pyyaml-6.0.3-cp312-cp312-win_arm64.whl", hash = "sha256:64386e5e707d03a7e172c0701abfb7e10f0fb753ee1d773128192742712a98fd", size = 140344, upload-time = "2025-09-25T21:32:22.617Z" }, + { url = "https://files.pythonhosted.org/packages/d1/11/0fd08f8192109f7169db964b5707a2f1e8b745d4e239b784a5a1dd80d1db/pyyaml-6.0.3-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:8da9669d359f02c0b91ccc01cac4a67f16afec0dac22c2ad09f46bee0697eba8", size = 181669, upload-time = "2025-09-25T21:32:23.673Z" }, + { url = "https://files.pythonhosted.org/packages/b1/16/95309993f1d3748cd644e02e38b75d50cbc0d9561d21f390a76242ce073f/pyyaml-6.0.3-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:2283a07e2c21a2aa78d9c4442724ec1eb15f5e42a723b99cb3d822d48f5f7ad1", size = 173252, upload-time = "2025-09-25T21:32:25.149Z" }, + { url = "https://files.pythonhosted.org/packages/50/31/b20f376d3f810b9b2371e72ef5adb33879b25edb7a6d072cb7ca0c486398/pyyaml-6.0.3-cp313-cp313-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:ee2922902c45ae8ccada2c5b501ab86c36525b883eff4255313a253a3160861c", size = 767081, upload-time = "2025-09-25T21:32:26.575Z" }, + { url = "https://files.pythonhosted.org/packages/49/1e/a55ca81e949270d5d4432fbbd19dfea5321eda7c41a849d443dc92fd1ff7/pyyaml-6.0.3-cp313-cp313-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:a33284e20b78bd4a18c8c2282d549d10bc8408a2a7ff57653c0cf0b9be0afce5", size = 841159, upload-time = "2025-09-25T21:32:27.727Z" }, + { url = "https://files.pythonhosted.org/packages/74/27/e5b8f34d02d9995b80abcef563ea1f8b56d20134d8f4e5e81733b1feceb2/pyyaml-6.0.3-cp313-cp313-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:0f29edc409a6392443abf94b9cf89ce99889a1dd5376d94316ae5145dfedd5d6", size = 801626, upload-time = "2025-09-25T21:32:28.878Z" }, + { url = "https://files.pythonhosted.org/packages/f9/11/ba845c23988798f40e52ba45f34849aa8a1f2d4af4b798588010792ebad6/pyyaml-6.0.3-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:f7057c9a337546edc7973c0d3ba84ddcdf0daa14533c2065749c9075001090e6", size = 753613, upload-time = "2025-09-25T21:32:30.178Z" }, + { url = "https://files.pythonhosted.org/packages/3d/e0/7966e1a7bfc0a45bf0a7fb6b98ea03fc9b8d84fa7f2229e9659680b69ee3/pyyaml-6.0.3-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:eda16858a3cab07b80edaf74336ece1f986ba330fdb8ee0d6c0d68fe82bc96be", size = 794115, upload-time = "2025-09-25T21:32:31.353Z" }, + { url = "https://files.pythonhosted.org/packages/de/94/980b50a6531b3019e45ddeada0626d45fa85cbe22300844a7983285bed3b/pyyaml-6.0.3-cp313-cp313-win32.whl", hash = "sha256:d0eae10f8159e8fdad514efdc92d74fd8d682c933a6dd088030f3834bc8e6b26", size = 137427, upload-time = "2025-09-25T21:32:32.58Z" }, + { url = "https://files.pythonhosted.org/packages/97/c9/39d5b874e8b28845e4ec2202b5da735d0199dbe5b8fb85f91398814a9a46/pyyaml-6.0.3-cp313-cp313-win_amd64.whl", hash = "sha256:79005a0d97d5ddabfeeea4cf676af11e647e41d81c9a7722a193022accdb6b7c", size = 154090, upload-time = "2025-09-25T21:32:33.659Z" }, + { url = "https://files.pythonhosted.org/packages/73/e8/2bdf3ca2090f68bb3d75b44da7bbc71843b19c9f2b9cb9b0f4ab7a5a4329/pyyaml-6.0.3-cp313-cp313-win_arm64.whl", hash = "sha256:5498cd1645aa724a7c71c8f378eb29ebe23da2fc0d7a08071d89469bf1d2defb", size = 140246, upload-time = "2025-09-25T21:32:34.663Z" }, + { url = "https://files.pythonhosted.org/packages/9d/8c/f4bd7f6465179953d3ac9bc44ac1a8a3e6122cf8ada906b4f96c60172d43/pyyaml-6.0.3-cp314-cp314-macosx_10_13_x86_64.whl", hash = "sha256:8d1fab6bb153a416f9aeb4b8763bc0f22a5586065f86f7664fc23339fc1c1fac", size = 181814, upload-time = "2025-09-25T21:32:35.712Z" }, + { url = "https://files.pythonhosted.org/packages/bd/9c/4d95bb87eb2063d20db7b60faa3840c1b18025517ae857371c4dd55a6b3a/pyyaml-6.0.3-cp314-cp314-macosx_11_0_arm64.whl", hash = "sha256:34d5fcd24b8445fadc33f9cf348c1047101756fd760b4dacb5c3e99755703310", size = 173809, upload-time = "2025-09-25T21:32:36.789Z" }, + { url = "https://files.pythonhosted.org/packages/92/b5/47e807c2623074914e29dabd16cbbdd4bf5e9b2db9f8090fa64411fc5382/pyyaml-6.0.3-cp314-cp314-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:501a031947e3a9025ed4405a168e6ef5ae3126c59f90ce0cd6f2bfc477be31b7", size = 766454, upload-time = "2025-09-25T21:32:37.966Z" }, + { url = "https://files.pythonhosted.org/packages/02/9e/e5e9b168be58564121efb3de6859c452fccde0ab093d8438905899a3a483/pyyaml-6.0.3-cp314-cp314-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:b3bc83488de33889877a0f2543ade9f70c67d66d9ebb4ac959502e12de895788", size = 836355, upload-time = "2025-09-25T21:32:39.178Z" }, + { url = "https://files.pythonhosted.org/packages/88/f9/16491d7ed2a919954993e48aa941b200f38040928474c9e85ea9e64222c3/pyyaml-6.0.3-cp314-cp314-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:c458b6d084f9b935061bc36216e8a69a7e293a2f1e68bf956dcd9e6cbcd143f5", size = 794175, upload-time = "2025-09-25T21:32:40.865Z" }, + { url = "https://files.pythonhosted.org/packages/dd/3f/5989debef34dc6397317802b527dbbafb2b4760878a53d4166579111411e/pyyaml-6.0.3-cp314-cp314-musllinux_1_2_aarch64.whl", hash = "sha256:7c6610def4f163542a622a73fb39f534f8c101d690126992300bf3207eab9764", size = 755228, upload-time = "2025-09-25T21:32:42.084Z" }, + { url = "https://files.pythonhosted.org/packages/d7/ce/af88a49043cd2e265be63d083fc75b27b6ed062f5f9fd6cdc223ad62f03e/pyyaml-6.0.3-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:5190d403f121660ce8d1d2c1bb2ef1bd05b5f68533fc5c2ea899bd15f4399b35", size = 789194, upload-time = "2025-09-25T21:32:43.362Z" }, + { url = "https://files.pythonhosted.org/packages/23/20/bb6982b26a40bb43951265ba29d4c246ef0ff59c9fdcdf0ed04e0687de4d/pyyaml-6.0.3-cp314-cp314-win_amd64.whl", hash = "sha256:4a2e8cebe2ff6ab7d1050ecd59c25d4c8bd7e6f400f5f82b96557ac0abafd0ac", size = 156429, upload-time = "2025-09-25T21:32:57.844Z" }, + { url = "https://files.pythonhosted.org/packages/f4/f4/a4541072bb9422c8a883ab55255f918fa378ecf083f5b85e87fc2b4eda1b/pyyaml-6.0.3-cp314-cp314-win_arm64.whl", hash = "sha256:93dda82c9c22deb0a405ea4dc5f2d0cda384168e466364dec6255b293923b2f3", size = 143912, upload-time = "2025-09-25T21:32:59.247Z" }, + { url = "https://files.pythonhosted.org/packages/7c/f9/07dd09ae774e4616edf6cda684ee78f97777bdd15847253637a6f052a62f/pyyaml-6.0.3-cp314-cp314t-macosx_10_13_x86_64.whl", hash = "sha256:02893d100e99e03eda1c8fd5c441d8c60103fd175728e23e431db1b589cf5ab3", size = 189108, upload-time = "2025-09-25T21:32:44.377Z" }, + { url = "https://files.pythonhosted.org/packages/4e/78/8d08c9fb7ce09ad8c38ad533c1191cf27f7ae1effe5bb9400a46d9437fcf/pyyaml-6.0.3-cp314-cp314t-macosx_11_0_arm64.whl", hash = "sha256:c1ff362665ae507275af2853520967820d9124984e0f7466736aea23d8611fba", size = 183641, upload-time = "2025-09-25T21:32:45.407Z" }, + { url = "https://files.pythonhosted.org/packages/7b/5b/3babb19104a46945cf816d047db2788bcaf8c94527a805610b0289a01c6b/pyyaml-6.0.3-cp314-cp314t-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:6adc77889b628398debc7b65c073bcb99c4a0237b248cacaf3fe8a557563ef6c", size = 831901, upload-time = "2025-09-25T21:32:48.83Z" }, + { url = "https://files.pythonhosted.org/packages/8b/cc/dff0684d8dc44da4d22a13f35f073d558c268780ce3c6ba1b87055bb0b87/pyyaml-6.0.3-cp314-cp314t-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:a80cb027f6b349846a3bf6d73b5e95e782175e52f22108cfa17876aaeff93702", size = 861132, upload-time = "2025-09-25T21:32:50.149Z" }, + { url = "https://files.pythonhosted.org/packages/b1/5e/f77dc6b9036943e285ba76b49e118d9ea929885becb0a29ba8a7c75e29fe/pyyaml-6.0.3-cp314-cp314t-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:00c4bdeba853cc34e7dd471f16b4114f4162dc03e6b7afcc2128711f0eca823c", size = 839261, upload-time = "2025-09-25T21:32:51.808Z" }, + { url = "https://files.pythonhosted.org/packages/ce/88/a9db1376aa2a228197c58b37302f284b5617f56a5d959fd1763fb1675ce6/pyyaml-6.0.3-cp314-cp314t-musllinux_1_2_aarch64.whl", hash = "sha256:66e1674c3ef6f541c35191caae2d429b967b99e02040f5ba928632d9a7f0f065", size = 805272, upload-time = "2025-09-25T21:32:52.941Z" }, + { url = "https://files.pythonhosted.org/packages/da/92/1446574745d74df0c92e6aa4a7b0b3130706a4142b2d1a5869f2eaa423c6/pyyaml-6.0.3-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:16249ee61e95f858e83976573de0f5b2893b3677ba71c9dd36b9cf8be9ac6d65", size = 829923, upload-time = "2025-09-25T21:32:54.537Z" }, + { url = "https://files.pythonhosted.org/packages/f0/7a/1c7270340330e575b92f397352af856a8c06f230aa3e76f86b39d01b416a/pyyaml-6.0.3-cp314-cp314t-win_amd64.whl", hash = "sha256:4ad1906908f2f5ae4e5a8ddfce73c320c2a1429ec52eafd27138b7f1cbe341c9", size = 174062, upload-time = "2025-09-25T21:32:55.767Z" }, + { url = "https://files.pythonhosted.org/packages/f1/12/de94a39c2ef588c7e6455cfbe7343d3b2dc9d6b6b2f40c4c6565744c873d/pyyaml-6.0.3-cp314-cp314t-win_arm64.whl", hash = "sha256:ebc55a14a21cb14062aa4162f906cd962b28e2e9ea38f9b4391244cd8de4ae0b", size = 149341, upload-time = "2025-09-25T21:32:56.828Z" }, +] + +[[package]] +name = "rich" +version = "14.2.0" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "markdown-it-py" }, + { name = "pygments" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/fb/d2/8920e102050a0de7bfabeb4c4614a49248cf8d5d7a8d01885fbb24dc767a/rich-14.2.0.tar.gz", hash = "sha256:73ff50c7c0c1c77c8243079283f4edb376f0f6442433aecb8ce7e6d0b92d1fe4", size = 219990, upload-time = "2025-10-09T14:16:53.064Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/25/7a/b0178788f8dc6cafce37a212c99565fa1fe7872c70c6c9c1e1a372d9d88f/rich-14.2.0-py3-none-any.whl", hash = "sha256:76bc51fe2e57d2b1be1f96c524b890b816e334ab4c1e45888799bfaab0021edd", size = 243393, upload-time = "2025-10-09T14:16:51.245Z" }, +] + +[[package]] +name = "rich-toolkit" +version = "0.20.1" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "click" }, + { name = "rich" }, + { name = "typing-extensions" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/29/63/3e427c62f1992945c997d4ec31e2fcb37d26aadbe5aa44ae5b29f7f64d26/rich_toolkit-0.20.1.tar.gz", hash = "sha256:c7336ae281f435c785acecaedc4b71d4b663dc73d9c8079fea96372527e822a4", size = 203473, upload-time = "2026-06-05T08:56:57.679Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/00/88/309f07d08155da2ba1d5ceb42d270fb42fbe34a807684543e3ffc10fe713/rich_toolkit-0.20.1-py3-none-any.whl", hash = "sha256:2a6d5f8e15759b9eba5a9ee63da10b275359ead20e5a0fc92bd5b4dbae8ce4bf", size = 35525, upload-time = "2026-06-05T08:56:58.586Z" }, +] + +[[package]] +name = "rignore" +version = "0.7.6" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/e5/f5/8bed2310abe4ae04b67a38374a4d311dd85220f5d8da56f47ae9361be0b0/rignore-0.7.6.tar.gz", hash = "sha256:00d3546cd793c30cb17921ce674d2c8f3a4b00501cb0e3dd0e82217dbeba2671", size = 57140, upload-time = "2025-11-05T21:41:21.968Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/86/7a/b970cd0138b0ece72eb28f086e933f9ed75b795716ad3de5ab22994b3b54/rignore-0.7.6-cp310-cp310-macosx_10_12_x86_64.whl", hash = "sha256:f3c74a7e5ee77aea669c95fdb3933f2a6c7549893700082e759128a29cf67e45", size = 884999, upload-time = "2025-11-05T20:42:38.373Z" }, + { url = "https://files.pythonhosted.org/packages/ca/05/23faca29616d8966ada63fb0e13c214107811fa9a0aba2275e4c7ca63bd5/rignore-0.7.6-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:b7202404958f5fe3474bac91f65350f0b1dde1a5e05089f2946549b7e91e79ec", size = 824824, upload-time = "2025-11-05T20:42:22.1Z" }, + { url = "https://files.pythonhosted.org/packages/fa/2e/05a1e61f04cf2548524224f0b5f21ca19ea58f7273a863bac10846b8ff69/rignore-0.7.6-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:6bde7c5835fa3905bfb7e329a4f1d7eccb676de63da7a3f934ddd5c06df20597", size = 899121, upload-time = "2025-11-05T20:40:48.94Z" }, + { url = "https://files.pythonhosted.org/packages/ff/35/71518847e10bdbf359badad8800e4681757a01f4777b3c5e03dbde8a42d8/rignore-0.7.6-cp310-cp310-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:626c3d4ba03af266694d25101bc1d8d16eda49c5feb86cedfec31c614fceca7d", size = 873813, upload-time = "2025-11-05T20:41:04.71Z" }, + { url = "https://files.pythonhosted.org/packages/f6/c8/32ae405d3e7fd4d9f9b7838f2fcca0a5005bb87fa514b83f83fd81c0df22/rignore-0.7.6-cp310-cp310-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:0a43841e651e7a05a4274b9026cc408d1912e64016ede8cd4c145dae5d0635be", size = 1168019, upload-time = "2025-11-05T20:41:20.723Z" }, + { url = "https://files.pythonhosted.org/packages/25/98/013c955982bc5b4719bf9a5bea58be317eea28aa12bfd004025e3cd7c000/rignore-0.7.6-cp310-cp310-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:7978c498dbf7f74d30cdb8859fe612167d8247f0acd377ae85180e34490725da", size = 942822, upload-time = "2025-11-05T20:41:36.99Z" }, + { url = "https://files.pythonhosted.org/packages/90/fb/9a3f3156c6ed30bcd597e63690353edac1fcffe9d382ad517722b56ac195/rignore-0.7.6-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:2d22f72ab695c07d2d96d2a645208daff17084441b5d58c07378c9dd6f9c4c87", size = 959820, upload-time = "2025-11-05T20:42:06.364Z" }, + { url = "https://files.pythonhosted.org/packages/5e/b2/93bf609633021e9658acaff24cfb055d8cdaf7f5855d10ebb35307900dda/rignore-0.7.6-cp310-cp310-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:d5bd8e1a91ed1a789b2cbe39eeea9204a6719d4f2cf443a9544b521a285a295f", size = 985050, upload-time = "2025-11-05T20:41:51.124Z" }, + { url = "https://files.pythonhosted.org/packages/69/bc/ec2d040469bdfd7b743df10f2201c5d285009a4263d506edbf7a06a090bb/rignore-0.7.6-cp310-cp310-musllinux_1_2_aarch64.whl", hash = "sha256:bc1fc03efad5789365018e94ac4079f851a999bc154d1551c45179f7fcf45322", size = 1079164, upload-time = "2025-11-05T21:40:10.368Z" }, + { url = "https://files.pythonhosted.org/packages/df/26/4b635f4ea5baf4baa8ba8eee06163f6af6e76dfbe72deb57da34bb24b19d/rignore-0.7.6-cp310-cp310-musllinux_1_2_armv7l.whl", hash = "sha256:ce2617fe28c51367fd8abfd4eeea9e61664af63c17d4ea00353d8ef56dfb95fa", size = 1139028, upload-time = "2025-11-05T21:40:27.977Z" }, + { url = "https://files.pythonhosted.org/packages/6a/54/a3147ebd1e477b06eb24e2c2c56d951ae5faa9045b7b36d7892fec5080d9/rignore-0.7.6-cp310-cp310-musllinux_1_2_i686.whl", hash = "sha256:7c4ad2cee85068408e7819a38243043214e2c3047e9bd4c506f8de01c302709e", size = 1119024, upload-time = "2025-11-05T21:40:45.148Z" }, + { url = "https://files.pythonhosted.org/packages/fb/f4/27475db769a57cff18fe7e7267b36e6cdb5b1281caa185ba544171106cba/rignore-0.7.6-cp310-cp310-musllinux_1_2_x86_64.whl", hash = "sha256:02cd240bfd59ecc3907766f4839cbba20530a2e470abca09eaa82225e4d946fb", size = 1128531, upload-time = "2025-11-05T21:41:02.734Z" }, + { url = "https://files.pythonhosted.org/packages/97/32/6e782d3b352e4349fa0e90bf75b13cb7f11d8908b36d9e2b262224b65d9a/rignore-0.7.6-cp310-cp310-win32.whl", hash = "sha256:fe2bd8fa1ff555259df54c376abc73855cb02628a474a40d51b358c3a1ddc55b", size = 646817, upload-time = "2025-11-05T21:41:47.51Z" }, + { url = "https://files.pythonhosted.org/packages/c0/8a/53185c69abb3bb362e8a46b8089999f820bf15655629ff8395107633c8ab/rignore-0.7.6-cp310-cp310-win_amd64.whl", hash = "sha256:d80afd6071c78baf3765ec698841071b19e41c326f994cfa69b5a1df676f5d39", size = 727001, upload-time = "2025-11-05T21:41:32.778Z" }, + { url = "https://files.pythonhosted.org/packages/25/41/b6e2be3069ef3b7f24e35d2911bd6deb83d20ed5642ad81d5a6d1c015473/rignore-0.7.6-cp311-cp311-macosx_10_12_x86_64.whl", hash = "sha256:40be8226e12d6653abbebaffaea2885f80374c1c8f76fe5ca9e0cadd120a272c", size = 885285, upload-time = "2025-11-05T20:42:39.763Z" }, + { url = "https://files.pythonhosted.org/packages/52/66/ba7f561b6062402022887706a7f2b2c2e2e2a28f1e3839202b0a2f77e36d/rignore-0.7.6-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:182f4e5e4064d947c756819446a7d4cdede8e756b8c81cf9e509683fe38778d7", size = 823882, upload-time = "2025-11-05T20:42:23.488Z" }, + { url = "https://files.pythonhosted.org/packages/f5/81/4087453df35a90b07370647b19017029324950c1b9137d54bf1f33843f17/rignore-0.7.6-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:16b63047648a916a87be1e51bb5c009063f1b8b6f5afe4f04f875525507e63dc", size = 899362, upload-time = "2025-11-05T20:40:51.111Z" }, + { url = "https://files.pythonhosted.org/packages/fb/c9/390a8fdfabb76d71416be773bd9f162977bd483084f68daf19da1dec88a6/rignore-0.7.6-cp311-cp311-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:ba5524f5178deca4d7695e936604ebc742acb8958f9395776e1fcb8133f8257a", size = 873633, upload-time = "2025-11-05T20:41:06.193Z" }, + { url = "https://files.pythonhosted.org/packages/df/c9/79404fcb0faa76edfbc9df0901f8ef18568d1104919ebbbad6d608c888d1/rignore-0.7.6-cp311-cp311-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:62020dbb89a1dd4b84ab3d60547b3b2eb2723641d5fb198463643f71eaaed57d", size = 1167633, upload-time = "2025-11-05T20:41:22.491Z" }, + { url = "https://files.pythonhosted.org/packages/6e/8d/b3466d32d445d158a0aceb80919085baaae495b1f540fb942f91d93b5e5b/rignore-0.7.6-cp311-cp311-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:b34acd532769d5a6f153a52a98dcb81615c949ab11697ce26b2eb776af2e174d", size = 941434, upload-time = "2025-11-05T20:41:38.151Z" }, + { url = "https://files.pythonhosted.org/packages/e8/40/9cd949761a7af5bc27022a939c91ff622d29c7a0b66d0c13a863097dde2d/rignore-0.7.6-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:1c5e53b752f9de44dff7b3be3c98455ce3bf88e69d6dc0cf4f213346c5e3416c", size = 959461, upload-time = "2025-11-05T20:42:08.476Z" }, + { url = "https://files.pythonhosted.org/packages/b5/87/1e1a145731f73bdb7835e11f80da06f79a00d68b370d9a847de979575e6d/rignore-0.7.6-cp311-cp311-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:25b3536d13a5d6409ce85f23936f044576eeebf7b6db1d078051b288410fc049", size = 985323, upload-time = "2025-11-05T20:41:52.735Z" }, + { url = "https://files.pythonhosted.org/packages/6c/31/1ecff992fc3f59c4fcdcb6c07d5f6c1e6dfb55ccda19c083aca9d86fa1c6/rignore-0.7.6-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:6e01cad2b0b92f6b1993f29fc01f23f2d78caf4bf93b11096d28e9d578eb08ce", size = 1079173, upload-time = "2025-11-05T21:40:12.007Z" }, + { url = "https://files.pythonhosted.org/packages/17/18/162eedadb4c2282fa4c521700dbf93c9b14b8842e8354f7d72b445b8d593/rignore-0.7.6-cp311-cp311-musllinux_1_2_armv7l.whl", hash = "sha256:5991e46ab9b4868334c9e372ab0892b0150f3f586ff2b1e314272caeb38aaedb", size = 1139012, upload-time = "2025-11-05T21:40:29.399Z" }, + { url = "https://files.pythonhosted.org/packages/78/96/a9ca398a8af74bb143ad66c2a31303c894111977e28b0d0eab03867f1b43/rignore-0.7.6-cp311-cp311-musllinux_1_2_i686.whl", hash = "sha256:6c8ae562e5d1246cba5eaeb92a47b2a279e7637102828dde41dcbe291f529a3e", size = 1118827, upload-time = "2025-11-05T21:40:46.6Z" }, + { url = "https://files.pythonhosted.org/packages/9f/22/1c1a65047df864def9a047dbb40bc0b580b8289a4280e62779cd61ae21f2/rignore-0.7.6-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:aaf938530dcc0b47c4cfa52807aa2e5bfd5ca6d57a621125fe293098692f6345", size = 1128182, upload-time = "2025-11-05T21:41:04.239Z" }, + { url = "https://files.pythonhosted.org/packages/bd/f4/1526eb01fdc2235aca1fd9d0189bee4021d009a8dcb0161540238c24166e/rignore-0.7.6-cp311-cp311-win32.whl", hash = "sha256:166ebce373105dd485ec213a6a2695986346e60c94ff3d84eb532a237b24a4d5", size = 646547, upload-time = "2025-11-05T21:41:49.439Z" }, + { url = "https://files.pythonhosted.org/packages/7c/c8/dda0983e1845706beb5826459781549a840fe5a7eb934abc523e8cd17814/rignore-0.7.6-cp311-cp311-win_amd64.whl", hash = "sha256:44f35ee844b1a8cea50d056e6a595190ce9d42d3cccf9f19d280ae5f3058973a", size = 727139, upload-time = "2025-11-05T21:41:34.367Z" }, + { url = "https://files.pythonhosted.org/packages/e3/47/eb1206b7bf65970d41190b879e1723fc6bbdb2d45e53565f28991a8d9d96/rignore-0.7.6-cp311-cp311-win_arm64.whl", hash = "sha256:14b58f3da4fa3d5c3fa865cab49821675371f5e979281c683e131ae29159a581", size = 657598, upload-time = "2025-11-05T21:41:23.758Z" }, + { url = "https://files.pythonhosted.org/packages/0b/0e/012556ef3047a2628842b44e753bb15f4dc46806780ff090f1e8fe4bf1eb/rignore-0.7.6-cp312-cp312-macosx_10_12_x86_64.whl", hash = "sha256:03e82348cb7234f8d9b2834f854400ddbbd04c0f8f35495119e66adbd37827a8", size = 883488, upload-time = "2025-11-05T20:42:41.359Z" }, + { url = "https://files.pythonhosted.org/packages/93/b0/d4f1f3fe9eb3f8e382d45ce5b0547ea01c4b7e0b4b4eb87bcd66a1d2b888/rignore-0.7.6-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:b9e624f6be6116ea682e76c5feb71ea91255c67c86cb75befe774365b2931961", size = 820411, upload-time = "2025-11-05T20:42:24.782Z" }, + { url = "https://files.pythonhosted.org/packages/4a/c8/dea564b36dedac8de21c18e1851789545bc52a0c22ece9843444d5608a6a/rignore-0.7.6-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:bda49950d405aa8d0ebe26af807c4e662dd281d926530f03f29690a2e07d649a", size = 897821, upload-time = "2025-11-05T20:40:52.613Z" }, + { url = "https://files.pythonhosted.org/packages/b3/2b/ee96db17ac1835e024c5d0742eefb7e46de60020385ac883dd3d1cde2c1f/rignore-0.7.6-cp312-cp312-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:b5fd5ab3840b8c16851d327ed06e9b8be6459702a53e5ab1fc4073b684b3789e", size = 873963, upload-time = "2025-11-05T20:41:07.49Z" }, + { url = "https://files.pythonhosted.org/packages/a5/8c/ad5a57bbb9d14d5c7e5960f712a8a0b902472ea3f4a2138cbf70d1777b75/rignore-0.7.6-cp312-cp312-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:ced2a248352636a5c77504cb755dc02c2eef9a820a44d3f33061ce1bb8a7f2d2", size = 1169216, upload-time = "2025-11-05T20:41:23.73Z" }, + { url = "https://files.pythonhosted.org/packages/80/e6/5b00bc2a6bc1701e6878fca798cf5d9125eb3113193e33078b6fc0d99123/rignore-0.7.6-cp312-cp312-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:a04a3b73b75ddc12c9c9b21efcdaab33ca3832941d6f1d67bffd860941cd448a", size = 942942, upload-time = "2025-11-05T20:41:39.393Z" }, + { url = "https://files.pythonhosted.org/packages/85/e5/7f99bd0cc9818a91d0e8b9acc65b792e35750e3bdccd15a7ee75e64efca4/rignore-0.7.6-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:d24321efac92140b7ec910ac7c53ab0f0c86a41133d2bb4b0e6a7c94967f44dd", size = 959787, upload-time = "2025-11-05T20:42:09.765Z" }, + { url = "https://files.pythonhosted.org/packages/55/54/2ffea79a7c1eabcede1926347ebc2a81bc6b81f447d05b52af9af14948b9/rignore-0.7.6-cp312-cp312-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:73c7aa109d41e593785c55fdaa89ad80b10330affa9f9d3e3a51fa695f739b20", size = 984245, upload-time = "2025-11-05T20:41:54.062Z" }, + { url = "https://files.pythonhosted.org/packages/41/f7/e80f55dfe0f35787fa482aa18689b9c8251e045076c35477deb0007b3277/rignore-0.7.6-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:1734dc49d1e9501b07852ef44421f84d9f378da9fbeda729e77db71f49cac28b", size = 1078647, upload-time = "2025-11-05T21:40:13.463Z" }, + { url = "https://files.pythonhosted.org/packages/d4/cf/2c64f0b6725149f7c6e7e5a909d14354889b4beaadddaa5fff023ec71084/rignore-0.7.6-cp312-cp312-musllinux_1_2_armv7l.whl", hash = "sha256:5719ea14ea2b652c0c0894be5dfde954e1853a80dea27dd2fbaa749618d837f5", size = 1139186, upload-time = "2025-11-05T21:40:31.27Z" }, + { url = "https://files.pythonhosted.org/packages/75/95/a86c84909ccc24af0d094b50d54697951e576c252a4d9f21b47b52af9598/rignore-0.7.6-cp312-cp312-musllinux_1_2_i686.whl", hash = "sha256:8e23424fc7ce35726854f639cb7968151a792c0c3d9d082f7f67e0c362cfecca", size = 1117604, upload-time = "2025-11-05T21:40:48.07Z" }, + { url = "https://files.pythonhosted.org/packages/7f/5e/13b249613fd5d18d58662490ab910a9f0be758981d1797789913adb4e918/rignore-0.7.6-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:3efdcf1dd84d45f3e2bd2f93303d9be103888f56dfa7c3349b5bf4f0657ec696", size = 1127725, upload-time = "2025-11-05T21:41:05.804Z" }, + { url = "https://files.pythonhosted.org/packages/c7/28/fa5dcd1e2e16982c359128664e3785f202d3eca9b22dd0b2f91c4b3d242f/rignore-0.7.6-cp312-cp312-win32.whl", hash = "sha256:ccca9d1a8b5234c76b71546fc3c134533b013f40495f394a65614a81f7387046", size = 646145, upload-time = "2025-11-05T21:41:51.096Z" }, + { url = "https://files.pythonhosted.org/packages/26/87/69387fb5dd81a0f771936381431780b8cf66fcd2cfe9495e1aaf41548931/rignore-0.7.6-cp312-cp312-win_amd64.whl", hash = "sha256:c96a285e4a8bfec0652e0bfcf42b1aabcdda1e7625f5006d188e3b1c87fdb543", size = 726090, upload-time = "2025-11-05T21:41:36.485Z" }, + { url = "https://files.pythonhosted.org/packages/24/5f/e8418108dcda8087fb198a6f81caadbcda9fd115d61154bf0df4d6d3619b/rignore-0.7.6-cp312-cp312-win_arm64.whl", hash = "sha256:a64a750e7a8277a323f01ca50b7784a764845f6cce2fe38831cb93f0508d0051", size = 656317, upload-time = "2025-11-05T21:41:25.305Z" }, + { url = "https://files.pythonhosted.org/packages/b7/8a/a4078f6e14932ac7edb171149c481de29969d96ddee3ece5dc4c26f9e0c3/rignore-0.7.6-cp313-cp313-macosx_10_12_x86_64.whl", hash = "sha256:2bdab1d31ec9b4fb1331980ee49ea051c0d7f7bb6baa28b3125ef03cdc48fdaf", size = 883057, upload-time = "2025-11-05T20:42:42.741Z" }, + { url = "https://files.pythonhosted.org/packages/f9/8f/f8daacd177db4bf7c2223bab41e630c52711f8af9ed279be2058d2fe4982/rignore-0.7.6-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:90f0a00ce0c866c275bf888271f1dc0d2140f29b82fcf33cdbda1e1a6af01010", size = 820150, upload-time = "2025-11-05T20:42:26.545Z" }, + { url = "https://files.pythonhosted.org/packages/36/31/b65b837e39c3f7064c426754714ac633b66b8c2290978af9d7f513e14aa9/rignore-0.7.6-cp313-cp313-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:c1ad295537041dc2ed4b540fb1a3906bd9ede6ccdad3fe79770cd89e04e3c73c", size = 897406, upload-time = "2025-11-05T20:40:53.854Z" }, + { url = "https://files.pythonhosted.org/packages/ca/58/1970ce006c427e202ac7c081435719a076c478f07b3a23f469227788dc23/rignore-0.7.6-cp313-cp313-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:f782dbd3a65a5ac85adfff69e5c6b101285ef3f845c3a3cae56a54bebf9fe116", size = 874050, upload-time = "2025-11-05T20:41:08.922Z" }, + { url = "https://files.pythonhosted.org/packages/d4/00/eb45db9f90137329072a732273be0d383cb7d7f50ddc8e0bceea34c1dfdf/rignore-0.7.6-cp313-cp313-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:65cece3b36e5b0826d946494734c0e6aaf5a0337e18ff55b071438efe13d559e", size = 1167835, upload-time = "2025-11-05T20:41:24.997Z" }, + { url = "https://files.pythonhosted.org/packages/f3/f1/6f1d72ddca41a64eed569680587a1236633587cc9f78136477ae69e2c88a/rignore-0.7.6-cp313-cp313-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:d7e4bb66c13cd7602dc8931822c02dfbbd5252015c750ac5d6152b186f0a8be0", size = 941945, upload-time = "2025-11-05T20:41:40.628Z" }, + { url = "https://files.pythonhosted.org/packages/48/6f/2f178af1c1a276a065f563ec1e11e7a9e23d4996fd0465516afce4b5c636/rignore-0.7.6-cp313-cp313-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:297e500c15766e196f68aaaa70e8b6db85fa23fdc075b880d8231fdfba738cd7", size = 959067, upload-time = "2025-11-05T20:42:11.09Z" }, + { url = "https://files.pythonhosted.org/packages/5b/db/423a81c4c1e173877c7f9b5767dcaf1ab50484a94f60a0b2ed78be3fa765/rignore-0.7.6-cp313-cp313-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:a07084211a8d35e1a5b1d32b9661a5ed20669970b369df0cf77da3adea3405de", size = 984438, upload-time = "2025-11-05T20:41:55.443Z" }, + { url = "https://files.pythonhosted.org/packages/31/eb/c4f92cc3f2825d501d3c46a244a671eb737fc1bcf7b05a3ecd34abb3e0d7/rignore-0.7.6-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:181eb2a975a22256a1441a9d2f15eb1292839ea3f05606620bd9e1938302cf79", size = 1078365, upload-time = "2025-11-05T21:40:15.148Z" }, + { url = "https://files.pythonhosted.org/packages/26/09/99442f02794bd7441bfc8ed1c7319e890449b816a7493b2db0e30af39095/rignore-0.7.6-cp313-cp313-musllinux_1_2_armv7l.whl", hash = "sha256:7bbcdc52b5bf9f054b34ce4af5269df5d863d9c2456243338bc193c28022bd7b", size = 1139066, upload-time = "2025-11-05T21:40:32.771Z" }, + { url = "https://files.pythonhosted.org/packages/2c/88/bcfc21e520bba975410e9419450f4b90a2ac8236b9a80fd8130e87d098af/rignore-0.7.6-cp313-cp313-musllinux_1_2_i686.whl", hash = "sha256:f2e027a6da21a7c8c0d87553c24ca5cc4364def18d146057862c23a96546238e", size = 1118036, upload-time = "2025-11-05T21:40:49.646Z" }, + { url = "https://files.pythonhosted.org/packages/e2/25/d37215e4562cda5c13312636393aea0bafe38d54d4e0517520a4cc0753ec/rignore-0.7.6-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:ee4a18b82cbbc648e4aac1510066682fe62beb5dc88e2c67c53a83954e541360", size = 1127550, upload-time = "2025-11-05T21:41:07.648Z" }, + { url = "https://files.pythonhosted.org/packages/dc/76/a264ab38bfa1620ec12a8ff1c07778da89e16d8c0f3450b0333020d3d6dc/rignore-0.7.6-cp313-cp313-win32.whl", hash = "sha256:a7d7148b6e5e95035d4390396895adc384d37ff4e06781a36fe573bba7c283e5", size = 646097, upload-time = "2025-11-05T21:41:53.201Z" }, + { url = "https://files.pythonhosted.org/packages/62/44/3c31b8983c29ea8832b6082ddb1d07b90379c2d993bd20fce4487b71b4f4/rignore-0.7.6-cp313-cp313-win_amd64.whl", hash = "sha256:b037c4b15a64dced08fc12310ee844ec2284c4c5c1ca77bc37d0a04f7bff386e", size = 726170, upload-time = "2025-11-05T21:41:38.131Z" }, + { url = "https://files.pythonhosted.org/packages/aa/41/e26a075cab83debe41a42661262f606166157df84e0e02e2d904d134c0d8/rignore-0.7.6-cp313-cp313-win_arm64.whl", hash = "sha256:e47443de9b12fe569889bdbe020abe0e0b667516ee2ab435443f6d0869bd2804", size = 656184, upload-time = "2025-11-05T21:41:27.396Z" }, + { url = "https://files.pythonhosted.org/packages/9a/b9/1f5bd82b87e5550cd843ceb3768b4a8ef274eb63f29333cf2f29644b3d75/rignore-0.7.6-cp314-cp314-macosx_10_12_x86_64.whl", hash = "sha256:8e41be9fa8f2f47239ded8920cc283699a052ac4c371f77f5ac017ebeed75732", size = 882632, upload-time = "2025-11-05T20:42:44.063Z" }, + { url = "https://files.pythonhosted.org/packages/e9/6b/07714a3efe4a8048864e8a5b7db311ba51b921e15268b17defaebf56d3db/rignore-0.7.6-cp314-cp314-macosx_11_0_arm64.whl", hash = "sha256:6dc1e171e52cefa6c20e60c05394a71165663b48bca6c7666dee4f778f2a7d90", size = 820760, upload-time = "2025-11-05T20:42:27.885Z" }, + { url = "https://files.pythonhosted.org/packages/ac/0f/348c829ea2d8d596e856371b14b9092f8a5dfbb62674ec9b3f67e4939a9d/rignore-0.7.6-cp314-cp314-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:0ce2268837c3600f82ab8db58f5834009dc638ee17103582960da668963bebc5", size = 899044, upload-time = "2025-11-05T20:40:55.336Z" }, + { url = "https://files.pythonhosted.org/packages/f0/30/2e1841a19b4dd23878d73edd5d82e998a83d5ed9570a89675f140ca8b2ad/rignore-0.7.6-cp314-cp314-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:690a3e1b54bfe77e89c4bacb13f046e642f8baadafc61d68f5a726f324a76ab6", size = 874144, upload-time = "2025-11-05T20:41:10.195Z" }, + { url = "https://files.pythonhosted.org/packages/c2/bf/0ce9beb2e5f64c30e3580bef09f5829236889f01511a125f98b83169b993/rignore-0.7.6-cp314-cp314-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:09d12ac7a0b6210c07bcd145007117ebd8abe99c8eeb383e9e4673910c2754b2", size = 1168062, upload-time = "2025-11-05T20:41:26.511Z" }, + { url = "https://files.pythonhosted.org/packages/b9/8b/571c178414eb4014969865317da8a02ce4cf5241a41676ef91a59aab24de/rignore-0.7.6-cp314-cp314-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:2a2b2b74a8c60203b08452479b90e5ce3dbe96a916214bc9eb2e5af0b6a9beb0", size = 942542, upload-time = "2025-11-05T20:41:41.838Z" }, + { url = "https://files.pythonhosted.org/packages/19/62/7a3cf601d5a45137a7e2b89d10c05b5b86499190c4b7ca5c3c47d79ee519/rignore-0.7.6-cp314-cp314-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:8fc5a531ef02131e44359419a366bfac57f773ea58f5278c2cdd915f7d10ea94", size = 958739, upload-time = "2025-11-05T20:42:12.463Z" }, + { url = "https://files.pythonhosted.org/packages/5f/1f/4261f6a0d7caf2058a5cde2f5045f565ab91aa7badc972b57d19ce58b14e/rignore-0.7.6-cp314-cp314-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:b7a1f77d9c4cd7e76229e252614d963442686bfe12c787a49f4fe481df49e7a9", size = 984138, upload-time = "2025-11-05T20:41:56.775Z" }, + { url = "https://files.pythonhosted.org/packages/2b/bf/628dfe19c75e8ce1f45f7c248f5148b17dfa89a817f8e3552ab74c3ae812/rignore-0.7.6-cp314-cp314-musllinux_1_2_aarch64.whl", hash = "sha256:ead81f728682ba72b5b1c3d5846b011d3e0174da978de87c61645f2ed36659a7", size = 1079299, upload-time = "2025-11-05T21:40:16.639Z" }, + { url = "https://files.pythonhosted.org/packages/af/a5/be29c50f5c0c25c637ed32db8758fdf5b901a99e08b608971cda8afb293b/rignore-0.7.6-cp314-cp314-musllinux_1_2_armv7l.whl", hash = "sha256:12ffd50f520c22ffdabed8cd8bfb567d9ac165b2b854d3e679f4bcaef11a9441", size = 1139618, upload-time = "2025-11-05T21:40:34.507Z" }, + { url = "https://files.pythonhosted.org/packages/2a/40/3c46cd7ce4fa05c20b525fd60f599165e820af66e66f2c371cd50644558f/rignore-0.7.6-cp314-cp314-musllinux_1_2_i686.whl", hash = "sha256:e5a16890fbe3c894f8ca34b0fcacc2c200398d4d46ae654e03bc9b3dbf2a0a72", size = 1117626, upload-time = "2025-11-05T21:40:51.494Z" }, + { url = "https://files.pythonhosted.org/packages/8c/b9/aea926f263b8a29a23c75c2e0d8447965eb1879d3feb53cfcf84db67ed58/rignore-0.7.6-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:3abab3bf99e8a77488ef6c7c9a799fac22224c28fe9f25cc21aa7cc2b72bfc0b", size = 1128144, upload-time = "2025-11-05T21:41:09.169Z" }, + { url = "https://files.pythonhosted.org/packages/a4/f6/0d6242f8d0df7f2ecbe91679fefc1f75e7cd2072cb4f497abaab3f0f8523/rignore-0.7.6-cp314-cp314-win32.whl", hash = "sha256:eeef421c1782953c4375aa32f06ecae470c1285c6381eee2a30d2e02a5633001", size = 646385, upload-time = "2025-11-05T21:41:55.105Z" }, + { url = "https://files.pythonhosted.org/packages/d5/38/c0dcd7b10064f084343d6af26fe9414e46e9619c5f3224b5272e8e5d9956/rignore-0.7.6-cp314-cp314-win_amd64.whl", hash = "sha256:6aeed503b3b3d5af939b21d72a82521701a4bd3b89cd761da1e7dc78621af304", size = 725738, upload-time = "2025-11-05T21:41:39.736Z" }, + { url = "https://files.pythonhosted.org/packages/d9/7a/290f868296c1ece914d565757ab363b04730a728b544beb567ceb3b2d96f/rignore-0.7.6-cp314-cp314-win_arm64.whl", hash = "sha256:104f215b60b3c984c386c3e747d6ab4376d5656478694e22c7bd2f788ddd8304", size = 656008, upload-time = "2025-11-05T21:41:29.028Z" }, + { url = "https://files.pythonhosted.org/packages/ca/d2/3c74e3cd81fe8ea08a8dcd2d755c09ac2e8ad8fe409508904557b58383d3/rignore-0.7.6-cp314-cp314t-macosx_10_12_x86_64.whl", hash = "sha256:bb24a5b947656dd94cb9e41c4bc8b23cec0c435b58be0d74a874f63c259549e8", size = 882835, upload-time = "2025-11-05T20:42:45.443Z" }, + { url = "https://files.pythonhosted.org/packages/77/61/a772a34b6b63154877433ac2d048364815b24c2dd308f76b212c408101a2/rignore-0.7.6-cp314-cp314t-macosx_11_0_arm64.whl", hash = "sha256:5b1e33c9501cefe24b70a1eafd9821acfd0ebf0b35c3a379430a14df089993e3", size = 820301, upload-time = "2025-11-05T20:42:29.226Z" }, + { url = "https://files.pythonhosted.org/packages/71/30/054880b09c0b1b61d17eeb15279d8bf729c0ba52b36c3ada52fb827cbb3c/rignore-0.7.6-cp314-cp314t-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:bec3994665a44454df86deb762061e05cd4b61e3772f5b07d1882a8a0d2748d5", size = 897611, upload-time = "2025-11-05T20:40:56.475Z" }, + { url = "https://files.pythonhosted.org/packages/1e/40/b2d1c169f833d69931bf232600eaa3c7998ba4f9a402e43a822dad2ea9f2/rignore-0.7.6-cp314-cp314t-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:26cba2edfe3cff1dfa72bddf65d316ddebf182f011f2f61538705d6dbaf54986", size = 873875, upload-time = "2025-11-05T20:41:11.561Z" }, + { url = "https://files.pythonhosted.org/packages/55/59/ca5ae93d83a1a60e44b21d87deb48b177a8db1b85e82fc8a9abb24a8986d/rignore-0.7.6-cp314-cp314t-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:ffa86694fec604c613696cb91e43892aa22e1fec5f9870e48f111c603e5ec4e9", size = 1167245, upload-time = "2025-11-05T20:41:28.29Z" }, + { url = "https://files.pythonhosted.org/packages/a5/52/cf3dce392ba2af806cba265aad6bcd9c48bb2a6cb5eee448d3319f6e505b/rignore-0.7.6-cp314-cp314t-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:48efe2ed95aa8104145004afb15cdfa02bea5cdde8b0344afeb0434f0d989aa2", size = 941750, upload-time = "2025-11-05T20:41:43.111Z" }, + { url = "https://files.pythonhosted.org/packages/ec/be/3f344c6218d779395e785091d05396dfd8b625f6aafbe502746fcd880af2/rignore-0.7.6-cp314-cp314t-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:8dcae43eb44b7f2457fef7cc87f103f9a0013017a6f4e62182c565e924948f21", size = 958896, upload-time = "2025-11-05T20:42:13.784Z" }, + { url = "https://files.pythonhosted.org/packages/c9/34/d3fa71938aed7d00dcad87f0f9bcb02ad66c85d6ffc83ba31078ce53646a/rignore-0.7.6-cp314-cp314t-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:2cd649a7091c0dad2f11ef65630d30c698d505cbe8660dd395268e7c099cc99f", size = 983992, upload-time = "2025-11-05T20:41:58.022Z" }, + { url = "https://files.pythonhosted.org/packages/24/a4/52a697158e9920705bdbd0748d59fa63e0f3233fb92e9df9a71afbead6ca/rignore-0.7.6-cp314-cp314t-musllinux_1_2_aarch64.whl", hash = "sha256:42de84b0289d478d30ceb7ae59023f7b0527786a9a5b490830e080f0e4ea5aeb", size = 1078181, upload-time = "2025-11-05T21:40:18.151Z" }, + { url = "https://files.pythonhosted.org/packages/ac/65/aa76dbcdabf3787a6f0fd61b5cc8ed1e88580590556d6c0207960d2384bb/rignore-0.7.6-cp314-cp314t-musllinux_1_2_armv7l.whl", hash = "sha256:875a617e57b53b4acbc5a91de418233849711c02e29cc1f4f9febb2f928af013", size = 1139232, upload-time = "2025-11-05T21:40:35.966Z" }, + { url = "https://files.pythonhosted.org/packages/08/44/31b31a49b3233c6842acc1c0731aa1e7fb322a7170612acf30327f700b44/rignore-0.7.6-cp314-cp314t-musllinux_1_2_i686.whl", hash = "sha256:8703998902771e96e49968105207719f22926e4431b108450f3f430b4e268b7c", size = 1117349, upload-time = "2025-11-05T21:40:53.013Z" }, + { url = "https://files.pythonhosted.org/packages/e9/ae/1b199a2302c19c658cf74e5ee1427605234e8c91787cfba0015f2ace145b/rignore-0.7.6-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:602ef33f3e1b04c1e9a10a3c03f8bc3cef2d2383dcc250d309be42b49923cabc", size = 1127702, upload-time = "2025-11-05T21:41:10.881Z" }, + { url = "https://files.pythonhosted.org/packages/fc/d3/18210222b37e87e36357f7b300b7d98c6dd62b133771e71ae27acba83a4f/rignore-0.7.6-cp314-cp314t-win32.whl", hash = "sha256:c1d8f117f7da0a4a96a8daef3da75bc090e3792d30b8b12cfadc240c631353f9", size = 647033, upload-time = "2025-11-05T21:42:00.095Z" }, + { url = "https://files.pythonhosted.org/packages/3e/87/033eebfbee3ec7d92b3bb1717d8f68c88e6fc7de54537040f3b3a405726f/rignore-0.7.6-cp314-cp314t-win_amd64.whl", hash = "sha256:ca36e59408bec81de75d307c568c2d0d410fb880b1769be43611472c61e85c96", size = 725647, upload-time = "2025-11-05T21:41:44.449Z" }, + { url = "https://files.pythonhosted.org/packages/79/62/b88e5879512c55b8ee979c666ee6902adc4ed05007226de266410ae27965/rignore-0.7.6-cp314-cp314t-win_arm64.whl", hash = "sha256:b83adabeb3e8cf662cabe1931b83e165b88c526fa6af6b3aa90429686e474896", size = 656035, upload-time = "2025-11-05T21:41:31.13Z" }, + { url = "https://files.pythonhosted.org/packages/85/12/62d690b4644c330d7ac0f739b7f078190ab4308faa909a60842d0e4af5b2/rignore-0.7.6-pp310-pypy310_pp73-macosx_10_12_x86_64.whl", hash = "sha256:c3d3a523af1cd4ed2c0cba8d277a32d329b0c96ef9901fb7ca45c8cfaccf31a5", size = 887462, upload-time = "2025-11-05T20:42:50.804Z" }, + { url = "https://files.pythonhosted.org/packages/05/bc/6528a0e97ed2bd7a7c329183367d1ffbc5b9762ae8348d88dae72cc9d1f5/rignore-0.7.6-pp310-pypy310_pp73-macosx_11_0_arm64.whl", hash = "sha256:990853566e65184a506e1e2af2d15045afad3ebaebb8859cb85b882081915110", size = 826918, upload-time = "2025-11-05T20:42:33.689Z" }, + { url = "https://files.pythonhosted.org/packages/3e/2c/7d7bad116e09a04e9e1688c6f891fa2d4fd33f11b69ac0bd92419ddebeae/rignore-0.7.6-pp310-pypy310_pp73-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:1cab9ff2e436ce7240d7ee301c8ef806ed77c1fd6b8a8239ff65f9bbbcb5b8a3", size = 900922, upload-time = "2025-11-05T20:41:00.361Z" }, + { url = "https://files.pythonhosted.org/packages/09/ba/e5ea89fbde8e37a90ce456e31c5e9d85512cef5ae38e0f4d2426eb776a19/rignore-0.7.6-pp310-pypy310_pp73-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:d1a6671b2082c13bfd9a5cf4ce64670f832a6d41470556112c4ab0b6519b2fc4", size = 876987, upload-time = "2025-11-05T20:41:16.219Z" }, + { url = "https://files.pythonhosted.org/packages/d0/fb/93d14193f0ec0c3d35b763f0a000e9780f63b2031f3d3756442c2152622d/rignore-0.7.6-pp310-pypy310_pp73-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:2468729b4c5295c199d084ab88a40afcb7c8b974276805105239c07855bbacee", size = 1171110, upload-time = "2025-11-05T20:41:32.631Z" }, + { url = "https://files.pythonhosted.org/packages/9e/46/08436312ff96ffa29cfa4e1a987efc37e094531db46ba5e9fda9bb792afd/rignore-0.7.6-pp310-pypy310_pp73-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:775710777fd71e5fdf54df69cdc249996a1d6f447a2b5bfb86dbf033fddd9cf9", size = 943339, upload-time = "2025-11-05T20:41:47.128Z" }, + { url = "https://files.pythonhosted.org/packages/34/28/3b3c51328f505cfaf7e53f408f78a1e955d561135d02f9cb0341ea99f69a/rignore-0.7.6-pp310-pypy310_pp73-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:4565407f4a77f72cf9d91469e75d15d375f755f0a01236bb8aaa176278cc7085", size = 961680, upload-time = "2025-11-05T20:42:18.061Z" }, + { url = "https://files.pythonhosted.org/packages/5c/9e/cbff75c8676d4f4a90bd58a1581249d255c7305141b0868f0abc0324836b/rignore-0.7.6-pp310-pypy310_pp73-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:dc44c33f8fb2d5c9da748de7a6e6653a78aa740655e7409895e94a247ffa97c8", size = 987045, upload-time = "2025-11-05T20:42:02.315Z" }, + { url = "https://files.pythonhosted.org/packages/8c/25/d802d1d369502a7ddb8816059e7c79d2d913e17df975b863418e0aca4d8a/rignore-0.7.6-pp310-pypy310_pp73-musllinux_1_2_aarch64.whl", hash = "sha256:8f32478f05540513c11923e8838afab9efef0131d66dca7f67f0e1bbd118af6a", size = 1080310, upload-time = "2025-11-05T21:40:23.184Z" }, + { url = "https://files.pythonhosted.org/packages/43/f0/250b785c2e473b1ab763eaf2be820934c2a5409a722e94b279dddac21c7d/rignore-0.7.6-pp310-pypy310_pp73-musllinux_1_2_armv7l.whl", hash = "sha256:1b63a3dd76225ea35b01dd6596aa90b275b5d0f71d6dc28fce6dd295d98614aa", size = 1140998, upload-time = "2025-11-05T21:40:40.603Z" }, + { url = "https://files.pythonhosted.org/packages/f5/d6/bb42fd2a8bba6aea327962656e20621fd495523259db40cfb4c5f760f05c/rignore-0.7.6-pp310-pypy310_pp73-musllinux_1_2_i686.whl", hash = "sha256:fe6c41175c36554a4ef0994cd1b4dbd6d73156fca779066456b781707402048e", size = 1121178, upload-time = "2025-11-05T21:40:57.585Z" }, + { url = "https://files.pythonhosted.org/packages/97/f4/aeb548374129dce3dc191a4bb598c944d9ed663f467b9af830315d86059c/rignore-0.7.6-pp310-pypy310_pp73-musllinux_1_2_x86_64.whl", hash = "sha256:9a0c6792406ae36f4e7664dc772da909451d46432ff8485774526232d4885063", size = 1130190, upload-time = "2025-11-05T21:41:16.403Z" }, + { url = "https://files.pythonhosted.org/packages/82/78/a6250ff0c49a3cdb943910ada4116e708118e9b901c878cfae616c80a904/rignore-0.7.6-pp311-pypy311_pp73-macosx_10_12_x86_64.whl", hash = "sha256:a20b6fb61bcced9a83dfcca6599ad45182b06ba720cff7c8d891e5b78db5b65f", size = 886470, upload-time = "2025-11-05T20:42:52.314Z" }, + { url = "https://files.pythonhosted.org/packages/35/af/c69c0c51b8f9f7914d95c4ea91c29a2ac067572048cae95dd6d2efdbe05d/rignore-0.7.6-pp311-pypy311_pp73-macosx_11_0_arm64.whl", hash = "sha256:392dcabfecbe176c9ebbcb40d85a5e86a5989559c4f988c2741da7daf1b5be25", size = 825976, upload-time = "2025-11-05T20:42:35.118Z" }, + { url = "https://files.pythonhosted.org/packages/f1/d2/1b264f56132264ea609d3213ab603d6a27016b19559a1a1ede1a66a03dcd/rignore-0.7.6-pp311-pypy311_pp73-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:22baa462abdc36fdd5a5e2dae423107723351b85ff093762f9261148b9d0a04a", size = 899739, upload-time = "2025-11-05T20:41:01.518Z" }, + { url = "https://files.pythonhosted.org/packages/55/e4/b3c5dfdd8d8a10741dfe7199ef45d19a0e42d0c13aa377c83bd6caf65d90/rignore-0.7.6-pp311-pypy311_pp73-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:53fb28882d2538cb2d231972146c4927a9d9455e62b209f85d634408c4103538", size = 874843, upload-time = "2025-11-05T20:41:17.687Z" }, + { url = "https://files.pythonhosted.org/packages/cc/10/d6f3750233881a2a154cefc9a6a0a9b19da526b19f7f08221b552c6f827d/rignore-0.7.6-pp311-pypy311_pp73-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:87409f7eeb1103d6b77f3472a3a0d9a5953e3ae804a55080bdcb0120ee43995b", size = 1170348, upload-time = "2025-11-05T20:41:34.21Z" }, + { url = "https://files.pythonhosted.org/packages/6e/10/ad98ca05c9771c15af734cee18114a3c280914b6e34fde9ffea2e61e88aa/rignore-0.7.6-pp311-pypy311_pp73-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:684014e42e4341ab3ea23a203551857fcc03a7f8ae96ca3aefb824663f55db32", size = 942315, upload-time = "2025-11-05T20:41:48.508Z" }, + { url = "https://files.pythonhosted.org/packages/de/00/ab5c0f872acb60d534e687e629c17e0896c62da9b389c66d3aa16b817aa8/rignore-0.7.6-pp311-pypy311_pp73-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:77356ebb01ba13f8a425c3d30fcad40e57719c0e37670d022d560884a30e4767", size = 961047, upload-time = "2025-11-05T20:42:19.403Z" }, + { url = "https://files.pythonhosted.org/packages/b8/86/3030fdc363a8f0d1cd155b4c453d6db9bab47a24fcc64d03f61d9d78fe6a/rignore-0.7.6-pp311-pypy311_pp73-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:6cbd8a48abbd3747a6c830393cd578782fab5d43f4deea48c5f5e344b8fed2b0", size = 986090, upload-time = "2025-11-05T20:42:03.581Z" }, + { url = "https://files.pythonhosted.org/packages/33/b8/133aa4002cee0ebbb39362f94e4898eec7fbd09cec9fcbce1cd65b355b7f/rignore-0.7.6-pp311-pypy311_pp73-musllinux_1_2_aarch64.whl", hash = "sha256:2673225dcec7f90497e79438c35e34638d0d0391ccea3cbb79bfb9adc0dc5bd7", size = 1079656, upload-time = "2025-11-05T21:40:24.89Z" }, + { url = "https://files.pythonhosted.org/packages/67/56/36d5d34210e5e7dfcd134eed8335b19e80ae940ee758f493e4f2b344dd70/rignore-0.7.6-pp311-pypy311_pp73-musllinux_1_2_armv7l.whl", hash = "sha256:c081f17290d8a2b96052b79207622aa635686ea39d502b976836384ede3d303c", size = 1139789, upload-time = "2025-11-05T21:40:42.119Z" }, + { url = "https://files.pythonhosted.org/packages/6b/5b/bb4f9420802bf73678033a4a55ab1bede36ce2e9b41fec5f966d83d932b3/rignore-0.7.6-pp311-pypy311_pp73-musllinux_1_2_i686.whl", hash = "sha256:57e8327aacc27f921968cb2a174f9e47b084ce9a7dd0122c8132d22358f6bd79", size = 1120308, upload-time = "2025-11-05T21:40:59.402Z" }, + { url = "https://files.pythonhosted.org/packages/ce/8b/a1299085b28a2f6135e30370b126e3c5055b61908622f2488ade67641479/rignore-0.7.6-pp311-pypy311_pp73-musllinux_1_2_x86_64.whl", hash = "sha256:d8955b57e42f2a5434670d5aa7b75eaf6e74602ccd8955dddf7045379cd762fb", size = 1129444, upload-time = "2025-11-05T21:41:17.906Z" }, +] + +[[package]] +name = "ruff" +version = "0.15.20" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/43/dc/35b341fc554ba02f217fc10da57d1a75168cfbcf75b0ef2202176d4c4f2d/ruff-0.15.20.tar.gz", hash = "sha256:1416eb04349192646b54de98f146c4f59afe37d0decfc02c3cbbf396f3a28566", size = 4755489, upload-time = "2026-06-25T17:20:37.578Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/94/d9/2d5014f0253ba541d2061d9fa7193f48e941c8b21bb88a7ff9bbe0bd0596/ruff-0.15.20-py3-none-linux_armv6l.whl", hash = "sha256:00e188c53e499c3c1637f73c91dcf2fb56d576cab76ce1be50a27c4e80e37078", size = 10839665, upload-time = "2026-06-25T17:19:44.702Z" }, + { url = "https://files.pythonhosted.org/packages/c6/d3/ac1798ba64f670698867fcfc591d50e7e421bef137db564858f619a30fcf/ruff-0.15.20-py3-none-macosx_10_12_x86_64.whl", hash = "sha256:9ebd1fd9b9c95fc0bd7b2761aebec1f030013d2e193a2901b224af68fe47251b", size = 11208649, upload-time = "2026-06-25T17:19:48.787Z" }, + { url = "https://files.pythonhosted.org/packages/47/47/d3ac899991202095dfcf3d5176be4272642be3cf981a2f1a30f72a2afb95/ruff-0.15.20-py3-none-macosx_11_0_arm64.whl", hash = "sha256:c5b16cdd67ca108185cd36dce98c576350c03b1660a751de725fb049193a0632", size = 10622638, upload-time = "2026-06-25T17:19:51.354Z" }, + { url = "https://files.pythonhosted.org/packages/33/13/4e043fe30aa94d4ff5213a9881fc296d12960f5971b234a5263fdc225312/ruff-0.15.20-py3-none-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:3413bb3c3d2ca6a8208f1f4809cd2dca3c6de6d0b491c0e70847672bde6e6efd", size = 10984227, upload-time = "2026-06-25T17:19:54.044Z" }, + { url = "https://files.pythonhosted.org/packages/76/e6/92e7bf40388bc5800073b96564f56264f7e48bfd1a498f5ced6ae6d5a769/ruff-0.15.20-py3-none-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:bd7ec42b3bb3da066488db093308a69c4ac5ee6d2af333a86ba6e2eb2e7dd44b", size = 10622882, upload-time = "2026-06-25T17:19:57.037Z" }, + { url = "https://files.pythonhosted.org/packages/13/7a/43460be3f24495a3aa46d4b16873e2c4941b3b5f0b00cf88c03b7b94b339/ruff-0.15.20-py3-none-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:e1a36ad0eb77fba9aabfb69ede54de6f376d04ac18ebea022847046d340a8267", size = 11474808, upload-time = "2026-06-25T17:20:00.357Z" }, + { url = "https://files.pythonhosted.org/packages/27/a0/f37077884873221c6b33b4ab49eb18f9f88e54a16a25a5bca59bef46dd66/ruff-0.15.20-py3-none-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:b6df3b1e4610432f0386dba04d853b5f08cbbc903410c6fcc02f620f05aff53c", size = 12293094, upload-time = "2026-06-25T17:20:03.446Z" }, + { url = "https://files.pythonhosted.org/packages/a6/74/165545b60256a9704c21ac0ec4a0d07933b320812f9584836c9f4aca4292/ruff-0.15.20-py3-none-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:e89f198a1ea6ef0d727c1cf16088bc91a6cb0ab947dedc966715691647186eae", size = 11526176, upload-time = "2026-06-25T17:20:06.301Z" }, + { url = "https://files.pythonhosted.org/packages/86/b1/a976a136d40ade83ce743578399865f57001003a409acadc0ecbb3051082/ruff-0.15.20-py3-none-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:309809086c2acb67624950a3c8133e80f32d0d3e27106c0cd60ff26657c9f24b", size = 11520767, upload-time = "2026-06-25T17:20:09.191Z" }, + { url = "https://files.pythonhosted.org/packages/19/0f/f032696cb01c9b54c0263fa393474d7758f1cdc021a01b04e3cbc2500999/ruff-0.15.20-py3-none-manylinux_2_31_riscv64.whl", hash = "sha256:2d2374caa2f2c2f9e2b7da0a50802cfb8b79f55a9b5e49379f564544fbf56487", size = 11500132, upload-time = "2026-06-25T17:20:13.602Z" }, + { url = "https://files.pythonhosted.org/packages/4b/f4/51b1a14bc69e8c224b15dab9cce8e99b425e0455d462caa2b3c9be2b6a8e/ruff-0.15.20-py3-none-musllinux_1_2_aarch64.whl", hash = "sha256:a1ed17b65293e0c2f22fc387bc13198a5de94bf4429589b0ff6946b0feaf21a3", size = 10943828, upload-time = "2026-06-25T17:20:16.635Z" }, + { url = "https://files.pythonhosted.org/packages/71/4b/fe267640783cd02bf6c5cc290b1df1051be2ec294c678b5c15fe19e52343/ruff-0.15.20-py3-none-musllinux_1_2_armv7l.whl", hash = "sha256:f701305e66b38ea6c91882490eb73459796808e4c6362a1b765255e0cdcd4053", size = 10645418, upload-time = "2026-06-25T17:20:19.4Z" }, + { url = "https://files.pythonhosted.org/packages/b0/c0/a65aa4ec2f5e87a1df32dc3ec1fede434fe3dfd5cbcf3b503cafc676ab54/ruff-0.15.20-py3-none-musllinux_1_2_i686.whl", hash = "sha256:5b9c0c367ad8e5d0d5b5b8537864c469a0a0e55417aadfbeca41fa61333be9f4", size = 11211770, upload-time = "2026-06-25T17:20:22.033Z" }, + { url = "https://files.pythonhosted.org/packages/5a/a4/0caa331d954ae2723d729d351c989cb4ca8b6077d5c6c2cb6de75e98c041/ruff-0.15.20-py3-none-musllinux_1_2_x86_64.whl", hash = "sha256:01cc00dd58f0df339d0e902219dd53990ea99996a0344e5d9cc8d45d5307e460", size = 11618698, upload-time = "2026-06-25T17:20:25.259Z" }, + { url = "https://files.pythonhosted.org/packages/10/9b/5f14927848d2fd4aa891fd88d883788c5a7baba561c7874732364045708c/ruff-0.15.20-py3-none-win32.whl", hash = "sha256:ed65ef510e43a137207e0f01cfcf998aeddb1aeeda5c9d35023e910284d7cf21", size = 10857322, upload-time = "2026-06-25T17:20:28.612Z" }, + { url = "https://files.pythonhosted.org/packages/fa/f0/fe47c501f9dea92a26d788ff98bb5d92ed4cb4c88792c5c88af6b697dc8e/ruff-0.15.20-py3-none-win_amd64.whl", hash = "sha256:a525c81c70fb0380344dd1d8745d8cc1c890b7fc94a58d5a07bd8eb9557b8415", size = 11993274, upload-time = "2026-06-25T17:20:31.871Z" }, + { url = "https://files.pythonhosted.org/packages/d7/2b/9555445e1201d92b3195f45cdb153a0b68f24e0a4273f6e3d5ab46e212bb/ruff-0.15.20-py3-none-win_arm64.whl", hash = "sha256:2f5b2a6d614e8700388806a14996c40fab2c47b819ef57d790a34878858ed9ca", size = 11343498, upload-time = "2026-06-25T17:20:35.03Z" }, +] + +[[package]] +name = "sentry-sdk" +version = "2.49.0" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "certifi" }, + { name = "urllib3" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/02/94/23ac26616a883f492428d9ee9ad6eee391612125326b784dbfc30e1e7bab/sentry_sdk-2.49.0.tar.gz", hash = "sha256:c1878599cde410d481c04ef50ee3aedd4f600e4d0d253f4763041e468b332c30", size = 387228, upload-time = "2026-01-08T09:56:25.642Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/88/43/1c586f9f413765201234541857cb82fda076f4b0f7bad4a0ec248da39cf3/sentry_sdk-2.49.0-py2.py3-none-any.whl", hash = "sha256:6ea78499133874445a20fe9c826c9e960070abeb7ae0cdf930314ab16bb97aa0", size = 415693, upload-time = "2026-01-08T09:56:21.872Z" }, +] + +[[package]] +name = "shellingham" +version = "1.5.4" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/58/15/8b3609fd3830ef7b27b655beb4b4e9c62313a4e8da8c676e142cc210d58e/shellingham-1.5.4.tar.gz", hash = "sha256:8dbca0739d487e5bd35ab3ca4b36e11c4078f3a234bfce294b0a0291363404de", size = 10310, upload-time = "2023-10-24T04:13:40.426Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/e0/f9/0595336914c5619e5f28a1fb793285925a8cd4b432c9da0a987836c7f822/shellingham-1.5.4-py2.py3-none-any.whl", hash = "sha256:7ecfff8f2fd72616f7481040475a65b2bf8af90a56c89140852d1120324e8686", size = 9755, upload-time = "2023-10-24T04:13:38.866Z" }, +] + +[[package]] +name = "smokeshow" +version = "0.5.0" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "httpx" }, + { name = "typer" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/75/94/c99b76517c268ef8d5c2ff88faba5a019664bd69e4754944afa294b4f24c/smokeshow-0.5.0.tar.gz", hash = "sha256:91dcabc29ac3116bff59b4d8a7bda4ae3ccc4c70742a38cec7127b8162e4a0f6", size = 101349, upload-time = "2025-01-07T19:41:51.732Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/77/10/0d23e4953eb7c1e1ad848084b3115f19234f34f907658ed11bed0d826aee/smokeshow-0.5.0-py3-none-any.whl", hash = "sha256:da12a960fc7cb525efc4035a0c3c9363b6217ea7e66bc39b9ed3cd8bed6eeedc", size = 8389, upload-time = "2025-01-07T19:41:49.194Z" }, +] + +[[package]] +name = "starlette" +version = "0.50.0" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "anyio" }, + { name = "typing-extensions", marker = "python_full_version < '3.13'" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/ba/b8/73a0e6a6e079a9d9cfa64113d771e421640b6f679a52eeb9b32f72d871a1/starlette-0.50.0.tar.gz", hash = "sha256:a2a17b22203254bcbc2e1f926d2d55f3f9497f769416b3190768befe598fa3ca", size = 2646985, upload-time = "2025-11-01T15:25:27.516Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/d9/52/1064f510b141bd54025f9b55105e26d1fa970b9be67ad766380a3c9b74b0/starlette-0.50.0-py3-none-any.whl", hash = "sha256:9e5391843ec9b6e472eed1365a78c8098cfceb7a74bfd4d6b1c0c0095efb3bca", size = 74033, upload-time = "2025-11-01T15:25:25.461Z" }, +] + +[[package]] +name = "tomli" +version = "2.3.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/52/ed/3f73f72945444548f33eba9a87fc7a6e969915e7b1acc8260b30e1f76a2f/tomli-2.3.0.tar.gz", hash = "sha256:64be704a875d2a59753d80ee8a533c3fe183e3f06807ff7dc2232938ccb01549", size = 17392, upload-time = "2025-10-08T22:01:47.119Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/b3/2e/299f62b401438d5fe1624119c723f5d877acc86a4c2492da405626665f12/tomli-2.3.0-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:88bd15eb972f3664f5ed4b57c1634a97153b4bac4479dcb6a495f41921eb7f45", size = 153236, upload-time = "2025-10-08T22:01:00.137Z" }, + { url = "https://files.pythonhosted.org/packages/86/7f/d8fffe6a7aefdb61bced88fcb5e280cfd71e08939da5894161bd71bea022/tomli-2.3.0-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:883b1c0d6398a6a9d29b508c331fa56adbcdff647f6ace4dfca0f50e90dfd0ba", size = 148084, upload-time = "2025-10-08T22:01:01.63Z" }, + { url = "https://files.pythonhosted.org/packages/47/5c/24935fb6a2ee63e86d80e4d3b58b222dafaf438c416752c8b58537c8b89a/tomli-2.3.0-cp311-cp311-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:d1381caf13ab9f300e30dd8feadb3de072aeb86f1d34a8569453ff32a7dea4bf", size = 234832, upload-time = "2025-10-08T22:01:02.543Z" }, + { url = "https://files.pythonhosted.org/packages/89/da/75dfd804fc11e6612846758a23f13271b76d577e299592b4371a4ca4cd09/tomli-2.3.0-cp311-cp311-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:a0e285d2649b78c0d9027570d4da3425bdb49830a6156121360b3f8511ea3441", size = 242052, upload-time = "2025-10-08T22:01:03.836Z" }, + { url = "https://files.pythonhosted.org/packages/70/8c/f48ac899f7b3ca7eb13af73bacbc93aec37f9c954df3c08ad96991c8c373/tomli-2.3.0-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:0a154a9ae14bfcf5d8917a59b51ffd5a3ac1fd149b71b47a3a104ca4edcfa845", size = 239555, upload-time = "2025-10-08T22:01:04.834Z" }, + { url = "https://files.pythonhosted.org/packages/ba/28/72f8afd73f1d0e7829bfc093f4cb98ce0a40ffc0cc997009ee1ed94ba705/tomli-2.3.0-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:74bf8464ff93e413514fefd2be591c3b0b23231a77f901db1eb30d6f712fc42c", size = 245128, upload-time = "2025-10-08T22:01:05.84Z" }, + { url = "https://files.pythonhosted.org/packages/b6/eb/a7679c8ac85208706d27436e8d421dfa39d4c914dcf5fa8083a9305f58d9/tomli-2.3.0-cp311-cp311-win32.whl", hash = "sha256:00b5f5d95bbfc7d12f91ad8c593a1659b6387b43f054104cda404be6bda62456", size = 96445, upload-time = "2025-10-08T22:01:06.896Z" }, + { url = "https://files.pythonhosted.org/packages/0a/fe/3d3420c4cb1ad9cb462fb52967080575f15898da97e21cb6f1361d505383/tomli-2.3.0-cp311-cp311-win_amd64.whl", hash = "sha256:4dc4ce8483a5d429ab602f111a93a6ab1ed425eae3122032db7e9acf449451be", size = 107165, upload-time = "2025-10-08T22:01:08.107Z" }, + { url = "https://files.pythonhosted.org/packages/ff/b7/40f36368fcabc518bb11c8f06379a0fd631985046c038aca08c6d6a43c6e/tomli-2.3.0-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:d7d86942e56ded512a594786a5ba0a5e521d02529b3826e7761a05138341a2ac", size = 154891, upload-time = "2025-10-08T22:01:09.082Z" }, + { url = "https://files.pythonhosted.org/packages/f9/3f/d9dd692199e3b3aab2e4e4dd948abd0f790d9ded8cd10cbaae276a898434/tomli-2.3.0-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:73ee0b47d4dad1c5e996e3cd33b8a76a50167ae5f96a2607cbe8cc773506ab22", size = 148796, upload-time = "2025-10-08T22:01:10.266Z" }, + { url = "https://files.pythonhosted.org/packages/60/83/59bff4996c2cf9f9387a0f5a3394629c7efa5ef16142076a23a90f1955fa/tomli-2.3.0-cp312-cp312-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:792262b94d5d0a466afb5bc63c7daa9d75520110971ee269152083270998316f", size = 242121, upload-time = "2025-10-08T22:01:11.332Z" }, + { url = "https://files.pythonhosted.org/packages/45/e5/7c5119ff39de8693d6baab6c0b6dcb556d192c165596e9fc231ea1052041/tomli-2.3.0-cp312-cp312-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:4f195fe57ecceac95a66a75ac24d9d5fbc98ef0962e09b2eddec5d39375aae52", size = 250070, upload-time = "2025-10-08T22:01:12.498Z" }, + { url = "https://files.pythonhosted.org/packages/45/12/ad5126d3a278f27e6701abde51d342aa78d06e27ce2bb596a01f7709a5a2/tomli-2.3.0-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:e31d432427dcbf4d86958c184b9bfd1e96b5b71f8eb17e6d02531f434fd335b8", size = 245859, upload-time = "2025-10-08T22:01:13.551Z" }, + { url = "https://files.pythonhosted.org/packages/fb/a1/4d6865da6a71c603cfe6ad0e6556c73c76548557a8d658f9e3b142df245f/tomli-2.3.0-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:7b0882799624980785240ab732537fcfc372601015c00f7fc367c55308c186f6", size = 250296, upload-time = "2025-10-08T22:01:14.614Z" }, + { url = "https://files.pythonhosted.org/packages/a0/b7/a7a7042715d55c9ba6e8b196d65d2cb662578b4d8cd17d882d45322b0d78/tomli-2.3.0-cp312-cp312-win32.whl", hash = "sha256:ff72b71b5d10d22ecb084d345fc26f42b5143c5533db5e2eaba7d2d335358876", size = 97124, upload-time = "2025-10-08T22:01:15.629Z" }, + { url = "https://files.pythonhosted.org/packages/06/1e/f22f100db15a68b520664eb3328fb0ae4e90530887928558112c8d1f4515/tomli-2.3.0-cp312-cp312-win_amd64.whl", hash = "sha256:1cb4ed918939151a03f33d4242ccd0aa5f11b3547d0cf30f7c74a408a5b99878", size = 107698, upload-time = "2025-10-08T22:01:16.51Z" }, + { url = "https://files.pythonhosted.org/packages/89/48/06ee6eabe4fdd9ecd48bf488f4ac783844fd777f547b8d1b61c11939974e/tomli-2.3.0-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:5192f562738228945d7b13d4930baffda67b69425a7f0da96d360b0a3888136b", size = 154819, upload-time = "2025-10-08T22:01:17.964Z" }, + { url = "https://files.pythonhosted.org/packages/f1/01/88793757d54d8937015c75dcdfb673c65471945f6be98e6a0410fba167ed/tomli-2.3.0-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:be71c93a63d738597996be9528f4abe628d1adf5e6eb11607bc8fe1a510b5dae", size = 148766, upload-time = "2025-10-08T22:01:18.959Z" }, + { url = "https://files.pythonhosted.org/packages/42/17/5e2c956f0144b812e7e107f94f1cc54af734eb17b5191c0bbfb72de5e93e/tomli-2.3.0-cp313-cp313-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:c4665508bcbac83a31ff8ab08f424b665200c0e1e645d2bd9ab3d3e557b6185b", size = 240771, upload-time = "2025-10-08T22:01:20.106Z" }, + { url = "https://files.pythonhosted.org/packages/d5/f4/0fbd014909748706c01d16824eadb0307115f9562a15cbb012cd9b3512c5/tomli-2.3.0-cp313-cp313-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:4021923f97266babc6ccab9f5068642a0095faa0a51a246a6a02fccbb3514eaf", size = 248586, upload-time = "2025-10-08T22:01:21.164Z" }, + { url = "https://files.pythonhosted.org/packages/30/77/fed85e114bde5e81ecf9bc5da0cc69f2914b38f4708c80ae67d0c10180c5/tomli-2.3.0-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:a4ea38c40145a357d513bffad0ed869f13c1773716cf71ccaa83b0fa0cc4e42f", size = 244792, upload-time = "2025-10-08T22:01:22.417Z" }, + { url = "https://files.pythonhosted.org/packages/55/92/afed3d497f7c186dc71e6ee6d4fcb0acfa5f7d0a1a2878f8beae379ae0cc/tomli-2.3.0-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:ad805ea85eda330dbad64c7ea7a4556259665bdf9d2672f5dccc740eb9d3ca05", size = 248909, upload-time = "2025-10-08T22:01:23.859Z" }, + { url = "https://files.pythonhosted.org/packages/f8/84/ef50c51b5a9472e7265ce1ffc7f24cd4023d289e109f669bdb1553f6a7c2/tomli-2.3.0-cp313-cp313-win32.whl", hash = "sha256:97d5eec30149fd3294270e889b4234023f2c69747e555a27bd708828353ab606", size = 96946, upload-time = "2025-10-08T22:01:24.893Z" }, + { url = "https://files.pythonhosted.org/packages/b2/b7/718cd1da0884f281f95ccfa3a6cc572d30053cba64603f79d431d3c9b61b/tomli-2.3.0-cp313-cp313-win_amd64.whl", hash = "sha256:0c95ca56fbe89e065c6ead5b593ee64b84a26fca063b5d71a1122bf26e533999", size = 107705, upload-time = "2025-10-08T22:01:26.153Z" }, + { url = "https://files.pythonhosted.org/packages/19/94/aeafa14a52e16163008060506fcb6aa1949d13548d13752171a755c65611/tomli-2.3.0-cp314-cp314-macosx_10_13_x86_64.whl", hash = "sha256:cebc6fe843e0733ee827a282aca4999b596241195f43b4cc371d64fc6639da9e", size = 154244, upload-time = "2025-10-08T22:01:27.06Z" }, + { url = "https://files.pythonhosted.org/packages/db/e4/1e58409aa78eefa47ccd19779fc6f36787edbe7d4cd330eeeedb33a4515b/tomli-2.3.0-cp314-cp314-macosx_11_0_arm64.whl", hash = "sha256:4c2ef0244c75aba9355561272009d934953817c49f47d768070c3c94355c2aa3", size = 148637, upload-time = "2025-10-08T22:01:28.059Z" }, + { url = "https://files.pythonhosted.org/packages/26/b6/d1eccb62f665e44359226811064596dd6a366ea1f985839c566cd61525ae/tomli-2.3.0-cp314-cp314-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:c22a8bf253bacc0cf11f35ad9808b6cb75ada2631c2d97c971122583b129afbc", size = 241925, upload-time = "2025-10-08T22:01:29.066Z" }, + { url = "https://files.pythonhosted.org/packages/70/91/7cdab9a03e6d3d2bb11beae108da5bdc1c34bdeb06e21163482544ddcc90/tomli-2.3.0-cp314-cp314-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:0eea8cc5c5e9f89c9b90c4896a8deefc74f518db5927d0e0e8d4a80953d774d0", size = 249045, upload-time = "2025-10-08T22:01:31.98Z" }, + { url = "https://files.pythonhosted.org/packages/15/1b/8c26874ed1f6e4f1fcfeb868db8a794cbe9f227299402db58cfcc858766c/tomli-2.3.0-cp314-cp314-musllinux_1_2_aarch64.whl", hash = "sha256:b74a0e59ec5d15127acdabd75ea17726ac4c5178ae51b85bfe39c4f8a278e879", size = 245835, upload-time = "2025-10-08T22:01:32.989Z" }, + { url = "https://files.pythonhosted.org/packages/fd/42/8e3c6a9a4b1a1360c1a2a39f0b972cef2cc9ebd56025168c4137192a9321/tomli-2.3.0-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:b5870b50c9db823c595983571d1296a6ff3e1b88f734a4c8f6fc6188397de005", size = 253109, upload-time = "2025-10-08T22:01:34.052Z" }, + { url = "https://files.pythonhosted.org/packages/22/0c/b4da635000a71b5f80130937eeac12e686eefb376b8dee113b4a582bba42/tomli-2.3.0-cp314-cp314-win32.whl", hash = "sha256:feb0dacc61170ed7ab602d3d972a58f14ee3ee60494292d384649a3dc38ef463", size = 97930, upload-time = "2025-10-08T22:01:35.082Z" }, + { url = "https://files.pythonhosted.org/packages/b9/74/cb1abc870a418ae99cd5c9547d6bce30701a954e0e721821df483ef7223c/tomli-2.3.0-cp314-cp314-win_amd64.whl", hash = "sha256:b273fcbd7fc64dc3600c098e39136522650c49bca95df2d11cf3b626422392c8", size = 107964, upload-time = "2025-10-08T22:01:36.057Z" }, + { url = "https://files.pythonhosted.org/packages/54/78/5c46fff6432a712af9f792944f4fcd7067d8823157949f4e40c56b8b3c83/tomli-2.3.0-cp314-cp314t-macosx_10_13_x86_64.whl", hash = "sha256:940d56ee0410fa17ee1f12b817b37a4d4e4dc4d27340863cc67236c74f582e77", size = 163065, upload-time = "2025-10-08T22:01:37.27Z" }, + { url = "https://files.pythonhosted.org/packages/39/67/f85d9bd23182f45eca8939cd2bc7050e1f90c41f4a2ecbbd5963a1d1c486/tomli-2.3.0-cp314-cp314t-macosx_11_0_arm64.whl", hash = "sha256:f85209946d1fe94416debbb88d00eb92ce9cd5266775424ff81bc959e001acaf", size = 159088, upload-time = "2025-10-08T22:01:38.235Z" }, + { url = "https://files.pythonhosted.org/packages/26/5a/4b546a0405b9cc0659b399f12b6adb750757baf04250b148d3c5059fc4eb/tomli-2.3.0-cp314-cp314t-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:a56212bdcce682e56b0aaf79e869ba5d15a6163f88d5451cbde388d48b13f530", size = 268193, upload-time = "2025-10-08T22:01:39.712Z" }, + { url = "https://files.pythonhosted.org/packages/42/4f/2c12a72ae22cf7b59a7fe75b3465b7aba40ea9145d026ba41cb382075b0e/tomli-2.3.0-cp314-cp314t-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:c5f3ffd1e098dfc032d4d3af5c0ac64f6d286d98bc148698356847b80fa4de1b", size = 275488, upload-time = "2025-10-08T22:01:40.773Z" }, + { url = "https://files.pythonhosted.org/packages/92/04/a038d65dbe160c3aa5a624e93ad98111090f6804027d474ba9c37c8ae186/tomli-2.3.0-cp314-cp314t-musllinux_1_2_aarch64.whl", hash = "sha256:5e01decd096b1530d97d5d85cb4dff4af2d8347bd35686654a004f8dea20fc67", size = 272669, upload-time = "2025-10-08T22:01:41.824Z" }, + { url = "https://files.pythonhosted.org/packages/be/2f/8b7c60a9d1612a7cbc39ffcca4f21a73bf368a80fc25bccf8253e2563267/tomli-2.3.0-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:8a35dd0e643bb2610f156cca8db95d213a90015c11fee76c946aa62b7ae7e02f", size = 279709, upload-time = "2025-10-08T22:01:43.177Z" }, + { url = "https://files.pythonhosted.org/packages/7e/46/cc36c679f09f27ded940281c38607716c86cf8ba4a518d524e349c8b4874/tomli-2.3.0-cp314-cp314t-win32.whl", hash = "sha256:a1f7f282fe248311650081faafa5f4732bdbfef5d45fe3f2e702fbc6f2d496e0", size = 107563, upload-time = "2025-10-08T22:01:44.233Z" }, + { url = "https://files.pythonhosted.org/packages/84/ff/426ca8683cf7b753614480484f6437f568fd2fda2edbdf57a2d3d8b27a0b/tomli-2.3.0-cp314-cp314t-win_amd64.whl", hash = "sha256:70a251f8d4ba2d9ac2542eecf008b3c8a9fc5c3f9f02c56a9d7952612be2fdba", size = 119756, upload-time = "2025-10-08T22:01:45.234Z" }, + { url = "https://files.pythonhosted.org/packages/77/b8/0135fadc89e73be292b473cb820b4f5a08197779206b33191e801feeae40/tomli-2.3.0-py3-none-any.whl", hash = "sha256:e95b1af3c5b07d9e643909b5abbec77cd9f1217e6d0bca72b0234736b9fb1f1b", size = 14408, upload-time = "2025-10-08T22:01:46.04Z" }, +] + +[[package]] +name = "ty" +version = "0.0.56" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/55/07/fb29aea5235b0aa8ecfc4d1cc6ddf9fba8b863d67d96c6d345694d644c43/ty-0.0.56.tar.gz", hash = "sha256:84d114dc3796361c0fc72945016eabd74d46b9ee64f198cb0e485719704681e5", size = 6050123, upload-time = "2026-07-01T16:44:56.036Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/dc/48/bce79e7ca5c1cc529d3e0d37ddd1121aea4b68a4f749974ad1cc77161871/ty-0.0.56-py3-none-linux_armv6l.whl", hash = "sha256:186d4a53e15747c947e1ec3d7eec8e345d8e40a1ca10e634c585db52497e87dd", size = 11643066, upload-time = "2026-07-01T16:44:18.374Z" }, + { url = "https://files.pythonhosted.org/packages/80/d1/22555d8a1d719661f10050f3865d877bbf497da908961c75fe22217dd18a/ty-0.0.56-py3-none-macosx_10_12_x86_64.whl", hash = "sha256:aae1a980fd9535da0469b7ba2b2e1b54a907743a5e0f442dd57eee9f5bfd034c", size = 11407487, upload-time = "2026-07-01T16:44:20.956Z" }, + { url = "https://files.pythonhosted.org/packages/cf/2d/b3b7a74ce8bc59ef48843ad80179bb0d9598bbd6cfc0d11d519bdf6b1352/ty-0.0.56-py3-none-macosx_11_0_arm64.whl", hash = "sha256:afd3058c0a6c5f241e814734f133008c93ee805f61c9cf4ce7412b8822b5d9ad", size = 10962270, upload-time = "2026-07-01T16:44:22.959Z" }, + { url = "https://files.pythonhosted.org/packages/64/ac/6c2fd7de0304a8a7218a756af74f7e62a5e8540fdb175e0a869e51042345/ty-0.0.56-py3-none-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:058b52f7a823ac13aae3cae30809dd6b5145794b64d8478f9ef38c75d79b4483", size = 11471406, upload-time = "2026-07-01T16:44:25.327Z" }, + { url = "https://files.pythonhosted.org/packages/50/b6/11d861156861c03c7726b74558f9a0e0092661aff83a4fda1279df28c425/ty-0.0.56-py3-none-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:2c66e00c1522add1f2bbdd2e45828c953b35c306b7bef03ec9169c75a63699a0", size = 11445612, upload-time = "2026-07-01T16:44:27.531Z" }, + { url = "https://files.pythonhosted.org/packages/fb/ba/09df108582090f3c0770ec4bc8675affed60248f6793a78d909be16211d9/ty-0.0.56-py3-none-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:40903d71c669a30691b5a5d5728056c7877a1bd6be4f233a38883a8b28cf34d7", size = 12093889, upload-time = "2026-07-01T16:44:29.548Z" }, + { url = "https://files.pythonhosted.org/packages/d7/f7/dbb4b4ccb69cd64c209ae55b1ab788ace8222c2bc1f6845be9e7cbedbf25/ty-0.0.56-py3-none-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:63fe3947fe0c46c69a7d950e6832ee70a9ec17321fefbff3d2e3c20baf9e5bd0", size = 12666337, upload-time = "2026-07-01T16:44:31.586Z" }, + { url = "https://files.pythonhosted.org/packages/86/e9/73f903fe4a3d9ea02f26f57c1eb07e3b1029ec92b0e8c2364718893440e3/ty-0.0.56-py3-none-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:71a0c1a72f9854532e710e119b6871ffe4542c8a65146f1f65dcd78fecd885b4", size = 12280247, upload-time = "2026-07-01T16:44:33.637Z" }, + { url = "https://files.pythonhosted.org/packages/d6/90/cebd222495832f1a00dcd321ba25f3cab804221a4991b992c2178bec68ee/ty-0.0.56-py3-none-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:70d1665596494e24d8ebd198438872b5a56ec3cae5f2bcf6c673be797acc4e3c", size = 11991107, upload-time = "2026-07-01T16:44:36.122Z" }, + { url = "https://files.pythonhosted.org/packages/b7/07/8f7337a07250f42d975cdb6decf47fc5b421e6c7da5e3e7be1e85f63a7e5/ty-0.0.56-py3-none-manylinux_2_31_riscv64.whl", hash = "sha256:778f99e51558afc1dbbe48ee38ab6aae7b31390ed8c1a1ef1499b295e9f1e82f", size = 12298970, upload-time = "2026-07-01T16:44:38.243Z" }, + { url = "https://files.pythonhosted.org/packages/3c/b9/a52cd59034a48f5f18c6b155cc2cc36861d874b6d0af204b12c898024c3d/ty-0.0.56-py3-none-musllinux_1_2_aarch64.whl", hash = "sha256:867bc5708e0066bb4ff6c7db524bd5deea2676c62bfe71d3303138b3be850af0", size = 11425683, upload-time = "2026-07-01T16:44:40.473Z" }, + { url = "https://files.pythonhosted.org/packages/1d/2e/48e42d33357d52eefb695c0c3fcfc96879b73668a7447d1d1e0ad774fedc/ty-0.0.56-py3-none-musllinux_1_2_armv7l.whl", hash = "sha256:a6012f4189c928edb330a37deb9930f982380bd4aa7c4b8e0428eec9651c7551", size = 11469258, upload-time = "2026-07-01T16:44:42.513Z" }, + { url = "https://files.pythonhosted.org/packages/d5/01/ad1b4138be1e3fa97863af3925aa2134f17a593240c35dc38c3429fb5ad1/ty-0.0.56-py3-none-musllinux_1_2_i686.whl", hash = "sha256:8ee83de1a7ff4cc32837ec06134ce391d441bc5b35ecd8d3cfe053f120f3e4c1", size = 11758736, upload-time = "2026-07-01T16:44:44.567Z" }, + { url = "https://files.pythonhosted.org/packages/09/34/9d81967ff240eaa57e9249728ef7b7790747cf6d3c9a98ec86b2cfdcc8ee/ty-0.0.56-py3-none-musllinux_1_2_x86_64.whl", hash = "sha256:62619b3b0e2c6248ef30d3f0e2f2217ae9893040585be07f32324242f197cd6f", size = 12100242, upload-time = "2026-07-01T16:44:46.584Z" }, + { url = "https://files.pythonhosted.org/packages/c3/36/f51d4666d2de6cf33c1f3a1fcc4bb6b70b197dd6ceaa491eef71d78fe8e8/ty-0.0.56-py3-none-win32.whl", hash = "sha256:b30687bb5cd9729d34c889a289edf32770388d9bb05243e534e723fb45e0381b", size = 11093759, upload-time = "2026-07-01T16:44:49.171Z" }, + { url = "https://files.pythonhosted.org/packages/5e/b4/8fb5d4acfa4afb152245b20fa263069a7547bd1f8e4bfca4eda280c897d7/ty-0.0.56-py3-none-win_amd64.whl", hash = "sha256:ad4c8c47b6f4e3f9ed3fc0b1a5d650088d229e17dd8f63c1826d6bbe94cc3235", size = 12100327, upload-time = "2026-07-01T16:44:51.26Z" }, + { url = "https://files.pythonhosted.org/packages/b8/fc/6a183e71edde90d0c35c2303f23f7a45b6891d1a2c45daf7b8f869831e19/ty-0.0.56-py3-none-win_arm64.whl", hash = "sha256:57538f273d444a5f1293fa7860e967178afe3917611fc5eff16b64e1204fe0d6", size = 11538780, upload-time = "2026-07-01T16:44:53.8Z" }, +] + +[[package]] +name = "typer" +version = "0.26.8" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "annotated-doc" }, + { name = "colorama", marker = "sys_platform == 'win32'" }, + { name = "rich" }, + { name = "shellingham" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/7c/f7/68adc395201b20b872d68e975386832e8005ffeacedd43a1d837a32815be/typer-0.26.8.tar.gz", hash = "sha256:c244a6bd558886fe3f8780efb6bdd28bb9aff005a94eedebaa5cb32926fe2f7e", size = 202097, upload-time = "2026-06-26T09:22:45.705Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/80/87/b9fd69c92c6102a066e1b86a35243f53e70bd4c709f2a26d9f4fee4f4dc0/typer-0.26.8-py3-none-any.whl", hash = "sha256:3512ca79ac5c11113414b36e80281b872884477722440691c89d1112e321a49c", size = 122564, upload-time = "2026-06-26T09:22:44.72Z" }, +] + +[[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 = "typing-inspection" +version = "0.4.2" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "typing-extensions" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/55/e3/70399cb7dd41c10ac53367ae42139cf4b1ca5f36bb3dc6c9d33acdb43655/typing_inspection-0.4.2.tar.gz", hash = "sha256:ba561c48a67c5958007083d386c3295464928b01faa735ab8547c5692e87f464", size = 75949, upload-time = "2025-10-01T02:14:41.687Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/dc/9b/47798a6c91d8bdb567fe2698fe81e0c6b7cb7ef4d13da4114b41d239f65d/typing_inspection-0.4.2-py3-none-any.whl", hash = "sha256:4ed1cacbdc298c220f1bd249ed5287caa16f34d44ef4e9c3d0cbad5b521545e7", size = 14611, upload-time = "2025-10-01T02:14:40.154Z" }, +] + +[[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 = "uvicorn" +version = "0.50.1" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "click" }, + { name = "h11" }, + { name = "typing-extensions", marker = "python_full_version < '3.11'" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/61/bb/88735238d7ead151c28d5432551170f17746c70c257aa66e8d7e64eca7a3/uvicorn-0.50.1.tar.gz", hash = "sha256:ccb3061887829fd8471cfa6fc65b2594689342ee00792e5d257d34871755b09f", size = 93722, upload-time = "2026-07-06T07:52:25.238Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/f2/59/02c7859295b001e77b8079fc9df9f8161b7c872cbf7ea8dab70cf51d61f1/uvicorn-0.50.1-py3-none-any.whl", hash = "sha256:8139bce59602f55d497c9ed77af3117b0b5fa033e3e887193fa00a5968c38c57", size = 72843, upload-time = "2026-07-06T07:52:23.62Z" }, +] + +[package.optional-dependencies] +standard = [ + { name = "colorama", marker = "sys_platform == 'win32'" }, + { name = "httptools" }, + { name = "python-dotenv" }, + { name = "pyyaml" }, + { name = "uvloop", marker = "platform_python_implementation != 'PyPy' and sys_platform != 'cygwin' and sys_platform != 'win32'" }, + { name = "watchfiles" }, + { name = "websockets" }, +] + +[[package]] +name = "uvloop" +version = "0.22.1" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/06/f0/18d39dbd1971d6d62c4629cc7fa67f74821b0dc1f5a77af43719de7936a7/uvloop-0.22.1.tar.gz", hash = "sha256:6c84bae345b9147082b17371e3dd5d42775bddce91f885499017f4607fdaf39f", size = 2443250, upload-time = "2025-10-16T22:17:19.342Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/eb/14/ecceb239b65adaaf7fde510aa8bd534075695d1e5f8dadfa32b5723d9cfb/uvloop-0.22.1-cp310-cp310-macosx_10_9_universal2.whl", hash = "sha256:ef6f0d4cc8a9fa1f6a910230cd53545d9a14479311e87e3cb225495952eb672c", size = 1343335, upload-time = "2025-10-16T22:16:11.43Z" }, + { url = "https://files.pythonhosted.org/packages/ba/ae/6f6f9af7f590b319c94532b9567409ba11f4fa71af1148cab1bf48a07048/uvloop-0.22.1-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:7cd375a12b71d33d46af85a3343b35d98e8116134ba404bd657b3b1d15988792", size = 742903, upload-time = "2025-10-16T22:16:12.979Z" }, + { url = "https://files.pythonhosted.org/packages/09/bd/3667151ad0702282a1f4d5d29288fce8a13c8b6858bf0978c219cd52b231/uvloop-0.22.1-cp310-cp310-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:ac33ed96229b7790eb729702751c0e93ac5bc3bcf52ae9eccbff30da09194b86", size = 3648499, upload-time = "2025-10-16T22:16:14.451Z" }, + { url = "https://files.pythonhosted.org/packages/b3/f6/21657bb3beb5f8c57ce8be3b83f653dd7933c2fd00545ed1b092d464799a/uvloop-0.22.1-cp310-cp310-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:481c990a7abe2c6f4fc3d98781cc9426ebd7f03a9aaa7eb03d3bfc68ac2a46bd", size = 3700133, upload-time = "2025-10-16T22:16:16.272Z" }, + { url = "https://files.pythonhosted.org/packages/09/e0/604f61d004ded805f24974c87ddd8374ef675644f476f01f1df90e4cdf72/uvloop-0.22.1-cp310-cp310-musllinux_1_2_aarch64.whl", hash = "sha256:a592b043a47ad17911add5fbd087c76716d7c9ccc1d64ec9249ceafd735f03c2", size = 3512681, upload-time = "2025-10-16T22:16:18.07Z" }, + { url = "https://files.pythonhosted.org/packages/bb/ce/8491fd370b0230deb5eac69c7aae35b3be527e25a911c0acdffb922dc1cd/uvloop-0.22.1-cp310-cp310-musllinux_1_2_x86_64.whl", hash = "sha256:1489cf791aa7b6e8c8be1c5a080bae3a672791fcb4e9e12249b05862a2ca9cec", size = 3615261, upload-time = "2025-10-16T22:16:19.596Z" }, + { url = "https://files.pythonhosted.org/packages/c7/d5/69900f7883235562f1f50d8184bb7dd84a2fb61e9ec63f3782546fdbd057/uvloop-0.22.1-cp311-cp311-macosx_10_9_universal2.whl", hash = "sha256:c60ebcd36f7b240b30788554b6f0782454826a0ed765d8430652621b5de674b9", size = 1352420, upload-time = "2025-10-16T22:16:21.187Z" }, + { url = "https://files.pythonhosted.org/packages/a8/73/c4e271b3bce59724e291465cc936c37758886a4868787da0278b3b56b905/uvloop-0.22.1-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:3b7f102bf3cb1995cfeaee9321105e8f5da76fdb104cdad8986f85461a1b7b77", size = 748677, upload-time = "2025-10-16T22:16:22.558Z" }, + { url = "https://files.pythonhosted.org/packages/86/94/9fb7fad2f824d25f8ecac0d70b94d0d48107ad5ece03769a9c543444f78a/uvloop-0.22.1-cp311-cp311-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:53c85520781d84a4b8b230e24a5af5b0778efdb39142b424990ff1ef7c48ba21", size = 3753819, upload-time = "2025-10-16T22:16:23.903Z" }, + { url = "https://files.pythonhosted.org/packages/74/4f/256aca690709e9b008b7108bc85fba619a2bc37c6d80743d18abad16ee09/uvloop-0.22.1-cp311-cp311-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:56a2d1fae65fd82197cb8c53c367310b3eabe1bbb9fb5a04d28e3e3520e4f702", size = 3804529, upload-time = "2025-10-16T22:16:25.246Z" }, + { url = "https://files.pythonhosted.org/packages/7f/74/03c05ae4737e871923d21a76fe28b6aad57f5c03b6e6bfcfa5ad616013e4/uvloop-0.22.1-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:40631b049d5972c6755b06d0bfe8233b1bd9a8a6392d9d1c45c10b6f9e9b2733", size = 3621267, upload-time = "2025-10-16T22:16:26.819Z" }, + { url = "https://files.pythonhosted.org/packages/75/be/f8e590fe61d18b4a92070905497aec4c0e64ae1761498cad09023f3f4b3e/uvloop-0.22.1-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:535cc37b3a04f6cd2c1ef65fa1d370c9a35b6695df735fcff5427323f2cd5473", size = 3723105, upload-time = "2025-10-16T22:16:28.252Z" }, + { url = "https://files.pythonhosted.org/packages/3d/ff/7f72e8170be527b4977b033239a83a68d5c881cc4775fca255c677f7ac5d/uvloop-0.22.1-cp312-cp312-macosx_10_13_universal2.whl", hash = "sha256:fe94b4564e865d968414598eea1a6de60adba0c040ba4ed05ac1300de402cd42", size = 1359936, upload-time = "2025-10-16T22:16:29.436Z" }, + { url = "https://files.pythonhosted.org/packages/c3/c6/e5d433f88fd54d81ef4be58b2b7b0cea13c442454a1db703a1eea0db1a59/uvloop-0.22.1-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:51eb9bd88391483410daad430813d982010f9c9c89512321f5b60e2cddbdddd6", size = 752769, upload-time = "2025-10-16T22:16:30.493Z" }, + { url = "https://files.pythonhosted.org/packages/24/68/a6ac446820273e71aa762fa21cdcc09861edd3536ff47c5cd3b7afb10eeb/uvloop-0.22.1-cp312-cp312-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:700e674a166ca5778255e0e1dc4e9d79ab2acc57b9171b79e65feba7184b3370", size = 4317413, upload-time = "2025-10-16T22:16:31.644Z" }, + { url = "https://files.pythonhosted.org/packages/5f/6f/e62b4dfc7ad6518e7eff2516f680d02a0f6eb62c0c212e152ca708a0085e/uvloop-0.22.1-cp312-cp312-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:7b5b1ac819a3f946d3b2ee07f09149578ae76066d70b44df3fa990add49a82e4", size = 4426307, upload-time = "2025-10-16T22:16:32.917Z" }, + { url = "https://files.pythonhosted.org/packages/90/60/97362554ac21e20e81bcef1150cb2a7e4ffdaf8ea1e5b2e8bf7a053caa18/uvloop-0.22.1-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:e047cc068570bac9866237739607d1313b9253c3051ad84738cbb095be0537b2", size = 4131970, upload-time = "2025-10-16T22:16:34.015Z" }, + { url = "https://files.pythonhosted.org/packages/99/39/6b3f7d234ba3964c428a6e40006340f53ba37993f46ed6e111c6e9141d18/uvloop-0.22.1-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:512fec6815e2dd45161054592441ef76c830eddaad55c8aa30952e6fe1ed07c0", size = 4296343, upload-time = "2025-10-16T22:16:35.149Z" }, + { url = "https://files.pythonhosted.org/packages/89/8c/182a2a593195bfd39842ea68ebc084e20c850806117213f5a299dfc513d9/uvloop-0.22.1-cp313-cp313-macosx_10_13_universal2.whl", hash = "sha256:561577354eb94200d75aca23fbde86ee11be36b00e52a4eaf8f50fb0c86b7705", size = 1358611, upload-time = "2025-10-16T22:16:36.833Z" }, + { url = "https://files.pythonhosted.org/packages/d2/14/e301ee96a6dc95224b6f1162cd3312f6d1217be3907b79173b06785f2fe7/uvloop-0.22.1-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:1cdf5192ab3e674ca26da2eada35b288d2fa49fdd0f357a19f0e7c4e7d5077c8", size = 751811, upload-time = "2025-10-16T22:16:38.275Z" }, + { url = "https://files.pythonhosted.org/packages/b7/02/654426ce265ac19e2980bfd9ea6590ca96a56f10c76e63801a2df01c0486/uvloop-0.22.1-cp313-cp313-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:6e2ea3d6190a2968f4a14a23019d3b16870dd2190cd69c8180f7c632d21de68d", size = 4288562, upload-time = "2025-10-16T22:16:39.375Z" }, + { url = "https://files.pythonhosted.org/packages/15/c0/0be24758891ef825f2065cd5db8741aaddabe3e248ee6acc5e8a80f04005/uvloop-0.22.1-cp313-cp313-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:0530a5fbad9c9e4ee3f2b33b148c6a64d47bbad8000ea63704fa8260f4cf728e", size = 4366890, upload-time = "2025-10-16T22:16:40.547Z" }, + { url = "https://files.pythonhosted.org/packages/d2/53/8369e5219a5855869bcee5f4d317f6da0e2c669aecf0ef7d371e3d084449/uvloop-0.22.1-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:bc5ef13bbc10b5335792360623cc378d52d7e62c2de64660616478c32cd0598e", size = 4119472, upload-time = "2025-10-16T22:16:41.694Z" }, + { url = "https://files.pythonhosted.org/packages/f8/ba/d69adbe699b768f6b29a5eec7b47dd610bd17a69de51b251126a801369ea/uvloop-0.22.1-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:1f38ec5e3f18c8a10ded09742f7fb8de0108796eb673f30ce7762ce1b8550cad", size = 4239051, upload-time = "2025-10-16T22:16:43.224Z" }, + { url = "https://files.pythonhosted.org/packages/90/cd/b62bdeaa429758aee8de8b00ac0dd26593a9de93d302bff3d21439e9791d/uvloop-0.22.1-cp314-cp314-macosx_10_13_universal2.whl", hash = "sha256:3879b88423ec7e97cd4eba2a443aa26ed4e59b45e6b76aabf13fe2f27023a142", size = 1362067, upload-time = "2025-10-16T22:16:44.503Z" }, + { url = "https://files.pythonhosted.org/packages/0d/f8/a132124dfda0777e489ca86732e85e69afcd1ff7686647000050ba670689/uvloop-0.22.1-cp314-cp314-macosx_10_13_x86_64.whl", hash = "sha256:4baa86acedf1d62115c1dc6ad1e17134476688f08c6efd8a2ab076e815665c74", size = 752423, upload-time = "2025-10-16T22:16:45.968Z" }, + { url = "https://files.pythonhosted.org/packages/a3/94/94af78c156f88da4b3a733773ad5ba0b164393e357cc4bd0ab2e2677a7d6/uvloop-0.22.1-cp314-cp314-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:297c27d8003520596236bdb2335e6b3f649480bd09e00d1e3a99144b691d2a35", size = 4272437, upload-time = "2025-10-16T22:16:47.451Z" }, + { url = "https://files.pythonhosted.org/packages/b5/35/60249e9fd07b32c665192cec7af29e06c7cd96fa1d08b84f012a56a0b38e/uvloop-0.22.1-cp314-cp314-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:c1955d5a1dd43198244d47664a5858082a3239766a839b2102a269aaff7a4e25", size = 4292101, upload-time = "2025-10-16T22:16:49.318Z" }, + { url = "https://files.pythonhosted.org/packages/02/62/67d382dfcb25d0a98ce73c11ed1a6fba5037a1a1d533dcbb7cab033a2636/uvloop-0.22.1-cp314-cp314-musllinux_1_2_aarch64.whl", hash = "sha256:b31dc2fccbd42adc73bc4e7cdbae4fc5086cf378979e53ca5d0301838c5682c6", size = 4114158, upload-time = "2025-10-16T22:16:50.517Z" }, + { url = "https://files.pythonhosted.org/packages/f0/7a/f1171b4a882a5d13c8b7576f348acfe6074d72eaf52cccef752f748d4a9f/uvloop-0.22.1-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:93f617675b2d03af4e72a5333ef89450dfaa5321303ede6e67ba9c9d26878079", size = 4177360, upload-time = "2025-10-16T22:16:52.646Z" }, + { url = "https://files.pythonhosted.org/packages/79/7b/b01414f31546caf0919da80ad57cbfe24c56b151d12af68cee1b04922ca8/uvloop-0.22.1-cp314-cp314t-macosx_10_13_universal2.whl", hash = "sha256:37554f70528f60cad66945b885eb01f1bb514f132d92b6eeed1c90fd54ed6289", size = 1454790, upload-time = "2025-10-16T22:16:54.355Z" }, + { url = "https://files.pythonhosted.org/packages/d4/31/0bb232318dd838cad3fa8fb0c68c8b40e1145b32025581975e18b11fab40/uvloop-0.22.1-cp314-cp314t-macosx_10_13_x86_64.whl", hash = "sha256:b76324e2dc033a0b2f435f33eb88ff9913c156ef78e153fb210e03c13da746b3", size = 796783, upload-time = "2025-10-16T22:16:55.906Z" }, + { url = "https://files.pythonhosted.org/packages/42/38/c9b09f3271a7a723a5de69f8e237ab8e7803183131bc57c890db0b6bb872/uvloop-0.22.1-cp314-cp314t-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:badb4d8e58ee08dad957002027830d5c3b06aea446a6a3744483c2b3b745345c", size = 4647548, upload-time = "2025-10-16T22:16:57.008Z" }, + { url = "https://files.pythonhosted.org/packages/c1/37/945b4ca0ac27e3dc4952642d4c900edd030b3da6c9634875af6e13ae80e5/uvloop-0.22.1-cp314-cp314t-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:b91328c72635f6f9e0282e4a57da7470c7350ab1c9f48546c0f2866205349d21", size = 4467065, upload-time = "2025-10-16T22:16:58.206Z" }, + { url = "https://files.pythonhosted.org/packages/97/cc/48d232f33d60e2e2e0b42f4e73455b146b76ebe216487e862700457fbf3c/uvloop-0.22.1-cp314-cp314t-musllinux_1_2_aarch64.whl", hash = "sha256:daf620c2995d193449393d6c62131b3fbd40a63bf7b307a1527856ace637fe88", size = 4328384, upload-time = "2025-10-16T22:16:59.36Z" }, + { url = "https://files.pythonhosted.org/packages/e4/16/c1fd27e9549f3c4baf1dc9c20c456cd2f822dbf8de9f463824b0c0357e06/uvloop-0.22.1-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:6cde23eeda1a25c75b2e07d39970f3374105d5eafbaab2a4482be82f272d5a5e", size = 4296730, upload-time = "2025-10-16T22:17:00.744Z" }, +] + +[[package]] +name = "watchfiles" +version = "1.1.1" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "anyio" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/c2/c9/8869df9b2a2d6c59d79220a4db37679e74f807c559ffe5265e08b227a210/watchfiles-1.1.1.tar.gz", hash = "sha256:a173cb5c16c4f40ab19cecf48a534c409f7ea983ab8fed0741304a1c0a31b3f2", size = 94440, upload-time = "2025-10-14T15:06:21.08Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/a7/1a/206e8cf2dd86fddf939165a57b4df61607a1e0add2785f170a3f616b7d9f/watchfiles-1.1.1-cp310-cp310-macosx_10_12_x86_64.whl", hash = "sha256:eef58232d32daf2ac67f42dea51a2c80f0d03379075d44a587051e63cc2e368c", size = 407318, upload-time = "2025-10-14T15:04:18.753Z" }, + { url = "https://files.pythonhosted.org/packages/b3/0f/abaf5262b9c496b5dad4ed3c0e799cbecb1f8ea512ecb6ddd46646a9fca3/watchfiles-1.1.1-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:03fa0f5237118a0c5e496185cafa92878568b652a2e9a9382a5151b1a0380a43", size = 394478, upload-time = "2025-10-14T15:04:20.297Z" }, + { url = "https://files.pythonhosted.org/packages/b1/04/9cc0ba88697b34b755371f5ace8d3a4d9a15719c07bdc7bd13d7d8c6a341/watchfiles-1.1.1-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:8ca65483439f9c791897f7db49202301deb6e15fe9f8fe2fed555bf986d10c31", size = 449894, upload-time = "2025-10-14T15:04:21.527Z" }, + { url = "https://files.pythonhosted.org/packages/d2/9c/eda4615863cd8621e89aed4df680d8c3ec3da6a4cf1da113c17decd87c7f/watchfiles-1.1.1-cp310-cp310-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:f0ab1c1af0cb38e3f598244c17919fb1a84d1629cc08355b0074b6d7f53138ac", size = 459065, upload-time = "2025-10-14T15:04:22.795Z" }, + { url = "https://files.pythonhosted.org/packages/84/13/f28b3f340157d03cbc8197629bc109d1098764abe1e60874622a0be5c112/watchfiles-1.1.1-cp310-cp310-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:3bc570d6c01c206c46deb6e935a260be44f186a2f05179f52f7fcd2be086a94d", size = 488377, upload-time = "2025-10-14T15:04:24.138Z" }, + { url = "https://files.pythonhosted.org/packages/86/93/cfa597fa9389e122488f7ffdbd6db505b3b915ca7435ecd7542e855898c2/watchfiles-1.1.1-cp310-cp310-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:e84087b432b6ac94778de547e08611266f1f8ffad28c0ee4c82e028b0fc5966d", size = 595837, upload-time = "2025-10-14T15:04:25.057Z" }, + { url = "https://files.pythonhosted.org/packages/57/1e/68c1ed5652b48d89fc24d6af905d88ee4f82fa8bc491e2666004e307ded1/watchfiles-1.1.1-cp310-cp310-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:620bae625f4cb18427b1bb1a2d9426dc0dd5a5ba74c7c2cdb9de405f7b129863", size = 473456, upload-time = "2025-10-14T15:04:26.497Z" }, + { url = "https://files.pythonhosted.org/packages/d5/dc/1a680b7458ffa3b14bb64878112aefc8f2e4f73c5af763cbf0bd43100658/watchfiles-1.1.1-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:544364b2b51a9b0c7000a4b4b02f90e9423d97fbbf7e06689236443ebcad81ab", size = 455614, upload-time = "2025-10-14T15:04:27.539Z" }, + { url = "https://files.pythonhosted.org/packages/61/a5/3d782a666512e01eaa6541a72ebac1d3aae191ff4a31274a66b8dd85760c/watchfiles-1.1.1-cp310-cp310-musllinux_1_1_aarch64.whl", hash = "sha256:bbe1ef33d45bc71cf21364df962af171f96ecaeca06bd9e3d0b583efb12aec82", size = 630690, upload-time = "2025-10-14T15:04:28.495Z" }, + { url = "https://files.pythonhosted.org/packages/9b/73/bb5f38590e34687b2a9c47a244aa4dd50c56a825969c92c9c5fc7387cea1/watchfiles-1.1.1-cp310-cp310-musllinux_1_1_x86_64.whl", hash = "sha256:1a0bb430adb19ef49389e1ad368450193a90038b5b752f4ac089ec6942c4dff4", size = 622459, upload-time = "2025-10-14T15:04:29.491Z" }, + { url = "https://files.pythonhosted.org/packages/f1/ac/c9bb0ec696e07a20bd58af5399aeadaef195fb2c73d26baf55180fe4a942/watchfiles-1.1.1-cp310-cp310-win32.whl", hash = "sha256:3f6d37644155fb5beca5378feb8c1708d5783145f2a0f1c4d5a061a210254844", size = 272663, upload-time = "2025-10-14T15:04:30.435Z" }, + { url = "https://files.pythonhosted.org/packages/11/a0/a60c5a7c2ec59fa062d9a9c61d02e3b6abd94d32aac2d8344c4bdd033326/watchfiles-1.1.1-cp310-cp310-win_amd64.whl", hash = "sha256:a36d8efe0f290835fd0f33da35042a1bb5dc0e83cbc092dcf69bce442579e88e", size = 287453, upload-time = "2025-10-14T15:04:31.53Z" }, + { url = "https://files.pythonhosted.org/packages/1f/f8/2c5f479fb531ce2f0564eda479faecf253d886b1ab3630a39b7bf7362d46/watchfiles-1.1.1-cp311-cp311-macosx_10_12_x86_64.whl", hash = "sha256:f57b396167a2565a4e8b5e56a5a1c537571733992b226f4f1197d79e94cf0ae5", size = 406529, upload-time = "2025-10-14T15:04:32.899Z" }, + { url = "https://files.pythonhosted.org/packages/fe/cd/f515660b1f32f65df671ddf6f85bfaca621aee177712874dc30a97397977/watchfiles-1.1.1-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:421e29339983e1bebc281fab40d812742268ad057db4aee8c4d2bce0af43b741", size = 394384, upload-time = "2025-10-14T15:04:33.761Z" }, + { url = "https://files.pythonhosted.org/packages/7b/c3/28b7dc99733eab43fca2d10f55c86e03bd6ab11ca31b802abac26b23d161/watchfiles-1.1.1-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:6e43d39a741e972bab5d8100b5cdacf69db64e34eb19b6e9af162bccf63c5cc6", size = 448789, upload-time = "2025-10-14T15:04:34.679Z" }, + { url = "https://files.pythonhosted.org/packages/4a/24/33e71113b320030011c8e4316ccca04194bf0cbbaeee207f00cbc7d6b9f5/watchfiles-1.1.1-cp311-cp311-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:f537afb3276d12814082a2e9b242bdcf416c2e8fd9f799a737990a1dbe906e5b", size = 460521, upload-time = "2025-10-14T15:04:35.963Z" }, + { url = "https://files.pythonhosted.org/packages/f4/c3/3c9a55f255aa57b91579ae9e98c88704955fa9dac3e5614fb378291155df/watchfiles-1.1.1-cp311-cp311-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:b2cd9e04277e756a2e2d2543d65d1e2166d6fd4c9b183f8808634fda23f17b14", size = 488722, upload-time = "2025-10-14T15:04:37.091Z" }, + { url = "https://files.pythonhosted.org/packages/49/36/506447b73eb46c120169dc1717fe2eff07c234bb3232a7200b5f5bd816e9/watchfiles-1.1.1-cp311-cp311-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:5f3f58818dc0b07f7d9aa7fe9eb1037aecb9700e63e1f6acfed13e9fef648f5d", size = 596088, upload-time = "2025-10-14T15:04:38.39Z" }, + { url = "https://files.pythonhosted.org/packages/82/ab/5f39e752a9838ec4d52e9b87c1e80f1ee3ccdbe92e183c15b6577ab9de16/watchfiles-1.1.1-cp311-cp311-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:9bb9f66367023ae783551042d31b1d7fd422e8289eedd91f26754a66f44d5cff", size = 472923, upload-time = "2025-10-14T15:04:39.666Z" }, + { url = "https://files.pythonhosted.org/packages/af/b9/a419292f05e302dea372fa7e6fda5178a92998411f8581b9830d28fb9edb/watchfiles-1.1.1-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:aebfd0861a83e6c3d1110b78ad54704486555246e542be3e2bb94195eabb2606", size = 456080, upload-time = "2025-10-14T15:04:40.643Z" }, + { url = "https://files.pythonhosted.org/packages/b0/c3/d5932fd62bde1a30c36e10c409dc5d54506726f08cb3e1d8d0ba5e2bc8db/watchfiles-1.1.1-cp311-cp311-musllinux_1_1_aarch64.whl", hash = "sha256:5fac835b4ab3c6487b5dbad78c4b3724e26bcc468e886f8ba8cc4306f68f6701", size = 629432, upload-time = "2025-10-14T15:04:41.789Z" }, + { url = "https://files.pythonhosted.org/packages/f7/77/16bddd9779fafb795f1a94319dc965209c5641db5bf1edbbccace6d1b3c0/watchfiles-1.1.1-cp311-cp311-musllinux_1_1_x86_64.whl", hash = "sha256:399600947b170270e80134ac854e21b3ccdefa11a9529a3decc1327088180f10", size = 623046, upload-time = "2025-10-14T15:04:42.718Z" }, + { url = "https://files.pythonhosted.org/packages/46/ef/f2ecb9a0f342b4bfad13a2787155c6ee7ce792140eac63a34676a2feeef2/watchfiles-1.1.1-cp311-cp311-win32.whl", hash = "sha256:de6da501c883f58ad50db3a32ad397b09ad29865b5f26f64c24d3e3281685849", size = 271473, upload-time = "2025-10-14T15:04:43.624Z" }, + { url = "https://files.pythonhosted.org/packages/94/bc/f42d71125f19731ea435c3948cad148d31a64fccde3867e5ba4edee901f9/watchfiles-1.1.1-cp311-cp311-win_amd64.whl", hash = "sha256:35c53bd62a0b885bf653ebf6b700d1bf05debb78ad9292cf2a942b23513dc4c4", size = 287598, upload-time = "2025-10-14T15:04:44.516Z" }, + { url = "https://files.pythonhosted.org/packages/57/c9/a30f897351f95bbbfb6abcadafbaca711ce1162f4db95fc908c98a9165f3/watchfiles-1.1.1-cp311-cp311-win_arm64.whl", hash = "sha256:57ca5281a8b5e27593cb7d82c2ac927ad88a96ed406aa446f6344e4328208e9e", size = 277210, upload-time = "2025-10-14T15:04:45.883Z" }, + { url = "https://files.pythonhosted.org/packages/74/d5/f039e7e3c639d9b1d09b07ea412a6806d38123f0508e5f9b48a87b0a76cc/watchfiles-1.1.1-cp312-cp312-macosx_10_12_x86_64.whl", hash = "sha256:8c89f9f2f740a6b7dcc753140dd5e1ab9215966f7a3530d0c0705c83b401bd7d", size = 404745, upload-time = "2025-10-14T15:04:46.731Z" }, + { url = "https://files.pythonhosted.org/packages/a5/96/a881a13aa1349827490dab2d363c8039527060cfcc2c92cc6d13d1b1049e/watchfiles-1.1.1-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:bd404be08018c37350f0d6e34676bd1e2889990117a2b90070b3007f172d0610", size = 391769, upload-time = "2025-10-14T15:04:48.003Z" }, + { url = "https://files.pythonhosted.org/packages/4b/5b/d3b460364aeb8da471c1989238ea0e56bec24b6042a68046adf3d9ddb01c/watchfiles-1.1.1-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:8526e8f916bb5b9a0a777c8317c23ce65de259422bba5b31325a6fa6029d33af", size = 449374, upload-time = "2025-10-14T15:04:49.179Z" }, + { url = "https://files.pythonhosted.org/packages/b9/44/5769cb62d4ed055cb17417c0a109a92f007114a4e07f30812a73a4efdb11/watchfiles-1.1.1-cp312-cp312-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:2edc3553362b1c38d9f06242416a5d8e9fe235c204a4072e988ce2e5bb1f69f6", size = 459485, upload-time = "2025-10-14T15:04:50.155Z" }, + { url = "https://files.pythonhosted.org/packages/19/0c/286b6301ded2eccd4ffd0041a1b726afda999926cf720aab63adb68a1e36/watchfiles-1.1.1-cp312-cp312-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:30f7da3fb3f2844259cba4720c3fc7138eb0f7b659c38f3bfa65084c7fc7abce", size = 488813, upload-time = "2025-10-14T15:04:51.059Z" }, + { url = "https://files.pythonhosted.org/packages/c7/2b/8530ed41112dd4a22f4dcfdb5ccf6a1baad1ff6eed8dc5a5f09e7e8c41c7/watchfiles-1.1.1-cp312-cp312-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:f8979280bdafff686ba5e4d8f97840f929a87ed9cdf133cbbd42f7766774d2aa", size = 594816, upload-time = "2025-10-14T15:04:52.031Z" }, + { url = "https://files.pythonhosted.org/packages/ce/d2/f5f9fb49489f184f18470d4f99f4e862a4b3e9ac2865688eb2099e3d837a/watchfiles-1.1.1-cp312-cp312-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:dcc5c24523771db3a294c77d94771abcfcb82a0e0ee8efd910c37c59ec1b31bb", size = 475186, upload-time = "2025-10-14T15:04:53.064Z" }, + { url = "https://files.pythonhosted.org/packages/cf/68/5707da262a119fb06fbe214d82dd1fe4a6f4af32d2d14de368d0349eb52a/watchfiles-1.1.1-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:1db5d7ae38ff20153d542460752ff397fcf5c96090c1230803713cf3147a6803", size = 456812, upload-time = "2025-10-14T15:04:55.174Z" }, + { url = "https://files.pythonhosted.org/packages/66/ab/3cbb8756323e8f9b6f9acb9ef4ec26d42b2109bce830cc1f3468df20511d/watchfiles-1.1.1-cp312-cp312-musllinux_1_1_aarch64.whl", hash = "sha256:28475ddbde92df1874b6c5c8aaeb24ad5be47a11f87cde5a28ef3835932e3e94", size = 630196, upload-time = "2025-10-14T15:04:56.22Z" }, + { url = "https://files.pythonhosted.org/packages/78/46/7152ec29b8335f80167928944a94955015a345440f524d2dfe63fc2f437b/watchfiles-1.1.1-cp312-cp312-musllinux_1_1_x86_64.whl", hash = "sha256:36193ed342f5b9842edd3532729a2ad55c4160ffcfa3700e0d54be496b70dd43", size = 622657, upload-time = "2025-10-14T15:04:57.521Z" }, + { url = "https://files.pythonhosted.org/packages/0a/bf/95895e78dd75efe9a7f31733607f384b42eb5feb54bd2eb6ed57cc2e94f4/watchfiles-1.1.1-cp312-cp312-win32.whl", hash = "sha256:859e43a1951717cc8de7f4c77674a6d389b106361585951d9e69572823f311d9", size = 272042, upload-time = "2025-10-14T15:04:59.046Z" }, + { url = "https://files.pythonhosted.org/packages/87/0a/90eb755f568de2688cb220171c4191df932232c20946966c27a59c400850/watchfiles-1.1.1-cp312-cp312-win_amd64.whl", hash = "sha256:91d4c9a823a8c987cce8fa2690923b069966dabb196dd8d137ea2cede885fde9", size = 288410, upload-time = "2025-10-14T15:05:00.081Z" }, + { url = "https://files.pythonhosted.org/packages/36/76/f322701530586922fbd6723c4f91ace21364924822a8772c549483abed13/watchfiles-1.1.1-cp312-cp312-win_arm64.whl", hash = "sha256:a625815d4a2bdca61953dbba5a39d60164451ef34c88d751f6c368c3ea73d404", size = 278209, upload-time = "2025-10-14T15:05:01.168Z" }, + { url = "https://files.pythonhosted.org/packages/bb/f4/f750b29225fe77139f7ae5de89d4949f5a99f934c65a1f1c0b248f26f747/watchfiles-1.1.1-cp313-cp313-macosx_10_12_x86_64.whl", hash = "sha256:130e4876309e8686a5e37dba7d5e9bc77e6ed908266996ca26572437a5271e18", size = 404321, upload-time = "2025-10-14T15:05:02.063Z" }, + { url = "https://files.pythonhosted.org/packages/2b/f9/f07a295cde762644aa4c4bb0f88921d2d141af45e735b965fb2e87858328/watchfiles-1.1.1-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:5f3bde70f157f84ece3765b42b4a52c6ac1a50334903c6eaf765362f6ccca88a", size = 391783, upload-time = "2025-10-14T15:05:03.052Z" }, + { url = "https://files.pythonhosted.org/packages/bc/11/fc2502457e0bea39a5c958d86d2cb69e407a4d00b85735ca724bfa6e0d1a/watchfiles-1.1.1-cp313-cp313-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:14e0b1fe858430fc0251737ef3824c54027bedb8c37c38114488b8e131cf8219", size = 449279, upload-time = "2025-10-14T15:05:04.004Z" }, + { url = "https://files.pythonhosted.org/packages/e3/1f/d66bc15ea0b728df3ed96a539c777acfcad0eb78555ad9efcaa1274688f0/watchfiles-1.1.1-cp313-cp313-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:f27db948078f3823a6bb3b465180db8ebecf26dd5dae6f6180bd87383b6b4428", size = 459405, upload-time = "2025-10-14T15:05:04.942Z" }, + { url = "https://files.pythonhosted.org/packages/be/90/9f4a65c0aec3ccf032703e6db02d89a157462fbb2cf20dd415128251cac0/watchfiles-1.1.1-cp313-cp313-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:059098c3a429f62fc98e8ec62b982230ef2c8df68c79e826e37b895bc359a9c0", size = 488976, upload-time = "2025-10-14T15:05:05.905Z" }, + { url = "https://files.pythonhosted.org/packages/37/57/ee347af605d867f712be7029bb94c8c071732a4b44792e3176fa3c612d39/watchfiles-1.1.1-cp313-cp313-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:bfb5862016acc9b869bb57284e6cb35fdf8e22fe59f7548858e2f971d045f150", size = 595506, upload-time = "2025-10-14T15:05:06.906Z" }, + { url = "https://files.pythonhosted.org/packages/a8/78/cc5ab0b86c122047f75e8fc471c67a04dee395daf847d3e59381996c8707/watchfiles-1.1.1-cp313-cp313-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:319b27255aacd9923b8a276bb14d21a5f7ff82564c744235fc5eae58d95422ae", size = 474936, upload-time = "2025-10-14T15:05:07.906Z" }, + { url = "https://files.pythonhosted.org/packages/62/da/def65b170a3815af7bd40a3e7010bf6ab53089ef1b75d05dd5385b87cf08/watchfiles-1.1.1-cp313-cp313-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:c755367e51db90e75b19454b680903631d41f9e3607fbd941d296a020c2d752d", size = 456147, upload-time = "2025-10-14T15:05:09.138Z" }, + { url = "https://files.pythonhosted.org/packages/57/99/da6573ba71166e82d288d4df0839128004c67d2778d3b566c138695f5c0b/watchfiles-1.1.1-cp313-cp313-musllinux_1_1_aarch64.whl", hash = "sha256:c22c776292a23bfc7237a98f791b9ad3144b02116ff10d820829ce62dff46d0b", size = 630007, upload-time = "2025-10-14T15:05:10.117Z" }, + { url = "https://files.pythonhosted.org/packages/a8/51/7439c4dd39511368849eb1e53279cd3454b4a4dbace80bab88feeb83c6b5/watchfiles-1.1.1-cp313-cp313-musllinux_1_1_x86_64.whl", hash = "sha256:3a476189be23c3686bc2f4321dd501cb329c0a0469e77b7b534ee10129ae6374", size = 622280, upload-time = "2025-10-14T15:05:11.146Z" }, + { url = "https://files.pythonhosted.org/packages/95/9c/8ed97d4bba5db6fdcdb2b298d3898f2dd5c20f6b73aee04eabe56c59677e/watchfiles-1.1.1-cp313-cp313-win32.whl", hash = "sha256:bf0a91bfb5574a2f7fc223cf95eeea79abfefa404bf1ea5e339c0c1560ae99a0", size = 272056, upload-time = "2025-10-14T15:05:12.156Z" }, + { url = "https://files.pythonhosted.org/packages/1f/f3/c14e28429f744a260d8ceae18bf58c1d5fa56b50d006a7a9f80e1882cb0d/watchfiles-1.1.1-cp313-cp313-win_amd64.whl", hash = "sha256:52e06553899e11e8074503c8e716d574adeeb7e68913115c4b3653c53f9bae42", size = 288162, upload-time = "2025-10-14T15:05:13.208Z" }, + { url = "https://files.pythonhosted.org/packages/dc/61/fe0e56c40d5cd29523e398d31153218718c5786b5e636d9ae8ae79453d27/watchfiles-1.1.1-cp313-cp313-win_arm64.whl", hash = "sha256:ac3cc5759570cd02662b15fbcd9d917f7ecd47efe0d6b40474eafd246f91ea18", size = 277909, upload-time = "2025-10-14T15:05:14.49Z" }, + { url = "https://files.pythonhosted.org/packages/79/42/e0a7d749626f1e28c7108a99fb9bf524b501bbbeb9b261ceecde644d5a07/watchfiles-1.1.1-cp313-cp313t-macosx_10_12_x86_64.whl", hash = "sha256:563b116874a9a7ce6f96f87cd0b94f7faf92d08d0021e837796f0a14318ef8da", size = 403389, upload-time = "2025-10-14T15:05:15.777Z" }, + { url = "https://files.pythonhosted.org/packages/15/49/08732f90ce0fbbc13913f9f215c689cfc9ced345fb1bcd8829a50007cc8d/watchfiles-1.1.1-cp313-cp313t-macosx_11_0_arm64.whl", hash = "sha256:3ad9fe1dae4ab4212d8c91e80b832425e24f421703b5a42ef2e4a1e215aff051", size = 389964, upload-time = "2025-10-14T15:05:16.85Z" }, + { url = "https://files.pythonhosted.org/packages/27/0d/7c315d4bd5f2538910491a0393c56bf70d333d51bc5b34bee8e68e8cea19/watchfiles-1.1.1-cp313-cp313t-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:ce70f96a46b894b36eba678f153f052967a0d06d5b5a19b336ab0dbbd029f73e", size = 448114, upload-time = "2025-10-14T15:05:17.876Z" }, + { url = "https://files.pythonhosted.org/packages/c3/24/9e096de47a4d11bc4df41e9d1e61776393eac4cb6eb11b3e23315b78b2cc/watchfiles-1.1.1-cp313-cp313t-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:cb467c999c2eff23a6417e58d75e5828716f42ed8289fe6b77a7e5a91036ca70", size = 460264, upload-time = "2025-10-14T15:05:18.962Z" }, + { url = "https://files.pythonhosted.org/packages/cc/0f/e8dea6375f1d3ba5fcb0b3583e2b493e77379834c74fd5a22d66d85d6540/watchfiles-1.1.1-cp313-cp313t-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:836398932192dae4146c8f6f737d74baeac8b70ce14831a239bdb1ca882fc261", size = 487877, upload-time = "2025-10-14T15:05:20.094Z" }, + { url = "https://files.pythonhosted.org/packages/ac/5b/df24cfc6424a12deb41503b64d42fbea6b8cb357ec62ca84a5a3476f654a/watchfiles-1.1.1-cp313-cp313t-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:743185e7372b7bc7c389e1badcc606931a827112fbbd37f14c537320fca08620", size = 595176, upload-time = "2025-10-14T15:05:21.134Z" }, + { url = "https://files.pythonhosted.org/packages/8f/b5/853b6757f7347de4e9b37e8cc3289283fb983cba1ab4d2d7144694871d9c/watchfiles-1.1.1-cp313-cp313t-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:afaeff7696e0ad9f02cbb8f56365ff4686ab205fcf9c4c5b6fdfaaa16549dd04", size = 473577, upload-time = "2025-10-14T15:05:22.306Z" }, + { url = "https://files.pythonhosted.org/packages/e1/f7/0a4467be0a56e80447c8529c9fce5b38eab4f513cb3d9bf82e7392a5696b/watchfiles-1.1.1-cp313-cp313t-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:3f7eb7da0eb23aa2ba036d4f616d46906013a68caf61b7fdbe42fc8b25132e77", size = 455425, upload-time = "2025-10-14T15:05:23.348Z" }, + { url = "https://files.pythonhosted.org/packages/8e/e0/82583485ea00137ddf69bc84a2db88bd92ab4a6e3c405e5fb878ead8d0e7/watchfiles-1.1.1-cp313-cp313t-musllinux_1_1_aarch64.whl", hash = "sha256:831a62658609f0e5c64178211c942ace999517f5770fe9436be4c2faeba0c0ef", size = 628826, upload-time = "2025-10-14T15:05:24.398Z" }, + { url = "https://files.pythonhosted.org/packages/28/9a/a785356fccf9fae84c0cc90570f11702ae9571036fb25932f1242c82191c/watchfiles-1.1.1-cp313-cp313t-musllinux_1_1_x86_64.whl", hash = "sha256:f9a2ae5c91cecc9edd47e041a930490c31c3afb1f5e6d71de3dc671bfaca02bf", size = 622208, upload-time = "2025-10-14T15:05:25.45Z" }, + { url = "https://files.pythonhosted.org/packages/c3/f4/0872229324ef69b2c3edec35e84bd57a1289e7d3fe74588048ed8947a323/watchfiles-1.1.1-cp314-cp314-macosx_10_12_x86_64.whl", hash = "sha256:d1715143123baeeaeadec0528bb7441103979a1d5f6fd0e1f915383fea7ea6d5", size = 404315, upload-time = "2025-10-14T15:05:26.501Z" }, + { url = "https://files.pythonhosted.org/packages/7b/22/16d5331eaed1cb107b873f6ae1b69e9ced582fcf0c59a50cd84f403b1c32/watchfiles-1.1.1-cp314-cp314-macosx_11_0_arm64.whl", hash = "sha256:39574d6370c4579d7f5d0ad940ce5b20db0e4117444e39b6d8f99db5676c52fd", size = 390869, upload-time = "2025-10-14T15:05:27.649Z" }, + { url = "https://files.pythonhosted.org/packages/b2/7e/5643bfff5acb6539b18483128fdc0ef2cccc94a5b8fbda130c823e8ed636/watchfiles-1.1.1-cp314-cp314-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:7365b92c2e69ee952902e8f70f3ba6360d0d596d9299d55d7d386df84b6941fb", size = 449919, upload-time = "2025-10-14T15:05:28.701Z" }, + { url = "https://files.pythonhosted.org/packages/51/2e/c410993ba5025a9f9357c376f48976ef0e1b1aefb73b97a5ae01a5972755/watchfiles-1.1.1-cp314-cp314-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:bfff9740c69c0e4ed32416f013f3c45e2ae42ccedd1167ef2d805c000b6c71a5", size = 460845, upload-time = "2025-10-14T15:05:30.064Z" }, + { url = "https://files.pythonhosted.org/packages/8e/a4/2df3b404469122e8680f0fcd06079317e48db58a2da2950fb45020947734/watchfiles-1.1.1-cp314-cp314-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:b27cf2eb1dda37b2089e3907d8ea92922b673c0c427886d4edc6b94d8dfe5db3", size = 489027, upload-time = "2025-10-14T15:05:31.064Z" }, + { url = "https://files.pythonhosted.org/packages/ea/84/4587ba5b1f267167ee715b7f66e6382cca6938e0a4b870adad93e44747e6/watchfiles-1.1.1-cp314-cp314-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:526e86aced14a65a5b0ec50827c745597c782ff46b571dbfe46192ab9e0b3c33", size = 595615, upload-time = "2025-10-14T15:05:32.074Z" }, + { url = "https://files.pythonhosted.org/packages/6a/0f/c6988c91d06e93cd0bb3d4a808bcf32375ca1904609835c3031799e3ecae/watchfiles-1.1.1-cp314-cp314-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:04e78dd0b6352db95507fd8cb46f39d185cf8c74e4cf1e4fbad1d3df96faf510", size = 474836, upload-time = "2025-10-14T15:05:33.209Z" }, + { url = "https://files.pythonhosted.org/packages/b4/36/ded8aebea91919485b7bbabbd14f5f359326cb5ec218cd67074d1e426d74/watchfiles-1.1.1-cp314-cp314-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:5c85794a4cfa094714fb9c08d4a218375b2b95b8ed1666e8677c349906246c05", size = 455099, upload-time = "2025-10-14T15:05:34.189Z" }, + { url = "https://files.pythonhosted.org/packages/98/e0/8c9bdba88af756a2fce230dd365fab2baf927ba42cd47521ee7498fd5211/watchfiles-1.1.1-cp314-cp314-musllinux_1_1_aarch64.whl", hash = "sha256:74d5012b7630714b66be7b7b7a78855ef7ad58e8650c73afc4c076a1f480a8d6", size = 630626, upload-time = "2025-10-14T15:05:35.216Z" }, + { url = "https://files.pythonhosted.org/packages/2a/84/a95db05354bf2d19e438520d92a8ca475e578c647f78f53197f5a2f17aaf/watchfiles-1.1.1-cp314-cp314-musllinux_1_1_x86_64.whl", hash = "sha256:8fbe85cb3201c7d380d3d0b90e63d520f15d6afe217165d7f98c9c649654db81", size = 622519, upload-time = "2025-10-14T15:05:36.259Z" }, + { url = "https://files.pythonhosted.org/packages/1d/ce/d8acdc8de545de995c339be67711e474c77d643555a9bb74a9334252bd55/watchfiles-1.1.1-cp314-cp314-win32.whl", hash = "sha256:3fa0b59c92278b5a7800d3ee7733da9d096d4aabcfabb9a928918bd276ef9b9b", size = 272078, upload-time = "2025-10-14T15:05:37.63Z" }, + { url = "https://files.pythonhosted.org/packages/c4/c9/a74487f72d0451524be827e8edec251da0cc1fcf111646a511ae752e1a3d/watchfiles-1.1.1-cp314-cp314-win_amd64.whl", hash = "sha256:c2047d0b6cea13b3316bdbafbfa0c4228ae593d995030fda39089d36e64fc03a", size = 287664, upload-time = "2025-10-14T15:05:38.95Z" }, + { url = "https://files.pythonhosted.org/packages/df/b8/8ac000702cdd496cdce998c6f4ee0ca1f15977bba51bdf07d872ebdfc34c/watchfiles-1.1.1-cp314-cp314-win_arm64.whl", hash = "sha256:842178b126593addc05acf6fce960d28bc5fae7afbaa2c6c1b3a7b9460e5be02", size = 277154, upload-time = "2025-10-14T15:05:39.954Z" }, + { url = "https://files.pythonhosted.org/packages/47/a8/e3af2184707c29f0f14b1963c0aace6529f9d1b8582d5b99f31bbf42f59e/watchfiles-1.1.1-cp314-cp314t-macosx_10_12_x86_64.whl", hash = "sha256:88863fbbc1a7312972f1c511f202eb30866370ebb8493aef2812b9ff28156a21", size = 403820, upload-time = "2025-10-14T15:05:40.932Z" }, + { url = "https://files.pythonhosted.org/packages/c0/ec/e47e307c2f4bd75f9f9e8afbe3876679b18e1bcec449beca132a1c5ffb2d/watchfiles-1.1.1-cp314-cp314t-macosx_11_0_arm64.whl", hash = "sha256:55c7475190662e202c08c6c0f4d9e345a29367438cf8e8037f3155e10a88d5a5", size = 390510, upload-time = "2025-10-14T15:05:41.945Z" }, + { url = "https://files.pythonhosted.org/packages/d5/a0/ad235642118090f66e7b2f18fd5c42082418404a79205cdfca50b6309c13/watchfiles-1.1.1-cp314-cp314t-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:3f53fa183d53a1d7a8852277c92b967ae99c2d4dcee2bfacff8868e6e30b15f7", size = 448408, upload-time = "2025-10-14T15:05:43.385Z" }, + { url = "https://files.pythonhosted.org/packages/df/85/97fa10fd5ff3332ae17e7e40e20784e419e28521549780869f1413742e9d/watchfiles-1.1.1-cp314-cp314t-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:6aae418a8b323732fa89721d86f39ec8f092fc2af67f4217a2b07fd3e93c6101", size = 458968, upload-time = "2025-10-14T15:05:44.404Z" }, + { url = "https://files.pythonhosted.org/packages/47/c2/9059c2e8966ea5ce678166617a7f75ecba6164375f3b288e50a40dc6d489/watchfiles-1.1.1-cp314-cp314t-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:f096076119da54a6080e8920cbdaac3dbee667eb91dcc5e5b78840b87415bd44", size = 488096, upload-time = "2025-10-14T15:05:45.398Z" }, + { url = "https://files.pythonhosted.org/packages/94/44/d90a9ec8ac309bc26db808a13e7bfc0e4e78b6fc051078a554e132e80160/watchfiles-1.1.1-cp314-cp314t-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:00485f441d183717038ed2e887a7c868154f216877653121068107b227a2f64c", size = 596040, upload-time = "2025-10-14T15:05:46.502Z" }, + { url = "https://files.pythonhosted.org/packages/95/68/4e3479b20ca305cfc561db3ed207a8a1c745ee32bf24f2026a129d0ddb6e/watchfiles-1.1.1-cp314-cp314t-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:a55f3e9e493158d7bfdb60a1165035f1cf7d320914e7b7ea83fe22c6023b58fc", size = 473847, upload-time = "2025-10-14T15:05:47.484Z" }, + { url = "https://files.pythonhosted.org/packages/4f/55/2af26693fd15165c4ff7857e38330e1b61ab8c37d15dc79118cdba115b7a/watchfiles-1.1.1-cp314-cp314t-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:8c91ed27800188c2ae96d16e3149f199d62f86c7af5f5f4d2c61a3ed8cd3666c", size = 455072, upload-time = "2025-10-14T15:05:48.928Z" }, + { url = "https://files.pythonhosted.org/packages/66/1d/d0d200b10c9311ec25d2273f8aad8c3ef7cc7ea11808022501811208a750/watchfiles-1.1.1-cp314-cp314t-musllinux_1_1_aarch64.whl", hash = "sha256:311ff15a0bae3714ffb603e6ba6dbfba4065ab60865d15a6ec544133bdb21099", size = 629104, upload-time = "2025-10-14T15:05:49.908Z" }, + { url = "https://files.pythonhosted.org/packages/e3/bd/fa9bb053192491b3867ba07d2343d9f2252e00811567d30ae8d0f78136fe/watchfiles-1.1.1-cp314-cp314t-musllinux_1_1_x86_64.whl", hash = "sha256:a916a2932da8f8ab582f242c065f5c81bed3462849ca79ee357dd9551b0e9b01", size = 622112, upload-time = "2025-10-14T15:05:50.941Z" }, + { url = "https://files.pythonhosted.org/packages/ba/4c/a888c91e2e326872fa4705095d64acd8aa2fb9c1f7b9bd0588f33850516c/watchfiles-1.1.1-pp310-pypy310_pp73-macosx_10_12_x86_64.whl", hash = "sha256:17ef139237dfced9da49fb7f2232c86ca9421f666d78c264c7ffca6601d154c3", size = 409611, upload-time = "2025-10-14T15:06:05.809Z" }, + { url = "https://files.pythonhosted.org/packages/1e/c7/5420d1943c8e3ce1a21c0a9330bcf7edafb6aa65d26b21dbb3267c9e8112/watchfiles-1.1.1-pp310-pypy310_pp73-macosx_11_0_arm64.whl", hash = "sha256:672b8adf25b1a0d35c96b5888b7b18699d27d4194bac8beeae75be4b7a3fc9b2", size = 396889, upload-time = "2025-10-14T15:06:07.035Z" }, + { url = "https://files.pythonhosted.org/packages/0c/e5/0072cef3804ce8d3aaddbfe7788aadff6b3d3f98a286fdbee9fd74ca59a7/watchfiles-1.1.1-pp310-pypy310_pp73-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:77a13aea58bc2b90173bc69f2a90de8e282648939a00a602e1dc4ee23e26b66d", size = 451616, upload-time = "2025-10-14T15:06:08.072Z" }, + { url = "https://files.pythonhosted.org/packages/83/4e/b87b71cbdfad81ad7e83358b3e447fedd281b880a03d64a760fe0a11fc2e/watchfiles-1.1.1-pp310-pypy310_pp73-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:0b495de0bb386df6a12b18335a0285dda90260f51bdb505503c02bcd1ce27a8b", size = 458413, upload-time = "2025-10-14T15:06:09.209Z" }, + { url = "https://files.pythonhosted.org/packages/d3/8e/e500f8b0b77be4ff753ac94dc06b33d8f0d839377fee1b78e8c8d8f031bf/watchfiles-1.1.1-pp311-pypy311_pp73-macosx_10_12_x86_64.whl", hash = "sha256:db476ab59b6765134de1d4fe96a1a9c96ddf091683599be0f26147ea1b2e4b88", size = 408250, upload-time = "2025-10-14T15:06:10.264Z" }, + { url = "https://files.pythonhosted.org/packages/bd/95/615e72cd27b85b61eec764a5ca51bd94d40b5adea5ff47567d9ebc4d275a/watchfiles-1.1.1-pp311-pypy311_pp73-macosx_11_0_arm64.whl", hash = "sha256:89eef07eee5e9d1fda06e38822ad167a044153457e6fd997f8a858ab7564a336", size = 396117, upload-time = "2025-10-14T15:06:11.28Z" }, + { url = "https://files.pythonhosted.org/packages/c9/81/e7fe958ce8a7fb5c73cc9fb07f5aeaf755e6aa72498c57d760af760c91f8/watchfiles-1.1.1-pp311-pypy311_pp73-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:ce19e06cbda693e9e7686358af9cd6f5d61312ab8b00488bc36f5aabbaf77e24", size = 450493, upload-time = "2025-10-14T15:06:12.321Z" }, + { url = "https://files.pythonhosted.org/packages/6e/d4/ed38dd3b1767193de971e694aa544356e63353c33a85d948166b5ff58b9e/watchfiles-1.1.1-pp311-pypy311_pp73-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:3e6f39af2eab0118338902798b5aa6664f46ff66bc0280de76fca67a7f262a49", size = 457546, upload-time = "2025-10-14T15:06:13.372Z" }, +] + +[[package]] +name = "websockets" +version = "15.0.1" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/21/e6/26d09fab466b7ca9c7737474c52be4f76a40301b08362eb2dbc19dcc16c1/websockets-15.0.1.tar.gz", hash = "sha256:82544de02076bafba038ce055ee6412d68da13ab47f0c60cab827346de828dee", size = 177016, upload-time = "2025-03-05T20:03:41.606Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/1e/da/6462a9f510c0c49837bbc9345aca92d767a56c1fb2939e1579df1e1cdcf7/websockets-15.0.1-cp310-cp310-macosx_10_9_universal2.whl", hash = "sha256:d63efaa0cd96cf0c5fe4d581521d9fa87744540d4bc999ae6e08595a1014b45b", size = 175423, upload-time = "2025-03-05T20:01:35.363Z" }, + { url = "https://files.pythonhosted.org/packages/1c/9f/9d11c1a4eb046a9e106483b9ff69bce7ac880443f00e5ce64261b47b07e7/websockets-15.0.1-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:ac60e3b188ec7574cb761b08d50fcedf9d77f1530352db4eef1707fe9dee7205", size = 173080, upload-time = "2025-03-05T20:01:37.304Z" }, + { url = "https://files.pythonhosted.org/packages/d5/4f/b462242432d93ea45f297b6179c7333dd0402b855a912a04e7fc61c0d71f/websockets-15.0.1-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:5756779642579d902eed757b21b0164cd6fe338506a8083eb58af5c372e39d9a", size = 173329, upload-time = "2025-03-05T20:01:39.668Z" }, + { url = "https://files.pythonhosted.org/packages/6e/0c/6afa1f4644d7ed50284ac59cc70ef8abd44ccf7d45850d989ea7310538d0/websockets-15.0.1-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:0fdfe3e2a29e4db3659dbd5bbf04560cea53dd9610273917799f1cde46aa725e", size = 182312, upload-time = "2025-03-05T20:01:41.815Z" }, + { url = "https://files.pythonhosted.org/packages/dd/d4/ffc8bd1350b229ca7a4db2a3e1c482cf87cea1baccd0ef3e72bc720caeec/websockets-15.0.1-cp310-cp310-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:4c2529b320eb9e35af0fa3016c187dffb84a3ecc572bcee7c3ce302bfeba52bf", size = 181319, upload-time = "2025-03-05T20:01:43.967Z" }, + { url = "https://files.pythonhosted.org/packages/97/3a/5323a6bb94917af13bbb34009fac01e55c51dfde354f63692bf2533ffbc2/websockets-15.0.1-cp310-cp310-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:ac1e5c9054fe23226fb11e05a6e630837f074174c4c2f0fe442996112a6de4fb", size = 181631, upload-time = "2025-03-05T20:01:46.104Z" }, + { url = "https://files.pythonhosted.org/packages/a6/cc/1aeb0f7cee59ef065724041bb7ed667b6ab1eeffe5141696cccec2687b66/websockets-15.0.1-cp310-cp310-musllinux_1_2_aarch64.whl", hash = "sha256:5df592cd503496351d6dc14f7cdad49f268d8e618f80dce0cd5a36b93c3fc08d", size = 182016, upload-time = "2025-03-05T20:01:47.603Z" }, + { url = "https://files.pythonhosted.org/packages/79/f9/c86f8f7af208e4161a7f7e02774e9d0a81c632ae76db2ff22549e1718a51/websockets-15.0.1-cp310-cp310-musllinux_1_2_i686.whl", hash = "sha256:0a34631031a8f05657e8e90903e656959234f3a04552259458aac0b0f9ae6fd9", size = 181426, upload-time = "2025-03-05T20:01:48.949Z" }, + { url = "https://files.pythonhosted.org/packages/c7/b9/828b0bc6753db905b91df6ae477c0b14a141090df64fb17f8a9d7e3516cf/websockets-15.0.1-cp310-cp310-musllinux_1_2_x86_64.whl", hash = "sha256:3d00075aa65772e7ce9e990cab3ff1de702aa09be3940d1dc88d5abf1ab8a09c", size = 181360, upload-time = "2025-03-05T20:01:50.938Z" }, + { url = "https://files.pythonhosted.org/packages/89/fb/250f5533ec468ba6327055b7d98b9df056fb1ce623b8b6aaafb30b55d02e/websockets-15.0.1-cp310-cp310-win32.whl", hash = "sha256:1234d4ef35db82f5446dca8e35a7da7964d02c127b095e172e54397fb6a6c256", size = 176388, upload-time = "2025-03-05T20:01:52.213Z" }, + { url = "https://files.pythonhosted.org/packages/1c/46/aca7082012768bb98e5608f01658ff3ac8437e563eca41cf068bd5849a5e/websockets-15.0.1-cp310-cp310-win_amd64.whl", hash = "sha256:39c1fec2c11dc8d89bba6b2bf1556af381611a173ac2b511cf7231622058af41", size = 176830, upload-time = "2025-03-05T20:01:53.922Z" }, + { url = "https://files.pythonhosted.org/packages/9f/32/18fcd5919c293a398db67443acd33fde142f283853076049824fc58e6f75/websockets-15.0.1-cp311-cp311-macosx_10_9_universal2.whl", hash = "sha256:823c248b690b2fd9303ba00c4f66cd5e2d8c3ba4aa968b2779be9532a4dad431", size = 175423, upload-time = "2025-03-05T20:01:56.276Z" }, + { url = "https://files.pythonhosted.org/packages/76/70/ba1ad96b07869275ef42e2ce21f07a5b0148936688c2baf7e4a1f60d5058/websockets-15.0.1-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:678999709e68425ae2593acf2e3ebcbcf2e69885a5ee78f9eb80e6e371f1bf57", size = 173082, upload-time = "2025-03-05T20:01:57.563Z" }, + { url = "https://files.pythonhosted.org/packages/86/f2/10b55821dd40eb696ce4704a87d57774696f9451108cff0d2824c97e0f97/websockets-15.0.1-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:d50fd1ee42388dcfb2b3676132c78116490976f1300da28eb629272d5d93e905", size = 173330, upload-time = "2025-03-05T20:01:59.063Z" }, + { url = "https://files.pythonhosted.org/packages/a5/90/1c37ae8b8a113d3daf1065222b6af61cc44102da95388ac0018fcb7d93d9/websockets-15.0.1-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:d99e5546bf73dbad5bf3547174cd6cb8ba7273062a23808ffea025ecb1cf8562", size = 182878, upload-time = "2025-03-05T20:02:00.305Z" }, + { url = "https://files.pythonhosted.org/packages/8e/8d/96e8e288b2a41dffafb78e8904ea7367ee4f891dafc2ab8d87e2124cb3d3/websockets-15.0.1-cp311-cp311-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:66dd88c918e3287efc22409d426c8f729688d89a0c587c88971a0faa2c2f3792", size = 181883, upload-time = "2025-03-05T20:02:03.148Z" }, + { url = "https://files.pythonhosted.org/packages/93/1f/5d6dbf551766308f6f50f8baf8e9860be6182911e8106da7a7f73785f4c4/websockets-15.0.1-cp311-cp311-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:8dd8327c795b3e3f219760fa603dcae1dcc148172290a8ab15158cf85a953413", size = 182252, upload-time = "2025-03-05T20:02:05.29Z" }, + { url = "https://files.pythonhosted.org/packages/d4/78/2d4fed9123e6620cbf1706c0de8a1632e1a28e7774d94346d7de1bba2ca3/websockets-15.0.1-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:8fdc51055e6ff4adeb88d58a11042ec9a5eae317a0a53d12c062c8a8865909e8", size = 182521, upload-time = "2025-03-05T20:02:07.458Z" }, + { url = "https://files.pythonhosted.org/packages/e7/3b/66d4c1b444dd1a9823c4a81f50231b921bab54eee2f69e70319b4e21f1ca/websockets-15.0.1-cp311-cp311-musllinux_1_2_i686.whl", hash = "sha256:693f0192126df6c2327cce3baa7c06f2a117575e32ab2308f7f8216c29d9e2e3", size = 181958, upload-time = "2025-03-05T20:02:09.842Z" }, + { url = "https://files.pythonhosted.org/packages/08/ff/e9eed2ee5fed6f76fdd6032ca5cd38c57ca9661430bb3d5fb2872dc8703c/websockets-15.0.1-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:54479983bd5fb469c38f2f5c7e3a24f9a4e70594cd68cd1fa6b9340dadaff7cf", size = 181918, upload-time = "2025-03-05T20:02:11.968Z" }, + { url = "https://files.pythonhosted.org/packages/d8/75/994634a49b7e12532be6a42103597b71098fd25900f7437d6055ed39930a/websockets-15.0.1-cp311-cp311-win32.whl", hash = "sha256:16b6c1b3e57799b9d38427dda63edcbe4926352c47cf88588c0be4ace18dac85", size = 176388, upload-time = "2025-03-05T20:02:13.32Z" }, + { url = "https://files.pythonhosted.org/packages/98/93/e36c73f78400a65f5e236cd376713c34182e6663f6889cd45a4a04d8f203/websockets-15.0.1-cp311-cp311-win_amd64.whl", hash = "sha256:27ccee0071a0e75d22cb35849b1db43f2ecd3e161041ac1ee9d2352ddf72f065", size = 176828, upload-time = "2025-03-05T20:02:14.585Z" }, + { url = "https://files.pythonhosted.org/packages/51/6b/4545a0d843594f5d0771e86463606a3988b5a09ca5123136f8a76580dd63/websockets-15.0.1-cp312-cp312-macosx_10_13_universal2.whl", hash = "sha256:3e90baa811a5d73f3ca0bcbf32064d663ed81318ab225ee4f427ad4e26e5aff3", size = 175437, upload-time = "2025-03-05T20:02:16.706Z" }, + { url = "https://files.pythonhosted.org/packages/f4/71/809a0f5f6a06522af902e0f2ea2757f71ead94610010cf570ab5c98e99ed/websockets-15.0.1-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:592f1a9fe869c778694f0aa806ba0374e97648ab57936f092fd9d87f8bc03665", size = 173096, upload-time = "2025-03-05T20:02:18.832Z" }, + { url = "https://files.pythonhosted.org/packages/3d/69/1a681dd6f02180916f116894181eab8b2e25b31e484c5d0eae637ec01f7c/websockets-15.0.1-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:0701bc3cfcb9164d04a14b149fd74be7347a530ad3bbf15ab2c678a2cd3dd9a2", size = 173332, upload-time = "2025-03-05T20:02:20.187Z" }, + { url = "https://files.pythonhosted.org/packages/a6/02/0073b3952f5bce97eafbb35757f8d0d54812b6174ed8dd952aa08429bcc3/websockets-15.0.1-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:e8b56bdcdb4505c8078cb6c7157d9811a85790f2f2b3632c7d1462ab5783d215", size = 183152, upload-time = "2025-03-05T20:02:22.286Z" }, + { url = "https://files.pythonhosted.org/packages/74/45/c205c8480eafd114b428284840da0b1be9ffd0e4f87338dc95dc6ff961a1/websockets-15.0.1-cp312-cp312-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:0af68c55afbd5f07986df82831c7bff04846928ea8d1fd7f30052638788bc9b5", size = 182096, upload-time = "2025-03-05T20:02:24.368Z" }, + { url = "https://files.pythonhosted.org/packages/14/8f/aa61f528fba38578ec553c145857a181384c72b98156f858ca5c8e82d9d3/websockets-15.0.1-cp312-cp312-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:64dee438fed052b52e4f98f76c5790513235efaa1ef7f3f2192c392cd7c91b65", size = 182523, upload-time = "2025-03-05T20:02:25.669Z" }, + { url = "https://files.pythonhosted.org/packages/ec/6d/0267396610add5bc0d0d3e77f546d4cd287200804fe02323797de77dbce9/websockets-15.0.1-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:d5f6b181bb38171a8ad1d6aa58a67a6aa9d4b38d0f8c5f496b9e42561dfc62fe", size = 182790, upload-time = "2025-03-05T20:02:26.99Z" }, + { url = "https://files.pythonhosted.org/packages/02/05/c68c5adbf679cf610ae2f74a9b871ae84564462955d991178f95a1ddb7dd/websockets-15.0.1-cp312-cp312-musllinux_1_2_i686.whl", hash = "sha256:5d54b09eba2bada6011aea5375542a157637b91029687eb4fdb2dab11059c1b4", size = 182165, upload-time = "2025-03-05T20:02:30.291Z" }, + { url = "https://files.pythonhosted.org/packages/29/93/bb672df7b2f5faac89761cb5fa34f5cec45a4026c383a4b5761c6cea5c16/websockets-15.0.1-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:3be571a8b5afed347da347bfcf27ba12b069d9d7f42cb8c7028b5e98bbb12597", size = 182160, upload-time = "2025-03-05T20:02:31.634Z" }, + { url = "https://files.pythonhosted.org/packages/ff/83/de1f7709376dc3ca9b7eeb4b9a07b4526b14876b6d372a4dc62312bebee0/websockets-15.0.1-cp312-cp312-win32.whl", hash = "sha256:c338ffa0520bdb12fbc527265235639fb76e7bc7faafbb93f6ba80d9c06578a9", size = 176395, upload-time = "2025-03-05T20:02:33.017Z" }, + { url = "https://files.pythonhosted.org/packages/7d/71/abf2ebc3bbfa40f391ce1428c7168fb20582d0ff57019b69ea20fa698043/websockets-15.0.1-cp312-cp312-win_amd64.whl", hash = "sha256:fcd5cf9e305d7b8338754470cf69cf81f420459dbae8a3b40cee57417f4614a7", size = 176841, upload-time = "2025-03-05T20:02:34.498Z" }, + { url = "https://files.pythonhosted.org/packages/cb/9f/51f0cf64471a9d2b4d0fc6c534f323b664e7095640c34562f5182e5a7195/websockets-15.0.1-cp313-cp313-macosx_10_13_universal2.whl", hash = "sha256:ee443ef070bb3b6ed74514f5efaa37a252af57c90eb33b956d35c8e9c10a1931", size = 175440, upload-time = "2025-03-05T20:02:36.695Z" }, + { url = "https://files.pythonhosted.org/packages/8a/05/aa116ec9943c718905997412c5989f7ed671bc0188ee2ba89520e8765d7b/websockets-15.0.1-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:5a939de6b7b4e18ca683218320fc67ea886038265fd1ed30173f5ce3f8e85675", size = 173098, upload-time = "2025-03-05T20:02:37.985Z" }, + { url = "https://files.pythonhosted.org/packages/ff/0b/33cef55ff24f2d92924923c99926dcce78e7bd922d649467f0eda8368923/websockets-15.0.1-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:746ee8dba912cd6fc889a8147168991d50ed70447bf18bcda7039f7d2e3d9151", size = 173329, upload-time = "2025-03-05T20:02:39.298Z" }, + { url = "https://files.pythonhosted.org/packages/31/1d/063b25dcc01faa8fada1469bdf769de3768b7044eac9d41f734fd7b6ad6d/websockets-15.0.1-cp313-cp313-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:595b6c3969023ecf9041b2936ac3827e4623bfa3ccf007575f04c5a6aa318c22", size = 183111, upload-time = "2025-03-05T20:02:40.595Z" }, + { url = "https://files.pythonhosted.org/packages/93/53/9a87ee494a51bf63e4ec9241c1ccc4f7c2f45fff85d5bde2ff74fcb68b9e/websockets-15.0.1-cp313-cp313-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:3c714d2fc58b5ca3e285461a4cc0c9a66bd0e24c5da9911e30158286c9b5be7f", size = 182054, upload-time = "2025-03-05T20:02:41.926Z" }, + { url = "https://files.pythonhosted.org/packages/ff/b2/83a6ddf56cdcbad4e3d841fcc55d6ba7d19aeb89c50f24dd7e859ec0805f/websockets-15.0.1-cp313-cp313-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:0f3c1e2ab208db911594ae5b4f79addeb3501604a165019dd221c0bdcabe4db8", size = 182496, upload-time = "2025-03-05T20:02:43.304Z" }, + { url = "https://files.pythonhosted.org/packages/98/41/e7038944ed0abf34c45aa4635ba28136f06052e08fc2168520bb8b25149f/websockets-15.0.1-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:229cf1d3ca6c1804400b0a9790dc66528e08a6a1feec0d5040e8b9eb14422375", size = 182829, upload-time = "2025-03-05T20:02:48.812Z" }, + { url = "https://files.pythonhosted.org/packages/e0/17/de15b6158680c7623c6ef0db361da965ab25d813ae54fcfeae2e5b9ef910/websockets-15.0.1-cp313-cp313-musllinux_1_2_i686.whl", hash = "sha256:756c56e867a90fb00177d530dca4b097dd753cde348448a1012ed6c5131f8b7d", size = 182217, upload-time = "2025-03-05T20:02:50.14Z" }, + { url = "https://files.pythonhosted.org/packages/33/2b/1f168cb6041853eef0362fb9554c3824367c5560cbdaad89ac40f8c2edfc/websockets-15.0.1-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:558d023b3df0bffe50a04e710bc87742de35060580a293c2a984299ed83bc4e4", size = 182195, upload-time = "2025-03-05T20:02:51.561Z" }, + { url = "https://files.pythonhosted.org/packages/86/eb/20b6cdf273913d0ad05a6a14aed4b9a85591c18a987a3d47f20fa13dcc47/websockets-15.0.1-cp313-cp313-win32.whl", hash = "sha256:ba9e56e8ceeeedb2e080147ba85ffcd5cd0711b89576b83784d8605a7df455fa", size = 176393, upload-time = "2025-03-05T20:02:53.814Z" }, + { url = "https://files.pythonhosted.org/packages/1b/6c/c65773d6cab416a64d191d6ee8a8b1c68a09970ea6909d16965d26bfed1e/websockets-15.0.1-cp313-cp313-win_amd64.whl", hash = "sha256:e09473f095a819042ecb2ab9465aee615bd9c2028e4ef7d933600a8401c79561", size = 176837, upload-time = "2025-03-05T20:02:55.237Z" }, + { url = "https://files.pythonhosted.org/packages/02/9e/d40f779fa16f74d3468357197af8d6ad07e7c5a27ea1ca74ceb38986f77a/websockets-15.0.1-pp310-pypy310_pp73-macosx_10_15_x86_64.whl", hash = "sha256:0c9e74d766f2818bb95f84c25be4dea09841ac0f734d1966f415e4edfc4ef1c3", size = 173109, upload-time = "2025-03-05T20:03:17.769Z" }, + { url = "https://files.pythonhosted.org/packages/bc/cd/5b887b8585a593073fd92f7c23ecd3985cd2c3175025a91b0d69b0551372/websockets-15.0.1-pp310-pypy310_pp73-macosx_11_0_arm64.whl", hash = "sha256:1009ee0c7739c08a0cd59de430d6de452a55e42d6b522de7aa15e6f67db0b8e1", size = 173343, upload-time = "2025-03-05T20:03:19.094Z" }, + { url = "https://files.pythonhosted.org/packages/fe/ae/d34f7556890341e900a95acf4886833646306269f899d58ad62f588bf410/websockets-15.0.1-pp310-pypy310_pp73-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:76d1f20b1c7a2fa82367e04982e708723ba0e7b8d43aa643d3dcd404d74f1475", size = 174599, upload-time = "2025-03-05T20:03:21.1Z" }, + { url = "https://files.pythonhosted.org/packages/71/e6/5fd43993a87db364ec60fc1d608273a1a465c0caba69176dd160e197ce42/websockets-15.0.1-pp310-pypy310_pp73-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:f29d80eb9a9263b8d109135351caf568cc3f80b9928bccde535c235de55c22d9", size = 174207, upload-time = "2025-03-05T20:03:23.221Z" }, + { url = "https://files.pythonhosted.org/packages/2b/fb/c492d6daa5ec067c2988ac80c61359ace5c4c674c532985ac5a123436cec/websockets-15.0.1-pp310-pypy310_pp73-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:b359ed09954d7c18bbc1680f380c7301f92c60bf924171629c5db97febb12f04", size = 174155, upload-time = "2025-03-05T20:03:25.321Z" }, + { url = "https://files.pythonhosted.org/packages/68/a1/dcb68430b1d00b698ae7a7e0194433bce4f07ded185f0ee5fb21e2a2e91e/websockets-15.0.1-pp310-pypy310_pp73-win_amd64.whl", hash = "sha256:cad21560da69f4ce7658ca2cb83138fb4cf695a2ba3e475e0559e05991aa8122", size = 176884, upload-time = "2025-03-05T20:03:27.934Z" }, + { url = "https://files.pythonhosted.org/packages/fa/a8/5b41e0da817d64113292ab1f8247140aac61cbf6cfd085d6a0fa77f4984f/websockets-15.0.1-py3-none-any.whl", hash = "sha256:f7a866fbc1e97b5c617ee4116daaa09b722101d4a3c170c787450ba409f9736f", size = 169743, upload-time = "2025-03-05T20:03:39.41Z" }, +] + +[[package]] +name = "zizmor" +version = "1.26.1" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/e4/a0/a29b38e24981b4bb41db4f292b2c9fb9ddf8b05d6b724abddd7bd108b621/zizmor-1.26.1.tar.gz", hash = "sha256:0c2cc575007a4db99d89d5acc6120cfa7b61504bc2394c3b50af348c73f1916e", size = 535275, upload-time = "2026-06-21T02:47:21.265Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/40/a9/2f47f7db8db9491025e00a7f1a0f25d32b642c0285b2fe070ac63e679b47/zizmor-1.26.1-py3-none-macosx_10_12_x86_64.whl", hash = "sha256:7ea21ca959c8e888de238fee81d73a1fdf89a82067eac75b8f1acdbd23e2eeaf", size = 9086061, upload-time = "2026-06-21T02:46:57.492Z" }, + { url = "https://files.pythonhosted.org/packages/58/92/cf6801f01e1d65cbda89a2e2926ea42caf1daad9ffa3f1fc88e4c68f48a9/zizmor-1.26.1-py3-none-macosx_11_0_arm64.whl", hash = "sha256:78083b495593f8b0b9dec14036a0836a5afcddda8a40738336ff4e399476b741", size = 8626865, upload-time = "2026-06-21T02:47:00.323Z" }, + { url = "https://files.pythonhosted.org/packages/b4/b1/ff38fc2921f1fb13244bb3a3642c4b45ecf3946c279942aafcb5dbf55a57/zizmor-1.26.1-py3-none-manylinux_2_24_aarch64.whl", hash = "sha256:bb7ebbe565a3742eb49a590352127ad549bb122b9b4ff9424ebab7525fa3b6b6", size = 8843965, upload-time = "2026-06-21T02:47:03.318Z" }, + { url = "https://files.pythonhosted.org/packages/3d/06/c07fd0eeef0427d93e99d552d5386526fbcd0bf05fc95cd37bdc6229fccb/zizmor-1.26.1-py3-none-manylinux_2_28_armv7l.whl", hash = "sha256:d3049010b6bd6f849413b6d20c28e0c677b90e0a5b2bc73cbee7f7bd86dc5828", size = 8386985, upload-time = "2026-06-21T02:47:05.741Z" }, + { url = "https://files.pythonhosted.org/packages/a8/2b/61ab13d45d6ce57ef5a08bb3246981f62e30bb4938098b17bb7b88110b79/zizmor-1.26.1-py3-none-manylinux_2_28_x86_64.whl", hash = "sha256:6a958d8a0941d7e1d0de8436670b5cb7fc64c8028b4d16e3f519ccc77f953cef", size = 9257232, upload-time = "2026-06-21T02:47:07.941Z" }, + { url = "https://files.pythonhosted.org/packages/e3/ad/bd74a96cb02045414ec5b573cd97ff3b82a97fd0bd6658f93c36a011c439/zizmor-1.26.1-py3-none-musllinux_1_2_aarch64.whl", hash = "sha256:d2744cdf944436ca7a009ae8b626a017a40381ec990216abd6cf6b8beb23323a", size = 8873798, upload-time = "2026-06-21T02:47:10.472Z" }, + { url = "https://files.pythonhosted.org/packages/b8/7e/fb3d608ee11e2f619d43ad93bab46eb7b32769fa82b1d86fd23f27c2585b/zizmor-1.26.1-py3-none-musllinux_1_2_armv7l.whl", hash = "sha256:44099f426af9da750ff9f548a0084e11d7d83e0158fe1a2778672398d728efdd", size = 8350857, upload-time = "2026-06-21T02:47:12.748Z" }, + { url = "https://files.pythonhosted.org/packages/27/cc/82d7a838c2d490071555c364f90eb851044b3eeefc1d68612179a2cd1ae5/zizmor-1.26.1-py3-none-musllinux_1_2_x86_64.whl", hash = "sha256:8313cc264dec792f00a7328eb7c8e89e7d62d54f950fc897d1e6a5a6e5762203", size = 9351148, upload-time = "2026-06-21T02:47:14.777Z" }, + { url = "https://files.pythonhosted.org/packages/f9/ee/d2a2301f30b9e1bf0d721bfd31739acd71e048757f9ba79279583eb30ac0/zizmor-1.26.1-py3-none-win32.whl", hash = "sha256:c96d7787d69fb298eae939e00dfdf7f534d7dfbd9cc17ab442c0650a56851415", size = 7531021, upload-time = "2026-06-21T02:47:17.247Z" }, + { url = "https://files.pythonhosted.org/packages/91/58/ad561f3a5057d3c0f152002e180a3a5745e72ea9d69bf66450ef9f5d3fe5/zizmor-1.26.1-py3-none-win_amd64.whl", hash = "sha256:0a05acf6068609fb6df3b137276cf18a686226a1e0e207941cb34a85929f16cf", size = 8616584, upload-time = "2026-06-21T02:47:19.094Z" }, +] diff --git a/tests/fixtures/real-world-locks/uv/flask-3.1.3/LICENSE.txt b/tests/fixtures/real-world-locks/uv/flask-3.1.3/LICENSE.txt new file mode 100644 index 00000000..9d227a0c --- /dev/null +++ b/tests/fixtures/real-world-locks/uv/flask-3.1.3/LICENSE.txt @@ -0,0 +1,28 @@ +Copyright 2010 Pallets + +Redistribution and use in source and binary forms, with or without +modification, are permitted provided that the following conditions are +met: + +1. Redistributions of source code must retain the above copyright + notice, this list of conditions and the following disclaimer. + +2. Redistributions in binary form must reproduce the above copyright + notice, this list of conditions and the following disclaimer in the + documentation and/or other materials provided with the distribution. + +3. Neither the name of the copyright holder nor the names of its + contributors may be used to endorse or promote products derived from + this software without specific prior written permission. + +THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS +"AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT +LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A +PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT +HOLDER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, +SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED +TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR +PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF +LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING +NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS +SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. diff --git a/tests/fixtures/real-world-locks/uv/flask-3.1.3/pyproject.toml b/tests/fixtures/real-world-locks/uv/flask-3.1.3/pyproject.toml new file mode 100644 index 00000000..697d2077 --- /dev/null +++ b/tests/fixtures/real-world-locks/uv/flask-3.1.3/pyproject.toml @@ -0,0 +1,279 @@ +[project] +name = "Flask" +version = "3.1.3" +description = "A simple framework for building complex web applications." +readme = "README.md" +license = "BSD-3-Clause" +license-files = ["LICENSE.txt"] +maintainers = [{name = "Pallets", email = "contact@palletsprojects.com"}] +classifiers = [ + "Development Status :: 5 - Production/Stable", + "Environment :: Web Environment", + "Framework :: Flask", + "Intended Audience :: Developers", + "Operating System :: OS Independent", + "Programming Language :: Python", + "Topic :: Internet :: WWW/HTTP :: Dynamic Content", + "Topic :: Internet :: WWW/HTTP :: WSGI", + "Topic :: Internet :: WWW/HTTP :: WSGI :: Application", + "Topic :: Software Development :: Libraries :: Application Frameworks", + "Typing :: Typed", +] +requires-python = ">=3.9" +dependencies = [ + "blinker>=1.9.0", + "click>=8.1.3", + "importlib-metadata>=3.6.0; python_version < '3.10'", + "itsdangerous>=2.2.0", + "jinja2>=3.1.2", + "markupsafe>=2.1.1", + "werkzeug>=3.1.0", +] + +[project.optional-dependencies] +async = ["asgiref>=3.2"] +dotenv = ["python-dotenv"] + +[dependency-groups] +dev = [ + "ruff", + "tox", + "tox-uv", +] +docs = [ + "pallets-sphinx-themes", + "sphinx<9", + "sphinx-tabs", + "sphinxcontrib-log-cabinet", +] +docs-auto = [ + "sphinx-autobuild", +] +gha-update = [ + "gha-update ; python_full_version >= '3.12'", +] +pre-commit = [ + "pre-commit", + "pre-commit-uv", +] +tests = [ + "asgiref", + "greenlet", + "pytest", + "python-dotenv", +] +typing = [ + "asgiref", + "cryptography", + "mypy", + "pyright", + "pytest", + "python-dotenv", + "types-contextvars", + "types-dataclasses", +] + +[project.urls] +Donate = "https://palletsprojects.com/donate" +Documentation = "https://flask.palletsprojects.com/" +Changes = "https://flask.palletsprojects.com/page/changes/" +Source = "https://github.com/pallets/flask/" +Chat = "https://discord.gg/pallets" + +[project.scripts] +flask = "flask.cli:main" + +[build-system] +requires = ["flit_core>=3.11,<4"] +build-backend = "flit_core.buildapi" + +[tool.flit.module] +name = "flask" + +[tool.flit.sdist] +include = [ + "docs/", + "examples/", + "tests/", + "CHANGES.rst", + "uv.lock" +] +exclude = [ + "docs/_build/", +] + +[tool.uv] +default-groups = ["dev", "pre-commit", "tests", "typing"] + +[tool.pytest.ini_options] +testpaths = ["tests"] +filterwarnings = [ + "error", +] + +[tool.coverage.run] +branch = true +source = ["flask", "tests"] + +[tool.coverage.paths] +source = ["src", "*/site-packages"] + +[tool.coverage.report] +exclude_also = [ + "if t.TYPE_CHECKING", + "raise NotImplementedError", + ": \\.{3}", +] + +[tool.mypy] +python_version = "3.9" +files = ["src", "tests/type_check"] +show_error_codes = true +pretty = true +strict = true + +[[tool.mypy.overrides]] +module = [ + "asgiref.*", + "dotenv.*", + "cryptography.*", + "importlib_metadata", +] +ignore_missing_imports = true + +[tool.pyright] +pythonVersion = "3.9" +include = ["src", "tests/type_check"] +typeCheckingMode = "basic" + +[tool.ruff] +src = ["src"] +fix = true +show-fixes = true +output-format = "full" + +[tool.ruff.lint] +select = [ + "B", # flake8-bugbear + "E", # pycodestyle error + "F", # pyflakes + "I", # isort + "UP", # pyupgrade + "W", # pycodestyle warning +] + +[tool.ruff.lint.isort] +force-single-line = true +order-by-type = false + +[tool.codespell] +ignore-words-list = "te" + +[tool.tox] +env_list = [ + "py3.14", "py3.14t", + "py3.13", "py3.12", "py3.11", "py3.10", "py3.9", + "pypy3.11", + "tests-min", "tests-dev", + "style", + "typing", + "docs", +] + +[tool.tox.env_run_base] +description = "pytest on latest dependency versions" +runner = "uv-venv-lock-runner" +package = "wheel" +wheel_build_env = ".pkg" +constrain_package_deps = true +use_frozen_constraints = true +dependency_groups = ["tests"] +env_tmp_dir = "{toxworkdir}/tmp/{envname}" +commands = [[ + "pytest", "-v", "--tb=short", "--basetemp={env_tmp_dir}", + {replace = "posargs", default = [], extend = true}, +]] + +[tool.tox.env.tests-min] +description = "pytest on minimum dependency versions" +base_python = ["3.14"] +commands = [ + [ + "uv", "pip", "install", + "blinker==1.9.0", + "click==8.1.3", + "itsdangerous==2.2.0", + "jinja2==3.1.2", + "markupsafe==2.1.1", + "werkzeug==3.1.0", + ], + [ + "pytest", "-v", "--tb=short", "--basetemp={env_tmp_dir}", + {replace = "posargs", default = [], extend = true}, + ], +] + +[tool.tox.env.tests-dev] +description = "pytest on development dependency versions (git main branch)" +base_python = ["3.10"] +commands = [ + [ + "uv", "pip", "install", + "https://github.com/pallets-eco/blinker/archive/refs/heads/main.tar.gz", + "https://github.com/pallets/click/archive/refs/heads/main.tar.gz", + "https://github.com/pallets/itsdangerous/archive/refs/heads/main.tar.gz", + "https://github.com/pallets/jinja/archive/refs/heads/main.tar.gz", + "https://github.com/pallets/markupsafe/archive/refs/heads/main.tar.gz", + "https://github.com/pallets/werkzeug/archive/refs/heads/main.tar.gz", + ], + [ + "pytest", "-v", "--tb=short", "--basetemp={env_tmp_dir}", + {replace = "posargs", default = [], extend = true}, + ], +] + +[tool.tox.env.style] +description = "run all pre-commit hooks on all files" +dependency_groups = ["pre-commit"] +skip_install = true +commands = [["pre-commit", "run", "--all-files"]] + +[tool.tox.env.typing] +description = "run static type checkers" +dependency_groups = ["typing"] +commands = [ + ["mypy"], + ["pyright"], +] + +[tool.tox.env.docs] +description = "build docs" +dependency_groups = ["docs"] +commands = [["sphinx-build", "-E", "-W", "-b", "dirhtml", "docs", "docs/_build/dirhtml"]] + +[tool.tox.env.docs-auto] +description = "continuously rebuild docs and start a local server" +dependency_groups = ["docs", "docs-auto"] +commands = [["sphinx-autobuild", "-W", "-b", "dirhtml", "--watch", "src", "docs", "docs/_build/dirhtml"]] + +[tool.tox.env.update-actions] +description = "update GitHub Actions pins" +labels = ["update"] +dependency_groups = ["gha-update"] +skip_install = true +commands = [["gha-update"]] + +[tool.tox.env.update-pre_commit] +description = "update pre-commit pins" +labels = ["update"] +dependency_groups = ["pre-commit"] +skip_install = true +commands = [["pre-commit", "autoupdate", "--freeze", "-j4"]] + +[tool.tox.env.update-requirements] +description = "update uv lock" +labels = ["update"] +dependency_groups = [] +no_default_groups = true +skip_install = true +commands = [["uv", "lock", {replace = "posargs", default = ["-U"], extend = true}]] diff --git a/tests/fixtures/real-world-locks/uv/flask-3.1.3/uv.lock b/tests/fixtures/real-world-locks/uv/flask-3.1.3/uv.lock new file mode 100644 index 00000000..6d5e07e2 --- /dev/null +++ b/tests/fixtures/real-world-locks/uv/flask-3.1.3/uv.lock @@ -0,0 +1,2405 @@ +version = 1 +revision = 3 +requires-python = ">=3.9" +resolution-markers = [ + "python_full_version >= '3.12'", + "python_full_version == '3.11.*'", + "python_full_version == '3.10.*'", + "python_full_version < '3.10'", +] + +[[package]] +name = "alabaster" +version = "0.7.16" +source = { registry = "https://pypi.org/simple" } +resolution-markers = [ + "python_full_version < '3.10'", +] +sdist = { url = "https://files.pythonhosted.org/packages/c9/3e/13dd8e5ed9094e734ac430b5d0eb4f2bb001708a8b7856cbf8e084e001ba/alabaster-0.7.16.tar.gz", hash = "sha256:75a8b99c28a5dad50dd7f8ccdd447a121ddb3892da9e53d1ca5cca3106d58d65", size = 23776, upload-time = "2024-01-10T00:56:10.189Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/32/34/d4e1c02d3bee589efb5dfa17f88ea08bdb3e3eac12bc475462aec52ed223/alabaster-0.7.16-py3-none-any.whl", hash = "sha256:b46733c07dce03ae4e150330b975c75737fa60f0a7c591b6c8bf4928a28e2c92", size = 13511, upload-time = "2024-01-10T00:56:08.388Z" }, +] + +[[package]] +name = "alabaster" +version = "1.0.0" +source = { registry = "https://pypi.org/simple" } +resolution-markers = [ + "python_full_version >= '3.12'", + "python_full_version == '3.11.*'", + "python_full_version == '3.10.*'", +] +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 = "anyio" +version = "4.12.1" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "exceptiongroup", marker = "python_full_version < '3.11'" }, + { name = "idna" }, + { name = "typing-extensions", marker = "python_full_version < '3.13'" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/96/f0/5eb65b2bb0d09ac6776f2eb54adee6abe8228ea05b20a5ad0e4945de8aac/anyio-4.12.1.tar.gz", hash = "sha256:41cfcc3a4c85d3f05c932da7c26d0201ac36f72abd4435ba90d0464a3ffed703", size = 228685, upload-time = "2026-01-06T11:45:21.246Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/38/0e/27be9fdef66e72d64c0cdc3cc2823101b80585f8119b5c112c2e8f5f7dab/anyio-4.12.1-py3-none-any.whl", hash = "sha256:d405828884fc140aa80a3c667b8beed277f1dfedec42ba031bd6ac3db606ab6c", size = 113592, upload-time = "2026-01-06T11:45:19.497Z" }, +] + +[[package]] +name = "asgiref" +version = "3.11.0" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "typing-extensions", marker = "python_full_version < '3.11'" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/76/b9/4db2509eabd14b4a8c71d1b24c8d5734c52b8560a7b1e1a8b56c8d25568b/asgiref-3.11.0.tar.gz", hash = "sha256:13acff32519542a1736223fb79a715acdebe24286d98e8b164a73085f40da2c4", size = 37969, upload-time = "2025-11-19T15:32:20.106Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/91/be/317c2c55b8bbec407257d45f5c8d1b6867abc76d12043f2d3d58c538a4ea/asgiref-3.11.0-py3-none-any.whl", hash = "sha256:1db9021efadb0d9512ce8ffaf72fcef601c7b73a8807a1bb2ef143dc6b14846d", size = 24096, upload-time = "2025-11-19T15:32:19.004Z" }, +] + +[[package]] +name = "babel" +version = "2.17.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/7d/6b/d52e42361e1aa00709585ecc30b3f9684b3ab62530771402248b1b1d6240/babel-2.17.0.tar.gz", hash = "sha256:0c54cffb19f690cdcc52a3b50bcbf71e07a808d1c80d549f2459b9d2cf0afb9d", size = 9951852, upload-time = "2025-02-01T15:17:41.026Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/b7/b8/3fe70c75fe32afc4bb507f75563d39bc5642255d1d94f1f23604725780bf/babel-2.17.0-py3-none-any.whl", hash = "sha256:4d0b53093fdfb4b21c92b5213dba5a1b23885afa8383709427046b21c366e5f2", size = 10182537, upload-time = "2025-02-01T15:17:37.39Z" }, +] + +[[package]] +name = "blinker" +version = "1.9.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/21/28/9b3f50ce0e048515135495f198351908d99540d69bfdc8c1d15b73dc55ce/blinker-1.9.0.tar.gz", hash = "sha256:b4ce2265a7abece45e7cc896e98dbebe6cead56bcf805a3d23136d145f5445bf", size = 22460, upload-time = "2024-11-08T17:25:47.436Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/10/cb/f2ad4230dc2eb1a74edf38f1a38b9b52277f75bef262d8908e60d957e13c/blinker-1.9.0-py3-none-any.whl", hash = "sha256:ba0efaa9080b619ff2f3459d1d500c57bddea4a6b424b60a91141db6fd2f08bc", size = 8458, upload-time = "2024-11-08T17:25:46.184Z" }, +] + +[[package]] +name = "cachetools" +version = "6.2.4" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/bc/1d/ede8680603f6016887c062a2cf4fc8fdba905866a3ab8831aa8aa651320c/cachetools-6.2.4.tar.gz", hash = "sha256:82c5c05585e70b6ba2d3ae09ea60b79548872185d2f24ae1f2709d37299fd607", size = 31731, upload-time = "2025-12-15T18:24:53.744Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/2c/fc/1d7b80d0eb7b714984ce40efc78859c022cd930e402f599d8ca9e39c78a4/cachetools-6.2.4-py3-none-any.whl", hash = "sha256:69a7a52634fed8b8bf6e24a050fb60bff1c9bd8f6d24572b99c32d4e71e62a51", size = 11551, upload-time = "2025-12-15T18:24:52.332Z" }, +] + +[[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", version = "2.23", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version < '3.10' and implementation_name != 'PyPy'" }, + { name = "pycparser", version = "3.0", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version >= '3.10' and 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/93/d7/516d984057745a6cd96575eea814fe1edd6646ee6efd552fb7b0921dec83/cffi-2.0.0-cp310-cp310-macosx_10_13_x86_64.whl", hash = "sha256:0cf2d91ecc3fcc0625c2c530fe004f82c110405f101548512cce44322fa8ac44", size = 184283, upload-time = "2025-09-08T23:22:08.01Z" }, + { url = "https://files.pythonhosted.org/packages/9e/84/ad6a0b408daa859246f57c03efd28e5dd1b33c21737c2db84cae8c237aa5/cffi-2.0.0-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:f73b96c41e3b2adedc34a7356e64c8eb96e03a3782b535e043a986276ce12a49", size = 180504, upload-time = "2025-09-08T23:22:10.637Z" }, + { 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/e2/cc/027d7fb82e58c48ea717149b03bcadcbdc293553edb283af792bd4bcbb3f/cffi-2.0.0-cp310-cp310-win32.whl", hash = "sha256:1f72fb8906754ac8a2cc3f9f5aaa298070652a0ffae577e0ea9bd480dc3c931a", size = 172184, upload-time = "2025-09-08T23:22:23.328Z" }, + { url = "https://files.pythonhosted.org/packages/33/fa/072dd15ae27fbb4e06b437eb6e944e75b068deb09e2a2826039e49ee2045/cffi-2.0.0-cp310-cp310-win_amd64.whl", hash = "sha256:b18a3ed7d5b3bd8d9ef7a8cb226502c6bf8308df1525e1cc676c3680e7176739", size = 182790, upload-time = "2025-09-08T23:22:24.752Z" }, + { url = "https://files.pythonhosted.org/packages/12/4a/3dfd5f7850cbf0d06dc84ba9aa00db766b52ca38d8b86e3a38314d52498c/cffi-2.0.0-cp311-cp311-macosx_10_13_x86_64.whl", hash = "sha256:b4c854ef3adc177950a8dfc81a86f5115d2abd545751a304c5bcf2c2c7283cfe", size = 184344, upload-time = "2025-09-08T23:22:26.456Z" }, + { url = "https://files.pythonhosted.org/packages/4f/8b/f0e4c441227ba756aafbe78f117485b25bb26b1c059d01f137fa6d14896b/cffi-2.0.0-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:2de9a304e27f7596cd03d16f1b7c72219bd944e99cc52b84d0145aefb07cbd3c", size = 180560, upload-time = "2025-09-08T23:22:28.197Z" }, + { 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/2b/c0/015b25184413d7ab0a410775fdb4a50fca20f5589b5dab1dbbfa3baad8ce/cffi-2.0.0-cp311-cp311-win32.whl", hash = "sha256:c649e3a33450ec82378822b3dad03cc228b8f5963c0c12fc3b1e0ab940f768a5", size = 172076, upload-time = "2025-09-08T23:22:40.95Z" }, + { url = "https://files.pythonhosted.org/packages/ae/8f/dc5531155e7070361eb1b7e4c1a9d896d0cb21c49f807a6c03fd63fc877e/cffi-2.0.0-cp311-cp311-win_amd64.whl", hash = "sha256:66f011380d0e49ed280c789fbd08ff0d40968ee7b665575489afa95c98196ab5", size = 182820, upload-time = "2025-09-08T23:22:42.463Z" }, + { url = "https://files.pythonhosted.org/packages/95/5c/1b493356429f9aecfd56bc171285a4c4ac8697f76e9bbbbb105e537853a1/cffi-2.0.0-cp311-cp311-win_arm64.whl", hash = "sha256:c6638687455baf640e37344fe26d37c404db8b80d037c3d29f58fe8d1c3b194d", size = 177635, upload-time = "2025-09-08T23:22:43.623Z" }, + { url = "https://files.pythonhosted.org/packages/ea/47/4f61023ea636104d4f16ab488e268b93008c3d0bb76893b1b31db1f96802/cffi-2.0.0-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:6d02d6655b0e54f54c4ef0b94eb6be0607b70853c45ce98bd278dc7de718be5d", size = 185271, upload-time = "2025-09-08T23:22:44.795Z" }, + { url = "https://files.pythonhosted.org/packages/df/a2/781b623f57358e360d62cdd7a8c681f074a71d445418a776eef0aadb4ab4/cffi-2.0.0-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:8eca2a813c1cb7ad4fb74d368c2ffbbb4789d377ee5bb8df98373c2cc0dee76c", size = 181048, upload-time = "2025-09-08T23:22:45.938Z" }, + { 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" }, + { url = "https://files.pythonhosted.org/packages/7b/2b/2b6435f76bfeb6bbf055596976da087377ede68df465419d192acf00c437/cffi-2.0.0-cp312-cp312-win32.whl", hash = "sha256:da902562c3e9c550df360bfa53c035b2f241fed6d9aef119048073680ace4a18", size = 172932, upload-time = "2025-09-08T23:22:57.188Z" }, + { url = "https://files.pythonhosted.org/packages/f8/ed/13bd4418627013bec4ed6e54283b1959cf6db888048c7cf4b4c3b5b36002/cffi-2.0.0-cp312-cp312-win_amd64.whl", hash = "sha256:da68248800ad6320861f129cd9c1bf96ca849a2771a59e0344e88681905916f5", size = 183557, upload-time = "2025-09-08T23:22:58.351Z" }, + { url = "https://files.pythonhosted.org/packages/95/31/9f7f93ad2f8eff1dbc1c3656d7ca5bfd8fb52c9d786b4dcf19b2d02217fa/cffi-2.0.0-cp312-cp312-win_arm64.whl", hash = "sha256:4671d9dd5ec934cb9a73e7ee9676f9362aba54f7f34910956b84d727b0d73fb6", size = 177762, upload-time = "2025-09-08T23:22:59.668Z" }, + { url = "https://files.pythonhosted.org/packages/4b/8d/a0a47a0c9e413a658623d014e91e74a50cdd2c423f7ccfd44086ef767f90/cffi-2.0.0-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:00bdf7acc5f795150faa6957054fbbca2439db2f775ce831222b66f192f03beb", size = 185230, upload-time = "2025-09-08T23:23:00.879Z" }, + { url = "https://files.pythonhosted.org/packages/4a/d2/a6c0296814556c68ee32009d9c2ad4f85f2707cdecfd7727951ec228005d/cffi-2.0.0-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:45d5e886156860dc35862657e1494b9bae8dfa63bf56796f2fb56e1679fc0bca", size = 181043, upload-time = "2025-09-08T23:23:02.231Z" }, + { url = "https://files.pythonhosted.org/packages/b0/1e/d22cc63332bd59b06481ceaac49d6c507598642e2230f201649058a7e704/cffi-2.0.0-cp313-cp313-manylinux1_i686.manylinux2014_i686.manylinux_2_17_i686.manylinux_2_5_i686.whl", hash = "sha256:07b271772c100085dd28b74fa0cd81c8fb1a3ba18b21e03d7c27f3436a10606b", size = 212446, upload-time = "2025-09-08T23:23:03.472Z" }, + { url = "https://files.pythonhosted.org/packages/a9/f5/a2c23eb03b61a0b8747f211eb716446c826ad66818ddc7810cc2cc19b3f2/cffi-2.0.0-cp313-cp313-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:d48a880098c96020b02d5a1f7d9251308510ce8858940e6fa99ece33f610838b", size = 220101, upload-time = "2025-09-08T23:23:04.792Z" }, + { url = "https://files.pythonhosted.org/packages/f2/7f/e6647792fc5850d634695bc0e6ab4111ae88e89981d35ac269956605feba/cffi-2.0.0-cp313-cp313-manylinux2014_ppc64le.manylinux_2_17_ppc64le.whl", hash = "sha256:f93fd8e5c8c0a4aa1f424d6173f14a892044054871c771f8566e4008eaa359d2", size = 207948, upload-time = "2025-09-08T23:23:06.127Z" }, + { url = "https://files.pythonhosted.org/packages/cb/1e/a5a1bd6f1fb30f22573f76533de12a00bf274abcdc55c8edab639078abb6/cffi-2.0.0-cp313-cp313-manylinux2014_s390x.manylinux_2_17_s390x.whl", hash = "sha256:dd4f05f54a52fb558f1ba9f528228066954fee3ebe629fc1660d874d040ae5a3", size = 206422, upload-time = "2025-09-08T23:23:07.753Z" }, + { url = "https://files.pythonhosted.org/packages/98/df/0a1755e750013a2081e863e7cd37e0cdd02664372c754e5560099eb7aa44/cffi-2.0.0-cp313-cp313-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:c8d3b5532fc71b7a77c09192b4a5a200ea992702734a2e9279a37f2478236f26", size = 219499, upload-time = "2025-09-08T23:23:09.648Z" }, + { url = "https://files.pythonhosted.org/packages/50/e1/a969e687fcf9ea58e6e2a928ad5e2dd88cc12f6f0ab477e9971f2309b57c/cffi-2.0.0-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:d9b29c1f0ae438d5ee9acb31cadee00a58c46cc9c0b2f9038c6b0b3470877a8c", size = 222928, upload-time = "2025-09-08T23:23:10.928Z" }, + { url = "https://files.pythonhosted.org/packages/36/54/0362578dd2c9e557a28ac77698ed67323ed5b9775ca9d3fe73fe191bb5d8/cffi-2.0.0-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:6d50360be4546678fc1b79ffe7a66265e28667840010348dd69a314145807a1b", size = 221302, upload-time = "2025-09-08T23:23:12.42Z" }, + { url = "https://files.pythonhosted.org/packages/eb/6d/bf9bda840d5f1dfdbf0feca87fbdb64a918a69bca42cfa0ba7b137c48cb8/cffi-2.0.0-cp313-cp313-win32.whl", hash = "sha256:74a03b9698e198d47562765773b4a8309919089150a0bb17d829ad7b44b60d27", size = 172909, upload-time = "2025-09-08T23:23:14.32Z" }, + { url = "https://files.pythonhosted.org/packages/37/18/6519e1ee6f5a1e579e04b9ddb6f1676c17368a7aba48299c3759bbc3c8b3/cffi-2.0.0-cp313-cp313-win_amd64.whl", hash = "sha256:19f705ada2530c1167abacb171925dd886168931e0a7b78f5bffcae5c6b5be75", size = 183402, upload-time = "2025-09-08T23:23:15.535Z" }, + { url = "https://files.pythonhosted.org/packages/cb/0e/02ceeec9a7d6ee63bb596121c2c8e9b3a9e150936f4fbef6ca1943e6137c/cffi-2.0.0-cp313-cp313-win_arm64.whl", hash = "sha256:256f80b80ca3853f90c21b23ee78cd008713787b1b1e93eae9f3d6a7134abd91", size = 177780, upload-time = "2025-09-08T23:23:16.761Z" }, + { url = "https://files.pythonhosted.org/packages/92/c4/3ce07396253a83250ee98564f8d7e9789fab8e58858f35d07a9a2c78de9f/cffi-2.0.0-cp314-cp314-macosx_10_13_x86_64.whl", hash = "sha256:fc33c5141b55ed366cfaad382df24fe7dcbc686de5be719b207bb248e3053dc5", size = 185320, upload-time = "2025-09-08T23:23:18.087Z" }, + { url = "https://files.pythonhosted.org/packages/59/dd/27e9fa567a23931c838c6b02d0764611c62290062a6d4e8ff7863daf9730/cffi-2.0.0-cp314-cp314-macosx_11_0_arm64.whl", hash = "sha256:c654de545946e0db659b3400168c9ad31b5d29593291482c43e3564effbcee13", size = 181487, upload-time = "2025-09-08T23:23:19.622Z" }, + { url = "https://files.pythonhosted.org/packages/d6/43/0e822876f87ea8a4ef95442c3d766a06a51fc5298823f884ef87aaad168c/cffi-2.0.0-cp314-cp314-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:24b6f81f1983e6df8db3adc38562c83f7d4a0c36162885ec7f7b77c7dcbec97b", size = 220049, upload-time = "2025-09-08T23:23:20.853Z" }, + { url = "https://files.pythonhosted.org/packages/b4/89/76799151d9c2d2d1ead63c2429da9ea9d7aac304603de0c6e8764e6e8e70/cffi-2.0.0-cp314-cp314-manylinux2014_ppc64le.manylinux_2_17_ppc64le.whl", hash = "sha256:12873ca6cb9b0f0d3a0da705d6086fe911591737a59f28b7936bdfed27c0d47c", size = 207793, upload-time = "2025-09-08T23:23:22.08Z" }, + { url = "https://files.pythonhosted.org/packages/bb/dd/3465b14bb9e24ee24cb88c9e3730f6de63111fffe513492bf8c808a3547e/cffi-2.0.0-cp314-cp314-manylinux2014_s390x.manylinux_2_17_s390x.whl", hash = "sha256:d9b97165e8aed9272a6bb17c01e3cc5871a594a446ebedc996e2397a1c1ea8ef", size = 206300, upload-time = "2025-09-08T23:23:23.314Z" }, + { url = "https://files.pythonhosted.org/packages/47/d9/d83e293854571c877a92da46fdec39158f8d7e68da75bf73581225d28e90/cffi-2.0.0-cp314-cp314-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:afb8db5439b81cf9c9d0c80404b60c3cc9c3add93e114dcae767f1477cb53775", size = 219244, upload-time = "2025-09-08T23:23:24.541Z" }, + { url = "https://files.pythonhosted.org/packages/2b/0f/1f177e3683aead2bb00f7679a16451d302c436b5cbf2505f0ea8146ef59e/cffi-2.0.0-cp314-cp314-musllinux_1_2_aarch64.whl", hash = "sha256:737fe7d37e1a1bffe70bd5754ea763a62a066dc5913ca57e957824b72a85e205", size = 222828, upload-time = "2025-09-08T23:23:26.143Z" }, + { url = "https://files.pythonhosted.org/packages/c6/0f/cafacebd4b040e3119dcb32fed8bdef8dfe94da653155f9d0b9dc660166e/cffi-2.0.0-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:38100abb9d1b1435bc4cc340bb4489635dc2f0da7456590877030c9b3d40b0c1", size = 220926, upload-time = "2025-09-08T23:23:27.873Z" }, + { url = "https://files.pythonhosted.org/packages/3e/aa/df335faa45b395396fcbc03de2dfcab242cd61a9900e914fe682a59170b1/cffi-2.0.0-cp314-cp314-win32.whl", hash = "sha256:087067fa8953339c723661eda6b54bc98c5625757ea62e95eb4898ad5e776e9f", size = 175328, upload-time = "2025-09-08T23:23:44.61Z" }, + { url = "https://files.pythonhosted.org/packages/bb/92/882c2d30831744296ce713f0feb4c1cd30f346ef747b530b5318715cc367/cffi-2.0.0-cp314-cp314-win_amd64.whl", hash = "sha256:203a48d1fb583fc7d78a4c6655692963b860a417c0528492a6bc21f1aaefab25", size = 185650, upload-time = "2025-09-08T23:23:45.848Z" }, + { url = "https://files.pythonhosted.org/packages/9f/2c/98ece204b9d35a7366b5b2c6539c350313ca13932143e79dc133ba757104/cffi-2.0.0-cp314-cp314-win_arm64.whl", hash = "sha256:dbd5c7a25a7cb98f5ca55d258b103a2054f859a46ae11aaf23134f9cc0d356ad", size = 180687, upload-time = "2025-09-08T23:23:47.105Z" }, + { url = "https://files.pythonhosted.org/packages/3e/61/c768e4d548bfa607abcda77423448df8c471f25dbe64fb2ef6d555eae006/cffi-2.0.0-cp314-cp314t-macosx_10_13_x86_64.whl", hash = "sha256:9a67fc9e8eb39039280526379fb3a70023d77caec1852002b4da7e8b270c4dd9", size = 188773, upload-time = "2025-09-08T23:23:29.347Z" }, + { url = "https://files.pythonhosted.org/packages/2c/ea/5f76bce7cf6fcd0ab1a1058b5af899bfbef198bea4d5686da88471ea0336/cffi-2.0.0-cp314-cp314t-macosx_11_0_arm64.whl", hash = "sha256:7a66c7204d8869299919db4d5069a82f1561581af12b11b3c9f48c584eb8743d", size = 185013, upload-time = "2025-09-08T23:23:30.63Z" }, + { url = "https://files.pythonhosted.org/packages/be/b4/c56878d0d1755cf9caa54ba71e5d049479c52f9e4afc230f06822162ab2f/cffi-2.0.0-cp314-cp314t-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:7cc09976e8b56f8cebd752f7113ad07752461f48a58cbba644139015ac24954c", size = 221593, upload-time = "2025-09-08T23:23:31.91Z" }, + { url = "https://files.pythonhosted.org/packages/e0/0d/eb704606dfe8033e7128df5e90fee946bbcb64a04fcdaa97321309004000/cffi-2.0.0-cp314-cp314t-manylinux2014_ppc64le.manylinux_2_17_ppc64le.whl", hash = "sha256:92b68146a71df78564e4ef48af17551a5ddd142e5190cdf2c5624d0c3ff5b2e8", size = 209354, upload-time = "2025-09-08T23:23:33.214Z" }, + { url = "https://files.pythonhosted.org/packages/d8/19/3c435d727b368ca475fb8742ab97c9cb13a0de600ce86f62eab7fa3eea60/cffi-2.0.0-cp314-cp314t-manylinux2014_s390x.manylinux_2_17_s390x.whl", hash = "sha256:b1e74d11748e7e98e2f426ab176d4ed720a64412b6a15054378afdb71e0f37dc", size = 208480, upload-time = "2025-09-08T23:23:34.495Z" }, + { url = "https://files.pythonhosted.org/packages/d0/44/681604464ed9541673e486521497406fadcc15b5217c3e326b061696899a/cffi-2.0.0-cp314-cp314t-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:28a3a209b96630bca57cce802da70c266eb08c6e97e5afd61a75611ee6c64592", size = 221584, upload-time = "2025-09-08T23:23:36.096Z" }, + { url = "https://files.pythonhosted.org/packages/25/8e/342a504ff018a2825d395d44d63a767dd8ebc927ebda557fecdaca3ac33a/cffi-2.0.0-cp314-cp314t-musllinux_1_2_aarch64.whl", hash = "sha256:7553fb2090d71822f02c629afe6042c299edf91ba1bf94951165613553984512", size = 224443, upload-time = "2025-09-08T23:23:37.328Z" }, + { url = "https://files.pythonhosted.org/packages/e1/5e/b666bacbbc60fbf415ba9988324a132c9a7a0448a9a8f125074671c0f2c3/cffi-2.0.0-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:6c6c373cfc5c83a975506110d17457138c8c63016b563cc9ed6e056a82f13ce4", size = 223437, upload-time = "2025-09-08T23:23:38.945Z" }, + { url = "https://files.pythonhosted.org/packages/a0/1d/ec1a60bd1a10daa292d3cd6bb0b359a81607154fb8165f3ec95fe003b85c/cffi-2.0.0-cp314-cp314t-win32.whl", hash = "sha256:1fc9ea04857caf665289b7a75923f2c6ed559b8298a1b8c49e59f7dd95c8481e", size = 180487, upload-time = "2025-09-08T23:23:40.423Z" }, + { url = "https://files.pythonhosted.org/packages/bf/41/4c1168c74fac325c0c8156f04b6749c8b6a8f405bbf91413ba088359f60d/cffi-2.0.0-cp314-cp314t-win_amd64.whl", hash = "sha256:d68b6cef7827e8641e8ef16f4494edda8b36104d79773a334beaa1e3521430f6", size = 191726, upload-time = "2025-09-08T23:23:41.742Z" }, + { url = "https://files.pythonhosted.org/packages/ae/3a/dbeec9d1ee0844c679f6bb5d6ad4e9f198b1224f4e7a32825f47f6192b0c/cffi-2.0.0-cp314-cp314t-win_arm64.whl", hash = "sha256:0a1527a803f0a659de1af2e1fd700213caba79377e27e4693648c2923da066f9", size = 184195, upload-time = "2025-09-08T23:23:43.004Z" }, + { url = "https://files.pythonhosted.org/packages/c0/cc/08ed5a43f2996a16b462f64a7055c6e962803534924b9b2f1371d8c00b7b/cffi-2.0.0-cp39-cp39-macosx_10_13_x86_64.whl", hash = "sha256:fe562eb1a64e67dd297ccc4f5addea2501664954f2692b69a76449ec7913ecbf", size = 184288, upload-time = "2025-09-08T23:23:48.404Z" }, + { url = "https://files.pythonhosted.org/packages/3d/de/38d9726324e127f727b4ecc376bc85e505bfe61ef130eaf3f290c6847dd4/cffi-2.0.0-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:de8dad4425a6ca6e4e5e297b27b5c824ecc7581910bf9aee86cb6835e6812aa7", size = 180509, upload-time = "2025-09-08T23:23:49.73Z" }, + { url = "https://files.pythonhosted.org/packages/9b/13/c92e36358fbcc39cf0962e83223c9522154ee8630e1df7c0b3a39a8124e2/cffi-2.0.0-cp39-cp39-manylinux1_i686.manylinux2014_i686.manylinux_2_17_i686.manylinux_2_5_i686.whl", hash = "sha256:4647afc2f90d1ddd33441e5b0e85b16b12ddec4fca55f0d9671fef036ecca27c", size = 208813, upload-time = "2025-09-08T23:23:51.263Z" }, + { url = "https://files.pythonhosted.org/packages/15/12/a7a79bd0df4c3bff744b2d7e52cc1b68d5e7e427b384252c42366dc1ecbc/cffi-2.0.0-cp39-cp39-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:3f4d46d8b35698056ec29bca21546e1551a205058ae1a181d871e278b0b28165", size = 216498, upload-time = "2025-09-08T23:23:52.494Z" }, + { url = "https://files.pythonhosted.org/packages/a3/ad/5c51c1c7600bdd7ed9a24a203ec255dccdd0ebf4527f7b922a0bde2fb6ed/cffi-2.0.0-cp39-cp39-manylinux2014_ppc64le.manylinux_2_17_ppc64le.whl", hash = "sha256:e6e73b9e02893c764e7e8d5bb5ce277f1a009cd5243f8228f75f842bf937c534", size = 203243, upload-time = "2025-09-08T23:23:53.836Z" }, + { url = "https://files.pythonhosted.org/packages/32/f2/81b63e288295928739d715d00952c8c6034cb6c6a516b17d37e0c8be5600/cffi-2.0.0-cp39-cp39-manylinux2014_s390x.manylinux_2_17_s390x.whl", hash = "sha256:cb527a79772e5ef98fb1d700678fe031e353e765d1ca2d409c92263c6d43e09f", size = 203158, upload-time = "2025-09-08T23:23:55.169Z" }, + { url = "https://files.pythonhosted.org/packages/1f/74/cc4096ce66f5939042ae094e2e96f53426a979864aa1f96a621ad128be27/cffi-2.0.0-cp39-cp39-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:61d028e90346df14fedc3d1e5441df818d095f3b87d286825dfcbd6459b7ef63", size = 216548, upload-time = "2025-09-08T23:23:56.506Z" }, + { url = "https://files.pythonhosted.org/packages/e8/be/f6424d1dc46b1091ffcc8964fa7c0ab0cd36839dd2761b49c90481a6ba1b/cffi-2.0.0-cp39-cp39-musllinux_1_2_aarch64.whl", hash = "sha256:0f6084a0ea23d05d20c3edcda20c3d006f9b6f3fefeac38f59262e10cef47ee2", size = 218897, upload-time = "2025-09-08T23:23:57.825Z" }, + { url = "https://files.pythonhosted.org/packages/f7/e0/dda537c2309817edf60109e39265f24f24aa7f050767e22c98c53fe7f48b/cffi-2.0.0-cp39-cp39-musllinux_1_2_i686.whl", hash = "sha256:1cd13c99ce269b3ed80b417dcd591415d3372bcac067009b6e0f59c7d4015e65", size = 211249, upload-time = "2025-09-08T23:23:59.139Z" }, + { url = "https://files.pythonhosted.org/packages/2b/e7/7c769804eb75e4c4b35e658dba01de1640a351a9653c3d49ca89d16ccc91/cffi-2.0.0-cp39-cp39-musllinux_1_2_x86_64.whl", hash = "sha256:89472c9762729b5ae1ad974b777416bfda4ac5642423fa93bd57a09204712322", size = 218041, upload-time = "2025-09-08T23:24:00.496Z" }, + { url = "https://files.pythonhosted.org/packages/aa/d9/6218d78f920dcd7507fc16a766b5ef8f3b913cc7aa938e7fc80b9978d089/cffi-2.0.0-cp39-cp39-win32.whl", hash = "sha256:2081580ebb843f759b9f617314a24ed5738c51d2aee65d31e02f6f7a2b97707a", size = 172138, upload-time = "2025-09-08T23:24:01.7Z" }, + { url = "https://files.pythonhosted.org/packages/54/8f/a1e836f82d8e32a97e6b29cc8f641779181ac7363734f12df27db803ebda/cffi-2.0.0-cp39-cp39-win_amd64.whl", hash = "sha256:b882b3df248017dba09d6b16defe9b5c407fe32fc7c65a9c69798e6175601be9", size = 182794, upload-time = "2025-09-08T23:24:02.943Z" }, +] + +[[package]] +name = "cfgv" +version = "3.4.0" +source = { registry = "https://pypi.org/simple" } +resolution-markers = [ + "python_full_version < '3.10'", +] +sdist = { url = "https://files.pythonhosted.org/packages/11/74/539e56497d9bd1d484fd863dd69cbbfa653cd2aa27abfe35653494d85e94/cfgv-3.4.0.tar.gz", hash = "sha256:e52591d4c5f5dead8e0f673fb16db7949d2cfb3f7da4582893288f0ded8fe560", size = 7114, upload-time = "2023-08-12T20:38:17.776Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/c5/55/51844dd50c4fc7a33b653bfaba4c2456f06955289ca770a5dbd5fd267374/cfgv-3.4.0-py2.py3-none-any.whl", hash = "sha256:b7265b1f29fd3316bfcd2b330d63d024f2bfd8bcb8b0272f8e19a504856c48f9", size = 7249, upload-time = "2023-08-12T20:38:16.269Z" }, +] + +[[package]] +name = "cfgv" +version = "3.5.0" +source = { registry = "https://pypi.org/simple" } +resolution-markers = [ + "python_full_version >= '3.12'", + "python_full_version == '3.11.*'", + "python_full_version == '3.10.*'", +] +sdist = { url = "https://files.pythonhosted.org/packages/4e/b5/721b8799b04bf9afe054a3899c6cf4e880fcf8563cc71c15610242490a0c/cfgv-3.5.0.tar.gz", hash = "sha256:d5b1034354820651caa73ede66a6294d6e95c1b00acc5e9b098e917404669132", size = 7334, upload-time = "2025-11-19T20:55:51.612Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/db/3c/33bac158f8ab7f89b2e59426d5fe2e4f63f7ed25df84c036890172b412b5/cfgv-3.5.0-py2.py3-none-any.whl", hash = "sha256:a8dc6b26ad22ff227d2634a65cb388215ce6cc96bbcc5cfde7641ae87e8dacc0", size = 7445, upload-time = "2025-11-19T20:55:50.744Z" }, +] + +[[package]] +name = "chardet" +version = "5.2.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/f3/0d/f7b6ab21ec75897ed80c17d79b15951a719226b9fababf1e40ea74d69079/chardet-5.2.0.tar.gz", hash = "sha256:1b3b6ff479a8c414bc3fa2c0852995695c4a026dcd6d0633b2dd092ca39c1cf7", size = 2069618, upload-time = "2023-08-01T19:23:02.662Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/38/6f/f5fbc992a329ee4e0f288c1fe0e2ad9485ed064cac731ed2fe47dcc38cbf/chardet-5.2.0-py3-none-any.whl", hash = "sha256:e1cf59446890a00105fe7b7912492ea04b6e6f06d4b742b2c788469e34c82970", size = 199385, upload-time = "2023-08-01T19:23:00.661Z" }, +] + +[[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/97/45/4b3a1239bbacd321068ea6e7ac28875b03ab8bc0aa0966452db17cd36714/charset_normalizer-3.4.4-cp313-cp313-macosx_10_13_universal2.whl", hash = "sha256:e1f185f86a6f3403aa2420e815904c67b2f9ebc443f045edd0de921108345794", size = 208091, upload-time = "2025-10-14T04:41:13.346Z" }, + { url = "https://files.pythonhosted.org/packages/7d/62/73a6d7450829655a35bb88a88fca7d736f9882a27eacdca2c6d505b57e2e/charset_normalizer-3.4.4-cp313-cp313-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:6b39f987ae8ccdf0d2642338faf2abb1862340facc796048b604ef14919e55ed", size = 147936, upload-time = "2025-10-14T04:41:14.461Z" }, + { url = "https://files.pythonhosted.org/packages/89/c5/adb8c8b3d6625bef6d88b251bbb0d95f8205831b987631ab0c8bb5d937c2/charset_normalizer-3.4.4-cp313-cp313-manylinux2014_armv7l.manylinux_2_17_armv7l.manylinux_2_31_armv7l.whl", hash = "sha256:3162d5d8ce1bb98dd51af660f2121c55d0fa541b46dff7bb9b9f86ea1d87de72", size = 144180, upload-time = "2025-10-14T04:41:15.588Z" }, + { url = "https://files.pythonhosted.org/packages/91/ed/9706e4070682d1cc219050b6048bfd293ccf67b3d4f5a4f39207453d4b99/charset_normalizer-3.4.4-cp313-cp313-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:81d5eb2a312700f4ecaa977a8235b634ce853200e828fbadf3a9c50bab278328", size = 161346, upload-time = "2025-10-14T04:41:16.738Z" }, + { url = "https://files.pythonhosted.org/packages/d5/0d/031f0d95e4972901a2f6f09ef055751805ff541511dc1252ba3ca1f80cf5/charset_normalizer-3.4.4-cp313-cp313-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:5bd2293095d766545ec1a8f612559f6b40abc0eb18bb2f5d1171872d34036ede", size = 158874, upload-time = "2025-10-14T04:41:17.923Z" }, + { url = "https://files.pythonhosted.org/packages/f5/83/6ab5883f57c9c801ce5e5677242328aa45592be8a00644310a008d04f922/charset_normalizer-3.4.4-cp313-cp313-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:a8a8b89589086a25749f471e6a900d3f662d1d3b6e2e59dcecf787b1cc3a1894", size = 153076, upload-time = "2025-10-14T04:41:19.106Z" }, + { url = "https://files.pythonhosted.org/packages/75/1e/5ff781ddf5260e387d6419959ee89ef13878229732732ee73cdae01800f2/charset_normalizer-3.4.4-cp313-cp313-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:bc7637e2f80d8530ee4a78e878bce464f70087ce73cf7c1caf142416923b98f1", size = 150601, upload-time = "2025-10-14T04:41:20.245Z" }, + { url = "https://files.pythonhosted.org/packages/d7/57/71be810965493d3510a6ca79b90c19e48696fb1ff964da319334b12677f0/charset_normalizer-3.4.4-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:f8bf04158c6b607d747e93949aa60618b61312fe647a6369f88ce2ff16043490", size = 150376, upload-time = "2025-10-14T04:41:21.398Z" }, + { url = "https://files.pythonhosted.org/packages/e5/d5/c3d057a78c181d007014feb7e9f2e65905a6c4ef182c0ddf0de2924edd65/charset_normalizer-3.4.4-cp313-cp313-musllinux_1_2_armv7l.whl", hash = "sha256:554af85e960429cf30784dd47447d5125aaa3b99a6f0683589dbd27e2f45da44", size = 144825, upload-time = "2025-10-14T04:41:22.583Z" }, + { url = "https://files.pythonhosted.org/packages/e6/8c/d0406294828d4976f275ffbe66f00266c4b3136b7506941d87c00cab5272/charset_normalizer-3.4.4-cp313-cp313-musllinux_1_2_ppc64le.whl", hash = "sha256:74018750915ee7ad843a774364e13a3db91682f26142baddf775342c3f5b1133", size = 162583, upload-time = "2025-10-14T04:41:23.754Z" }, + { url = "https://files.pythonhosted.org/packages/d7/24/e2aa1f18c8f15c4c0e932d9287b8609dd30ad56dbe41d926bd846e22fb8d/charset_normalizer-3.4.4-cp313-cp313-musllinux_1_2_riscv64.whl", hash = "sha256:c0463276121fdee9c49b98908b3a89c39be45d86d1dbaa22957e38f6321d4ce3", size = 150366, upload-time = "2025-10-14T04:41:25.27Z" }, + { url = "https://files.pythonhosted.org/packages/e4/5b/1e6160c7739aad1e2df054300cc618b06bf784a7a164b0f238360721ab86/charset_normalizer-3.4.4-cp313-cp313-musllinux_1_2_s390x.whl", hash = "sha256:362d61fd13843997c1c446760ef36f240cf81d3ebf74ac62652aebaf7838561e", size = 160300, upload-time = "2025-10-14T04:41:26.725Z" }, + { url = "https://files.pythonhosted.org/packages/7a/10/f882167cd207fbdd743e55534d5d9620e095089d176d55cb22d5322f2afd/charset_normalizer-3.4.4-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:9a26f18905b8dd5d685d6d07b0cdf98a79f3c7a918906af7cc143ea2e164c8bc", size = 154465, upload-time = "2025-10-14T04:41:28.322Z" }, + { url = "https://files.pythonhosted.org/packages/89/66/c7a9e1b7429be72123441bfdbaf2bc13faab3f90b933f664db506dea5915/charset_normalizer-3.4.4-cp313-cp313-win32.whl", hash = "sha256:9b35f4c90079ff2e2edc5b26c0c77925e5d2d255c42c74fdb70fb49b172726ac", size = 99404, upload-time = "2025-10-14T04:41:29.95Z" }, + { url = "https://files.pythonhosted.org/packages/c4/26/b9924fa27db384bdcd97ab83b4f0a8058d96ad9626ead570674d5e737d90/charset_normalizer-3.4.4-cp313-cp313-win_amd64.whl", hash = "sha256:b435cba5f4f750aa6c0a0d92c541fb79f69a387c91e61f1795227e4ed9cece14", size = 107092, upload-time = "2025-10-14T04:41:31.188Z" }, + { url = "https://files.pythonhosted.org/packages/af/8f/3ed4bfa0c0c72a7ca17f0380cd9e4dd842b09f664e780c13cff1dcf2ef1b/charset_normalizer-3.4.4-cp313-cp313-win_arm64.whl", hash = "sha256:542d2cee80be6f80247095cc36c418f7bddd14f4a6de45af91dfad36d817bba2", size = 100408, upload-time = "2025-10-14T04:41:32.624Z" }, + { url = "https://files.pythonhosted.org/packages/2a/35/7051599bd493e62411d6ede36fd5af83a38f37c4767b92884df7301db25d/charset_normalizer-3.4.4-cp314-cp314-macosx_10_13_universal2.whl", hash = "sha256:da3326d9e65ef63a817ecbcc0df6e94463713b754fe293eaa03da99befb9a5bd", size = 207746, upload-time = "2025-10-14T04:41:33.773Z" }, + { url = "https://files.pythonhosted.org/packages/10/9a/97c8d48ef10d6cd4fcead2415523221624bf58bcf68a802721a6bc807c8f/charset_normalizer-3.4.4-cp314-cp314-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:8af65f14dc14a79b924524b1e7fffe304517b2bff5a58bf64f30b98bbc5079eb", size = 147889, upload-time = "2025-10-14T04:41:34.897Z" }, + { url = "https://files.pythonhosted.org/packages/10/bf/979224a919a1b606c82bd2c5fa49b5c6d5727aa47b4312bb27b1734f53cd/charset_normalizer-3.4.4-cp314-cp314-manylinux2014_armv7l.manylinux_2_17_armv7l.manylinux_2_31_armv7l.whl", hash = "sha256:74664978bb272435107de04e36db5a9735e78232b85b77d45cfb38f758efd33e", size = 143641, upload-time = "2025-10-14T04:41:36.116Z" }, + { url = "https://files.pythonhosted.org/packages/ba/33/0ad65587441fc730dc7bd90e9716b30b4702dc7b617e6ba4997dc8651495/charset_normalizer-3.4.4-cp314-cp314-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:752944c7ffbfdd10c074dc58ec2d5a8a4cd9493b314d367c14d24c17684ddd14", size = 160779, upload-time = "2025-10-14T04:41:37.229Z" }, + { url = "https://files.pythonhosted.org/packages/67/ed/331d6b249259ee71ddea93f6f2f0a56cfebd46938bde6fcc6f7b9a3d0e09/charset_normalizer-3.4.4-cp314-cp314-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:d1f13550535ad8cff21b8d757a3257963e951d96e20ec82ab44bc64aeb62a191", size = 159035, upload-time = "2025-10-14T04:41:38.368Z" }, + { url = "https://files.pythonhosted.org/packages/67/ff/f6b948ca32e4f2a4576aa129d8bed61f2e0543bf9f5f2b7fc3758ed005c9/charset_normalizer-3.4.4-cp314-cp314-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:ecaae4149d99b1c9e7b88bb03e3221956f68fd6d50be2ef061b2381b61d20838", size = 152542, upload-time = "2025-10-14T04:41:39.862Z" }, + { url = "https://files.pythonhosted.org/packages/16/85/276033dcbcc369eb176594de22728541a925b2632f9716428c851b149e83/charset_normalizer-3.4.4-cp314-cp314-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:cb6254dc36b47a990e59e1068afacdcd02958bdcce30bb50cc1700a8b9d624a6", size = 149524, upload-time = "2025-10-14T04:41:41.319Z" }, + { url = "https://files.pythonhosted.org/packages/9e/f2/6a2a1f722b6aba37050e626530a46a68f74e63683947a8acff92569f979a/charset_normalizer-3.4.4-cp314-cp314-musllinux_1_2_aarch64.whl", hash = "sha256:c8ae8a0f02f57a6e61203a31428fa1d677cbe50c93622b4149d5c0f319c1d19e", size = 150395, upload-time = "2025-10-14T04:41:42.539Z" }, + { url = "https://files.pythonhosted.org/packages/60/bb/2186cb2f2bbaea6338cad15ce23a67f9b0672929744381e28b0592676824/charset_normalizer-3.4.4-cp314-cp314-musllinux_1_2_armv7l.whl", hash = "sha256:47cc91b2f4dd2833fddaedd2893006b0106129d4b94fdb6af1f4ce5a9965577c", size = 143680, upload-time = "2025-10-14T04:41:43.661Z" }, + { url = "https://files.pythonhosted.org/packages/7d/a5/bf6f13b772fbb2a90360eb620d52ed8f796f3c5caee8398c3b2eb7b1c60d/charset_normalizer-3.4.4-cp314-cp314-musllinux_1_2_ppc64le.whl", hash = "sha256:82004af6c302b5d3ab2cfc4cc5f29db16123b1a8417f2e25f9066f91d4411090", size = 162045, upload-time = "2025-10-14T04:41:44.821Z" }, + { url = "https://files.pythonhosted.org/packages/df/c5/d1be898bf0dc3ef9030c3825e5d3b83f2c528d207d246cbabe245966808d/charset_normalizer-3.4.4-cp314-cp314-musllinux_1_2_riscv64.whl", hash = "sha256:2b7d8f6c26245217bd2ad053761201e9f9680f8ce52f0fcd8d0755aeae5b2152", size = 149687, upload-time = "2025-10-14T04:41:46.442Z" }, + { url = "https://files.pythonhosted.org/packages/a5/42/90c1f7b9341eef50c8a1cb3f098ac43b0508413f33affd762855f67a410e/charset_normalizer-3.4.4-cp314-cp314-musllinux_1_2_s390x.whl", hash = "sha256:799a7a5e4fb2d5898c60b640fd4981d6a25f1c11790935a44ce38c54e985f828", size = 160014, upload-time = "2025-10-14T04:41:47.631Z" }, + { url = "https://files.pythonhosted.org/packages/76/be/4d3ee471e8145d12795ab655ece37baed0929462a86e72372fd25859047c/charset_normalizer-3.4.4-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:99ae2cffebb06e6c22bdc25801d7b30f503cc87dbd283479e7b606f70aff57ec", size = 154044, upload-time = "2025-10-14T04:41:48.81Z" }, + { url = "https://files.pythonhosted.org/packages/b0/6f/8f7af07237c34a1defe7defc565a9bc1807762f672c0fde711a4b22bf9c0/charset_normalizer-3.4.4-cp314-cp314-win32.whl", hash = "sha256:f9d332f8c2a2fcbffe1378594431458ddbef721c1769d78e2cbc06280d8155f9", size = 99940, upload-time = "2025-10-14T04:41:49.946Z" }, + { url = "https://files.pythonhosted.org/packages/4b/51/8ade005e5ca5b0d80fb4aff72a3775b325bdc3d27408c8113811a7cbe640/charset_normalizer-3.4.4-cp314-cp314-win_amd64.whl", hash = "sha256:8a6562c3700cce886c5be75ade4a5db4214fda19fede41d9792d100288d8f94c", size = 107104, upload-time = "2025-10-14T04:41:51.051Z" }, + { url = "https://files.pythonhosted.org/packages/da/5f/6b8f83a55bb8278772c5ae54a577f3099025f9ade59d0136ac24a0df4bde/charset_normalizer-3.4.4-cp314-cp314-win_arm64.whl", hash = "sha256:de00632ca48df9daf77a2c65a484531649261ec9f25489917f09e455cb09ddb2", size = 100743, upload-time = "2025-10-14T04:41:52.122Z" }, + { url = "https://files.pythonhosted.org/packages/46/7c/0c4760bccf082737ca7ab84a4c2034fcc06b1f21cf3032ea98bd6feb1725/charset_normalizer-3.4.4-cp39-cp39-macosx_10_9_universal2.whl", hash = "sha256:a9768c477b9d7bd54bc0c86dbaebdec6f03306675526c9927c0e8a04e8f94af9", size = 209609, upload-time = "2025-10-14T04:42:10.922Z" }, + { url = "https://files.pythonhosted.org/packages/bb/a4/69719daef2f3d7f1819de60c9a6be981b8eeead7542d5ec4440f3c80e111/charset_normalizer-3.4.4-cp39-cp39-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:1bee1e43c28aa63cb16e5c14e582580546b08e535299b8b6158a7c9c768a1f3d", size = 149029, upload-time = "2025-10-14T04:42:12.38Z" }, + { url = "https://files.pythonhosted.org/packages/e6/21/8d4e1d6c1e6070d3672908b8e4533a71b5b53e71d16828cc24d0efec564c/charset_normalizer-3.4.4-cp39-cp39-manylinux2014_armv7l.manylinux_2_17_armv7l.manylinux_2_31_armv7l.whl", hash = "sha256:fd44c878ea55ba351104cb93cc85e74916eb8fa440ca7903e57575e97394f608", size = 144580, upload-time = "2025-10-14T04:42:13.549Z" }, + { url = "https://files.pythonhosted.org/packages/a7/0a/a616d001b3f25647a9068e0b9199f697ce507ec898cacb06a0d5a1617c99/charset_normalizer-3.4.4-cp39-cp39-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:0f04b14ffe5fdc8c4933862d8306109a2c51e0704acfa35d51598eb45a1e89fc", size = 162340, upload-time = "2025-10-14T04:42:14.892Z" }, + { url = "https://files.pythonhosted.org/packages/85/93/060b52deb249a5450460e0585c88a904a83aec474ab8e7aba787f45e79f2/charset_normalizer-3.4.4-cp39-cp39-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:cd09d08005f958f370f539f186d10aec3377d55b9eeb0d796025d4886119d76e", size = 159619, upload-time = "2025-10-14T04:42:16.676Z" }, + { url = "https://files.pythonhosted.org/packages/dd/21/0274deb1cc0632cd587a9a0ec6b4674d9108e461cb4cd40d457adaeb0564/charset_normalizer-3.4.4-cp39-cp39-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:4fe7859a4e3e8457458e2ff592f15ccb02f3da787fcd31e0183879c3ad4692a1", size = 153980, upload-time = "2025-10-14T04:42:17.917Z" }, + { url = "https://files.pythonhosted.org/packages/28/2b/e3d7d982858dccc11b31906976323d790dded2017a0572f093ff982d692f/charset_normalizer-3.4.4-cp39-cp39-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:fa09f53c465e532f4d3db095e0c55b615f010ad81803d383195b6b5ca6cbf5f3", size = 152174, upload-time = "2025-10-14T04:42:19.018Z" }, + { url = "https://files.pythonhosted.org/packages/6e/ff/4a269f8e35f1e58b2df52c131a1fa019acb7ef3f8697b7d464b07e9b492d/charset_normalizer-3.4.4-cp39-cp39-musllinux_1_2_aarch64.whl", hash = "sha256:7fa17817dc5625de8a027cb8b26d9fefa3ea28c8253929b8d6649e705d2835b6", size = 151666, upload-time = "2025-10-14T04:42:20.171Z" }, + { url = "https://files.pythonhosted.org/packages/da/c9/ec39870f0b330d58486001dd8e532c6b9a905f5765f58a6f8204926b4a93/charset_normalizer-3.4.4-cp39-cp39-musllinux_1_2_armv7l.whl", hash = "sha256:5947809c8a2417be3267efc979c47d76a079758166f7d43ef5ae8e9f92751f88", size = 145550, upload-time = "2025-10-14T04:42:21.324Z" }, + { url = "https://files.pythonhosted.org/packages/75/8f/d186ab99e40e0ed9f82f033d6e49001701c81244d01905dd4a6924191a30/charset_normalizer-3.4.4-cp39-cp39-musllinux_1_2_ppc64le.whl", hash = "sha256:4902828217069c3c5c71094537a8e623f5d097858ac6ca8252f7b4d10b7560f1", size = 163721, upload-time = "2025-10-14T04:42:22.46Z" }, + { url = "https://files.pythonhosted.org/packages/96/b1/6047663b9744df26a7e479ac1e77af7134b1fcf9026243bb48ee2d18810f/charset_normalizer-3.4.4-cp39-cp39-musllinux_1_2_riscv64.whl", hash = "sha256:7c308f7e26e4363d79df40ca5b2be1c6ba9f02bdbccfed5abddb7859a6ce72cf", size = 152127, upload-time = "2025-10-14T04:42:23.712Z" }, + { url = "https://files.pythonhosted.org/packages/59/78/e5a6eac9179f24f704d1be67d08704c3c6ab9f00963963524be27c18ed87/charset_normalizer-3.4.4-cp39-cp39-musllinux_1_2_s390x.whl", hash = "sha256:2c9d3c380143a1fedbff95a312aa798578371eb29da42106a29019368a475318", size = 161175, upload-time = "2025-10-14T04:42:24.87Z" }, + { url = "https://files.pythonhosted.org/packages/e5/43/0e626e42d54dd2f8dd6fc5e1c5ff00f05fbca17cb699bedead2cae69c62f/charset_normalizer-3.4.4-cp39-cp39-musllinux_1_2_x86_64.whl", hash = "sha256:cb01158d8b88ee68f15949894ccc6712278243d95f344770fa7593fa2d94410c", size = 155375, upload-time = "2025-10-14T04:42:27.246Z" }, + { url = "https://files.pythonhosted.org/packages/e9/91/d9615bf2e06f35e4997616ff31248c3657ed649c5ab9d35ea12fce54e380/charset_normalizer-3.4.4-cp39-cp39-win32.whl", hash = "sha256:2677acec1a2f8ef614c6888b5b4ae4060cc184174a938ed4e8ef690e15d3e505", size = 99692, upload-time = "2025-10-14T04:42:28.425Z" }, + { url = "https://files.pythonhosted.org/packages/d1/a9/6c040053909d9d1ef4fcab45fddec083aedc9052c10078339b47c8573ea8/charset_normalizer-3.4.4-cp39-cp39-win_amd64.whl", hash = "sha256:f8e160feb2aed042cd657a72acc0b481212ed28b1b9a95c0cee1621b524e1966", size = 107192, upload-time = "2025-10-14T04:42:29.482Z" }, + { url = "https://files.pythonhosted.org/packages/f0/c6/4fa536b2c0cd3edfb7ccf8469fa0f363ea67b7213a842b90909ca33dd851/charset_normalizer-3.4.4-cp39-cp39-win_arm64.whl", hash = "sha256:b5d84d37db046c5ca74ee7bb47dd6cbc13f80665fdde3e8040bdd3fb015ecb50", size = 100220, upload-time = "2025-10-14T04:42:30.632Z" }, + { 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.1.8" +source = { registry = "https://pypi.org/simple" } +resolution-markers = [ + "python_full_version < '3.10'", +] +dependencies = [ + { name = "colorama", marker = "python_full_version < '3.10' and sys_platform == 'win32'" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/b9/2e/0090cbf739cee7d23781ad4b89a9894a41538e4fcf4c31dcdd705b78eb8b/click-8.1.8.tar.gz", hash = "sha256:ed53c9d8990d83c2a27deae68e4ee337473f6330c040a31d4225c9574d16096a", size = 226593, upload-time = "2024-12-21T18:38:44.339Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/7e/d4/7ebdbd03970677812aac39c869717059dbb71a4cfc033ca6e5221787892c/click-8.1.8-py3-none-any.whl", hash = "sha256:63c132bbbed01578a06712a2d1f497bb62d9c1c0d329b7903a866228027263b2", size = 98188, upload-time = "2024-12-21T18:38:41.666Z" }, +] + +[[package]] +name = "click" +version = "8.3.1" +source = { registry = "https://pypi.org/simple" } +resolution-markers = [ + "python_full_version >= '3.12'", + "python_full_version == '3.11.*'", + "python_full_version == '3.10.*'", +] +dependencies = [ + { name = "colorama", marker = "python_full_version >= '3.10' and 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 = "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 = "cryptography" +version = "46.0.3" +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/9f/33/c00162f49c0e2fe8064a62cb92b93e50c74a72bc370ab92f86112b33ff62/cryptography-46.0.3.tar.gz", hash = "sha256:a8b17438104fed022ce745b362294d9ce35b4c2e45c1d958ad4a4b019285f4a1", size = 749258, upload-time = "2025-10-15T23:18:31.74Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/1d/42/9c391dd801d6cf0d561b5890549d4b27bafcc53b39c31a817e69d87c625b/cryptography-46.0.3-cp311-abi3-macosx_10_9_universal2.whl", hash = "sha256:109d4ddfadf17e8e7779c39f9b18111a09efb969a301a31e987416a0191ed93a", size = 7225004, upload-time = "2025-10-15T23:16:52.239Z" }, + { url = "https://files.pythonhosted.org/packages/1c/67/38769ca6b65f07461eb200e85fc1639b438bdc667be02cf7f2cd6a64601c/cryptography-46.0.3-cp311-abi3-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:09859af8466b69bc3c27bdf4f5d84a665e0f7ab5088412e9e2ec49758eca5cbc", size = 4296667, upload-time = "2025-10-15T23:16:54.369Z" }, + { url = "https://files.pythonhosted.org/packages/5c/49/498c86566a1d80e978b42f0d702795f69887005548c041636df6ae1ca64c/cryptography-46.0.3-cp311-abi3-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:01ca9ff2885f3acc98c29f1860552e37f6d7c7d013d7334ff2a9de43a449315d", size = 4450807, upload-time = "2025-10-15T23:16:56.414Z" }, + { url = "https://files.pythonhosted.org/packages/4b/0a/863a3604112174c8624a2ac3c038662d9e59970c7f926acdcfaed8d61142/cryptography-46.0.3-cp311-abi3-manylinux_2_28_aarch64.whl", hash = "sha256:6eae65d4c3d33da080cff9c4ab1f711b15c1d9760809dad6ea763f3812d254cb", size = 4299615, upload-time = "2025-10-15T23:16:58.442Z" }, + { url = "https://files.pythonhosted.org/packages/64/02/b73a533f6b64a69f3cd3872acb6ebc12aef924d8d103133bb3ea750dc703/cryptography-46.0.3-cp311-abi3-manylinux_2_28_armv7l.manylinux_2_31_armv7l.whl", hash = "sha256:e5bf0ed4490068a2e72ac03d786693adeb909981cc596425d09032d372bcc849", size = 4016800, upload-time = "2025-10-15T23:17:00.378Z" }, + { url = "https://files.pythonhosted.org/packages/25/d5/16e41afbfa450cde85a3b7ec599bebefaef16b5c6ba4ec49a3532336ed72/cryptography-46.0.3-cp311-abi3-manylinux_2_28_ppc64le.whl", hash = "sha256:5ecfccd2329e37e9b7112a888e76d9feca2347f12f37918facbb893d7bb88ee8", size = 4984707, upload-time = "2025-10-15T23:17:01.98Z" }, + { url = "https://files.pythonhosted.org/packages/c9/56/e7e69b427c3878352c2fb9b450bd0e19ed552753491d39d7d0a2f5226d41/cryptography-46.0.3-cp311-abi3-manylinux_2_28_x86_64.whl", hash = "sha256:a2c0cd47381a3229c403062f764160d57d4d175e022c1df84e168c6251a22eec", size = 4482541, upload-time = "2025-10-15T23:17:04.078Z" }, + { url = "https://files.pythonhosted.org/packages/78/f6/50736d40d97e8483172f1bb6e698895b92a223dba513b0ca6f06b2365339/cryptography-46.0.3-cp311-abi3-manylinux_2_34_aarch64.whl", hash = "sha256:549e234ff32571b1f4076ac269fcce7a808d3bf98b76c8dd560e42dbc66d7d91", size = 4299464, upload-time = "2025-10-15T23:17:05.483Z" }, + { url = "https://files.pythonhosted.org/packages/00/de/d8e26b1a855f19d9994a19c702fa2e93b0456beccbcfe437eda00e0701f2/cryptography-46.0.3-cp311-abi3-manylinux_2_34_ppc64le.whl", hash = "sha256:c0a7bb1a68a5d3471880e264621346c48665b3bf1c3759d682fc0864c540bd9e", size = 4950838, upload-time = "2025-10-15T23:17:07.425Z" }, + { url = "https://files.pythonhosted.org/packages/8f/29/798fc4ec461a1c9e9f735f2fc58741b0daae30688f41b2497dcbc9ed1355/cryptography-46.0.3-cp311-abi3-manylinux_2_34_x86_64.whl", hash = "sha256:10b01676fc208c3e6feeb25a8b83d81767e8059e1fe86e1dc62d10a3018fa926", size = 4481596, upload-time = "2025-10-15T23:17:09.343Z" }, + { url = "https://files.pythonhosted.org/packages/15/8d/03cd48b20a573adfff7652b76271078e3045b9f49387920e7f1f631d125e/cryptography-46.0.3-cp311-abi3-musllinux_1_2_aarch64.whl", hash = "sha256:0abf1ffd6e57c67e92af68330d05760b7b7efb243aab8377e583284dbab72c71", size = 4426782, upload-time = "2025-10-15T23:17:11.22Z" }, + { url = "https://files.pythonhosted.org/packages/fa/b1/ebacbfe53317d55cf33165bda24c86523497a6881f339f9aae5c2e13e57b/cryptography-46.0.3-cp311-abi3-musllinux_1_2_x86_64.whl", hash = "sha256:a04bee9ab6a4da801eb9b51f1b708a1b5b5c9eb48c03f74198464c66f0d344ac", size = 4698381, upload-time = "2025-10-15T23:17:12.829Z" }, + { url = "https://files.pythonhosted.org/packages/96/92/8a6a9525893325fc057a01f654d7efc2c64b9de90413adcf605a85744ff4/cryptography-46.0.3-cp311-abi3-win32.whl", hash = "sha256:f260d0d41e9b4da1ed1e0f1ce571f97fe370b152ab18778e9e8f67d6af432018", size = 3055988, upload-time = "2025-10-15T23:17:14.65Z" }, + { url = "https://files.pythonhosted.org/packages/7e/bf/80fbf45253ea585a1e492a6a17efcb93467701fa79e71550a430c5e60df0/cryptography-46.0.3-cp311-abi3-win_amd64.whl", hash = "sha256:a9a3008438615669153eb86b26b61e09993921ebdd75385ddd748702c5adfddb", size = 3514451, upload-time = "2025-10-15T23:17:16.142Z" }, + { url = "https://files.pythonhosted.org/packages/2e/af/9b302da4c87b0beb9db4e756386a7c6c5b8003cd0e742277888d352ae91d/cryptography-46.0.3-cp311-abi3-win_arm64.whl", hash = "sha256:5d7f93296ee28f68447397bf5198428c9aeeab45705a55d53a6343455dcb2c3c", size = 2928007, upload-time = "2025-10-15T23:17:18.04Z" }, + { url = "https://files.pythonhosted.org/packages/f5/e2/a510aa736755bffa9d2f75029c229111a1d02f8ecd5de03078f4c18d91a3/cryptography-46.0.3-cp314-cp314t-macosx_10_9_universal2.whl", hash = "sha256:00a5e7e87938e5ff9ff5447ab086a5706a957137e6e433841e9d24f38a065217", size = 7158012, upload-time = "2025-10-15T23:17:19.982Z" }, + { url = "https://files.pythonhosted.org/packages/73/dc/9aa866fbdbb95b02e7f9d086f1fccfeebf8953509b87e3f28fff927ff8a0/cryptography-46.0.3-cp314-cp314t-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:c8daeb2d2174beb4575b77482320303f3d39b8e81153da4f0fb08eb5fe86a6c5", size = 4288728, upload-time = "2025-10-15T23:17:21.527Z" }, + { url = "https://files.pythonhosted.org/packages/c5/fd/bc1daf8230eaa075184cbbf5f8cd00ba9db4fd32d63fb83da4671b72ed8a/cryptography-46.0.3-cp314-cp314t-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:39b6755623145ad5eff1dab323f4eae2a32a77a7abef2c5089a04a3d04366715", size = 4435078, upload-time = "2025-10-15T23:17:23.042Z" }, + { url = "https://files.pythonhosted.org/packages/82/98/d3bd5407ce4c60017f8ff9e63ffee4200ab3e23fe05b765cab805a7db008/cryptography-46.0.3-cp314-cp314t-manylinux_2_28_aarch64.whl", hash = "sha256:db391fa7c66df6762ee3f00c95a89e6d428f4d60e7abc8328f4fe155b5ac6e54", size = 4293460, upload-time = "2025-10-15T23:17:24.885Z" }, + { url = "https://files.pythonhosted.org/packages/26/e9/e23e7900983c2b8af7a08098db406cf989d7f09caea7897e347598d4cd5b/cryptography-46.0.3-cp314-cp314t-manylinux_2_28_armv7l.manylinux_2_31_armv7l.whl", hash = "sha256:78a97cf6a8839a48c49271cdcbd5cf37ca2c1d6b7fdd86cc864f302b5e9bf459", size = 3995237, upload-time = "2025-10-15T23:17:26.449Z" }, + { url = "https://files.pythonhosted.org/packages/91/15/af68c509d4a138cfe299d0d7ddb14afba15233223ebd933b4bbdbc7155d3/cryptography-46.0.3-cp314-cp314t-manylinux_2_28_ppc64le.whl", hash = "sha256:dfb781ff7eaa91a6f7fd41776ec37c5853c795d3b358d4896fdbb5df168af422", size = 4967344, upload-time = "2025-10-15T23:17:28.06Z" }, + { url = "https://files.pythonhosted.org/packages/ca/e3/8643d077c53868b681af077edf6b3cb58288b5423610f21c62aadcbe99f4/cryptography-46.0.3-cp314-cp314t-manylinux_2_28_x86_64.whl", hash = "sha256:6f61efb26e76c45c4a227835ddeae96d83624fb0d29eb5df5b96e14ed1a0afb7", size = 4466564, upload-time = "2025-10-15T23:17:29.665Z" }, + { url = "https://files.pythonhosted.org/packages/0e/43/c1e8726fa59c236ff477ff2b5dc071e54b21e5a1e51aa2cee1676f1c986f/cryptography-46.0.3-cp314-cp314t-manylinux_2_34_aarch64.whl", hash = "sha256:23b1a8f26e43f47ceb6d6a43115f33a5a37d57df4ea0ca295b780ae8546e8044", size = 4292415, upload-time = "2025-10-15T23:17:31.686Z" }, + { url = "https://files.pythonhosted.org/packages/42/f9/2f8fefdb1aee8a8e3256a0568cffc4e6d517b256a2fe97a029b3f1b9fe7e/cryptography-46.0.3-cp314-cp314t-manylinux_2_34_ppc64le.whl", hash = "sha256:b419ae593c86b87014b9be7396b385491ad7f320bde96826d0dd174459e54665", size = 4931457, upload-time = "2025-10-15T23:17:33.478Z" }, + { url = "https://files.pythonhosted.org/packages/79/30/9b54127a9a778ccd6d27c3da7563e9f2d341826075ceab89ae3b41bf5be2/cryptography-46.0.3-cp314-cp314t-manylinux_2_34_x86_64.whl", hash = "sha256:50fc3343ac490c6b08c0cf0d704e881d0d660be923fd3076db3e932007e726e3", size = 4466074, upload-time = "2025-10-15T23:17:35.158Z" }, + { url = "https://files.pythonhosted.org/packages/ac/68/b4f4a10928e26c941b1b6a179143af9f4d27d88fe84a6a3c53592d2e76bf/cryptography-46.0.3-cp314-cp314t-musllinux_1_2_aarch64.whl", hash = "sha256:22d7e97932f511d6b0b04f2bfd818d73dcd5928db509460aaf48384778eb6d20", size = 4420569, upload-time = "2025-10-15T23:17:37.188Z" }, + { url = "https://files.pythonhosted.org/packages/a3/49/3746dab4c0d1979888f125226357d3262a6dd40e114ac29e3d2abdf1ec55/cryptography-46.0.3-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:d55f3dffadd674514ad19451161118fd010988540cee43d8bc20675e775925de", size = 4681941, upload-time = "2025-10-15T23:17:39.236Z" }, + { url = "https://files.pythonhosted.org/packages/fd/30/27654c1dbaf7e4a3531fa1fc77986d04aefa4d6d78259a62c9dc13d7ad36/cryptography-46.0.3-cp314-cp314t-win32.whl", hash = "sha256:8a6e050cb6164d3f830453754094c086ff2d0b2f3a897a1d9820f6139a1f0914", size = 3022339, upload-time = "2025-10-15T23:17:40.888Z" }, + { url = "https://files.pythonhosted.org/packages/f6/30/640f34ccd4d2a1bc88367b54b926b781b5a018d65f404d409aba76a84b1c/cryptography-46.0.3-cp314-cp314t-win_amd64.whl", hash = "sha256:760f83faa07f8b64e9c33fc963d790a2edb24efb479e3520c14a45741cd9b2db", size = 3494315, upload-time = "2025-10-15T23:17:42.769Z" }, + { url = "https://files.pythonhosted.org/packages/ba/8b/88cc7e3bd0a8e7b861f26981f7b820e1f46aa9d26cc482d0feba0ecb4919/cryptography-46.0.3-cp314-cp314t-win_arm64.whl", hash = "sha256:516ea134e703e9fe26bcd1277a4b59ad30586ea90c365a87781d7887a646fe21", size = 2919331, upload-time = "2025-10-15T23:17:44.468Z" }, + { url = "https://files.pythonhosted.org/packages/fd/23/45fe7f376a7df8daf6da3556603b36f53475a99ce4faacb6ba2cf3d82021/cryptography-46.0.3-cp38-abi3-macosx_10_9_universal2.whl", hash = "sha256:cb3d760a6117f621261d662bccc8ef5bc32ca673e037c83fbe565324f5c46936", size = 7218248, upload-time = "2025-10-15T23:17:46.294Z" }, + { url = "https://files.pythonhosted.org/packages/27/32/b68d27471372737054cbd34c84981f9edbc24fe67ca225d389799614e27f/cryptography-46.0.3-cp38-abi3-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:4b7387121ac7d15e550f5cb4a43aef2559ed759c35df7336c402bb8275ac9683", size = 4294089, upload-time = "2025-10-15T23:17:48.269Z" }, + { url = "https://files.pythonhosted.org/packages/26/42/fa8389d4478368743e24e61eea78846a0006caffaf72ea24a15159215a14/cryptography-46.0.3-cp38-abi3-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:15ab9b093e8f09daab0f2159bb7e47532596075139dd74365da52ecc9cb46c5d", size = 4440029, upload-time = "2025-10-15T23:17:49.837Z" }, + { url = "https://files.pythonhosted.org/packages/5f/eb/f483db0ec5ac040824f269e93dd2bd8a21ecd1027e77ad7bdf6914f2fd80/cryptography-46.0.3-cp38-abi3-manylinux_2_28_aarch64.whl", hash = "sha256:46acf53b40ea38f9c6c229599a4a13f0d46a6c3fa9ef19fc1a124d62e338dfa0", size = 4297222, upload-time = "2025-10-15T23:17:51.357Z" }, + { url = "https://files.pythonhosted.org/packages/fd/cf/da9502c4e1912cb1da3807ea3618a6829bee8207456fbbeebc361ec38ba3/cryptography-46.0.3-cp38-abi3-manylinux_2_28_armv7l.manylinux_2_31_armv7l.whl", hash = "sha256:10ca84c4668d066a9878890047f03546f3ae0a6b8b39b697457b7757aaf18dbc", size = 4012280, upload-time = "2025-10-15T23:17:52.964Z" }, + { url = "https://files.pythonhosted.org/packages/6b/8f/9adb86b93330e0df8b3dcf03eae67c33ba89958fc2e03862ef1ac2b42465/cryptography-46.0.3-cp38-abi3-manylinux_2_28_ppc64le.whl", hash = "sha256:36e627112085bb3b81b19fed209c05ce2a52ee8b15d161b7c643a7d5a88491f3", size = 4978958, upload-time = "2025-10-15T23:17:54.965Z" }, + { url = "https://files.pythonhosted.org/packages/d1/a0/5fa77988289c34bdb9f913f5606ecc9ada1adb5ae870bd0d1054a7021cc4/cryptography-46.0.3-cp38-abi3-manylinux_2_28_x86_64.whl", hash = "sha256:1000713389b75c449a6e979ffc7dcc8ac90b437048766cef052d4d30b8220971", size = 4473714, upload-time = "2025-10-15T23:17:56.754Z" }, + { url = "https://files.pythonhosted.org/packages/14/e5/fc82d72a58d41c393697aa18c9abe5ae1214ff6f2a5c18ac470f92777895/cryptography-46.0.3-cp38-abi3-manylinux_2_34_aarch64.whl", hash = "sha256:b02cf04496f6576afffef5ddd04a0cb7d49cf6be16a9059d793a30b035f6b6ac", size = 4296970, upload-time = "2025-10-15T23:17:58.588Z" }, + { url = "https://files.pythonhosted.org/packages/78/06/5663ed35438d0b09056973994f1aec467492b33bd31da36e468b01ec1097/cryptography-46.0.3-cp38-abi3-manylinux_2_34_ppc64le.whl", hash = "sha256:71e842ec9bc7abf543b47cf86b9a743baa95f4677d22baa4c7d5c69e49e9bc04", size = 4940236, upload-time = "2025-10-15T23:18:00.897Z" }, + { url = "https://files.pythonhosted.org/packages/fc/59/873633f3f2dcd8a053b8dd1d38f783043b5fce589c0f6988bf55ef57e43e/cryptography-46.0.3-cp38-abi3-manylinux_2_34_x86_64.whl", hash = "sha256:402b58fc32614f00980b66d6e56a5b4118e6cb362ae8f3fda141ba4689bd4506", size = 4472642, upload-time = "2025-10-15T23:18:02.749Z" }, + { url = "https://files.pythonhosted.org/packages/3d/39/8e71f3930e40f6877737d6f69248cf74d4e34b886a3967d32f919cc50d3b/cryptography-46.0.3-cp38-abi3-musllinux_1_2_aarch64.whl", hash = "sha256:ef639cb3372f69ec44915fafcd6698b6cc78fbe0c2ea41be867f6ed612811963", size = 4423126, upload-time = "2025-10-15T23:18:04.85Z" }, + { url = "https://files.pythonhosted.org/packages/cd/c7/f65027c2810e14c3e7268353b1681932b87e5a48e65505d8cc17c99e36ae/cryptography-46.0.3-cp38-abi3-musllinux_1_2_x86_64.whl", hash = "sha256:3b51b8ca4f1c6453d8829e1eb7299499ca7f313900dd4d89a24b8b87c0a780d4", size = 4686573, upload-time = "2025-10-15T23:18:06.908Z" }, + { url = "https://files.pythonhosted.org/packages/0a/6e/1c8331ddf91ca4730ab3086a0f1be19c65510a33b5a441cb334e7a2d2560/cryptography-46.0.3-cp38-abi3-win32.whl", hash = "sha256:6276eb85ef938dc035d59b87c8a7dc559a232f954962520137529d77b18ff1df", size = 3036695, upload-time = "2025-10-15T23:18:08.672Z" }, + { url = "https://files.pythonhosted.org/packages/90/45/b0d691df20633eff80955a0fc7695ff9051ffce8b69741444bd9ed7bd0db/cryptography-46.0.3-cp38-abi3-win_amd64.whl", hash = "sha256:416260257577718c05135c55958b674000baef9a1c7d9e8f306ec60d71db850f", size = 3501720, upload-time = "2025-10-15T23:18:10.632Z" }, + { url = "https://files.pythonhosted.org/packages/e8/cb/2da4cc83f5edb9c3257d09e1e7ab7b23f049c7962cae8d842bbef0a9cec9/cryptography-46.0.3-cp38-abi3-win_arm64.whl", hash = "sha256:d89c3468de4cdc4f08a57e214384d0471911a3830fcdaf7a8cc587e42a866372", size = 2918740, upload-time = "2025-10-15T23:18:12.277Z" }, + { url = "https://files.pythonhosted.org/packages/d9/cd/1a8633802d766a0fa46f382a77e096d7e209e0817892929655fe0586ae32/cryptography-46.0.3-pp310-pypy310_pp73-macosx_10_9_x86_64.whl", hash = "sha256:a23582810fedb8c0bc47524558fb6c56aac3fc252cb306072fd2815da2a47c32", size = 3689163, upload-time = "2025-10-15T23:18:13.821Z" }, + { url = "https://files.pythonhosted.org/packages/4c/59/6b26512964ace6480c3e54681a9859c974172fb141c38df11eadd8416947/cryptography-46.0.3-pp310-pypy310_pp73-win_amd64.whl", hash = "sha256:e7aec276d68421f9574040c26e2a7c3771060bc0cff408bae1dcb19d3ab1e63c", size = 3429474, upload-time = "2025-10-15T23:18:15.477Z" }, + { url = "https://files.pythonhosted.org/packages/06/8a/e60e46adab4362a682cf142c7dcb5bf79b782ab2199b0dcb81f55970807f/cryptography-46.0.3-pp311-pypy311_pp73-macosx_10_9_x86_64.whl", hash = "sha256:7ce938a99998ed3c8aa7e7272dca1a610401ede816d36d0693907d863b10d9ea", size = 3698132, upload-time = "2025-10-15T23:18:17.056Z" }, + { url = "https://files.pythonhosted.org/packages/da/38/f59940ec4ee91e93d3311f7532671a5cef5570eb04a144bf203b58552d11/cryptography-46.0.3-pp311-pypy311_pp73-manylinux_2_28_aarch64.whl", hash = "sha256:191bb60a7be5e6f54e30ba16fdfae78ad3a342a0599eb4193ba88e3f3d6e185b", size = 4243992, upload-time = "2025-10-15T23:18:18.695Z" }, + { url = "https://files.pythonhosted.org/packages/b0/0c/35b3d92ddebfdfda76bb485738306545817253d0a3ded0bfe80ef8e67aa5/cryptography-46.0.3-pp311-pypy311_pp73-manylinux_2_28_x86_64.whl", hash = "sha256:c70cc23f12726be8f8bc72e41d5065d77e4515efae3690326764ea1b07845cfb", size = 4409944, upload-time = "2025-10-15T23:18:20.597Z" }, + { url = "https://files.pythonhosted.org/packages/99/55/181022996c4063fc0e7666a47049a1ca705abb9c8a13830f074edb347495/cryptography-46.0.3-pp311-pypy311_pp73-manylinux_2_34_aarch64.whl", hash = "sha256:9394673a9f4de09e28b5356e7fff97d778f8abad85c9d5ac4a4b7e25a0de7717", size = 4242957, upload-time = "2025-10-15T23:18:22.18Z" }, + { url = "https://files.pythonhosted.org/packages/ba/af/72cd6ef29f9c5f731251acadaeb821559fe25f10852f44a63374c9ca08c1/cryptography-46.0.3-pp311-pypy311_pp73-manylinux_2_34_x86_64.whl", hash = "sha256:94cd0549accc38d1494e1f8de71eca837d0509d0d44bf11d158524b0e12cebf9", size = 4409447, upload-time = "2025-10-15T23:18:24.209Z" }, + { url = "https://files.pythonhosted.org/packages/0d/c3/e90f4a4feae6410f914f8ebac129b9ae7a8c92eb60a638012dde42030a9d/cryptography-46.0.3-pp311-pypy311_pp73-win_amd64.whl", hash = "sha256:6b5063083824e5509fdba180721d55909ffacccc8adbec85268b48439423d78c", size = 3438528, upload-time = "2025-10-15T23:18:26.227Z" }, +] + +[[package]] +name = "distlib" +version = "0.4.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/96/8e/709914eb2b5749865801041647dc7f4e6d00b549cfe88b65ca192995f07c/distlib-0.4.0.tar.gz", hash = "sha256:feec40075be03a04501a973d81f633735b4b69f98b05450592310c0f401a4e0d", size = 614605, upload-time = "2025-07-17T16:52:00.465Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/33/6b/e0547afaf41bf2c42e52430072fa5658766e3d65bd4b03a563d1b6336f57/distlib-0.4.0-py2.py3-none-any.whl", hash = "sha256:9659f7d87e46584a30b5780e43ac7a2143098441670ff0a49d5f9034c54a6c16", size = 469047, upload-time = "2025-07-17T16:51:58.613Z" }, +] + +[[package]] +name = "docutils" +version = "0.21.2" +source = { registry = "https://pypi.org/simple" } +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 = "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 = "filelock" +version = "3.19.1" +source = { registry = "https://pypi.org/simple" } +resolution-markers = [ + "python_full_version < '3.10'", +] +sdist = { url = "https://files.pythonhosted.org/packages/40/bb/0ab3e58d22305b6f5440629d20683af28959bf793d98d11950e305c1c326/filelock-3.19.1.tar.gz", hash = "sha256:66eda1888b0171c998b35be2bcc0f6d75c388a7ce20c3f3f37aa8e96c2dddf58", size = 17687, upload-time = "2025-08-14T16:56:03.016Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/42/14/42b2651a2f46b022ccd948bca9f2d5af0fd8929c4eec235b8d6d844fbe67/filelock-3.19.1-py3-none-any.whl", hash = "sha256:d38e30481def20772f5baf097c122c3babc4fcdb7e14e57049eb9d88c6dc017d", size = 15988, upload-time = "2025-08-14T16:56:01.633Z" }, +] + +[[package]] +name = "filelock" +version = "3.20.3" +source = { registry = "https://pypi.org/simple" } +resolution-markers = [ + "python_full_version >= '3.12'", + "python_full_version == '3.11.*'", + "python_full_version == '3.10.*'", +] +sdist = { url = "https://files.pythonhosted.org/packages/1d/65/ce7f1b70157833bf3cb851b556a37d4547ceafc158aa9b34b36782f23696/filelock-3.20.3.tar.gz", hash = "sha256:18c57ee915c7ec61cff0ecf7f0f869936c7c30191bb0cf406f1341778d0834e1", size = 19485, upload-time = "2026-01-09T17:55:05.421Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/b5/36/7fb70f04bf00bc646cd5bb45aa9eddb15e19437a28b8fb2b4a5249fac770/filelock-3.20.3-py3-none-any.whl", hash = "sha256:4b0dda527ee31078689fc205ec4f1c1bf7d56cf88b6dc9426c4f230e46c2dce1", size = 16701, upload-time = "2026-01-09T17:55:04.334Z" }, +] + +[[package]] +name = "flask" +version = "3.1.3" +source = { editable = "." } +dependencies = [ + { name = "blinker" }, + { name = "click", version = "8.1.8", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version < '3.10'" }, + { name = "click", version = "8.3.1", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version >= '3.10'" }, + { name = "importlib-metadata", marker = "python_full_version < '3.10'" }, + { name = "itsdangerous" }, + { name = "jinja2" }, + { name = "markupsafe" }, + { name = "werkzeug" }, +] + +[package.optional-dependencies] +async = [ + { name = "asgiref" }, +] +dotenv = [ + { name = "python-dotenv" }, +] + +[package.dev-dependencies] +dev = [ + { name = "ruff" }, + { name = "tox", version = "4.30.3", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version < '3.10'" }, + { name = "tox", version = "4.34.1", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version >= '3.10'" }, + { name = "tox-uv", version = "1.28.1", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version < '3.10'" }, + { name = "tox-uv", version = "1.29.0", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version >= '3.10'" }, +] +docs = [ + { name = "pallets-sphinx-themes" }, + { name = "sphinx", version = "7.4.7", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version < '3.10'" }, + { name = "sphinx", version = "8.1.3", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version == '3.10.*'" }, + { name = "sphinx", version = "8.2.3", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version >= '3.11'" }, + { name = "sphinx-tabs" }, + { name = "sphinxcontrib-log-cabinet" }, +] +docs-auto = [ + { name = "sphinx-autobuild", version = "2024.10.3", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version < '3.11'" }, + { name = "sphinx-autobuild", version = "2025.8.25", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version >= '3.11'" }, +] +gha-update = [ + { name = "gha-update", marker = "python_full_version >= '3.12'" }, +] +pre-commit = [ + { name = "pre-commit", version = "4.3.0", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version < '3.10'" }, + { name = "pre-commit", version = "4.5.1", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version >= '3.10'" }, + { name = "pre-commit-uv", version = "4.1.5", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version < '3.10'" }, + { name = "pre-commit-uv", version = "4.2.0", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version >= '3.10'" }, +] +tests = [ + { name = "asgiref" }, + { name = "greenlet", version = "3.2.4", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version < '3.10'" }, + { name = "greenlet", version = "3.3.1", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version >= '3.10'" }, + { name = "pytest", version = "8.4.2", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version < '3.10'" }, + { name = "pytest", version = "9.0.2", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version >= '3.10'" }, + { name = "python-dotenv" }, +] +typing = [ + { name = "asgiref" }, + { name = "cryptography" }, + { name = "mypy" }, + { name = "pyright" }, + { name = "pytest", version = "8.4.2", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version < '3.10'" }, + { name = "pytest", version = "9.0.2", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version >= '3.10'" }, + { name = "python-dotenv" }, + { name = "types-contextvars" }, + { name = "types-dataclasses" }, +] + +[package.metadata] +requires-dist = [ + { name = "asgiref", marker = "extra == 'async'", specifier = ">=3.2" }, + { name = "blinker", specifier = ">=1.9.0" }, + { name = "click", specifier = ">=8.1.3" }, + { name = "importlib-metadata", marker = "python_full_version < '3.10'", specifier = ">=3.6.0" }, + { name = "itsdangerous", specifier = ">=2.2.0" }, + { name = "jinja2", specifier = ">=3.1.2" }, + { name = "markupsafe", specifier = ">=2.1.1" }, + { name = "python-dotenv", marker = "extra == 'dotenv'" }, + { name = "werkzeug", specifier = ">=3.1.0" }, +] +provides-extras = ["async", "dotenv"] + +[package.metadata.requires-dev] +dev = [ + { name = "ruff" }, + { name = "tox" }, + { name = "tox-uv" }, +] +docs = [ + { name = "pallets-sphinx-themes" }, + { name = "sphinx", specifier = "<9" }, + { name = "sphinx-tabs" }, + { name = "sphinxcontrib-log-cabinet" }, +] +docs-auto = [{ name = "sphinx-autobuild" }] +gha-update = [{ name = "gha-update", marker = "python_full_version >= '3.12'" }] +pre-commit = [ + { name = "pre-commit" }, + { name = "pre-commit-uv" }, +] +tests = [ + { name = "asgiref" }, + { name = "greenlet" }, + { name = "pytest" }, + { name = "python-dotenv" }, +] +typing = [ + { name = "asgiref" }, + { name = "cryptography" }, + { name = "mypy" }, + { name = "pyright" }, + { name = "pytest" }, + { name = "python-dotenv" }, + { name = "types-contextvars" }, + { name = "types-dataclasses" }, +] + +[[package]] +name = "gha-update" +version = "0.2.0" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "click", version = "8.3.1", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version >= '3.12'" }, + { name = "httpx", marker = "python_full_version >= '3.12'" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/46/e8/eb710e08998a22b314cc068f14805cfdd12e3934a5496c8916c5a164c65a/gha_update-0.2.0.tar.gz", hash = "sha256:328ee0db09346ad13ee90646698cea2ec1f9035964ddd7c2a728a91034c3f4b0", size = 4756, upload-time = "2025-07-14T03:13:33.254Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/4b/29/a0e42b0b80d614aa82929f65cce0d51443eea296802b2094cadc5660321b/gha_update-0.2.0-py3-none-any.whl", hash = "sha256:ec5641bf23f71baa1232fc61b3059fb08456e1b78150d1e9c1bab69b37046e49", size = 5323, upload-time = "2025-07-14T03:13:31.932Z" }, +] + +[[package]] +name = "greenlet" +version = "3.2.4" +source = { registry = "https://pypi.org/simple" } +resolution-markers = [ + "python_full_version < '3.10'", +] +sdist = { url = "https://files.pythonhosted.org/packages/03/b8/704d753a5a45507a7aab61f18db9509302ed3d0a27ac7e0359ec2905b1a6/greenlet-3.2.4.tar.gz", hash = "sha256:0dca0d95ff849f9a364385f36ab49f50065d76964944638be9691e1832e9f86d", size = 188260, upload-time = "2025-08-07T13:24:33.51Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/7d/ed/6bfa4109fcb23a58819600392564fea69cdc6551ffd5e69ccf1d52a40cbc/greenlet-3.2.4-cp310-cp310-macosx_11_0_universal2.whl", hash = "sha256:8c68325b0d0acf8d91dde4e6f930967dd52a5302cd4062932a6b2e7c2969f47c", size = 271061, upload-time = "2025-08-07T13:17:15.373Z" }, + { url = "https://files.pythonhosted.org/packages/2a/fc/102ec1a2fc015b3a7652abab7acf3541d58c04d3d17a8d3d6a44adae1eb1/greenlet-3.2.4-cp310-cp310-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:94385f101946790ae13da500603491f04a76b6e4c059dab271b3ce2e283b2590", size = 629475, upload-time = "2025-08-07T13:42:54.009Z" }, + { url = "https://files.pythonhosted.org/packages/c5/26/80383131d55a4ac0fb08d71660fd77e7660b9db6bdb4e8884f46d9f2cc04/greenlet-3.2.4-cp310-cp310-manylinux2014_ppc64le.manylinux_2_17_ppc64le.whl", hash = "sha256:f10fd42b5ee276335863712fa3da6608e93f70629c631bf77145021600abc23c", size = 640802, upload-time = "2025-08-07T13:45:25.52Z" }, + { url = "https://files.pythonhosted.org/packages/9f/7c/e7833dbcd8f376f3326bd728c845d31dcde4c84268d3921afcae77d90d08/greenlet-3.2.4-cp310-cp310-manylinux2014_s390x.manylinux_2_17_s390x.whl", hash = "sha256:c8c9e331e58180d0d83c5b7999255721b725913ff6bc6cf39fa2a45841a4fd4b", size = 636703, upload-time = "2025-08-07T13:53:12.622Z" }, + { url = "https://files.pythonhosted.org/packages/e9/49/547b93b7c0428ede7b3f309bc965986874759f7d89e4e04aeddbc9699acb/greenlet-3.2.4-cp310-cp310-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:58b97143c9cc7b86fc458f215bd0932f1757ce649e05b640fea2e79b54cedb31", size = 635417, upload-time = "2025-08-07T13:18:25.189Z" }, + { url = "https://files.pythonhosted.org/packages/7f/91/ae2eb6b7979e2f9b035a9f612cf70f1bf54aad4e1d125129bef1eae96f19/greenlet-3.2.4-cp310-cp310-manylinux_2_24_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:c2ca18a03a8cfb5b25bc1cbe20f3d9a4c80d8c3b13ba3df49ac3961af0b1018d", size = 584358, upload-time = "2025-08-07T13:18:23.708Z" }, + { url = "https://files.pythonhosted.org/packages/f7/85/433de0c9c0252b22b16d413c9407e6cb3b41df7389afc366ca204dbc1393/greenlet-3.2.4-cp310-cp310-musllinux_1_1_aarch64.whl", hash = "sha256:9fe0a28a7b952a21e2c062cd5756d34354117796c6d9215a87f55e38d15402c5", size = 1113550, upload-time = "2025-08-07T13:42:37.467Z" }, + { url = "https://files.pythonhosted.org/packages/a1/8d/88f3ebd2bc96bf7747093696f4335a0a8a4c5acfcf1b757717c0d2474ba3/greenlet-3.2.4-cp310-cp310-musllinux_1_1_x86_64.whl", hash = "sha256:8854167e06950ca75b898b104b63cc646573aa5fef1353d4508ecdd1ee76254f", size = 1137126, upload-time = "2025-08-07T13:18:20.239Z" }, + { url = "https://files.pythonhosted.org/packages/f1/29/74242b7d72385e29bcc5563fba67dad94943d7cd03552bac320d597f29b2/greenlet-3.2.4-cp310-cp310-musllinux_1_2_aarch64.whl", hash = "sha256:f47617f698838ba98f4ff4189aef02e7343952df3a615f847bb575c3feb177a7", size = 1544904, upload-time = "2025-11-04T12:42:04.763Z" }, + { url = "https://files.pythonhosted.org/packages/c8/e2/1572b8eeab0f77df5f6729d6ab6b141e4a84ee8eb9bc8c1e7918f94eda6d/greenlet-3.2.4-cp310-cp310-musllinux_1_2_x86_64.whl", hash = "sha256:af41be48a4f60429d5cad9d22175217805098a9ef7c40bfef44f7669fb9d74d8", size = 1611228, upload-time = "2025-11-04T12:42:08.423Z" }, + { url = "https://files.pythonhosted.org/packages/d6/6f/b60b0291d9623c496638c582297ead61f43c4b72eef5e9c926ef4565ec13/greenlet-3.2.4-cp310-cp310-win_amd64.whl", hash = "sha256:73f49b5368b5359d04e18d15828eecc1806033db5233397748f4ca813ff1056c", size = 298654, upload-time = "2025-08-07T13:50:00.469Z" }, + { url = "https://files.pythonhosted.org/packages/a4/de/f28ced0a67749cac23fecb02b694f6473f47686dff6afaa211d186e2ef9c/greenlet-3.2.4-cp311-cp311-macosx_11_0_universal2.whl", hash = "sha256:96378df1de302bc38e99c3a9aa311967b7dc80ced1dcc6f171e99842987882a2", size = 272305, upload-time = "2025-08-07T13:15:41.288Z" }, + { url = "https://files.pythonhosted.org/packages/09/16/2c3792cba130000bf2a31c5272999113f4764fd9d874fb257ff588ac779a/greenlet-3.2.4-cp311-cp311-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:1ee8fae0519a337f2329cb78bd7a8e128ec0f881073d43f023c7b8d4831d5246", size = 632472, upload-time = "2025-08-07T13:42:55.044Z" }, + { url = "https://files.pythonhosted.org/packages/ae/8f/95d48d7e3d433e6dae5b1682e4292242a53f22df82e6d3dda81b1701a960/greenlet-3.2.4-cp311-cp311-manylinux2014_ppc64le.manylinux_2_17_ppc64le.whl", hash = "sha256:94abf90142c2a18151632371140b3dba4dee031633fe614cb592dbb6c9e17bc3", size = 644646, upload-time = "2025-08-07T13:45:26.523Z" }, + { url = "https://files.pythonhosted.org/packages/d5/5e/405965351aef8c76b8ef7ad370e5da58d57ef6068df197548b015464001a/greenlet-3.2.4-cp311-cp311-manylinux2014_s390x.manylinux_2_17_s390x.whl", hash = "sha256:4d1378601b85e2e5171b99be8d2dc85f594c79967599328f95c1dc1a40f1c633", size = 640519, upload-time = "2025-08-07T13:53:13.928Z" }, + { url = "https://files.pythonhosted.org/packages/25/5d/382753b52006ce0218297ec1b628e048c4e64b155379331f25a7316eb749/greenlet-3.2.4-cp311-cp311-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:0db5594dce18db94f7d1650d7489909b57afde4c580806b8d9203b6e79cdc079", size = 639707, upload-time = "2025-08-07T13:18:27.146Z" }, + { url = "https://files.pythonhosted.org/packages/1f/8e/abdd3f14d735b2929290a018ecf133c901be4874b858dd1c604b9319f064/greenlet-3.2.4-cp311-cp311-manylinux_2_24_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:2523e5246274f54fdadbce8494458a2ebdcdbc7b802318466ac5606d3cded1f8", size = 587684, upload-time = "2025-08-07T13:18:25.164Z" }, + { url = "https://files.pythonhosted.org/packages/5d/65/deb2a69c3e5996439b0176f6651e0052542bb6c8f8ec2e3fba97c9768805/greenlet-3.2.4-cp311-cp311-musllinux_1_1_aarch64.whl", hash = "sha256:1987de92fec508535687fb807a5cea1560f6196285a4cde35c100b8cd632cc52", size = 1116647, upload-time = "2025-08-07T13:42:38.655Z" }, + { url = "https://files.pythonhosted.org/packages/3f/cc/b07000438a29ac5cfb2194bfc128151d52f333cee74dd7dfe3fb733fc16c/greenlet-3.2.4-cp311-cp311-musllinux_1_1_x86_64.whl", hash = "sha256:55e9c5affaa6775e2c6b67659f3a71684de4c549b3dd9afca3bc773533d284fa", size = 1142073, upload-time = "2025-08-07T13:18:21.737Z" }, + { url = "https://files.pythonhosted.org/packages/67/24/28a5b2fa42d12b3d7e5614145f0bd89714c34c08be6aabe39c14dd52db34/greenlet-3.2.4-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:c9c6de1940a7d828635fbd254d69db79e54619f165ee7ce32fda763a9cb6a58c", size = 1548385, upload-time = "2025-11-04T12:42:11.067Z" }, + { url = "https://files.pythonhosted.org/packages/6a/05/03f2f0bdd0b0ff9a4f7b99333d57b53a7709c27723ec8123056b084e69cd/greenlet-3.2.4-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:03c5136e7be905045160b1b9fdca93dd6727b180feeafda6818e6496434ed8c5", size = 1613329, upload-time = "2025-11-04T12:42:12.928Z" }, + { url = "https://files.pythonhosted.org/packages/d8/0f/30aef242fcab550b0b3520b8e3561156857c94288f0332a79928c31a52cf/greenlet-3.2.4-cp311-cp311-win_amd64.whl", hash = "sha256:9c40adce87eaa9ddb593ccb0fa6a07caf34015a29bf8d344811665b573138db9", size = 299100, upload-time = "2025-08-07T13:44:12.287Z" }, + { url = "https://files.pythonhosted.org/packages/44/69/9b804adb5fd0671f367781560eb5eb586c4d495277c93bde4307b9e28068/greenlet-3.2.4-cp312-cp312-macosx_11_0_universal2.whl", hash = "sha256:3b67ca49f54cede0186854a008109d6ee71f66bd57bb36abd6d0a0267b540cdd", size = 274079, upload-time = "2025-08-07T13:15:45.033Z" }, + { url = "https://files.pythonhosted.org/packages/46/e9/d2a80c99f19a153eff70bc451ab78615583b8dac0754cfb942223d2c1a0d/greenlet-3.2.4-cp312-cp312-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:ddf9164e7a5b08e9d22511526865780a576f19ddd00d62f8a665949327fde8bb", size = 640997, upload-time = "2025-08-07T13:42:56.234Z" }, + { url = "https://files.pythonhosted.org/packages/3b/16/035dcfcc48715ccd345f3a93183267167cdd162ad123cd93067d86f27ce4/greenlet-3.2.4-cp312-cp312-manylinux2014_ppc64le.manylinux_2_17_ppc64le.whl", hash = "sha256:f28588772bb5fb869a8eb331374ec06f24a83a9c25bfa1f38b6993afe9c1e968", size = 655185, upload-time = "2025-08-07T13:45:27.624Z" }, + { url = "https://files.pythonhosted.org/packages/31/da/0386695eef69ffae1ad726881571dfe28b41970173947e7c558d9998de0f/greenlet-3.2.4-cp312-cp312-manylinux2014_s390x.manylinux_2_17_s390x.whl", hash = "sha256:5c9320971821a7cb77cfab8d956fa8e39cd07ca44b6070db358ceb7f8797c8c9", size = 649926, upload-time = "2025-08-07T13:53:15.251Z" }, + { url = "https://files.pythonhosted.org/packages/68/88/69bf19fd4dc19981928ceacbc5fd4bb6bc2215d53199e367832e98d1d8fe/greenlet-3.2.4-cp312-cp312-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:c60a6d84229b271d44b70fb6e5fa23781abb5d742af7b808ae3f6efd7c9c60f6", size = 651839, upload-time = "2025-08-07T13:18:30.281Z" }, + { url = "https://files.pythonhosted.org/packages/19/0d/6660d55f7373b2ff8152401a83e02084956da23ae58cddbfb0b330978fe9/greenlet-3.2.4-cp312-cp312-manylinux_2_24_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:3b3812d8d0c9579967815af437d96623f45c0f2ae5f04e366de62a12d83a8fb0", size = 607586, upload-time = "2025-08-07T13:18:28.544Z" }, + { url = "https://files.pythonhosted.org/packages/8e/1a/c953fdedd22d81ee4629afbb38d2f9d71e37d23caace44775a3a969147d4/greenlet-3.2.4-cp312-cp312-musllinux_1_1_aarch64.whl", hash = "sha256:abbf57b5a870d30c4675928c37278493044d7c14378350b3aa5d484fa65575f0", size = 1123281, upload-time = "2025-08-07T13:42:39.858Z" }, + { url = "https://files.pythonhosted.org/packages/3f/c7/12381b18e21aef2c6bd3a636da1088b888b97b7a0362fac2e4de92405f97/greenlet-3.2.4-cp312-cp312-musllinux_1_1_x86_64.whl", hash = "sha256:20fb936b4652b6e307b8f347665e2c615540d4b42b3b4c8a321d8286da7e520f", size = 1151142, upload-time = "2025-08-07T13:18:22.981Z" }, + { url = "https://files.pythonhosted.org/packages/27/45/80935968b53cfd3f33cf99ea5f08227f2646e044568c9b1555b58ffd61c2/greenlet-3.2.4-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:ee7a6ec486883397d70eec05059353b8e83eca9168b9f3f9a361971e77e0bcd0", size = 1564846, upload-time = "2025-11-04T12:42:15.191Z" }, + { url = "https://files.pythonhosted.org/packages/69/02/b7c30e5e04752cb4db6202a3858b149c0710e5453b71a3b2aec5d78a1aab/greenlet-3.2.4-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:326d234cbf337c9c3def0676412eb7040a35a768efc92504b947b3e9cfc7543d", size = 1633814, upload-time = "2025-11-04T12:42:17.175Z" }, + { url = "https://files.pythonhosted.org/packages/e9/08/b0814846b79399e585f974bbeebf5580fbe59e258ea7be64d9dfb253c84f/greenlet-3.2.4-cp312-cp312-win_amd64.whl", hash = "sha256:a7d4e128405eea3814a12cc2605e0e6aedb4035bf32697f72deca74de4105e02", size = 299899, upload-time = "2025-08-07T13:38:53.448Z" }, + { url = "https://files.pythonhosted.org/packages/49/e8/58c7f85958bda41dafea50497cbd59738c5c43dbbea5ee83d651234398f4/greenlet-3.2.4-cp313-cp313-macosx_11_0_universal2.whl", hash = "sha256:1a921e542453fe531144e91e1feedf12e07351b1cf6c9e8a3325ea600a715a31", size = 272814, upload-time = "2025-08-07T13:15:50.011Z" }, + { url = "https://files.pythonhosted.org/packages/62/dd/b9f59862e9e257a16e4e610480cfffd29e3fae018a68c2332090b53aac3d/greenlet-3.2.4-cp313-cp313-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:cd3c8e693bff0fff6ba55f140bf390fa92c994083f838fece0f63be121334945", size = 641073, upload-time = "2025-08-07T13:42:57.23Z" }, + { url = "https://files.pythonhosted.org/packages/f7/0b/bc13f787394920b23073ca3b6c4a7a21396301ed75a655bcb47196b50e6e/greenlet-3.2.4-cp313-cp313-manylinux2014_ppc64le.manylinux_2_17_ppc64le.whl", hash = "sha256:710638eb93b1fa52823aa91bf75326f9ecdfd5e0466f00789246a5280f4ba0fc", size = 655191, upload-time = "2025-08-07T13:45:29.752Z" }, + { url = "https://files.pythonhosted.org/packages/f2/d6/6adde57d1345a8d0f14d31e4ab9c23cfe8e2cd39c3baf7674b4b0338d266/greenlet-3.2.4-cp313-cp313-manylinux2014_s390x.manylinux_2_17_s390x.whl", hash = "sha256:c5111ccdc9c88f423426df3fd1811bfc40ed66264d35aa373420a34377efc98a", size = 649516, upload-time = "2025-08-07T13:53:16.314Z" }, + { url = "https://files.pythonhosted.org/packages/7f/3b/3a3328a788d4a473889a2d403199932be55b1b0060f4ddd96ee7cdfcad10/greenlet-3.2.4-cp313-cp313-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:d76383238584e9711e20ebe14db6c88ddcedc1829a9ad31a584389463b5aa504", size = 652169, upload-time = "2025-08-07T13:18:32.861Z" }, + { url = "https://files.pythonhosted.org/packages/ee/43/3cecdc0349359e1a527cbf2e3e28e5f8f06d3343aaf82ca13437a9aa290f/greenlet-3.2.4-cp313-cp313-manylinux_2_24_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:23768528f2911bcd7e475210822ffb5254ed10d71f4028387e5a99b4c6699671", size = 610497, upload-time = "2025-08-07T13:18:31.636Z" }, + { url = "https://files.pythonhosted.org/packages/b8/19/06b6cf5d604e2c382a6f31cafafd6f33d5dea706f4db7bdab184bad2b21d/greenlet-3.2.4-cp313-cp313-musllinux_1_1_aarch64.whl", hash = "sha256:00fadb3fedccc447f517ee0d3fd8fe49eae949e1cd0f6a611818f4f6fb7dc83b", size = 1121662, upload-time = "2025-08-07T13:42:41.117Z" }, + { url = "https://files.pythonhosted.org/packages/a2/15/0d5e4e1a66fab130d98168fe984c509249c833c1a3c16806b90f253ce7b9/greenlet-3.2.4-cp313-cp313-musllinux_1_1_x86_64.whl", hash = "sha256:d25c5091190f2dc0eaa3f950252122edbbadbb682aa7b1ef2f8af0f8c0afefae", size = 1149210, upload-time = "2025-08-07T13:18:24.072Z" }, + { url = "https://files.pythonhosted.org/packages/1c/53/f9c440463b3057485b8594d7a638bed53ba531165ef0ca0e6c364b5cc807/greenlet-3.2.4-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:6e343822feb58ac4d0a1211bd9399de2b3a04963ddeec21530fc426cc121f19b", size = 1564759, upload-time = "2025-11-04T12:42:19.395Z" }, + { url = "https://files.pythonhosted.org/packages/47/e4/3bb4240abdd0a8d23f4f88adec746a3099f0d86bfedb623f063b2e3b4df0/greenlet-3.2.4-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:ca7f6f1f2649b89ce02f6f229d7c19f680a6238af656f61e0115b24857917929", size = 1634288, upload-time = "2025-11-04T12:42:21.174Z" }, + { url = "https://files.pythonhosted.org/packages/0b/55/2321e43595e6801e105fcfdee02b34c0f996eb71e6ddffca6b10b7e1d771/greenlet-3.2.4-cp313-cp313-win_amd64.whl", hash = "sha256:554b03b6e73aaabec3745364d6239e9e012d64c68ccd0b8430c64ccc14939a8b", size = 299685, upload-time = "2025-08-07T13:24:38.824Z" }, + { url = "https://files.pythonhosted.org/packages/22/5c/85273fd7cc388285632b0498dbbab97596e04b154933dfe0f3e68156c68c/greenlet-3.2.4-cp314-cp314-macosx_11_0_universal2.whl", hash = "sha256:49a30d5fda2507ae77be16479bdb62a660fa51b1eb4928b524975b3bde77b3c0", size = 273586, upload-time = "2025-08-07T13:16:08.004Z" }, + { url = "https://files.pythonhosted.org/packages/d1/75/10aeeaa3da9332c2e761e4c50d4c3556c21113ee3f0afa2cf5769946f7a3/greenlet-3.2.4-cp314-cp314-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:299fd615cd8fc86267b47597123e3f43ad79c9d8a22bebdce535e53550763e2f", size = 686346, upload-time = "2025-08-07T13:42:59.944Z" }, + { url = "https://files.pythonhosted.org/packages/c0/aa/687d6b12ffb505a4447567d1f3abea23bd20e73a5bed63871178e0831b7a/greenlet-3.2.4-cp314-cp314-manylinux2014_ppc64le.manylinux_2_17_ppc64le.whl", hash = "sha256:c17b6b34111ea72fc5a4e4beec9711d2226285f0386ea83477cbb97c30a3f3a5", size = 699218, upload-time = "2025-08-07T13:45:30.969Z" }, + { url = "https://files.pythonhosted.org/packages/dc/8b/29aae55436521f1d6f8ff4e12fb676f3400de7fcf27fccd1d4d17fd8fecd/greenlet-3.2.4-cp314-cp314-manylinux2014_s390x.manylinux_2_17_s390x.whl", hash = "sha256:b4a1870c51720687af7fa3e7cda6d08d801dae660f75a76f3845b642b4da6ee1", size = 694659, upload-time = "2025-08-07T13:53:17.759Z" }, + { url = "https://files.pythonhosted.org/packages/92/2e/ea25914b1ebfde93b6fc4ff46d6864564fba59024e928bdc7de475affc25/greenlet-3.2.4-cp314-cp314-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:061dc4cf2c34852b052a8620d40f36324554bc192be474b9e9770e8c042fd735", size = 695355, upload-time = "2025-08-07T13:18:34.517Z" }, + { url = "https://files.pythonhosted.org/packages/72/60/fc56c62046ec17f6b0d3060564562c64c862948c9d4bc8aa807cf5bd74f4/greenlet-3.2.4-cp314-cp314-manylinux_2_24_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:44358b9bf66c8576a9f57a590d5f5d6e72fa4228b763d0e43fee6d3b06d3a337", size = 657512, upload-time = "2025-08-07T13:18:33.969Z" }, + { url = "https://files.pythonhosted.org/packages/23/6e/74407aed965a4ab6ddd93a7ded3180b730d281c77b765788419484cdfeef/greenlet-3.2.4-cp314-cp314-musllinux_1_2_aarch64.whl", hash = "sha256:2917bdf657f5859fbf3386b12d68ede4cf1f04c90c3a6bc1f013dd68a22e2269", size = 1612508, upload-time = "2025-11-04T12:42:23.427Z" }, + { url = "https://files.pythonhosted.org/packages/0d/da/343cd760ab2f92bac1845ca07ee3faea9fe52bee65f7bcb19f16ad7de08b/greenlet-3.2.4-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:015d48959d4add5d6c9f6c5210ee3803a830dce46356e3bc326d6776bde54681", size = 1680760, upload-time = "2025-11-04T12:42:25.341Z" }, + { url = "https://files.pythonhosted.org/packages/e3/a5/6ddab2b4c112be95601c13428db1d8b6608a8b6039816f2ba09c346c08fc/greenlet-3.2.4-cp314-cp314-win_amd64.whl", hash = "sha256:e37ab26028f12dbb0ff65f29a8d3d44a765c61e729647bf2ddfbbed621726f01", size = 303425, upload-time = "2025-08-07T13:32:27.59Z" }, + { url = "https://files.pythonhosted.org/packages/f7/c0/93885c4106d2626bf51fdec377d6aef740dfa5c4877461889a7cf8e565cc/greenlet-3.2.4-cp39-cp39-macosx_11_0_universal2.whl", hash = "sha256:b6a7c19cf0d2742d0809a4c05975db036fdff50cd294a93632d6a310bf9ac02c", size = 269859, upload-time = "2025-08-07T13:16:16.003Z" }, + { url = "https://files.pythonhosted.org/packages/4d/f5/33f05dc3ba10a02dedb1485870cf81c109227d3d3aa280f0e48486cac248/greenlet-3.2.4-cp39-cp39-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:27890167f55d2387576d1f41d9487ef171849ea0359ce1510ca6e06c8bece11d", size = 627610, upload-time = "2025-08-07T13:43:01.345Z" }, + { url = "https://files.pythonhosted.org/packages/b2/a7/9476decef51a0844195f99ed5dc611d212e9b3515512ecdf7321543a7225/greenlet-3.2.4-cp39-cp39-manylinux2014_ppc64le.manylinux_2_17_ppc64le.whl", hash = "sha256:18d9260df2b5fbf41ae5139e1be4e796d99655f023a636cd0e11e6406cca7d58", size = 639417, upload-time = "2025-08-07T13:45:32.094Z" }, + { url = "https://files.pythonhosted.org/packages/bd/e0/849b9159cbb176f8c0af5caaff1faffdece7a8417fcc6fe1869770e33e21/greenlet-3.2.4-cp39-cp39-manylinux2014_s390x.manylinux_2_17_s390x.whl", hash = "sha256:671df96c1f23c4a0d4077a325483c1503c96a1b7d9db26592ae770daa41233d4", size = 634751, upload-time = "2025-08-07T13:53:18.848Z" }, + { url = "https://files.pythonhosted.org/packages/5f/d3/844e714a9bbd39034144dca8b658dcd01839b72bb0ec7d8014e33e3705f0/greenlet-3.2.4-cp39-cp39-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:16458c245a38991aa19676900d48bd1a6f2ce3e16595051a4db9d012154e8433", size = 634020, upload-time = "2025-08-07T13:18:36.841Z" }, + { url = "https://files.pythonhosted.org/packages/6b/4c/f3de2a8de0e840ecb0253ad0dc7e2bb3747348e798ec7e397d783a3cb380/greenlet-3.2.4-cp39-cp39-manylinux_2_24_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:c9913f1a30e4526f432991f89ae263459b1c64d1608c0d22a5c79c287b3c70df", size = 582817, upload-time = "2025-08-07T13:18:35.48Z" }, + { url = "https://files.pythonhosted.org/packages/89/80/7332915adc766035c8980b161c2e5d50b2f941f453af232c164cff5e0aeb/greenlet-3.2.4-cp39-cp39-musllinux_1_1_aarch64.whl", hash = "sha256:b90654e092f928f110e0007f572007c9727b5265f7632c2fa7415b4689351594", size = 1111985, upload-time = "2025-08-07T13:42:42.425Z" }, + { url = "https://files.pythonhosted.org/packages/66/71/1928e2c80197353bcb9b50aa19c4d8e26ee6d7a900c564907665cf4b9a41/greenlet-3.2.4-cp39-cp39-musllinux_1_1_x86_64.whl", hash = "sha256:81701fd84f26330f0d5f4944d4e92e61afe6319dcd9775e39396e39d7c3e5f98", size = 1136137, upload-time = "2025-08-07T13:18:26.168Z" }, + { url = "https://files.pythonhosted.org/packages/4b/bf/7bd33643e48ed45dcc0e22572f650767832bd4e1287f97434943cc402148/greenlet-3.2.4-cp39-cp39-musllinux_1_2_aarch64.whl", hash = "sha256:28a3c6b7cd72a96f61b0e4b2a36f681025b60ae4779cc73c1535eb5f29560b10", size = 1542941, upload-time = "2025-11-04T12:42:27.427Z" }, + { url = "https://files.pythonhosted.org/packages/9b/74/4bc433f91d0d09a1c22954a371f9df928cb85e72640870158853a83415e5/greenlet-3.2.4-cp39-cp39-musllinux_1_2_x86_64.whl", hash = "sha256:52206cd642670b0b320a1fd1cbfd95bca0e043179c1d8a045f2c6109dfe973be", size = 1609685, upload-time = "2025-11-04T12:42:29.242Z" }, + { url = "https://files.pythonhosted.org/packages/89/48/a5dc74dde38aeb2b15d418cec76ed50e1dd3d620ccda84d8199703248968/greenlet-3.2.4-cp39-cp39-win32.whl", hash = "sha256:65458b409c1ed459ea899e939f0e1cdb14f58dbc803f2f93c5eab5694d32671b", size = 281400, upload-time = "2025-08-07T14:02:20.263Z" }, + { url = "https://files.pythonhosted.org/packages/e5/44/342c4591db50db1076b8bda86ed0ad59240e3e1da17806a4cf10a6d0e447/greenlet-3.2.4-cp39-cp39-win_amd64.whl", hash = "sha256:d2e685ade4dafd447ede19c31277a224a239a0a1a4eca4e6390efedf20260cfb", size = 298533, upload-time = "2025-08-07T13:56:34.168Z" }, +] + +[[package]] +name = "greenlet" +version = "3.3.1" +source = { registry = "https://pypi.org/simple" } +resolution-markers = [ + "python_full_version >= '3.12'", + "python_full_version == '3.11.*'", + "python_full_version == '3.10.*'", +] +sdist = { url = "https://files.pythonhosted.org/packages/8a/99/1cd3411c56a410994669062bd73dd58270c00cc074cac15f385a1fd91f8a/greenlet-3.3.1.tar.gz", hash = "sha256:41848f3230b58c08bb43dee542e74a2a2e34d3c59dc3076cec9151aeeedcae98", size = 184690, upload-time = "2026-01-23T15:31:02.076Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/fe/65/5b235b40581ad75ab97dcd8b4218022ae8e3ab77c13c919f1a1dfe9171fd/greenlet-3.3.1-cp310-cp310-macosx_11_0_universal2.whl", hash = "sha256:04bee4775f40ecefcdaa9d115ab44736cd4b9c5fba733575bfe9379419582e13", size = 273723, upload-time = "2026-01-23T15:30:37.521Z" }, + { url = "https://files.pythonhosted.org/packages/ce/ad/eb4729b85cba2d29499e0a04ca6fbdd8f540afd7be142fd571eea43d712f/greenlet-3.3.1-cp310-cp310-manylinux_2_24_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:50e1457f4fed12a50e427988a07f0f9df53cf0ee8da23fab16e6732c2ec909d4", size = 574874, upload-time = "2026-01-23T16:00:54.551Z" }, + { url = "https://files.pythonhosted.org/packages/87/32/57cad7fe4c8b82fdaa098c89498ef85ad92dfbb09d5eb713adedfc2ae1f5/greenlet-3.3.1-cp310-cp310-manylinux_2_24_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:070472cd156f0656f86f92e954591644e158fd65aa415ffbe2d44ca77656a8f5", size = 586309, upload-time = "2026-01-23T16:05:25.18Z" }, + { url = "https://files.pythonhosted.org/packages/66/66/f041005cb87055e62b0d68680e88ec1a57f4688523d5e2fb305841bc8307/greenlet-3.3.1-cp310-cp310-manylinux_2_24_s390x.manylinux_2_28_s390x.whl", hash = "sha256:1108b61b06b5224656121c3c8ee8876161c491cbe74e5c519e0634c837cf93d5", size = 597461, upload-time = "2026-01-23T16:15:51.943Z" }, + { url = "https://files.pythonhosted.org/packages/87/eb/8a1ec2da4d55824f160594a75a9d8354a5fe0a300fb1c48e7944265217e1/greenlet-3.3.1-cp310-cp310-manylinux_2_24_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:3a300354f27dd86bae5fbf7002e6dd2b3255cd372e9242c933faf5e859b703fe", size = 586985, upload-time = "2026-01-23T15:32:47.968Z" }, + { url = "https://files.pythonhosted.org/packages/15/1c/0621dd4321dd8c351372ee8f9308136acb628600658a49be1b7504208738/greenlet-3.3.1-cp310-cp310-musllinux_1_2_aarch64.whl", hash = "sha256:e84b51cbebf9ae573b5fbd15df88887815e3253fc000a7d0ff95170e8f7e9729", size = 1547271, upload-time = "2026-01-23T16:04:18.977Z" }, + { url = "https://files.pythonhosted.org/packages/9d/53/24047f8924c83bea7a59c8678d9571209c6bfe5f4c17c94a78c06024e9f2/greenlet-3.3.1-cp310-cp310-musllinux_1_2_x86_64.whl", hash = "sha256:e0093bd1a06d899892427217f0ff2a3c8f306182b8c754336d32e2d587c131b4", size = 1613427, upload-time = "2026-01-23T15:33:44.428Z" }, + { url = "https://files.pythonhosted.org/packages/ff/07/ac9bf1ec008916d1a3373cae212884c1dcff4a4ba0d41127ce81a8deb4e9/greenlet-3.3.1-cp310-cp310-win_amd64.whl", hash = "sha256:7932f5f57609b6a3b82cc11877709aa7a98e3308983ed93552a1c377069b20c8", size = 226100, upload-time = "2026-01-23T15:30:56.957Z" }, + { url = "https://files.pythonhosted.org/packages/ec/e8/2e1462c8fdbe0f210feb5ac7ad2d9029af8be3bf45bd9fa39765f821642f/greenlet-3.3.1-cp311-cp311-macosx_11_0_universal2.whl", hash = "sha256:5fd23b9bc6d37b563211c6abbb1b3cab27db385a4449af5c32e932f93017080c", size = 274974, upload-time = "2026-01-23T15:31:02.891Z" }, + { url = "https://files.pythonhosted.org/packages/7e/a8/530a401419a6b302af59f67aaf0b9ba1015855ea7e56c036b5928793c5bd/greenlet-3.3.1-cp311-cp311-manylinux_2_24_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:09f51496a0bfbaa9d74d36a52d2580d1ef5ed4fdfcff0a73730abfbbbe1403dd", size = 577175, upload-time = "2026-01-23T16:00:56.213Z" }, + { url = "https://files.pythonhosted.org/packages/8e/89/7e812bb9c05e1aaef9b597ac1d0962b9021d2c6269354966451e885c4e6b/greenlet-3.3.1-cp311-cp311-manylinux_2_24_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:cb0feb07fe6e6a74615ee62a880007d976cf739b6669cce95daa7373d4fc69c5", size = 590401, upload-time = "2026-01-23T16:05:26.365Z" }, + { url = "https://files.pythonhosted.org/packages/70/ae/e2d5f0e59b94a2269b68a629173263fa40b63da32f5c231307c349315871/greenlet-3.3.1-cp311-cp311-manylinux_2_24_s390x.manylinux_2_28_s390x.whl", hash = "sha256:67ea3fc73c8cd92f42467a72b75e8f05ed51a0e9b1d15398c913416f2dafd49f", size = 601161, upload-time = "2026-01-23T16:15:53.456Z" }, + { url = "https://files.pythonhosted.org/packages/5c/ae/8d472e1f5ac5efe55c563f3eabb38c98a44b832602e12910750a7c025802/greenlet-3.3.1-cp311-cp311-manylinux_2_24_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:39eda9ba259cc9801da05351eaa8576e9aa83eb9411e8f0c299e05d712a210f2", size = 590272, upload-time = "2026-01-23T15:32:49.411Z" }, + { url = "https://files.pythonhosted.org/packages/a8/51/0fde34bebfcadc833550717eade64e35ec8738e6b097d5d248274a01258b/greenlet-3.3.1-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:e2e7e882f83149f0a71ac822ebf156d902e7a5d22c9045e3e0d1daf59cee2cc9", size = 1550729, upload-time = "2026-01-23T16:04:20.867Z" }, + { url = "https://files.pythonhosted.org/packages/16/c9/2fb47bee83b25b119d5a35d580807bb8b92480a54b68fef009a02945629f/greenlet-3.3.1-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:80aa4d79eb5564f2e0a6144fcc744b5a37c56c4a92d60920720e99210d88db0f", size = 1615552, upload-time = "2026-01-23T15:33:45.743Z" }, + { url = "https://files.pythonhosted.org/packages/1f/54/dcf9f737b96606f82f8dd05becfb8d238db0633dd7397d542a296fe9cad3/greenlet-3.3.1-cp311-cp311-win_amd64.whl", hash = "sha256:32e4ca9777c5addcbf42ff3915d99030d8e00173a56f80001fb3875998fe410b", size = 226462, upload-time = "2026-01-23T15:36:50.422Z" }, + { url = "https://files.pythonhosted.org/packages/91/37/61e1015cf944ddd2337447d8e97fb423ac9bc21f9963fb5f206b53d65649/greenlet-3.3.1-cp311-cp311-win_arm64.whl", hash = "sha256:da19609432f353fed186cc1b85e9440db93d489f198b4bdf42ae19cc9d9ac9b4", size = 225715, upload-time = "2026-01-23T15:33:17.298Z" }, + { url = "https://files.pythonhosted.org/packages/f9/c8/9d76a66421d1ae24340dfae7e79c313957f6e3195c144d2c73333b5bfe34/greenlet-3.3.1-cp312-cp312-macosx_11_0_universal2.whl", hash = "sha256:7e806ca53acf6d15a888405880766ec84721aa4181261cd11a457dfe9a7a4975", size = 276443, upload-time = "2026-01-23T15:30:10.066Z" }, + { url = "https://files.pythonhosted.org/packages/81/99/401ff34bb3c032d1f10477d199724f5e5f6fbfb59816ad1455c79c1eb8e7/greenlet-3.3.1-cp312-cp312-manylinux_2_24_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:d842c94b9155f1c9b3058036c24ffb8ff78b428414a19792b2380be9cecf4f36", size = 597359, upload-time = "2026-01-23T16:00:57.394Z" }, + { url = "https://files.pythonhosted.org/packages/2b/bc/4dcc0871ed557792d304f50be0f7487a14e017952ec689effe2180a6ff35/greenlet-3.3.1-cp312-cp312-manylinux_2_24_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:20fedaadd422fa02695f82093f9a98bad3dab5fcda793c658b945fcde2ab27ba", size = 607805, upload-time = "2026-01-23T16:05:28.068Z" }, + { url = "https://files.pythonhosted.org/packages/3b/cd/7a7ca57588dac3389e97f7c9521cb6641fd8b6602faf1eaa4188384757df/greenlet-3.3.1-cp312-cp312-manylinux_2_24_s390x.manylinux_2_28_s390x.whl", hash = "sha256:c620051669fd04ac6b60ebc70478210119c56e2d5d5df848baec4312e260e4ca", size = 622363, upload-time = "2026-01-23T16:15:54.754Z" }, + { url = "https://files.pythonhosted.org/packages/cf/05/821587cf19e2ce1f2b24945d890b164401e5085f9d09cbd969b0c193cd20/greenlet-3.3.1-cp312-cp312-manylinux_2_24_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:14194f5f4305800ff329cbf02c5fcc88f01886cadd29941b807668a45f0d2336", size = 609947, upload-time = "2026-01-23T15:32:51.004Z" }, + { url = "https://files.pythonhosted.org/packages/a4/52/ee8c46ed9f8babaa93a19e577f26e3d28a519feac6350ed6f25f1afee7e9/greenlet-3.3.1-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:7b2fe4150a0cf59f847a67db8c155ac36aed89080a6a639e9f16df5d6c6096f1", size = 1567487, upload-time = "2026-01-23T16:04:22.125Z" }, + { url = "https://files.pythonhosted.org/packages/8f/7c/456a74f07029597626f3a6db71b273a3632aecb9afafeeca452cfa633197/greenlet-3.3.1-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:49f4ad195d45f4a66a0eb9c1ba4832bb380570d361912fa3554746830d332149", size = 1636087, upload-time = "2026-01-23T15:33:47.486Z" }, + { url = "https://files.pythonhosted.org/packages/34/2f/5e0e41f33c69655300a5e54aeb637cf8ff57f1786a3aba374eacc0228c1d/greenlet-3.3.1-cp312-cp312-win_amd64.whl", hash = "sha256:cc98b9c4e4870fa983436afa999d4eb16b12872fab7071423d5262fa7120d57a", size = 227156, upload-time = "2026-01-23T15:34:34.808Z" }, + { url = "https://files.pythonhosted.org/packages/c8/ab/717c58343cf02c5265b531384b248787e04d8160b8afe53d9eec053d7b44/greenlet-3.3.1-cp312-cp312-win_arm64.whl", hash = "sha256:bfb2d1763d777de5ee495c85309460f6fd8146e50ec9d0ae0183dbf6f0a829d1", size = 226403, upload-time = "2026-01-23T15:31:39.372Z" }, + { url = "https://files.pythonhosted.org/packages/ec/ab/d26750f2b7242c2b90ea2ad71de70cfcd73a948a49513188a0fc0d6fc15a/greenlet-3.3.1-cp313-cp313-macosx_11_0_universal2.whl", hash = "sha256:7ab327905cabb0622adca5971e488064e35115430cec2c35a50fd36e72a315b3", size = 275205, upload-time = "2026-01-23T15:30:24.556Z" }, + { url = "https://files.pythonhosted.org/packages/10/d3/be7d19e8fad7c5a78eeefb2d896a08cd4643e1e90c605c4be3b46264998f/greenlet-3.3.1-cp313-cp313-manylinux_2_24_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:65be2f026ca6a176f88fb935ee23c18333ccea97048076aef4db1ef5bc0713ac", size = 599284, upload-time = "2026-01-23T16:00:58.584Z" }, + { url = "https://files.pythonhosted.org/packages/ae/21/fe703aaa056fdb0f17e5afd4b5c80195bbdab701208918938bd15b00d39b/greenlet-3.3.1-cp313-cp313-manylinux_2_24_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:7a3ae05b3d225b4155bda56b072ceb09d05e974bc74be6c3fc15463cf69f33fd", size = 610274, upload-time = "2026-01-23T16:05:29.312Z" }, + { url = "https://files.pythonhosted.org/packages/06/00/95df0b6a935103c0452dad2203f5be8377e551b8466a29650c4c5a5af6cc/greenlet-3.3.1-cp313-cp313-manylinux_2_24_s390x.manylinux_2_28_s390x.whl", hash = "sha256:12184c61e5d64268a160226fb4818af4df02cfead8379d7f8b99a56c3a54ff3e", size = 624375, upload-time = "2026-01-23T16:15:55.915Z" }, + { url = "https://files.pythonhosted.org/packages/cb/86/5c6ab23bb3c28c21ed6bebad006515cfe08b04613eb105ca0041fecca852/greenlet-3.3.1-cp313-cp313-manylinux_2_24_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:6423481193bbbe871313de5fd06a082f2649e7ce6e08015d2a76c1e9186ca5b3", size = 612904, upload-time = "2026-01-23T15:32:52.317Z" }, + { url = "https://files.pythonhosted.org/packages/c2/f3/7949994264e22639e40718c2daf6f6df5169bf48fb038c008a489ec53a50/greenlet-3.3.1-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:33a956fe78bbbda82bfc95e128d61129b32d66bcf0a20a1f0c08aa4839ffa951", size = 1567316, upload-time = "2026-01-23T16:04:23.316Z" }, + { url = "https://files.pythonhosted.org/packages/8d/6e/d73c94d13b6465e9f7cd6231c68abde838bb22408596c05d9059830b7872/greenlet-3.3.1-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:4b065d3284be43728dd280f6f9a13990b56470b81be20375a207cdc814a983f2", size = 1636549, upload-time = "2026-01-23T15:33:48.643Z" }, + { url = "https://files.pythonhosted.org/packages/5e/b3/c9c23a6478b3bcc91f979ce4ca50879e4d0b2bd7b9a53d8ecded719b92e2/greenlet-3.3.1-cp313-cp313-win_amd64.whl", hash = "sha256:27289986f4e5b0edec7b5a91063c109f0276abb09a7e9bdab08437525977c946", size = 227042, upload-time = "2026-01-23T15:33:58.216Z" }, + { url = "https://files.pythonhosted.org/packages/90/e7/824beda656097edee36ab15809fd063447b200cc03a7f6a24c34d520bc88/greenlet-3.3.1-cp313-cp313-win_arm64.whl", hash = "sha256:2f080e028001c5273e0b42690eaf359aeef9cb1389da0f171ea51a5dc3c7608d", size = 226294, upload-time = "2026-01-23T15:30:52.73Z" }, + { url = "https://files.pythonhosted.org/packages/ae/fb/011c7c717213182caf78084a9bea51c8590b0afda98001f69d9f853a495b/greenlet-3.3.1-cp314-cp314-macosx_11_0_universal2.whl", hash = "sha256:bd59acd8529b372775cd0fcbc5f420ae20681c5b045ce25bd453ed8455ab99b5", size = 275737, upload-time = "2026-01-23T15:32:16.889Z" }, + { url = "https://files.pythonhosted.org/packages/41/2e/a3a417d620363fdbb08a48b1dd582956a46a61bf8fd27ee8164f9dfe87c2/greenlet-3.3.1-cp314-cp314-manylinux_2_24_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:b31c05dd84ef6871dd47120386aed35323c944d86c3d91a17c4b8d23df62f15b", size = 646422, upload-time = "2026-01-23T16:01:00.354Z" }, + { url = "https://files.pythonhosted.org/packages/b4/09/c6c4a0db47defafd2d6bab8ddfe47ad19963b4e30f5bed84d75328059f8c/greenlet-3.3.1-cp314-cp314-manylinux_2_24_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:02925a0bfffc41e542c70aa14c7eda3593e4d7e274bfcccca1827e6c0875902e", size = 658219, upload-time = "2026-01-23T16:05:30.956Z" }, + { url = "https://files.pythonhosted.org/packages/e2/89/b95f2ddcc5f3c2bc09c8ee8d77be312df7f9e7175703ab780f2014a0e781/greenlet-3.3.1-cp314-cp314-manylinux_2_24_s390x.manylinux_2_28_s390x.whl", hash = "sha256:3e0f3878ca3a3ff63ab4ea478585942b53df66ddde327b59ecb191b19dbbd62d", size = 671455, upload-time = "2026-01-23T16:15:57.232Z" }, + { url = "https://files.pythonhosted.org/packages/80/38/9d42d60dffb04b45f03dbab9430898352dba277758640751dc5cc316c521/greenlet-3.3.1-cp314-cp314-manylinux_2_24_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:34a729e2e4e4ffe9ae2408d5ecaf12f944853f40ad724929b7585bca808a9d6f", size = 660237, upload-time = "2026-01-23T15:32:53.967Z" }, + { url = "https://files.pythonhosted.org/packages/96/61/373c30b7197f9e756e4c81ae90a8d55dc3598c17673f91f4d31c3c689c3f/greenlet-3.3.1-cp314-cp314-musllinux_1_2_aarch64.whl", hash = "sha256:aec9ab04e82918e623415947921dea15851b152b822661cce3f8e4393c3df683", size = 1615261, upload-time = "2026-01-23T16:04:25.066Z" }, + { url = "https://files.pythonhosted.org/packages/fd/d3/ca534310343f5945316f9451e953dcd89b36fe7a19de652a1dc5a0eeef3f/greenlet-3.3.1-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:71c767cf281a80d02b6c1bdc41c9468e1f5a494fb11bc8688c360524e273d7b1", size = 1683719, upload-time = "2026-01-23T15:33:50.61Z" }, + { url = "https://files.pythonhosted.org/packages/52/cb/c21a3fd5d2c9c8b622e7bede6d6d00e00551a5ee474ea6d831b5f567a8b4/greenlet-3.3.1-cp314-cp314-win_amd64.whl", hash = "sha256:96aff77af063b607f2489473484e39a0bbae730f2ea90c9e5606c9b73c44174a", size = 228125, upload-time = "2026-01-23T15:32:45.265Z" }, + { url = "https://files.pythonhosted.org/packages/6a/8e/8a2db6d11491837af1de64b8aff23707c6e85241be13c60ed399a72e2ef8/greenlet-3.3.1-cp314-cp314-win_arm64.whl", hash = "sha256:b066e8b50e28b503f604fa538adc764a638b38cf8e81e025011d26e8a627fa79", size = 227519, upload-time = "2026-01-23T15:31:47.284Z" }, + { url = "https://files.pythonhosted.org/packages/28/24/cbbec49bacdcc9ec652a81d3efef7b59f326697e7edf6ed775a5e08e54c2/greenlet-3.3.1-cp314-cp314t-macosx_11_0_universal2.whl", hash = "sha256:3e63252943c921b90abb035ebe9de832c436401d9c45f262d80e2d06cc659242", size = 282706, upload-time = "2026-01-23T15:33:05.525Z" }, + { url = "https://files.pythonhosted.org/packages/86/2e/4f2b9323c144c4fe8842a4e0d92121465485c3c2c5b9e9b30a52e80f523f/greenlet-3.3.1-cp314-cp314t-manylinux_2_24_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:76e39058e68eb125de10c92524573924e827927df5d3891fbc97bd55764a8774", size = 651209, upload-time = "2026-01-23T16:01:01.517Z" }, + { url = "https://files.pythonhosted.org/packages/d9/87/50ca60e515f5bb55a2fbc5f0c9b5b156de7d2fc51a0a69abc9d23914a237/greenlet-3.3.1-cp314-cp314t-manylinux_2_24_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:c9f9d5e7a9310b7a2f416dd13d2e3fd8b42d803968ea580b7c0f322ccb389b97", size = 654300, upload-time = "2026-01-23T16:05:32.199Z" }, + { url = "https://files.pythonhosted.org/packages/7c/25/c51a63f3f463171e09cb586eb64db0861eb06667ab01a7968371a24c4f3b/greenlet-3.3.1-cp314-cp314t-manylinux_2_24_s390x.manylinux_2_28_s390x.whl", hash = "sha256:4b9721549a95db96689458a1e0ae32412ca18776ed004463df3a9299c1b257ab", size = 662574, upload-time = "2026-01-23T16:15:58.364Z" }, + { url = "https://files.pythonhosted.org/packages/1d/94/74310866dfa2b73dd08659a3d18762f83985ad3281901ba0ee9a815194fb/greenlet-3.3.1-cp314-cp314t-manylinux_2_24_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:92497c78adf3ac703b57f1e3813c2d874f27f71a178f9ea5887855da413cd6d2", size = 653842, upload-time = "2026-01-23T15:32:55.671Z" }, + { url = "https://files.pythonhosted.org/packages/97/43/8bf0ffa3d498eeee4c58c212a3905dd6146c01c8dc0b0a046481ca29b18c/greenlet-3.3.1-cp314-cp314t-musllinux_1_2_aarch64.whl", hash = "sha256:ed6b402bc74d6557a705e197d47f9063733091ed6357b3de33619d8a8d93ac53", size = 1614917, upload-time = "2026-01-23T16:04:26.276Z" }, + { url = "https://files.pythonhosted.org/packages/89/90/a3be7a5f378fc6e84abe4dcfb2ba32b07786861172e502388b4c90000d1b/greenlet-3.3.1-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:59913f1e5ada20fde795ba906916aea25d442abcc0593fba7e26c92b7ad76249", size = 1676092, upload-time = "2026-01-23T15:33:52.176Z" }, + { url = "https://files.pythonhosted.org/packages/e1/2b/98c7f93e6db9977aaee07eb1e51ca63bd5f779b900d362791d3252e60558/greenlet-3.3.1-cp314-cp314t-win_amd64.whl", hash = "sha256:301860987846c24cb8964bdec0e31a96ad4a2a801b41b4ef40963c1b44f33451", size = 233181, upload-time = "2026-01-23T15:33:00.29Z" }, +] + +[[package]] +name = "h11" +version = "0.16.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/01/ee/02a2c011bdab74c6fb3c75474d40b3052059d95df7e73351460c8588d963/h11-0.16.0.tar.gz", hash = "sha256:4e35b956cf45792e4caa5885e69fba00bdbc6ffafbfa020300e549b208ee5ff1", size = 101250, upload-time = "2025-04-24T03:35:25.427Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/04/4b/29cac41a4d98d144bf5f6d33995617b185d14b22401f75ca86f384e87ff1/h11-0.16.0-py3-none-any.whl", hash = "sha256:63cf8bbe7522de3bf65932fda1d9c2772064ffb3dae62d55932da54b31cb6c86", size = 37515, upload-time = "2025-04-24T03:35:24.344Z" }, +] + +[[package]] +name = "httpcore" +version = "1.0.9" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "certifi", marker = "python_full_version >= '3.12'" }, + { name = "h11", marker = "python_full_version >= '3.12'" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/06/94/82699a10bca87a5556c9c59b5963f2d039dbd239f25bc2a63907a05a14cb/httpcore-1.0.9.tar.gz", hash = "sha256:6e34463af53fd2ab5d807f399a9b45ea31c3dfa2276f15a2c3f00afff6e176e8", size = 85484, upload-time = "2025-04-24T22:06:22.219Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/7e/f5/f66802a942d491edb555dd61e3a9961140fd64c90bce1eafd741609d334d/httpcore-1.0.9-py3-none-any.whl", hash = "sha256:2d400746a40668fc9dec9810239072b40b4484b640a8c38fd654a024c7a1bf55", size = 78784, upload-time = "2025-04-24T22:06:20.566Z" }, +] + +[[package]] +name = "httpx" +version = "0.28.1" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "anyio", marker = "python_full_version >= '3.12'" }, + { name = "certifi", marker = "python_full_version >= '3.12'" }, + { name = "httpcore", marker = "python_full_version >= '3.12'" }, + { name = "idna", marker = "python_full_version >= '3.12'" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/b1/df/48c586a5fe32a0f01324ee087459e112ebb7224f646c0b5023f5e79e9956/httpx-0.28.1.tar.gz", hash = "sha256:75e98c5f16b0f35b567856f597f06ff2270a374470a5c2392242528e3e3e42fc", size = 141406, upload-time = "2024-12-06T15:37:23.222Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/2a/39/e50c7c3a983047577ee07d2a9e53faf5a69493943ec3f6a384bdc792deb2/httpx-0.28.1-py3-none-any.whl", hash = "sha256:d909fcccc110f8c7faf814ca82a9a4d816bc5a6dbfea25d6591d6985b8ba59ad", size = 73517, upload-time = "2024-12-06T15:37:21.509Z" }, +] + +[[package]] +name = "identify" +version = "2.6.15" +source = { registry = "https://pypi.org/simple" } +resolution-markers = [ + "python_full_version < '3.10'", +] +sdist = { url = "https://files.pythonhosted.org/packages/ff/e7/685de97986c916a6d93b3876139e00eef26ad5bbbd61925d670ae8013449/identify-2.6.15.tar.gz", hash = "sha256:e4f4864b96c6557ef2a1e1c951771838f4edc9df3a72ec7118b338801b11c7bf", size = 99311, upload-time = "2025-10-02T17:43:40.631Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/0f/1c/e5fd8f973d4f375adb21565739498e2e9a1e54c858a97b9a8ccfdc81da9b/identify-2.6.15-py2.py3-none-any.whl", hash = "sha256:1181ef7608e00704db228516541eb83a88a9f94433a8c80bb9b5bd54b1d81757", size = 99183, upload-time = "2025-10-02T17:43:39.137Z" }, +] + +[[package]] +name = "identify" +version = "2.6.16" +source = { registry = "https://pypi.org/simple" } +resolution-markers = [ + "python_full_version >= '3.12'", + "python_full_version == '3.11.*'", + "python_full_version == '3.10.*'", +] +sdist = { url = "https://files.pythonhosted.org/packages/5b/8d/e8b97e6bd3fb6fb271346f7981362f1e04d6a7463abd0de79e1fda17c067/identify-2.6.16.tar.gz", hash = "sha256:846857203b5511bbe94d5a352a48ef2359532bc8f6727b5544077a0dcfb24980", size = 99360, upload-time = "2026-01-12T18:58:58.201Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/b8/58/40fbbcefeda82364720eba5cf2270f98496bdfa19ea75b4cccae79c698e6/identify-2.6.16-py2.py3-none-any.whl", hash = "sha256:391ee4d77741d994189522896270b787aed8670389bfd60f326d677d64a6dfb0", size = 99202, upload-time = "2026-01-12T18:58:56.627Z" }, +] + +[[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.10'" }, +] +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.1.0" +source = { registry = "https://pypi.org/simple" } +resolution-markers = [ + "python_full_version < '3.10'", +] +sdist = { url = "https://files.pythonhosted.org/packages/f2/97/ebf4da567aa6827c909642694d71c9fcf53e5b504f2d96afea02718862f3/iniconfig-2.1.0.tar.gz", hash = "sha256:3abbd2e30b36733fee78f9c7f7308f2d0050e88f0087fd25c2645f63c773e1c7", size = 4793, upload-time = "2025-03-19T20:09:59.721Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/2c/e1/e6716421ea10d38022b952c159d5161ca1193197fb744506875fbb87ea7b/iniconfig-2.1.0-py3-none-any.whl", hash = "sha256:9deba5723312380e77435581c6bf4935c94cbfab9b1ed33ef8d238ea168eb760", size = 6050, upload-time = "2025-03-19T20:10:01.071Z" }, +] + +[[package]] +name = "iniconfig" +version = "2.3.0" +source = { registry = "https://pypi.org/simple" } +resolution-markers = [ + "python_full_version >= '3.12'", + "python_full_version == '3.11.*'", + "python_full_version == '3.10.*'", +] +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 = "itsdangerous" +version = "2.2.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/9c/cb/8ac0172223afbccb63986cc25049b154ecfb5e85932587206f42317be31d/itsdangerous-2.2.0.tar.gz", hash = "sha256:e0050c0b7da1eea53ffaf149c0cfbb5c6e2e2b69c4bef22c81fa6eb73e5f6173", size = 54410, upload-time = "2024-04-16T21:28:15.614Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/04/96/92447566d16df59b2a776c0fb82dbc4d9e07cd95062562af01e408583fc4/itsdangerous-2.2.0-py3-none-any.whl", hash = "sha256:c6242fc49e35958c8b15141343aa660db5fc54d4f13a1db01a3f5891b98700ef", size = 16234, upload-time = "2024-04-16T21:28:14.499Z" }, +] + +[[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 = "librt" +version = "0.7.8" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/e7/24/5f3646ff414285e0f7708fa4e946b9bf538345a41d1c375c439467721a5e/librt-0.7.8.tar.gz", hash = "sha256:1a4ede613941d9c3470b0368be851df6bb78ab218635512d0370b27a277a0862", size = 148323, upload-time = "2026-01-14T12:56:16.876Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/44/13/57b06758a13550c5f09563893b004f98e9537ee6ec67b7df85c3571c8832/librt-0.7.8-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:b45306a1fc5f53c9330fbee134d8b3227fe5da2ab09813b892790400aa49352d", size = 56521, upload-time = "2026-01-14T12:54:40.066Z" }, + { url = "https://files.pythonhosted.org/packages/c2/24/bbea34d1452a10612fb45ac8356f95351ba40c2517e429602160a49d1fd0/librt-0.7.8-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:864c4b7083eeee250ed55135d2127b260d7eb4b5e953a9e5df09c852e327961b", size = 58456, upload-time = "2026-01-14T12:54:41.471Z" }, + { url = "https://files.pythonhosted.org/packages/04/72/a168808f92253ec3a810beb1eceebc465701197dbc7e865a1c9ceb3c22c7/librt-0.7.8-cp310-cp310-manylinux1_i686.manylinux_2_28_i686.manylinux_2_5_i686.whl", hash = "sha256:6938cc2de153bc927ed8d71c7d2f2ae01b4e96359126c602721340eb7ce1a92d", size = 164392, upload-time = "2026-01-14T12:54:42.843Z" }, + { url = "https://files.pythonhosted.org/packages/14/5c/4c0d406f1b02735c2e7af8ff1ff03a6577b1369b91aa934a9fa2cc42c7ce/librt-0.7.8-cp310-cp310-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:66daa6ac5de4288a5bbfbe55b4caa7bf0cd26b3269c7a476ffe8ce45f837f87d", size = 172959, upload-time = "2026-01-14T12:54:44.602Z" }, + { url = "https://files.pythonhosted.org/packages/82/5f/3e85351c523f73ad8d938989e9a58c7f59fb9c17f761b9981b43f0025ce7/librt-0.7.8-cp310-cp310-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:4864045f49dc9c974dadb942ac56a74cd0479a2aafa51ce272c490a82322ea3c", size = 186717, upload-time = "2026-01-14T12:54:45.986Z" }, + { url = "https://files.pythonhosted.org/packages/08/f8/18bfe092e402d00fe00d33aa1e01dda1bd583ca100b393b4373847eade6d/librt-0.7.8-cp310-cp310-musllinux_1_2_aarch64.whl", hash = "sha256:a36515b1328dc5b3ffce79fe204985ca8572525452eacabee2166f44bb387b2c", size = 184585, upload-time = "2026-01-14T12:54:47.139Z" }, + { url = "https://files.pythonhosted.org/packages/4e/fc/f43972ff56fd790a9fa55028a52ccea1875100edbb856b705bd393b601e3/librt-0.7.8-cp310-cp310-musllinux_1_2_i686.whl", hash = "sha256:b7e7f140c5169798f90b80d6e607ed2ba5059784968a004107c88ad61fb3641d", size = 180497, upload-time = "2026-01-14T12:54:48.946Z" }, + { url = "https://files.pythonhosted.org/packages/e1/3a/25e36030315a410d3ad0b7d0f19f5f188e88d1613d7d3fd8150523ea1093/librt-0.7.8-cp310-cp310-musllinux_1_2_x86_64.whl", hash = "sha256:ff71447cb778a4f772ddc4ce360e6ba9c95527ed84a52096bd1bbf9fee2ec7c0", size = 200052, upload-time = "2026-01-14T12:54:50.382Z" }, + { url = "https://files.pythonhosted.org/packages/fc/b8/f3a5a1931ae2a6ad92bf6893b9ef44325b88641d58723529e2c2935e8abe/librt-0.7.8-cp310-cp310-win32.whl", hash = "sha256:047164e5f68b7a8ebdf9fae91a3c2161d3192418aadd61ddd3a86a56cbe3dc85", size = 43477, upload-time = "2026-01-14T12:54:51.815Z" }, + { url = "https://files.pythonhosted.org/packages/fe/91/c4202779366bc19f871b4ad25db10fcfa1e313c7893feb942f32668e8597/librt-0.7.8-cp310-cp310-win_amd64.whl", hash = "sha256:d6f254d096d84156a46a84861183c183d30734e52383602443292644d895047c", size = 49806, upload-time = "2026-01-14T12:54:53.149Z" }, + { url = "https://files.pythonhosted.org/packages/1b/a3/87ea9c1049f2c781177496ebee29430e4631f439b8553a4969c88747d5d8/librt-0.7.8-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:ff3e9c11aa260c31493d4b3197d1e28dd07768594a4f92bec4506849d736248f", size = 56507, upload-time = "2026-01-14T12:54:54.156Z" }, + { url = "https://files.pythonhosted.org/packages/5e/4a/23bcef149f37f771ad30203d561fcfd45b02bc54947b91f7a9ac34815747/librt-0.7.8-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:ddb52499d0b3ed4aa88746aaf6f36a08314677d5c346234c3987ddc506404eac", size = 58455, upload-time = "2026-01-14T12:54:55.978Z" }, + { url = "https://files.pythonhosted.org/packages/22/6e/46eb9b85c1b9761e0f42b6e6311e1cc544843ac897457062b9d5d0b21df4/librt-0.7.8-cp311-cp311-manylinux1_i686.manylinux_2_28_i686.manylinux_2_5_i686.whl", hash = "sha256:e9c0afebbe6ce177ae8edba0c7c4d626f2a0fc12c33bb993d163817c41a7a05c", size = 164956, upload-time = "2026-01-14T12:54:57.311Z" }, + { url = "https://files.pythonhosted.org/packages/7a/3f/aa7c7f6829fb83989feb7ba9aa11c662b34b4bd4bd5b262f2876ba3db58d/librt-0.7.8-cp311-cp311-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:631599598e2c76ded400c0a8722dec09217c89ff64dc54b060f598ed68e7d2a8", size = 174364, upload-time = "2026-01-14T12:54:59.089Z" }, + { url = "https://files.pythonhosted.org/packages/3f/2d/d57d154b40b11f2cb851c4df0d4c4456bacd9b1ccc4ecb593ddec56c1a8b/librt-0.7.8-cp311-cp311-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:9c1ba843ae20db09b9d5c80475376168feb2640ce91cd9906414f23cc267a1ff", size = 188034, upload-time = "2026-01-14T12:55:00.141Z" }, + { url = "https://files.pythonhosted.org/packages/59/f9/36c4dad00925c16cd69d744b87f7001792691857d3b79187e7a673e812fb/librt-0.7.8-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:b5b007bb22ea4b255d3ee39dfd06d12534de2fcc3438567d9f48cdaf67ae1ae3", size = 186295, upload-time = "2026-01-14T12:55:01.303Z" }, + { url = "https://files.pythonhosted.org/packages/23/9b/8a9889d3df5efb67695a67785028ccd58e661c3018237b73ad081691d0cb/librt-0.7.8-cp311-cp311-musllinux_1_2_i686.whl", hash = "sha256:dbd79caaf77a3f590cbe32dc2447f718772d6eea59656a7dcb9311161b10fa75", size = 181470, upload-time = "2026-01-14T12:55:02.492Z" }, + { url = "https://files.pythonhosted.org/packages/43/64/54d6ef11afca01fef8af78c230726a9394759f2addfbf7afc5e3cc032a45/librt-0.7.8-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:87808a8d1e0bd62a01cafc41f0fd6818b5a5d0ca0d8a55326a81643cdda8f873", size = 201713, upload-time = "2026-01-14T12:55:03.919Z" }, + { url = "https://files.pythonhosted.org/packages/2d/29/73e7ed2991330b28919387656f54109139b49e19cd72902f466bd44415fd/librt-0.7.8-cp311-cp311-win32.whl", hash = "sha256:31724b93baa91512bd0a376e7cf0b59d8b631ee17923b1218a65456fa9bda2e7", size = 43803, upload-time = "2026-01-14T12:55:04.996Z" }, + { url = "https://files.pythonhosted.org/packages/3f/de/66766ff48ed02b4d78deea30392ae200bcbd99ae61ba2418b49fd50a4831/librt-0.7.8-cp311-cp311-win_amd64.whl", hash = "sha256:978e8b5f13e52cf23a9e80f3286d7546baa70bc4ef35b51d97a709d0b28e537c", size = 50080, upload-time = "2026-01-14T12:55:06.489Z" }, + { url = "https://files.pythonhosted.org/packages/6f/e3/33450438ff3a8c581d4ed7f798a70b07c3206d298cf0b87d3806e72e3ed8/librt-0.7.8-cp311-cp311-win_arm64.whl", hash = "sha256:20e3946863d872f7cabf7f77c6c9d370b8b3d74333d3a32471c50d3a86c0a232", size = 43383, upload-time = "2026-01-14T12:55:07.49Z" }, + { url = "https://files.pythonhosted.org/packages/56/04/79d8fcb43cae376c7adbab7b2b9f65e48432c9eced62ac96703bcc16e09b/librt-0.7.8-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:9b6943885b2d49c48d0cff23b16be830ba46b0152d98f62de49e735c6e655a63", size = 57472, upload-time = "2026-01-14T12:55:08.528Z" }, + { url = "https://files.pythonhosted.org/packages/b4/ba/60b96e93043d3d659da91752689023a73981336446ae82078cddf706249e/librt-0.7.8-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:46ef1f4b9b6cc364b11eea0ecc0897314447a66029ee1e55859acb3dd8757c93", size = 58986, upload-time = "2026-01-14T12:55:09.466Z" }, + { url = "https://files.pythonhosted.org/packages/7c/26/5215e4cdcc26e7be7eee21955a7e13cbf1f6d7d7311461a6014544596fac/librt-0.7.8-cp312-cp312-manylinux1_i686.manylinux_2_28_i686.manylinux_2_5_i686.whl", hash = "sha256:907ad09cfab21e3c86e8f1f87858f7049d1097f77196959c033612f532b4e592", size = 168422, upload-time = "2026-01-14T12:55:10.499Z" }, + { url = "https://files.pythonhosted.org/packages/0f/84/e8d1bc86fa0159bfc24f3d798d92cafd3897e84c7fea7fe61b3220915d76/librt-0.7.8-cp312-cp312-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:2991b6c3775383752b3ca0204842743256f3ad3deeb1d0adc227d56b78a9a850", size = 177478, upload-time = "2026-01-14T12:55:11.577Z" }, + { url = "https://files.pythonhosted.org/packages/57/11/d0268c4b94717a18aa91df1100e767b010f87b7ae444dafaa5a2d80f33a6/librt-0.7.8-cp312-cp312-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:03679b9856932b8c8f674e87aa3c55ea11c9274301f76ae8dc4d281bda55cf62", size = 192439, upload-time = "2026-01-14T12:55:12.7Z" }, + { url = "https://files.pythonhosted.org/packages/8d/56/1e8e833b95fe684f80f8894ae4d8b7d36acc9203e60478fcae599120a975/librt-0.7.8-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:3968762fec1b2ad34ce57458b6de25dbb4142713e9ca6279a0d352fa4e9f452b", size = 191483, upload-time = "2026-01-14T12:55:13.838Z" }, + { url = "https://files.pythonhosted.org/packages/17/48/f11cf28a2cb6c31f282009e2208312aa84a5ee2732859f7856ee306176d5/librt-0.7.8-cp312-cp312-musllinux_1_2_i686.whl", hash = "sha256:bb7a7807523a31f03061288cc4ffc065d684c39db7644c676b47d89553c0d714", size = 185376, upload-time = "2026-01-14T12:55:15.017Z" }, + { url = "https://files.pythonhosted.org/packages/b8/6a/d7c116c6da561b9155b184354a60a3d5cdbf08fc7f3678d09c95679d13d9/librt-0.7.8-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:ad64a14b1e56e702e19b24aae108f18ad1bf7777f3af5fcd39f87d0c5a814449", size = 206234, upload-time = "2026-01-14T12:55:16.571Z" }, + { url = "https://files.pythonhosted.org/packages/61/de/1975200bb0285fc921c5981d9978ce6ce11ae6d797df815add94a5a848a3/librt-0.7.8-cp312-cp312-win32.whl", hash = "sha256:0241a6ed65e6666236ea78203a73d800dbed896cf12ae25d026d75dc1fcd1dac", size = 44057, upload-time = "2026-01-14T12:55:18.077Z" }, + { url = "https://files.pythonhosted.org/packages/8e/cd/724f2d0b3461426730d4877754b65d39f06a41ac9d0a92d5c6840f72b9ae/librt-0.7.8-cp312-cp312-win_amd64.whl", hash = "sha256:6db5faf064b5bab9675c32a873436b31e01d66ca6984c6f7f92621656033a708", size = 50293, upload-time = "2026-01-14T12:55:19.179Z" }, + { url = "https://files.pythonhosted.org/packages/bd/cf/7e899acd9ee5727ad8160fdcc9994954e79fab371c66535c60e13b968ffc/librt-0.7.8-cp312-cp312-win_arm64.whl", hash = "sha256:57175aa93f804d2c08d2edb7213e09276bd49097611aefc37e3fa38d1fb99ad0", size = 43574, upload-time = "2026-01-14T12:55:20.185Z" }, + { url = "https://files.pythonhosted.org/packages/a1/fe/b1f9de2829cf7fc7649c1dcd202cfd873837c5cc2fc9e526b0e7f716c3d2/librt-0.7.8-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:4c3995abbbb60b3c129490fa985dfe6cac11d88fc3c36eeb4fb1449efbbb04fc", size = 57500, upload-time = "2026-01-14T12:55:21.219Z" }, + { url = "https://files.pythonhosted.org/packages/eb/d4/4a60fbe2e53b825f5d9a77325071d61cd8af8506255067bf0c8527530745/librt-0.7.8-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:44e0c2cbc9bebd074cf2cdbe472ca185e824be4e74b1c63a8e934cea674bebf2", size = 59019, upload-time = "2026-01-14T12:55:22.256Z" }, + { url = "https://files.pythonhosted.org/packages/6a/37/61ff80341ba5159afa524445f2d984c30e2821f31f7c73cf166dcafa5564/librt-0.7.8-cp313-cp313-manylinux1_i686.manylinux_2_28_i686.manylinux_2_5_i686.whl", hash = "sha256:4d2f1e492cae964b3463a03dc77a7fe8742f7855d7258c7643f0ee32b6651dd3", size = 169015, upload-time = "2026-01-14T12:55:23.24Z" }, + { url = "https://files.pythonhosted.org/packages/1c/86/13d4f2d6a93f181ebf2fc953868826653ede494559da8268023fe567fca3/librt-0.7.8-cp313-cp313-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:451e7ffcef8f785831fdb791bd69211f47e95dc4c6ddff68e589058806f044c6", size = 178161, upload-time = "2026-01-14T12:55:24.826Z" }, + { url = "https://files.pythonhosted.org/packages/88/26/e24ef01305954fc4d771f1f09f3dd682f9eb610e1bec188ffb719374d26e/librt-0.7.8-cp313-cp313-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:3469e1af9f1380e093ae06bedcbdd11e407ac0b303a56bbe9afb1d6824d4982d", size = 193015, upload-time = "2026-01-14T12:55:26.04Z" }, + { url = "https://files.pythonhosted.org/packages/88/a0/92b6bd060e720d7a31ed474d046a69bd55334ec05e9c446d228c4b806ae3/librt-0.7.8-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:f11b300027ce19a34f6d24ebb0a25fd0e24a9d53353225a5c1e6cadbf2916b2e", size = 192038, upload-time = "2026-01-14T12:55:27.208Z" }, + { url = "https://files.pythonhosted.org/packages/06/bb/6f4c650253704279c3a214dad188101d1b5ea23be0606628bc6739456624/librt-0.7.8-cp313-cp313-musllinux_1_2_i686.whl", hash = "sha256:4adc73614f0d3c97874f02f2c7fd2a27854e7e24ad532ea6b965459c5b757eca", size = 186006, upload-time = "2026-01-14T12:55:28.594Z" }, + { url = "https://files.pythonhosted.org/packages/dc/00/1c409618248d43240cadf45f3efb866837fa77e9a12a71481912135eb481/librt-0.7.8-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:60c299e555f87e4c01b2eca085dfccda1dde87f5a604bb45c2906b8305819a93", size = 206888, upload-time = "2026-01-14T12:55:30.214Z" }, + { url = "https://files.pythonhosted.org/packages/d9/83/b2cfe8e76ff5c1c77f8a53da3d5de62d04b5ebf7cf913e37f8bca43b5d07/librt-0.7.8-cp313-cp313-win32.whl", hash = "sha256:b09c52ed43a461994716082ee7d87618096851319bf695d57ec123f2ab708951", size = 44126, upload-time = "2026-01-14T12:55:31.44Z" }, + { url = "https://files.pythonhosted.org/packages/a9/0b/c59d45de56a51bd2d3a401fc63449c0ac163e4ef7f523ea8b0c0dee86ec5/librt-0.7.8-cp313-cp313-win_amd64.whl", hash = "sha256:f8f4a901a3fa28969d6e4519deceab56c55a09d691ea7b12ca830e2fa3461e34", size = 50262, upload-time = "2026-01-14T12:55:33.01Z" }, + { url = "https://files.pythonhosted.org/packages/fc/b9/973455cec0a1ec592395250c474164c4a58ebf3e0651ee920fef1a2623f1/librt-0.7.8-cp313-cp313-win_arm64.whl", hash = "sha256:43d4e71b50763fcdcf64725ac680d8cfa1706c928b844794a7aa0fa9ac8e5f09", size = 43600, upload-time = "2026-01-14T12:55:34.054Z" }, + { url = "https://files.pythonhosted.org/packages/1a/73/fa8814c6ce2d49c3827829cadaa1589b0bf4391660bd4510899393a23ebc/librt-0.7.8-cp314-cp314-macosx_10_13_x86_64.whl", hash = "sha256:be927c3c94c74b05128089a955fba86501c3b544d1d300282cc1b4bd370cb418", size = 57049, upload-time = "2026-01-14T12:55:35.056Z" }, + { url = "https://files.pythonhosted.org/packages/53/fe/f6c70956da23ea235fd2e3cc16f4f0b4ebdfd72252b02d1164dd58b4e6c3/librt-0.7.8-cp314-cp314-macosx_11_0_arm64.whl", hash = "sha256:7b0803e9008c62a7ef79058233db7ff6f37a9933b8f2573c05b07ddafa226611", size = 58689, upload-time = "2026-01-14T12:55:36.078Z" }, + { url = "https://files.pythonhosted.org/packages/1f/4d/7a2481444ac5fba63050d9abe823e6bc16896f575bfc9c1e5068d516cdce/librt-0.7.8-cp314-cp314-manylinux1_i686.manylinux_2_28_i686.manylinux_2_5_i686.whl", hash = "sha256:79feb4d00b2a4e0e05c9c56df707934f41fcb5fe53fd9efb7549068d0495b758", size = 166808, upload-time = "2026-01-14T12:55:37.595Z" }, + { url = "https://files.pythonhosted.org/packages/ac/3c/10901d9e18639f8953f57c8986796cfbf4c1c514844a41c9197cf87cb707/librt-0.7.8-cp314-cp314-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:b9122094e3f24aa759c38f46bd8863433820654927370250f460ae75488b66ea", size = 175614, upload-time = "2026-01-14T12:55:38.756Z" }, + { url = "https://files.pythonhosted.org/packages/db/01/5cbdde0951a5090a80e5ba44e6357d375048123c572a23eecfb9326993a7/librt-0.7.8-cp314-cp314-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:7e03bea66af33c95ce3addf87a9bf1fcad8d33e757bc479957ddbc0e4f7207ac", size = 189955, upload-time = "2026-01-14T12:55:39.939Z" }, + { url = "https://files.pythonhosted.org/packages/6a/b4/e80528d2f4b7eaf1d437fcbd6fc6ba4cbeb3e2a0cb9ed5a79f47c7318706/librt-0.7.8-cp314-cp314-musllinux_1_2_aarch64.whl", hash = "sha256:f1ade7f31675db00b514b98f9ab9a7698c7282dad4be7492589109471852d398", size = 189370, upload-time = "2026-01-14T12:55:41.057Z" }, + { url = "https://files.pythonhosted.org/packages/c1/ab/938368f8ce31a9787ecd4becb1e795954782e4312095daf8fd22420227c8/librt-0.7.8-cp314-cp314-musllinux_1_2_i686.whl", hash = "sha256:a14229ac62adcf1b90a15992f1ab9c69ae8b99ffb23cb64a90878a6e8a2f5b81", size = 183224, upload-time = "2026-01-14T12:55:42.328Z" }, + { url = "https://files.pythonhosted.org/packages/3c/10/559c310e7a6e4014ac44867d359ef8238465fb499e7eb31b6bfe3e3f86f5/librt-0.7.8-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:5bcaaf624fd24e6a0cb14beac37677f90793a96864c67c064a91458611446e83", size = 203541, upload-time = "2026-01-14T12:55:43.501Z" }, + { url = "https://files.pythonhosted.org/packages/f8/db/a0db7acdb6290c215f343835c6efda5b491bb05c3ddc675af558f50fdba3/librt-0.7.8-cp314-cp314-win32.whl", hash = "sha256:7aa7d5457b6c542ecaed79cec4ad98534373c9757383973e638ccced0f11f46d", size = 40657, upload-time = "2026-01-14T12:55:44.668Z" }, + { url = "https://files.pythonhosted.org/packages/72/e0/4f9bdc2a98a798511e81edcd6b54fe82767a715e05d1921115ac70717f6f/librt-0.7.8-cp314-cp314-win_amd64.whl", hash = "sha256:3d1322800771bee4a91f3b4bd4e49abc7d35e65166821086e5afd1e6c0d9be44", size = 46835, upload-time = "2026-01-14T12:55:45.655Z" }, + { url = "https://files.pythonhosted.org/packages/f9/3d/59c6402e3dec2719655a41ad027a7371f8e2334aa794ed11533ad5f34969/librt-0.7.8-cp314-cp314-win_arm64.whl", hash = "sha256:5363427bc6a8c3b1719f8f3845ea53553d301382928a86e8fab7984426949bce", size = 39885, upload-time = "2026-01-14T12:55:47.138Z" }, + { url = "https://files.pythonhosted.org/packages/4e/9c/2481d80950b83085fb14ba3c595db56330d21bbc7d88a19f20165f3538db/librt-0.7.8-cp314-cp314t-macosx_10_13_x86_64.whl", hash = "sha256:ca916919793a77e4a98d4a1701e345d337ce53be4a16620f063191f7322ac80f", size = 59161, upload-time = "2026-01-14T12:55:48.45Z" }, + { url = "https://files.pythonhosted.org/packages/96/79/108df2cfc4e672336765d54e3ff887294c1cc36ea4335c73588875775527/librt-0.7.8-cp314-cp314t-macosx_11_0_arm64.whl", hash = "sha256:54feb7b4f2f6706bb82325e836a01be805770443e2400f706e824e91f6441dde", size = 61008, upload-time = "2026-01-14T12:55:49.527Z" }, + { url = "https://files.pythonhosted.org/packages/46/f2/30179898f9994a5637459d6e169b6abdc982012c0a4b2d4c26f50c06f911/librt-0.7.8-cp314-cp314t-manylinux1_i686.manylinux_2_28_i686.manylinux_2_5_i686.whl", hash = "sha256:39a4c76fee41007070f872b648cc2f711f9abf9a13d0c7162478043377b52c8e", size = 187199, upload-time = "2026-01-14T12:55:50.587Z" }, + { url = "https://files.pythonhosted.org/packages/b4/da/f7563db55cebdc884f518ba3791ad033becc25ff68eb70902b1747dc0d70/librt-0.7.8-cp314-cp314t-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:ac9c8a458245c7de80bc1b9765b177055efff5803f08e548dd4bb9ab9a8d789b", size = 198317, upload-time = "2026-01-14T12:55:51.991Z" }, + { url = "https://files.pythonhosted.org/packages/b3/6c/4289acf076ad371471fa86718c30ae353e690d3de6167f7db36f429272f1/librt-0.7.8-cp314-cp314t-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:95b67aa7eff150f075fda09d11f6bfb26edffd300f6ab1666759547581e8f666", size = 210334, upload-time = "2026-01-14T12:55:53.682Z" }, + { url = "https://files.pythonhosted.org/packages/4a/7f/377521ac25b78ac0a5ff44127a0360ee6d5ddd3ce7327949876a30533daa/librt-0.7.8-cp314-cp314t-musllinux_1_2_aarch64.whl", hash = "sha256:535929b6eff670c593c34ff435d5440c3096f20fa72d63444608a5aef64dd581", size = 211031, upload-time = "2026-01-14T12:55:54.827Z" }, + { url = "https://files.pythonhosted.org/packages/c5/b1/e1e96c3e20b23d00cf90f4aad48f0deb4cdfec2f0ed8380d0d85acf98bbf/librt-0.7.8-cp314-cp314t-musllinux_1_2_i686.whl", hash = "sha256:63937bd0f4d1cb56653dc7ae900d6c52c41f0015e25aaf9902481ee79943b33a", size = 204581, upload-time = "2026-01-14T12:55:56.811Z" }, + { url = "https://files.pythonhosted.org/packages/43/71/0f5d010e92ed9747e14bef35e91b6580533510f1e36a8a09eb79ee70b2f0/librt-0.7.8-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:cf243da9e42d914036fd362ac3fa77d80a41cadcd11ad789b1b5eec4daaf67ca", size = 224731, upload-time = "2026-01-14T12:55:58.175Z" }, + { url = "https://files.pythonhosted.org/packages/22/f0/07fb6ab5c39a4ca9af3e37554f9d42f25c464829254d72e4ebbd81da351c/librt-0.7.8-cp314-cp314t-win32.whl", hash = "sha256:171ca3a0a06c643bd0a2f62a8944e1902c94aa8e5da4db1ea9a8daf872685365", size = 41173, upload-time = "2026-01-14T12:55:59.315Z" }, + { url = "https://files.pythonhosted.org/packages/24/d4/7e4be20993dc6a782639625bd2f97f3c66125c7aa80c82426956811cfccf/librt-0.7.8-cp314-cp314t-win_amd64.whl", hash = "sha256:445b7304145e24c60288a2f172b5ce2ca35c0f81605f5299f3fa567e189d2e32", size = 47668, upload-time = "2026-01-14T12:56:00.261Z" }, + { url = "https://files.pythonhosted.org/packages/fc/85/69f92b2a7b3c0f88ffe107c86b952b397004b5b8ea5a81da3d9c04c04422/librt-0.7.8-cp314-cp314t-win_arm64.whl", hash = "sha256:8766ece9de08527deabcd7cb1b4f1a967a385d26e33e536d6d8913db6ef74f06", size = 40550, upload-time = "2026-01-14T12:56:01.542Z" }, + { url = "https://files.pythonhosted.org/packages/3b/9b/2668bb01f568bc89ace53736df950845f8adfcacdf6da087d5cef12110cb/librt-0.7.8-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:c7e8f88f79308d86d8f39c491773cbb533d6cb7fa6476f35d711076ee04fceb6", size = 56680, upload-time = "2026-01-14T12:56:02.602Z" }, + { url = "https://files.pythonhosted.org/packages/b3/d4/dbb3edf2d0ec4ba08dcaf1865833d32737ad208962d4463c022cea6e9d3c/librt-0.7.8-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:389bd25a0db916e1d6bcb014f11aa9676cedaa485e9ec3752dfe19f196fd377b", size = 58612, upload-time = "2026-01-14T12:56:03.616Z" }, + { url = "https://files.pythonhosted.org/packages/0f/c9/64b029de4ac9901fcd47832c650a0fd050555a452bd455ce8deddddfbb9f/librt-0.7.8-cp39-cp39-manylinux1_i686.manylinux_2_28_i686.manylinux_2_5_i686.whl", hash = "sha256:73fd300f501a052f2ba52ede721232212f3b06503fa12665408ecfc9d8fd149c", size = 163654, upload-time = "2026-01-14T12:56:04.975Z" }, + { url = "https://files.pythonhosted.org/packages/81/5c/95e2abb1b48eb8f8c7fc2ae945321a6b82777947eb544cc785c3f37165b2/librt-0.7.8-cp39-cp39-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:6d772edc6a5f7835635c7562f6688e031f0b97e31d538412a852c49c9a6c92d5", size = 172477, upload-time = "2026-01-14T12:56:06.103Z" }, + { url = "https://files.pythonhosted.org/packages/7e/27/9bdf12e05b0eb089dd008d9c8aabc05748aad9d40458ade5e627c9538158/librt-0.7.8-cp39-cp39-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:bfde8a130bd0f239e45503ab39fab239ace094d63ee1d6b67c25a63d741c0f71", size = 186220, upload-time = "2026-01-14T12:56:09.958Z" }, + { url = "https://files.pythonhosted.org/packages/53/6a/c3774f4cc95e68ed444a39f2c8bd383fd18673db7d6b98cfa709f6634b93/librt-0.7.8-cp39-cp39-musllinux_1_2_aarch64.whl", hash = "sha256:fdec6e2368ae4f796fc72fad7fd4bd1753715187e6d870932b0904609e7c878e", size = 183841, upload-time = "2026-01-14T12:56:11.109Z" }, + { url = "https://files.pythonhosted.org/packages/58/6b/48702c61cf83e9c04ad5cec8cad7e5e22a2cde23a13db8ef341598897ddd/librt-0.7.8-cp39-cp39-musllinux_1_2_i686.whl", hash = "sha256:00105e7d541a8f2ee5be52caacea98a005e0478cfe78c8080fbb7b5d2b340c63", size = 179751, upload-time = "2026-01-14T12:56:12.278Z" }, + { url = "https://files.pythonhosted.org/packages/35/87/5f607fc73a131d4753f4db948833063c6aad18e18a4e6fbf64316c37ae65/librt-0.7.8-cp39-cp39-musllinux_1_2_x86_64.whl", hash = "sha256:c6f8947d3dfd7f91066c5b4385812c18be26c9d5a99ca56667547f2c39149d94", size = 199319, upload-time = "2026-01-14T12:56:13.425Z" }, + { url = "https://files.pythonhosted.org/packages/6e/cc/b7c5ac28ae0f0645a9681248bae4ede665bba15d6f761c291853c5c5b78e/librt-0.7.8-cp39-cp39-win32.whl", hash = "sha256:41d7bb1e07916aeb12ae4a44e3025db3691c4149ab788d0315781b4d29b86afb", size = 43434, upload-time = "2026-01-14T12:56:14.781Z" }, + { url = "https://files.pythonhosted.org/packages/e4/5d/dce0c92f786495adf2c1e6784d9c50a52fb7feb1cfb17af97a08281a6e82/librt-0.7.8-cp39-cp39-win_amd64.whl", hash = "sha256:e90a8e237753c83b8e484d478d9a996dc5e39fd5bd4c6ce32563bc8123f132be", size = 49801, upload-time = "2026-01-14T12:56:15.827Z" }, +] + +[[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" }, + { url = "https://files.pythonhosted.org/packages/38/2f/907b9c7bbba283e68f20259574b13d005c121a0fa4c175f9bed27c4597ff/markupsafe-3.0.3-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:e1cf1972137e83c5d4c136c43ced9ac51d0e124706ee1c8aa8532c1287fa8795", size = 11622, upload-time = "2025-09-27T18:36:41.777Z" }, + { url = "https://files.pythonhosted.org/packages/9c/d9/5f7756922cdd676869eca1c4e3c0cd0df60ed30199ffd775e319089cb3ed/markupsafe-3.0.3-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:116bb52f642a37c115f517494ea5feb03889e04df47eeff5b130b1808ce7c219", size = 12029, upload-time = "2025-09-27T18:36:43.257Z" }, + { url = "https://files.pythonhosted.org/packages/00/07/575a68c754943058c78f30db02ee03a64b3c638586fba6a6dd56830b30a3/markupsafe-3.0.3-cp313-cp313-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:133a43e73a802c5562be9bbcd03d090aa5a1fe899db609c29e8c8d815c5f6de6", size = 24374, upload-time = "2025-09-27T18:36:44.508Z" }, + { url = "https://files.pythonhosted.org/packages/a9/21/9b05698b46f218fc0e118e1f8168395c65c8a2c750ae2bab54fc4bd4e0e8/markupsafe-3.0.3-cp313-cp313-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:ccfcd093f13f0f0b7fdd0f198b90053bf7b2f02a3927a30e63f3ccc9df56b676", size = 22980, upload-time = "2025-09-27T18:36:45.385Z" }, + { url = "https://files.pythonhosted.org/packages/7f/71/544260864f893f18b6827315b988c146b559391e6e7e8f7252839b1b846a/markupsafe-3.0.3-cp313-cp313-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:509fa21c6deb7a7a273d629cf5ec029bc209d1a51178615ddf718f5918992ab9", size = 21990, upload-time = "2025-09-27T18:36:46.916Z" }, + { url = "https://files.pythonhosted.org/packages/c2/28/b50fc2f74d1ad761af2f5dcce7492648b983d00a65b8c0e0cb457c82ebbe/markupsafe-3.0.3-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:a4afe79fb3de0b7097d81da19090f4df4f8d3a2b3adaa8764138aac2e44f3af1", size = 23784, upload-time = "2025-09-27T18:36:47.884Z" }, + { url = "https://files.pythonhosted.org/packages/ed/76/104b2aa106a208da8b17a2fb72e033a5a9d7073c68f7e508b94916ed47a9/markupsafe-3.0.3-cp313-cp313-musllinux_1_2_riscv64.whl", hash = "sha256:795e7751525cae078558e679d646ae45574b47ed6e7771863fcc079a6171a0fc", size = 21588, upload-time = "2025-09-27T18:36:48.82Z" }, + { url = "https://files.pythonhosted.org/packages/b5/99/16a5eb2d140087ebd97180d95249b00a03aa87e29cc224056274f2e45fd6/markupsafe-3.0.3-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:8485f406a96febb5140bfeca44a73e3ce5116b2501ac54fe953e488fb1d03b12", size = 23041, upload-time = "2025-09-27T18:36:49.797Z" }, + { url = "https://files.pythonhosted.org/packages/19/bc/e7140ed90c5d61d77cea142eed9f9c303f4c4806f60a1044c13e3f1471d0/markupsafe-3.0.3-cp313-cp313-win32.whl", hash = "sha256:bdd37121970bfd8be76c5fb069c7751683bdf373db1ed6c010162b2a130248ed", size = 14543, upload-time = "2025-09-27T18:36:51.584Z" }, + { url = "https://files.pythonhosted.org/packages/05/73/c4abe620b841b6b791f2edc248f556900667a5a1cf023a6646967ae98335/markupsafe-3.0.3-cp313-cp313-win_amd64.whl", hash = "sha256:9a1abfdc021a164803f4d485104931fb8f8c1efd55bc6b748d2f5774e78b62c5", size = 15113, upload-time = "2025-09-27T18:36:52.537Z" }, + { url = "https://files.pythonhosted.org/packages/f0/3a/fa34a0f7cfef23cf9500d68cb7c32dd64ffd58a12b09225fb03dd37d5b80/markupsafe-3.0.3-cp313-cp313-win_arm64.whl", hash = "sha256:7e68f88e5b8799aa49c85cd116c932a1ac15caaa3f5db09087854d218359e485", size = 13911, upload-time = "2025-09-27T18:36:53.513Z" }, + { url = "https://files.pythonhosted.org/packages/e4/d7/e05cd7efe43a88a17a37b3ae96e79a19e846f3f456fe79c57ca61356ef01/markupsafe-3.0.3-cp313-cp313t-macosx_10_13_x86_64.whl", hash = "sha256:218551f6df4868a8d527e3062d0fb968682fe92054e89978594c28e642c43a73", size = 11658, upload-time = "2025-09-27T18:36:54.819Z" }, + { url = "https://files.pythonhosted.org/packages/99/9e/e412117548182ce2148bdeacdda3bb494260c0b0184360fe0d56389b523b/markupsafe-3.0.3-cp313-cp313t-macosx_11_0_arm64.whl", hash = "sha256:3524b778fe5cfb3452a09d31e7b5adefeea8c5be1d43c4f810ba09f2ceb29d37", size = 12066, upload-time = "2025-09-27T18:36:55.714Z" }, + { url = "https://files.pythonhosted.org/packages/bc/e6/fa0ffcda717ef64a5108eaa7b4f5ed28d56122c9a6d70ab8b72f9f715c80/markupsafe-3.0.3-cp313-cp313t-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:4e885a3d1efa2eadc93c894a21770e4bc67899e3543680313b09f139e149ab19", size = 25639, upload-time = "2025-09-27T18:36:56.908Z" }, + { url = "https://files.pythonhosted.org/packages/96/ec/2102e881fe9d25fc16cb4b25d5f5cde50970967ffa5dddafdb771237062d/markupsafe-3.0.3-cp313-cp313t-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:8709b08f4a89aa7586de0aadc8da56180242ee0ada3999749b183aa23df95025", size = 23569, upload-time = "2025-09-27T18:36:57.913Z" }, + { url = "https://files.pythonhosted.org/packages/4b/30/6f2fce1f1f205fc9323255b216ca8a235b15860c34b6798f810f05828e32/markupsafe-3.0.3-cp313-cp313t-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:b8512a91625c9b3da6f127803b166b629725e68af71f8184ae7e7d54686a56d6", size = 23284, upload-time = "2025-09-27T18:36:58.833Z" }, + { url = "https://files.pythonhosted.org/packages/58/47/4a0ccea4ab9f5dcb6f79c0236d954acb382202721e704223a8aafa38b5c8/markupsafe-3.0.3-cp313-cp313t-musllinux_1_2_aarch64.whl", hash = "sha256:9b79b7a16f7fedff2495d684f2b59b0457c3b493778c9eed31111be64d58279f", size = 24801, upload-time = "2025-09-27T18:36:59.739Z" }, + { url = "https://files.pythonhosted.org/packages/6a/70/3780e9b72180b6fecb83a4814d84c3bf4b4ae4bf0b19c27196104149734c/markupsafe-3.0.3-cp313-cp313t-musllinux_1_2_riscv64.whl", hash = "sha256:12c63dfb4a98206f045aa9563db46507995f7ef6d83b2f68eda65c307c6829eb", size = 22769, upload-time = "2025-09-27T18:37:00.719Z" }, + { url = "https://files.pythonhosted.org/packages/98/c5/c03c7f4125180fc215220c035beac6b9cb684bc7a067c84fc69414d315f5/markupsafe-3.0.3-cp313-cp313t-musllinux_1_2_x86_64.whl", hash = "sha256:8f71bc33915be5186016f675cd83a1e08523649b0e33efdb898db577ef5bb009", size = 23642, upload-time = "2025-09-27T18:37:01.673Z" }, + { url = "https://files.pythonhosted.org/packages/80/d6/2d1b89f6ca4bff1036499b1e29a1d02d282259f3681540e16563f27ebc23/markupsafe-3.0.3-cp313-cp313t-win32.whl", hash = "sha256:69c0b73548bc525c8cb9a251cddf1931d1db4d2258e9599c28c07ef3580ef354", size = 14612, upload-time = "2025-09-27T18:37:02.639Z" }, + { url = "https://files.pythonhosted.org/packages/2b/98/e48a4bfba0a0ffcf9925fe2d69240bfaa19c6f7507b8cd09c70684a53c1e/markupsafe-3.0.3-cp313-cp313t-win_amd64.whl", hash = "sha256:1b4b79e8ebf6b55351f0d91fe80f893b4743f104bff22e90697db1590e47a218", size = 15200, upload-time = "2025-09-27T18:37:03.582Z" }, + { url = "https://files.pythonhosted.org/packages/0e/72/e3cc540f351f316e9ed0f092757459afbc595824ca724cbc5a5d4263713f/markupsafe-3.0.3-cp313-cp313t-win_arm64.whl", hash = "sha256:ad2cf8aa28b8c020ab2fc8287b0f823d0a7d8630784c31e9ee5edea20f406287", size = 13973, upload-time = "2025-09-27T18:37:04.929Z" }, + { url = "https://files.pythonhosted.org/packages/33/8a/8e42d4838cd89b7dde187011e97fe6c3af66d8c044997d2183fbd6d31352/markupsafe-3.0.3-cp314-cp314-macosx_10_13_x86_64.whl", hash = "sha256:eaa9599de571d72e2daf60164784109f19978b327a3910d3e9de8c97b5b70cfe", size = 11619, upload-time = "2025-09-27T18:37:06.342Z" }, + { url = "https://files.pythonhosted.org/packages/b5/64/7660f8a4a8e53c924d0fa05dc3a55c9cee10bbd82b11c5afb27d44b096ce/markupsafe-3.0.3-cp314-cp314-macosx_11_0_arm64.whl", hash = "sha256:c47a551199eb8eb2121d4f0f15ae0f923d31350ab9280078d1e5f12b249e0026", size = 12029, upload-time = "2025-09-27T18:37:07.213Z" }, + { url = "https://files.pythonhosted.org/packages/da/ef/e648bfd021127bef5fa12e1720ffed0c6cbb8310c8d9bea7266337ff06de/markupsafe-3.0.3-cp314-cp314-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:f34c41761022dd093b4b6896d4810782ffbabe30f2d443ff5f083e0cbbb8c737", size = 24408, upload-time = "2025-09-27T18:37:09.572Z" }, + { url = "https://files.pythonhosted.org/packages/41/3c/a36c2450754618e62008bf7435ccb0f88053e07592e6028a34776213d877/markupsafe-3.0.3-cp314-cp314-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:457a69a9577064c05a97c41f4e65148652db078a3a509039e64d3467b9e7ef97", size = 23005, upload-time = "2025-09-27T18:37:10.58Z" }, + { url = "https://files.pythonhosted.org/packages/bc/20/b7fdf89a8456b099837cd1dc21974632a02a999ec9bf7ca3e490aacd98e7/markupsafe-3.0.3-cp314-cp314-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:e8afc3f2ccfa24215f8cb28dcf43f0113ac3c37c2f0f0806d8c70e4228c5cf4d", size = 22048, upload-time = "2025-09-27T18:37:11.547Z" }, + { url = "https://files.pythonhosted.org/packages/9a/a7/591f592afdc734f47db08a75793a55d7fbcc6902a723ae4cfbab61010cc5/markupsafe-3.0.3-cp314-cp314-musllinux_1_2_aarch64.whl", hash = "sha256:ec15a59cf5af7be74194f7ab02d0f59a62bdcf1a537677ce67a2537c9b87fcda", size = 23821, upload-time = "2025-09-27T18:37:12.48Z" }, + { url = "https://files.pythonhosted.org/packages/7d/33/45b24e4f44195b26521bc6f1a82197118f74df348556594bd2262bda1038/markupsafe-3.0.3-cp314-cp314-musllinux_1_2_riscv64.whl", hash = "sha256:0eb9ff8191e8498cca014656ae6b8d61f39da5f95b488805da4bb029cccbfbaf", size = 21606, upload-time = "2025-09-27T18:37:13.485Z" }, + { url = "https://files.pythonhosted.org/packages/ff/0e/53dfaca23a69fbfbbf17a4b64072090e70717344c52eaaaa9c5ddff1e5f0/markupsafe-3.0.3-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:2713baf880df847f2bece4230d4d094280f4e67b1e813eec43b4c0e144a34ffe", size = 23043, upload-time = "2025-09-27T18:37:14.408Z" }, + { url = "https://files.pythonhosted.org/packages/46/11/f333a06fc16236d5238bfe74daccbca41459dcd8d1fa952e8fbd5dccfb70/markupsafe-3.0.3-cp314-cp314-win32.whl", hash = "sha256:729586769a26dbceff69f7a7dbbf59ab6572b99d94576a5592625d5b411576b9", size = 14747, upload-time = "2025-09-27T18:37:15.36Z" }, + { url = "https://files.pythonhosted.org/packages/28/52/182836104b33b444e400b14f797212f720cbc9ed6ba34c800639d154e821/markupsafe-3.0.3-cp314-cp314-win_amd64.whl", hash = "sha256:bdc919ead48f234740ad807933cdf545180bfbe9342c2bb451556db2ed958581", size = 15341, upload-time = "2025-09-27T18:37:16.496Z" }, + { url = "https://files.pythonhosted.org/packages/6f/18/acf23e91bd94fd7b3031558b1f013adfa21a8e407a3fdb32745538730382/markupsafe-3.0.3-cp314-cp314-win_arm64.whl", hash = "sha256:5a7d5dc5140555cf21a6fefbdbf8723f06fcd2f63ef108f2854de715e4422cb4", size = 14073, upload-time = "2025-09-27T18:37:17.476Z" }, + { url = "https://files.pythonhosted.org/packages/3c/f0/57689aa4076e1b43b15fdfa646b04653969d50cf30c32a102762be2485da/markupsafe-3.0.3-cp314-cp314t-macosx_10_13_x86_64.whl", hash = "sha256:1353ef0c1b138e1907ae78e2f6c63ff67501122006b0f9abad68fda5f4ffc6ab", size = 11661, upload-time = "2025-09-27T18:37:18.453Z" }, + { url = "https://files.pythonhosted.org/packages/89/c3/2e67a7ca217c6912985ec766c6393b636fb0c2344443ff9d91404dc4c79f/markupsafe-3.0.3-cp314-cp314t-macosx_11_0_arm64.whl", hash = "sha256:1085e7fbddd3be5f89cc898938f42c0b3c711fdcb37d75221de2666af647c175", size = 12069, upload-time = "2025-09-27T18:37:19.332Z" }, + { url = "https://files.pythonhosted.org/packages/f0/00/be561dce4e6ca66b15276e184ce4b8aec61fe83662cce2f7d72bd3249d28/markupsafe-3.0.3-cp314-cp314t-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:1b52b4fb9df4eb9ae465f8d0c228a00624de2334f216f178a995ccdcf82c4634", size = 25670, upload-time = "2025-09-27T18:37:20.245Z" }, + { url = "https://files.pythonhosted.org/packages/50/09/c419f6f5a92e5fadde27efd190eca90f05e1261b10dbd8cbcb39cd8ea1dc/markupsafe-3.0.3-cp314-cp314t-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:fed51ac40f757d41b7c48425901843666a6677e3e8eb0abcff09e4ba6e664f50", size = 23598, upload-time = "2025-09-27T18:37:21.177Z" }, + { url = "https://files.pythonhosted.org/packages/22/44/a0681611106e0b2921b3033fc19bc53323e0b50bc70cffdd19f7d679bb66/markupsafe-3.0.3-cp314-cp314t-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:f190daf01f13c72eac4efd5c430a8de82489d9cff23c364c3ea822545032993e", size = 23261, upload-time = "2025-09-27T18:37:22.167Z" }, + { url = "https://files.pythonhosted.org/packages/5f/57/1b0b3f100259dc9fffe780cfb60d4be71375510e435efec3d116b6436d43/markupsafe-3.0.3-cp314-cp314t-musllinux_1_2_aarch64.whl", hash = "sha256:e56b7d45a839a697b5eb268c82a71bd8c7f6c94d6fd50c3d577fa39a9f1409f5", size = 24835, upload-time = "2025-09-27T18:37:23.296Z" }, + { url = "https://files.pythonhosted.org/packages/26/6a/4bf6d0c97c4920f1597cc14dd720705eca0bf7c787aebc6bb4d1bead5388/markupsafe-3.0.3-cp314-cp314t-musllinux_1_2_riscv64.whl", hash = "sha256:f3e98bb3798ead92273dc0e5fd0f31ade220f59a266ffd8a4f6065e0a3ce0523", size = 22733, upload-time = "2025-09-27T18:37:24.237Z" }, + { url = "https://files.pythonhosted.org/packages/14/c7/ca723101509b518797fedc2fdf79ba57f886b4aca8a7d31857ba3ee8281f/markupsafe-3.0.3-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:5678211cb9333a6468fb8d8be0305520aa073f50d17f089b5b4b477ea6e67fdc", size = 23672, upload-time = "2025-09-27T18:37:25.271Z" }, + { url = "https://files.pythonhosted.org/packages/fb/df/5bd7a48c256faecd1d36edc13133e51397e41b73bb77e1a69deab746ebac/markupsafe-3.0.3-cp314-cp314t-win32.whl", hash = "sha256:915c04ba3851909ce68ccc2b8e2cd691618c4dc4c4232fb7982bca3f41fd8c3d", size = 14819, upload-time = "2025-09-27T18:37:26.285Z" }, + { url = "https://files.pythonhosted.org/packages/1a/8a/0402ba61a2f16038b48b39bccca271134be00c5c9f0f623208399333c448/markupsafe-3.0.3-cp314-cp314t-win_amd64.whl", hash = "sha256:4faffd047e07c38848ce017e8725090413cd80cbc23d86e55c587bf979e579c9", size = 15426, upload-time = "2025-09-27T18:37:27.316Z" }, + { url = "https://files.pythonhosted.org/packages/70/bc/6f1c2f612465f5fa89b95bead1f44dcb607670fd42891d8fdcd5d039f4f4/markupsafe-3.0.3-cp314-cp314t-win_arm64.whl", hash = "sha256:32001d6a8fc98c8cb5c947787c5d08b0a50663d139f1305bac5885d98d9b40fa", size = 14146, upload-time = "2025-09-27T18:37:28.327Z" }, + { url = "https://files.pythonhosted.org/packages/56/23/0d8c13a44bde9154821586520840643467aee574d8ce79a17da539ee7fed/markupsafe-3.0.3-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:15d939a21d546304880945ca1ecb8a039db6b4dc49b2c5a400387cdae6a62e26", size = 11623, upload-time = "2025-09-27T18:37:29.296Z" }, + { url = "https://files.pythonhosted.org/packages/fd/23/07a2cb9a8045d5f3f0890a8c3bc0859d7a47bfd9a560b563899bec7b72ed/markupsafe-3.0.3-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:f71a396b3bf33ecaa1626c255855702aca4d3d9fea5e051b41ac59a9c1c41edc", size = 12049, upload-time = "2025-09-27T18:37:30.234Z" }, + { url = "https://files.pythonhosted.org/packages/bc/e4/6be85eb81503f8e11b61c0b6369b6e077dcf0a74adbd9ebf6b349937b4e9/markupsafe-3.0.3-cp39-cp39-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:0f4b68347f8c5eab4a13419215bdfd7f8c9b19f2b25520968adfad23eb0ce60c", size = 21923, upload-time = "2025-09-27T18:37:31.177Z" }, + { url = "https://files.pythonhosted.org/packages/6f/bc/4dc914ead3fe6ddaef035341fee0fc956949bbd27335b611829292b89ee2/markupsafe-3.0.3-cp39-cp39-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:e8fc20152abba6b83724d7ff268c249fa196d8259ff481f3b1476383f8f24e42", size = 20543, upload-time = "2025-09-27T18:37:32.168Z" }, + { url = "https://files.pythonhosted.org/packages/89/6e/5fe81fbcfba4aef4093d5f856e5c774ec2057946052d18d168219b7bd9f9/markupsafe-3.0.3-cp39-cp39-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:949b8d66bc381ee8b007cd945914c721d9aba8e27f71959d750a46f7c282b20b", size = 20585, upload-time = "2025-09-27T18:37:33.166Z" }, + { url = "https://files.pythonhosted.org/packages/f6/f6/e0e5a3d3ae9c4020f696cd055f940ef86b64fe88de26f3a0308b9d3d048c/markupsafe-3.0.3-cp39-cp39-musllinux_1_2_aarch64.whl", hash = "sha256:3537e01efc9d4dccdf77221fb1cb3b8e1a38d5428920e0657ce299b20324d758", size = 21387, upload-time = "2025-09-27T18:37:34.185Z" }, + { url = "https://files.pythonhosted.org/packages/c8/25/651753ef4dea08ea790f4fbb65146a9a44a014986996ca40102e237aa49a/markupsafe-3.0.3-cp39-cp39-musllinux_1_2_riscv64.whl", hash = "sha256:591ae9f2a647529ca990bc681daebdd52c8791ff06c2bfa05b65163e28102ef2", size = 20133, upload-time = "2025-09-27T18:37:35.138Z" }, + { url = "https://files.pythonhosted.org/packages/dc/0a/c3cf2b4fef5f0426e8a6d7fce3cb966a17817c568ce59d76b92a233fdbec/markupsafe-3.0.3-cp39-cp39-musllinux_1_2_x86_64.whl", hash = "sha256:a320721ab5a1aba0a233739394eb907f8c8da5c98c9181d1161e77a0c8e36f2d", size = 20588, upload-time = "2025-09-27T18:37:36.096Z" }, + { url = "https://files.pythonhosted.org/packages/cd/1b/a7782984844bd519ad4ffdbebbba2671ec5d0ebbeac34736c15fb86399e8/markupsafe-3.0.3-cp39-cp39-win32.whl", hash = "sha256:df2449253ef108a379b8b5d6b43f4b1a8e81a061d6537becd5582fba5f9196d7", size = 14566, upload-time = "2025-09-27T18:37:37.09Z" }, + { url = "https://files.pythonhosted.org/packages/18/1f/8d9c20e1c9440e215a44be5ab64359e207fcb4f675543f1cf9a2a7f648d0/markupsafe-3.0.3-cp39-cp39-win_amd64.whl", hash = "sha256:7c3fb7d25180895632e5d3148dbdc29ea38ccb7fd210aa27acbd1201a1902c6e", size = 15053, upload-time = "2025-09-27T18:37:38.054Z" }, + { url = "https://files.pythonhosted.org/packages/4e/d3/fe08482b5cd995033556d45041a4f4e76e7f0521112a9c9991d40d39825f/markupsafe-3.0.3-cp39-cp39-win_arm64.whl", hash = "sha256:38664109c14ffc9e7437e86b4dceb442b0096dfe3541d7864d9cbe1da4cf36c8", size = 13928, upload-time = "2025-09-27T18:37:39.037Z" }, +] + +[[package]] +name = "mypy" +version = "1.19.1" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "librt", marker = "platform_python_implementation != 'PyPy'" }, + { name = "mypy-extensions" }, + { name = "pathspec" }, + { name = "tomli", marker = "python_full_version < '3.11'" }, + { name = "typing-extensions" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/f5/db/4efed9504bc01309ab9c2da7e352cc223569f05478012b5d9ece38fd44d2/mypy-1.19.1.tar.gz", hash = "sha256:19d88bb05303fe63f71dd2c6270daca27cb9401c4ca8255fe50d1d920e0eb9ba", size = 3582404, upload-time = "2025-12-15T05:03:48.42Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/2f/63/e499890d8e39b1ff2df4c0c6ce5d371b6844ee22b8250687a99fd2f657a8/mypy-1.19.1-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:5f05aa3d375b385734388e844bc01733bd33c644ab48e9684faa54e5389775ec", size = 13101333, upload-time = "2025-12-15T05:03:03.28Z" }, + { url = "https://files.pythonhosted.org/packages/72/4b/095626fc136fba96effc4fd4a82b41d688ab92124f8c4f7564bffe5cf1b0/mypy-1.19.1-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:022ea7279374af1a5d78dfcab853fe6a536eebfda4b59deab53cd21f6cd9f00b", size = 12164102, upload-time = "2025-12-15T05:02:33.611Z" }, + { url = "https://files.pythonhosted.org/packages/0c/5b/952928dd081bf88a83a5ccd49aaecfcd18fd0d2710c7ff07b8fb6f7032b9/mypy-1.19.1-cp310-cp310-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:ee4c11e460685c3e0c64a4c5de82ae143622410950d6be863303a1c4ba0e36d6", size = 12765799, upload-time = "2025-12-15T05:03:28.44Z" }, + { url = "https://files.pythonhosted.org/packages/2a/0d/93c2e4a287f74ef11a66fb6d49c7a9f05e47b0a4399040e6719b57f500d2/mypy-1.19.1-cp310-cp310-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:de759aafbae8763283b2ee5869c7255391fbc4de3ff171f8f030b5ec48381b74", size = 13522149, upload-time = "2025-12-15T05:02:36.011Z" }, + { url = "https://files.pythonhosted.org/packages/7b/0e/33a294b56aaad2b338d203e3a1d8b453637ac36cb278b45005e0901cf148/mypy-1.19.1-cp310-cp310-musllinux_1_2_x86_64.whl", hash = "sha256:ab43590f9cd5108f41aacf9fca31841142c786827a74ab7cc8a2eacb634e09a1", size = 13810105, upload-time = "2025-12-15T05:02:40.327Z" }, + { url = "https://files.pythonhosted.org/packages/0e/fd/3e82603a0cb66b67c5e7abababce6bf1a929ddf67bf445e652684af5c5a0/mypy-1.19.1-cp310-cp310-win_amd64.whl", hash = "sha256:2899753e2f61e571b3971747e302d5f420c3fd09650e1951e99f823bc3089dac", size = 10057200, upload-time = "2025-12-15T05:02:51.012Z" }, + { url = "https://files.pythonhosted.org/packages/ef/47/6b3ebabd5474d9cdc170d1342fbf9dddc1b0ec13ec90bf9004ee6f391c31/mypy-1.19.1-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:d8dfc6ab58ca7dda47d9237349157500468e404b17213d44fc1cb77bce532288", size = 13028539, upload-time = "2025-12-15T05:03:44.129Z" }, + { url = "https://files.pythonhosted.org/packages/5c/a6/ac7c7a88a3c9c54334f53a941b765e6ec6c4ebd65d3fe8cdcfbe0d0fd7db/mypy-1.19.1-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:e3f276d8493c3c97930e354b2595a44a21348b320d859fb4a2b9f66da9ed27ab", size = 12083163, upload-time = "2025-12-15T05:03:37.679Z" }, + { url = "https://files.pythonhosted.org/packages/67/af/3afa9cf880aa4a2c803798ac24f1d11ef72a0c8079689fac5cfd815e2830/mypy-1.19.1-cp311-cp311-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:2abb24cf3f17864770d18d673c85235ba52456b36a06b6afc1e07c1fdcd3d0e6", size = 12687629, upload-time = "2025-12-15T05:02:31.526Z" }, + { url = "https://files.pythonhosted.org/packages/2d/46/20f8a7114a56484ab268b0ab372461cb3a8f7deed31ea96b83a4e4cfcfca/mypy-1.19.1-cp311-cp311-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:a009ffa5a621762d0c926a078c2d639104becab69e79538a494bcccb62cc0331", size = 13436933, upload-time = "2025-12-15T05:03:15.606Z" }, + { url = "https://files.pythonhosted.org/packages/5b/f8/33b291ea85050a21f15da910002460f1f445f8007adb29230f0adea279cb/mypy-1.19.1-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:f7cee03c9a2e2ee26ec07479f38ea9c884e301d42c6d43a19d20fb014e3ba925", size = 13661754, upload-time = "2025-12-15T05:02:26.731Z" }, + { url = "https://files.pythonhosted.org/packages/fd/a3/47cbd4e85bec4335a9cd80cf67dbc02be21b5d4c9c23ad6b95d6c5196bac/mypy-1.19.1-cp311-cp311-win_amd64.whl", hash = "sha256:4b84a7a18f41e167f7995200a1d07a4a6810e89d29859df936f1c3923d263042", size = 10055772, upload-time = "2025-12-15T05:03:26.179Z" }, + { url = "https://files.pythonhosted.org/packages/06/8a/19bfae96f6615aa8a0604915512e0289b1fad33d5909bf7244f02935d33a/mypy-1.19.1-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:a8174a03289288c1f6c46d55cef02379b478bfbc8e358e02047487cad44c6ca1", size = 13206053, upload-time = "2025-12-15T05:03:46.622Z" }, + { url = "https://files.pythonhosted.org/packages/a5/34/3e63879ab041602154ba2a9f99817bb0c85c4df19a23a1443c8986e4d565/mypy-1.19.1-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:ffcebe56eb09ff0c0885e750036a095e23793ba6c2e894e7e63f6d89ad51f22e", size = 12219134, upload-time = "2025-12-15T05:03:24.367Z" }, + { url = "https://files.pythonhosted.org/packages/89/cc/2db6f0e95366b630364e09845672dbee0cbf0bbe753a204b29a944967cd9/mypy-1.19.1-cp312-cp312-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:b64d987153888790bcdb03a6473d321820597ab8dd9243b27a92153c4fa50fd2", size = 12731616, upload-time = "2025-12-15T05:02:44.725Z" }, + { url = "https://files.pythonhosted.org/packages/00/be/dd56c1fd4807bc1eba1cf18b2a850d0de7bacb55e158755eb79f77c41f8e/mypy-1.19.1-cp312-cp312-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:c35d298c2c4bba75feb2195655dfea8124d855dfd7343bf8b8c055421eaf0cf8", size = 13620847, upload-time = "2025-12-15T05:03:39.633Z" }, + { url = "https://files.pythonhosted.org/packages/6d/42/332951aae42b79329f743bf1da088cd75d8d4d9acc18fbcbd84f26c1af4e/mypy-1.19.1-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:34c81968774648ab5ac09c29a375fdede03ba253f8f8287847bd480782f73a6a", size = 13834976, upload-time = "2025-12-15T05:03:08.786Z" }, + { url = "https://files.pythonhosted.org/packages/6f/63/e7493e5f90e1e085c562bb06e2eb32cae27c5057b9653348d38b47daaecc/mypy-1.19.1-cp312-cp312-win_amd64.whl", hash = "sha256:b10e7c2cd7870ba4ad9b2d8a6102eb5ffc1f16ca35e3de6bfa390c1113029d13", size = 10118104, upload-time = "2025-12-15T05:03:10.834Z" }, + { url = "https://files.pythonhosted.org/packages/de/9f/a6abae693f7a0c697dbb435aac52e958dc8da44e92e08ba88d2e42326176/mypy-1.19.1-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:e3157c7594ff2ef1634ee058aafc56a82db665c9438fd41b390f3bde1ab12250", size = 13201927, upload-time = "2025-12-15T05:02:29.138Z" }, + { url = "https://files.pythonhosted.org/packages/9a/a4/45c35ccf6e1c65afc23a069f50e2c66f46bd3798cbe0d680c12d12935caa/mypy-1.19.1-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:bdb12f69bcc02700c2b47e070238f42cb87f18c0bc1fc4cdb4fb2bc5fd7a3b8b", size = 12206730, upload-time = "2025-12-15T05:03:01.325Z" }, + { url = "https://files.pythonhosted.org/packages/05/bb/cdcf89678e26b187650512620eec8368fded4cfd99cfcb431e4cdfd19dec/mypy-1.19.1-cp313-cp313-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:f859fb09d9583a985be9a493d5cfc5515b56b08f7447759a0c5deaf68d80506e", size = 12724581, upload-time = "2025-12-15T05:03:20.087Z" }, + { url = "https://files.pythonhosted.org/packages/d1/32/dd260d52babf67bad8e6770f8e1102021877ce0edea106e72df5626bb0ec/mypy-1.19.1-cp313-cp313-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:c9a6538e0415310aad77cb94004ca6482330fece18036b5f360b62c45814c4ef", size = 13616252, upload-time = "2025-12-15T05:02:49.036Z" }, + { url = "https://files.pythonhosted.org/packages/71/d0/5e60a9d2e3bd48432ae2b454b7ef2b62a960ab51292b1eda2a95edd78198/mypy-1.19.1-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:da4869fc5e7f62a88f3fe0b5c919d1d9f7ea3cef92d3689de2823fd27e40aa75", size = 13840848, upload-time = "2025-12-15T05:02:55.95Z" }, + { url = "https://files.pythonhosted.org/packages/98/76/d32051fa65ecf6cc8c6610956473abdc9b4c43301107476ac03559507843/mypy-1.19.1-cp313-cp313-win_amd64.whl", hash = "sha256:016f2246209095e8eda7538944daa1d60e1e8134d98983b9fc1e92c1fc0cb8dd", size = 10135510, upload-time = "2025-12-15T05:02:58.438Z" }, + { url = "https://files.pythonhosted.org/packages/de/eb/b83e75f4c820c4247a58580ef86fcd35165028f191e7e1ba57128c52782d/mypy-1.19.1-cp314-cp314-macosx_10_15_x86_64.whl", hash = "sha256:06e6170bd5836770e8104c8fdd58e5e725cfeb309f0a6c681a811f557e97eac1", size = 13199744, upload-time = "2025-12-15T05:03:30.823Z" }, + { url = "https://files.pythonhosted.org/packages/94/28/52785ab7bfa165f87fcbb61547a93f98bb20e7f82f90f165a1f69bce7b3d/mypy-1.19.1-cp314-cp314-macosx_11_0_arm64.whl", hash = "sha256:804bd67b8054a85447c8954215a906d6eff9cabeabe493fb6334b24f4bfff718", size = 12215815, upload-time = "2025-12-15T05:02:42.323Z" }, + { url = "https://files.pythonhosted.org/packages/0a/c6/bdd60774a0dbfb05122e3e925f2e9e846c009e479dcec4821dad881f5b52/mypy-1.19.1-cp314-cp314-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:21761006a7f497cb0d4de3d8ef4ca70532256688b0523eee02baf9eec895e27b", size = 12740047, upload-time = "2025-12-15T05:03:33.168Z" }, + { url = "https://files.pythonhosted.org/packages/32/2a/66ba933fe6c76bd40d1fe916a83f04fed253152f451a877520b3c4a5e41e/mypy-1.19.1-cp314-cp314-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:28902ee51f12e0f19e1e16fbe2f8f06b6637f482c459dd393efddd0ec7f82045", size = 13601998, upload-time = "2025-12-15T05:03:13.056Z" }, + { url = "https://files.pythonhosted.org/packages/e3/da/5055c63e377c5c2418760411fd6a63ee2b96cf95397259038756c042574f/mypy-1.19.1-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:481daf36a4c443332e2ae9c137dfee878fcea781a2e3f895d54bd3002a900957", size = 13807476, upload-time = "2025-12-15T05:03:17.977Z" }, + { url = "https://files.pythonhosted.org/packages/cd/09/4ebd873390a063176f06b0dbf1f7783dd87bd120eae7727fa4ae4179b685/mypy-1.19.1-cp314-cp314-win_amd64.whl", hash = "sha256:8bb5c6f6d043655e055be9b542aa5f3bdd30e4f3589163e85f93f3640060509f", size = 10281872, upload-time = "2025-12-15T05:03:05.549Z" }, + { url = "https://files.pythonhosted.org/packages/b5/f7/88436084550ca9af5e610fa45286be04c3b63374df3e021c762fe8c4369f/mypy-1.19.1-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:7bcfc336a03a1aaa26dfce9fff3e287a3ba99872a157561cbfcebe67c13308e3", size = 13102606, upload-time = "2025-12-15T05:02:46.833Z" }, + { url = "https://files.pythonhosted.org/packages/ca/a5/43dfad311a734b48a752790571fd9e12d61893849a01bff346a54011957f/mypy-1.19.1-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:b7951a701c07ea584c4fe327834b92a30825514c868b1f69c30445093fdd9d5a", size = 12164496, upload-time = "2025-12-15T05:03:41.947Z" }, + { url = "https://files.pythonhosted.org/packages/88/f0/efbfa391395cce2f2771f937e0620cfd185ec88f2b9cd88711028a768e96/mypy-1.19.1-cp39-cp39-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:b13cfdd6c87fc3efb69ea4ec18ef79c74c3f98b4e5498ca9b85ab3b2c2329a67", size = 12772068, upload-time = "2025-12-15T05:02:53.689Z" }, + { url = "https://files.pythonhosted.org/packages/25/05/58b3ba28f5aed10479e899a12d2120d582ba9fa6288851b20bf1c32cbb4f/mypy-1.19.1-cp39-cp39-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:4f28f99c824ecebcdaa2e55d82953e38ff60ee5ec938476796636b86afa3956e", size = 13520385, upload-time = "2025-12-15T05:02:38.328Z" }, + { url = "https://files.pythonhosted.org/packages/c5/a0/c006ccaff50b31e542ae69b92fe7e2f55d99fba3a55e01067dd564325f85/mypy-1.19.1-cp39-cp39-musllinux_1_2_x86_64.whl", hash = "sha256:c608937067d2fc5a4dd1a5ce92fd9e1398691b8c5d012d66e1ddd430e9244376", size = 13796221, upload-time = "2025-12-15T05:03:22.147Z" }, + { url = "https://files.pythonhosted.org/packages/b2/ff/8bdb051cd710f01b880472241bd36b3f817a8e1c5d5540d0b761675b6de2/mypy-1.19.1-cp39-cp39-win_amd64.whl", hash = "sha256:409088884802d511ee52ca067707b90c883426bd95514e8cfda8281dc2effe24", size = 10055456, upload-time = "2025-12-15T05:03:35.169Z" }, + { url = "https://files.pythonhosted.org/packages/8d/f4/4ce9a05ce5ded1de3ec1c1d96cf9f9504a04e54ce0ed55cfa38619a32b8d/mypy-1.19.1-py3-none-any.whl", hash = "sha256:f1235f5ea01b7db5468d53ece6aaddf1ad0b88d9e7462b86ef96fe04995d7247", size = 2471239, upload-time = "2025-12-15T05:03:07.248Z" }, +] + +[[package]] +name = "mypy-extensions" +version = "1.1.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/a2/6e/371856a3fb9d31ca8dac321cda606860fa4548858c0cc45d9d1d4ca2628b/mypy_extensions-1.1.0.tar.gz", hash = "sha256:52e68efc3284861e772bbcd66823fde5ae21fd2fdb51c62a211403730b916558", size = 6343, upload-time = "2025-04-22T14:54:24.164Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/79/7b/2c79738432f5c924bef5071f933bcc9efd0473bac3b4aa584a6f7c1c8df8/mypy_extensions-1.1.0-py3-none-any.whl", hash = "sha256:1be4cccdb0f2482337c4743e60421de3a356cd97508abadd57d47403e94f5505", size = 4963, upload-time = "2025-04-22T14:54:22.983Z" }, +] + +[[package]] +name = "nodeenv" +version = "1.10.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/24/bf/d1bda4f6168e0b2e9e5958945e01910052158313224ada5ce1fb2e1113b8/nodeenv-1.10.0.tar.gz", hash = "sha256:996c191ad80897d076bdfba80a41994c2b47c68e224c542b48feba42ba00f8bb", size = 55611, upload-time = "2025-12-20T14:08:54.006Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/88/b2/d0896bdcdc8d28a7fc5717c305f1a861c26e18c05047949fb371034d98bd/nodeenv-1.10.0-py2.py3-none-any.whl", hash = "sha256:5bb13e3eed2923615535339b3c620e76779af4cb4c6a90deccc9e36b274d3827", size = 23438, upload-time = "2025-12-20T14:08:52.782Z" }, +] + +[[package]] +name = "packaging" +version = "26.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 = "pallets-sphinx-themes" +version = "2.3.0" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "packaging" }, + { name = "sphinx", version = "7.4.7", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version < '3.10'" }, + { name = "sphinx", version = "8.1.3", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version == '3.10.*'" }, + { name = "sphinx", version = "8.2.3", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version >= '3.11'" }, + { name = "sphinx-notfound-page" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/3b/08/c57dd89e45dbc976930200a2cb7826ed76f3c9791454a9fcd1cde3f17177/pallets_sphinx_themes-2.3.0.tar.gz", hash = "sha256:6293ced11a1d5d3de7268af1acd60428732b5a9e6051a47a596c6d9a083e60d9", size = 21029, upload-time = "2024-10-24T18:52:38.574Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/ff/7d/a4aa06e452e031559dcfb066e035d2615ebfa6148e93514d7c36030004c1/pallets_sphinx_themes-2.3.0-py3-none-any.whl", hash = "sha256:7ed13de3743c462c2804e2aa63d96cc9ffa82cb76d0251cea03de9bcd9f8dbec", size = 24745, upload-time = "2024-10-24T18:52:37.265Z" }, +] + +[[package]] +name = "pathspec" +version = "1.0.3" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/4c/b2/bb8e495d5262bfec41ab5cb18f522f1012933347fb5d9e62452d446baca2/pathspec-1.0.3.tar.gz", hash = "sha256:bac5cf97ae2c2876e2d25ebb15078eb04d76e4b98921ee31c6f85ade8b59444d", size = 130841, upload-time = "2026-01-09T15:46:46.009Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/32/2b/121e912bd60eebd623f873fd090de0e84f322972ab25a7f9044c056804ed/pathspec-1.0.3-py3-none-any.whl", hash = "sha256:e80767021c1cc524aa3fb14bedda9c34406591343cc42797b386ce7b9354fb6c", size = 55021, upload-time = "2026-01-09T15:46:44.652Z" }, +] + +[[package]] +name = "platformdirs" +version = "4.4.0" +source = { registry = "https://pypi.org/simple" } +resolution-markers = [ + "python_full_version < '3.10'", +] +sdist = { url = "https://files.pythonhosted.org/packages/23/e8/21db9c9987b0e728855bd57bff6984f67952bea55d6f75e055c46b5383e8/platformdirs-4.4.0.tar.gz", hash = "sha256:ca753cf4d81dc309bc67b0ea38fd15dc97bc30ce419a7f58d13eb3bf14c4febf", size = 21634, upload-time = "2025-08-26T14:32:04.268Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/40/4b/2028861e724d3bd36227adfa20d3fd24c3fc6d52032f4a93c133be5d17ce/platformdirs-4.4.0-py3-none-any.whl", hash = "sha256:abd01743f24e5287cd7a5db3752faf1a2d65353f38ec26d98e25a6db65958c85", size = 18654, upload-time = "2025-08-26T14:32:02.735Z" }, +] + +[[package]] +name = "platformdirs" +version = "4.5.1" +source = { registry = "https://pypi.org/simple" } +resolution-markers = [ + "python_full_version >= '3.12'", + "python_full_version == '3.11.*'", + "python_full_version == '3.10.*'", +] +sdist = { url = "https://files.pythonhosted.org/packages/cf/86/0248f086a84f01b37aaec0fa567b397df1a119f73c16f6c7a9aac73ea309/platformdirs-4.5.1.tar.gz", hash = "sha256:61d5cdcc6065745cdd94f0f878977f8de9437be93de97c1c12f853c9c0cdcbda", size = 21715, upload-time = "2025-12-05T13:52:58.638Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/cb/28/3bfe2fa5a7b9c46fe7e13c97bda14c895fb10fa2ebf1d0abb90e0cea7ee1/platformdirs-4.5.1-py3-none-any.whl", hash = "sha256:d03afa3963c806a9bed9d5125c8f4cb2fdaf74a55ab60e5d59b3fde758104d31", size = 18731, upload-time = "2025-12-05T13:52:56.823Z" }, +] + +[[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 = "pre-commit" +version = "4.3.0" +source = { registry = "https://pypi.org/simple" } +resolution-markers = [ + "python_full_version < '3.10'", +] +dependencies = [ + { name = "cfgv", version = "3.4.0", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version < '3.10'" }, + { name = "identify", version = "2.6.15", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version < '3.10'" }, + { name = "nodeenv", marker = "python_full_version < '3.10'" }, + { name = "pyyaml", marker = "python_full_version < '3.10'" }, + { name = "virtualenv", marker = "python_full_version < '3.10'" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/ff/29/7cf5bbc236333876e4b41f56e06857a87937ce4bf91e117a6991a2dbb02a/pre_commit-4.3.0.tar.gz", hash = "sha256:499fe450cc9d42e9d58e606262795ecb64dd05438943c62b66f6a8673da30b16", size = 193792, upload-time = "2025-08-09T18:56:14.651Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/5b/a5/987a405322d78a73b66e39e4a90e4ef156fd7141bf71df987e50717c321b/pre_commit-4.3.0-py2.py3-none-any.whl", hash = "sha256:2b0747ad7e6e967169136edffee14c16e148a778a54e4f967921aa1ebf2308d8", size = 220965, upload-time = "2025-08-09T18:56:13.192Z" }, +] + +[[package]] +name = "pre-commit" +version = "4.5.1" +source = { registry = "https://pypi.org/simple" } +resolution-markers = [ + "python_full_version >= '3.12'", + "python_full_version == '3.11.*'", + "python_full_version == '3.10.*'", +] +dependencies = [ + { name = "cfgv", version = "3.5.0", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version >= '3.10'" }, + { name = "identify", version = "2.6.16", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version >= '3.10'" }, + { name = "nodeenv", marker = "python_full_version >= '3.10'" }, + { name = "pyyaml", marker = "python_full_version >= '3.10'" }, + { name = "virtualenv", marker = "python_full_version >= '3.10'" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/40/f1/6d86a29246dfd2e9b6237f0b5823717f60cad94d47ddc26afa916d21f525/pre_commit-4.5.1.tar.gz", hash = "sha256:eb545fcff725875197837263e977ea257a402056661f09dae08e4b149b030a61", size = 198232, upload-time = "2025-12-16T21:14:33.552Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/5d/19/fd3ef348460c80af7bb4669ea7926651d1f95c23ff2df18b9d24bab4f3fa/pre_commit-4.5.1-py2.py3-none-any.whl", hash = "sha256:3b3afd891e97337708c1674210f8eba659b52a38ea5f822ff142d10786221f77", size = 226437, upload-time = "2025-12-16T21:14:32.409Z" }, +] + +[[package]] +name = "pre-commit-uv" +version = "4.1.5" +source = { registry = "https://pypi.org/simple" } +resolution-markers = [ + "python_full_version < '3.10'", +] +dependencies = [ + { name = "pre-commit", version = "4.3.0", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version < '3.10'" }, + { name = "uv", marker = "python_full_version < '3.10'" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/3d/0c/e6ab71e93d8e78ffa36a1f8b6ce12014679e2b83b401404c12bb2840078f/pre_commit_uv-4.1.5.tar.gz", hash = "sha256:3f40714152b4f4aa484703b8dbfeb9baa0aaedb17207e0012b3561da756d577d", size = 6920, upload-time = "2025-08-27T14:44:40.178Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/f7/c6/747bc58da9f0665c607890c73b349b3934381e312272f584808182655898/pre_commit_uv-4.1.5-py3-none-any.whl", hash = "sha256:f4805e45615b898c4ca6ea37bdb60a05bb7830f986c303a06a378d6b50c3aa9e", size = 5653, upload-time = "2025-08-27T14:44:39.187Z" }, +] + +[[package]] +name = "pre-commit-uv" +version = "4.2.0" +source = { registry = "https://pypi.org/simple" } +resolution-markers = [ + "python_full_version >= '3.12'", + "python_full_version == '3.11.*'", + "python_full_version == '3.10.*'", +] +dependencies = [ + { name = "pre-commit", version = "4.5.1", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version >= '3.10'" }, + { name = "uv", marker = "python_full_version >= '3.10'" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/f6/42/84372bc99a841bfdd8b182a50186471a7f5e873d8e8bcec0d0cb6dabcbb0/pre_commit_uv-4.2.0.tar.gz", hash = "sha256:c32bb1d90235507726eee2aeef2be5fdab431a6f1906e3f1addb0a4e99b369d1", size = 6912, upload-time = "2025-10-09T19:30:48.354Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/87/9f/ec8491f6b3022489a4d36ce372214c10a34f90b425aa61ff2e0a8dc5b9d5/pre_commit_uv-4.2.0-py3-none-any.whl", hash = "sha256:cc1b56641e6c62d90a4d8b4f0af6f2610f1c397ce81af024e768c0f33715cb81", size = 5650, upload-time = "2025-10-09T19:30:47.257Z" }, +] + +[[package]] +name = "pycparser" +version = "2.23" +source = { registry = "https://pypi.org/simple" } +resolution-markers = [ + "python_full_version < '3.10'", +] +sdist = { url = "https://files.pythonhosted.org/packages/fe/cf/d2d3b9f5699fb1e4615c8e32ff220203e43b248e1dfcc6736ad9057731ca/pycparser-2.23.tar.gz", hash = "sha256:78816d4f24add8f10a06d6f05b4d424ad9e96cfebf68a4ddc99c65c0720d00c2", size = 173734, upload-time = "2025-09-09T13:23:47.91Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/a0/e3/59cd50310fc9b59512193629e1984c1f95e5c8ae6e5d8c69532ccc65a7fe/pycparser-2.23-py3-none-any.whl", hash = "sha256:e5c6e8d3fbad53479cab09ac03729e0a9faf2bee3db8208a550daf5af81a5934", size = 118140, upload-time = "2025-09-09T13:23:46.651Z" }, +] + +[[package]] +name = "pycparser" +version = "3.0" +source = { registry = "https://pypi.org/simple" } +resolution-markers = [ + "python_full_version >= '3.12'", + "python_full_version == '3.11.*'", + "python_full_version == '3.10.*'", +] +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 = "pyproject-api" +version = "1.9.1" +source = { registry = "https://pypi.org/simple" } +resolution-markers = [ + "python_full_version < '3.10'", +] +dependencies = [ + { name = "packaging", marker = "python_full_version < '3.10'" }, + { name = "tomli", marker = "python_full_version < '3.10'" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/19/fd/437901c891f58a7b9096511750247535e891d2d5a5a6eefbc9386a2b41d5/pyproject_api-1.9.1.tar.gz", hash = "sha256:43c9918f49daab37e302038fc1aed54a8c7a91a9fa935d00b9a485f37e0f5335", size = 22710, upload-time = "2025-05-12T14:41:58.025Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/ef/e6/c293c06695d4a3ab0260ef124a74ebadba5f4c511ce3a4259e976902c00b/pyproject_api-1.9.1-py3-none-any.whl", hash = "sha256:7d6238d92f8962773dd75b5f0c4a6a27cce092a14b623b811dba656f3b628948", size = 13158, upload-time = "2025-05-12T14:41:56.217Z" }, +] + +[[package]] +name = "pyproject-api" +version = "1.10.0" +source = { registry = "https://pypi.org/simple" } +resolution-markers = [ + "python_full_version >= '3.12'", + "python_full_version == '3.11.*'", + "python_full_version == '3.10.*'", +] +dependencies = [ + { name = "packaging", marker = "python_full_version >= '3.10'" }, + { name = "tomli", marker = "python_full_version == '3.10.*'" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/45/7b/c0e1333b61d41c69e59e5366e727b18c4992688caf0de1be10b3e5265f6b/pyproject_api-1.10.0.tar.gz", hash = "sha256:40c6f2d82eebdc4afee61c773ed208c04c19db4c4a60d97f8d7be3ebc0bbb330", size = 22785, upload-time = "2025-10-09T19:12:27.21Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/54/cc/cecf97be298bee2b2a37dd360618c819a2a7fd95251d8e480c1f0eb88f3b/pyproject_api-1.10.0-py3-none-any.whl", hash = "sha256:8757c41a79c0f4ab71b99abed52b97ecf66bd20b04fa59da43b5840bac105a09", size = 13218, upload-time = "2025-10-09T19:12:24.428Z" }, +] + +[[package]] +name = "pyright" +version = "1.1.408" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "nodeenv" }, + { name = "typing-extensions" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/74/b2/5db700e52554b8f025faa9c3c624c59f1f6c8841ba81ab97641b54322f16/pyright-1.1.408.tar.gz", hash = "sha256:f28f2321f96852fa50b5829ea492f6adb0e6954568d1caa3f3af3a5f555eb684", size = 4400578, upload-time = "2026-01-08T08:07:38.795Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/0c/82/a2c93e32800940d9573fb28c346772a14778b84ba7524e691b324620ab89/pyright-1.1.408-py3-none-any.whl", hash = "sha256:090b32865f4fdb1e0e6cd82bf5618480d48eecd2eb2e70f960982a3d9a4c17c1", size = 6399144, upload-time = "2026-01-08T08:07:37.082Z" }, +] + +[[package]] +name = "pytest" +version = "8.4.2" +source = { registry = "https://pypi.org/simple" } +resolution-markers = [ + "python_full_version < '3.10'", +] +dependencies = [ + { name = "colorama", marker = "python_full_version < '3.10' and sys_platform == 'win32'" }, + { name = "exceptiongroup", marker = "python_full_version < '3.10'" }, + { name = "iniconfig", version = "2.1.0", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version < '3.10'" }, + { name = "packaging", marker = "python_full_version < '3.10'" }, + { name = "pluggy", marker = "python_full_version < '3.10'" }, + { name = "pygments", marker = "python_full_version < '3.10'" }, + { name = "tomli", marker = "python_full_version < '3.10'" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/a3/5c/00a0e072241553e1a7496d638deababa67c5058571567b92a7eaa258397c/pytest-8.4.2.tar.gz", hash = "sha256:86c0d0b93306b961d58d62a4db4879f27fe25513d4b969df351abdddb3c30e01", size = 1519618, upload-time = "2025-09-04T14:34:22.711Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/a8/a4/20da314d277121d6534b3a980b29035dcd51e6744bd79075a6ce8fa4eb8d/pytest-8.4.2-py3-none-any.whl", hash = "sha256:872f880de3fc3a5bdc88a11b39c9710c3497a547cfa9320bc3c5e62fbf272e79", size = 365750, upload-time = "2025-09-04T14:34:20.226Z" }, +] + +[[package]] +name = "pytest" +version = "9.0.2" +source = { registry = "https://pypi.org/simple" } +resolution-markers = [ + "python_full_version >= '3.12'", + "python_full_version == '3.11.*'", + "python_full_version == '3.10.*'", +] +dependencies = [ + { name = "colorama", marker = "python_full_version >= '3.10' and sys_platform == 'win32'" }, + { name = "exceptiongroup", marker = "python_full_version == '3.10.*'" }, + { name = "iniconfig", version = "2.3.0", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version >= '3.10'" }, + { name = "packaging", marker = "python_full_version >= '3.10'" }, + { name = "pluggy", marker = "python_full_version >= '3.10'" }, + { name = "pygments", marker = "python_full_version >= '3.10'" }, + { name = "tomli", marker = "python_full_version == '3.10.*'" }, +] +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 = "python-dotenv" +version = "1.2.1" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/f0/26/19cadc79a718c5edbec86fd4919a6b6d3f681039a2f6d66d14be94e75fb9/python_dotenv-1.2.1.tar.gz", hash = "sha256:42667e897e16ab0d66954af0e60a9caa94f0fd4ecf3aaf6d2d260eec1aa36ad6", size = 44221, upload-time = "2025-10-26T15:12:10.434Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/14/1b/a298b06749107c305e1fe0f814c6c74aea7b2f1e10989cb30f544a1b3253/python_dotenv-1.2.1-py3-none-any.whl", hash = "sha256:b81ee9561e9ca4004139c6cbba3a238c32b03e4894671e181b671e8cb8425d61", size = 21230, upload-time = "2025-10-26T15:12:09.109Z" }, +] + +[[package]] +name = "pyyaml" +version = "6.0.3" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/05/8e/961c0007c59b8dd7729d542c61a4d537767a59645b82a0b521206e1e25c2/pyyaml-6.0.3.tar.gz", hash = "sha256:d76623373421df22fb4cf8817020cbb7ef15c725b9d5e45f17e189bfc384190f", size = 130960, upload-time = "2025-09-25T21:33:16.546Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/f4/a0/39350dd17dd6d6c6507025c0e53aef67a9293a6d37d3511f23ea510d5800/pyyaml-6.0.3-cp310-cp310-macosx_10_13_x86_64.whl", hash = "sha256:214ed4befebe12df36bcc8bc2b64b396ca31be9304b8f59e25c11cf94a4c033b", size = 184227, upload-time = "2025-09-25T21:31:46.04Z" }, + { url = "https://files.pythonhosted.org/packages/05/14/52d505b5c59ce73244f59c7a50ecf47093ce4765f116cdb98286a71eeca2/pyyaml-6.0.3-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:02ea2dfa234451bbb8772601d7b8e426c2bfa197136796224e50e35a78777956", size = 174019, upload-time = "2025-09-25T21:31:47.706Z" }, + { url = "https://files.pythonhosted.org/packages/43/f7/0e6a5ae5599c838c696adb4e6330a59f463265bfa1e116cfd1fbb0abaaae/pyyaml-6.0.3-cp310-cp310-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:b30236e45cf30d2b8e7b3e85881719e98507abed1011bf463a8fa23e9c3e98a8", size = 740646, upload-time = "2025-09-25T21:31:49.21Z" }, + { url = "https://files.pythonhosted.org/packages/2f/3a/61b9db1d28f00f8fd0ae760459a5c4bf1b941baf714e207b6eb0657d2578/pyyaml-6.0.3-cp310-cp310-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:66291b10affd76d76f54fad28e22e51719ef9ba22b29e1d7d03d6777a9174198", size = 840793, upload-time = "2025-09-25T21:31:50.735Z" }, + { url = "https://files.pythonhosted.org/packages/7a/1e/7acc4f0e74c4b3d9531e24739e0ab832a5edf40e64fbae1a9c01941cabd7/pyyaml-6.0.3-cp310-cp310-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:9c7708761fccb9397fe64bbc0395abcae8c4bf7b0eac081e12b809bf47700d0b", size = 770293, upload-time = "2025-09-25T21:31:51.828Z" }, + { url = "https://files.pythonhosted.org/packages/8b/ef/abd085f06853af0cd59fa5f913d61a8eab65d7639ff2a658d18a25d6a89d/pyyaml-6.0.3-cp310-cp310-musllinux_1_2_aarch64.whl", hash = "sha256:418cf3f2111bc80e0933b2cd8cd04f286338bb88bdc7bc8e6dd775ebde60b5e0", size = 732872, upload-time = "2025-09-25T21:31:53.282Z" }, + { url = "https://files.pythonhosted.org/packages/1f/15/2bc9c8faf6450a8b3c9fc5448ed869c599c0a74ba2669772b1f3a0040180/pyyaml-6.0.3-cp310-cp310-musllinux_1_2_x86_64.whl", hash = "sha256:5e0b74767e5f8c593e8c9b5912019159ed0533c70051e9cce3e8b6aa699fcd69", size = 758828, upload-time = "2025-09-25T21:31:54.807Z" }, + { url = "https://files.pythonhosted.org/packages/a3/00/531e92e88c00f4333ce359e50c19b8d1de9fe8d581b1534e35ccfbc5f393/pyyaml-6.0.3-cp310-cp310-win32.whl", hash = "sha256:28c8d926f98f432f88adc23edf2e6d4921ac26fb084b028c733d01868d19007e", size = 142415, upload-time = "2025-09-25T21:31:55.885Z" }, + { url = "https://files.pythonhosted.org/packages/2a/fa/926c003379b19fca39dd4634818b00dec6c62d87faf628d1394e137354d4/pyyaml-6.0.3-cp310-cp310-win_amd64.whl", hash = "sha256:bdb2c67c6c1390b63c6ff89f210c8fd09d9a1217a465701eac7316313c915e4c", size = 158561, upload-time = "2025-09-25T21:31:57.406Z" }, + { url = "https://files.pythonhosted.org/packages/6d/16/a95b6757765b7b031c9374925bb718d55e0a9ba8a1b6a12d25962ea44347/pyyaml-6.0.3-cp311-cp311-macosx_10_13_x86_64.whl", hash = "sha256:44edc647873928551a01e7a563d7452ccdebee747728c1080d881d68af7b997e", size = 185826, upload-time = "2025-09-25T21:31:58.655Z" }, + { url = "https://files.pythonhosted.org/packages/16/19/13de8e4377ed53079ee996e1ab0a9c33ec2faf808a4647b7b4c0d46dd239/pyyaml-6.0.3-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:652cb6edd41e718550aad172851962662ff2681490a8a711af6a4d288dd96824", size = 175577, upload-time = "2025-09-25T21:32:00.088Z" }, + { url = "https://files.pythonhosted.org/packages/0c/62/d2eb46264d4b157dae1275b573017abec435397aa59cbcdab6fc978a8af4/pyyaml-6.0.3-cp311-cp311-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:10892704fc220243f5305762e276552a0395f7beb4dbf9b14ec8fd43b57f126c", size = 775556, upload-time = "2025-09-25T21:32:01.31Z" }, + { url = "https://files.pythonhosted.org/packages/10/cb/16c3f2cf3266edd25aaa00d6c4350381c8b012ed6f5276675b9eba8d9ff4/pyyaml-6.0.3-cp311-cp311-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:850774a7879607d3a6f50d36d04f00ee69e7fc816450e5f7e58d7f17f1ae5c00", size = 882114, upload-time = "2025-09-25T21:32:03.376Z" }, + { url = "https://files.pythonhosted.org/packages/71/60/917329f640924b18ff085ab889a11c763e0b573da888e8404ff486657602/pyyaml-6.0.3-cp311-cp311-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:b8bb0864c5a28024fac8a632c443c87c5aa6f215c0b126c449ae1a150412f31d", size = 806638, upload-time = "2025-09-25T21:32:04.553Z" }, + { url = "https://files.pythonhosted.org/packages/dd/6f/529b0f316a9fd167281a6c3826b5583e6192dba792dd55e3203d3f8e655a/pyyaml-6.0.3-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:1d37d57ad971609cf3c53ba6a7e365e40660e3be0e5175fa9f2365a379d6095a", size = 767463, upload-time = "2025-09-25T21:32:06.152Z" }, + { url = "https://files.pythonhosted.org/packages/f2/6a/b627b4e0c1dd03718543519ffb2f1deea4a1e6d42fbab8021936a4d22589/pyyaml-6.0.3-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:37503bfbfc9d2c40b344d06b2199cf0e96e97957ab1c1b546fd4f87e53e5d3e4", size = 794986, upload-time = "2025-09-25T21:32:07.367Z" }, + { url = "https://files.pythonhosted.org/packages/45/91/47a6e1c42d9ee337c4839208f30d9f09caa9f720ec7582917b264defc875/pyyaml-6.0.3-cp311-cp311-win32.whl", hash = "sha256:8098f252adfa6c80ab48096053f512f2321f0b998f98150cea9bd23d83e1467b", size = 142543, upload-time = "2025-09-25T21:32:08.95Z" }, + { url = "https://files.pythonhosted.org/packages/da/e3/ea007450a105ae919a72393cb06f122f288ef60bba2dc64b26e2646fa315/pyyaml-6.0.3-cp311-cp311-win_amd64.whl", hash = "sha256:9f3bfb4965eb874431221a3ff3fdcddc7e74e3b07799e0e84ca4a0f867d449bf", size = 158763, upload-time = "2025-09-25T21:32:09.96Z" }, + { url = "https://files.pythonhosted.org/packages/d1/33/422b98d2195232ca1826284a76852ad5a86fe23e31b009c9886b2d0fb8b2/pyyaml-6.0.3-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:7f047e29dcae44602496db43be01ad42fc6f1cc0d8cd6c83d342306c32270196", size = 182063, upload-time = "2025-09-25T21:32:11.445Z" }, + { url = "https://files.pythonhosted.org/packages/89/a0/6cf41a19a1f2f3feab0e9c0b74134aa2ce6849093d5517a0c550fe37a648/pyyaml-6.0.3-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:fc09d0aa354569bc501d4e787133afc08552722d3ab34836a80547331bb5d4a0", size = 173973, upload-time = "2025-09-25T21:32:12.492Z" }, + { url = "https://files.pythonhosted.org/packages/ed/23/7a778b6bd0b9a8039df8b1b1d80e2e2ad78aa04171592c8a5c43a56a6af4/pyyaml-6.0.3-cp312-cp312-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:9149cad251584d5fb4981be1ecde53a1ca46c891a79788c0df828d2f166bda28", size = 775116, upload-time = "2025-09-25T21:32:13.652Z" }, + { url = "https://files.pythonhosted.org/packages/65/30/d7353c338e12baef4ecc1b09e877c1970bd3382789c159b4f89d6a70dc09/pyyaml-6.0.3-cp312-cp312-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:5fdec68f91a0c6739b380c83b951e2c72ac0197ace422360e6d5a959d8d97b2c", size = 844011, upload-time = "2025-09-25T21:32:15.21Z" }, + { url = "https://files.pythonhosted.org/packages/8b/9d/b3589d3877982d4f2329302ef98a8026e7f4443c765c46cfecc8858c6b4b/pyyaml-6.0.3-cp312-cp312-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:ba1cc08a7ccde2d2ec775841541641e4548226580ab850948cbfda66a1befcdc", size = 807870, upload-time = "2025-09-25T21:32:16.431Z" }, + { url = "https://files.pythonhosted.org/packages/05/c0/b3be26a015601b822b97d9149ff8cb5ead58c66f981e04fedf4e762f4bd4/pyyaml-6.0.3-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:8dc52c23056b9ddd46818a57b78404882310fb473d63f17b07d5c40421e47f8e", size = 761089, upload-time = "2025-09-25T21:32:17.56Z" }, + { url = "https://files.pythonhosted.org/packages/be/8e/98435a21d1d4b46590d5459a22d88128103f8da4c2d4cb8f14f2a96504e1/pyyaml-6.0.3-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:41715c910c881bc081f1e8872880d3c650acf13dfa8214bad49ed4cede7c34ea", size = 790181, upload-time = "2025-09-25T21:32:18.834Z" }, + { url = "https://files.pythonhosted.org/packages/74/93/7baea19427dcfbe1e5a372d81473250b379f04b1bd3c4c5ff825e2327202/pyyaml-6.0.3-cp312-cp312-win32.whl", hash = "sha256:96b533f0e99f6579b3d4d4995707cf36df9100d67e0c8303a0c55b27b5f99bc5", size = 137658, upload-time = "2025-09-25T21:32:20.209Z" }, + { url = "https://files.pythonhosted.org/packages/86/bf/899e81e4cce32febab4fb42bb97dcdf66bc135272882d1987881a4b519e9/pyyaml-6.0.3-cp312-cp312-win_amd64.whl", hash = "sha256:5fcd34e47f6e0b794d17de1b4ff496c00986e1c83f7ab2fb8fcfe9616ff7477b", size = 154003, upload-time = "2025-09-25T21:32:21.167Z" }, + { url = "https://files.pythonhosted.org/packages/1a/08/67bd04656199bbb51dbed1439b7f27601dfb576fb864099c7ef0c3e55531/pyyaml-6.0.3-cp312-cp312-win_arm64.whl", hash = "sha256:64386e5e707d03a7e172c0701abfb7e10f0fb753ee1d773128192742712a98fd", size = 140344, upload-time = "2025-09-25T21:32:22.617Z" }, + { url = "https://files.pythonhosted.org/packages/d1/11/0fd08f8192109f7169db964b5707a2f1e8b745d4e239b784a5a1dd80d1db/pyyaml-6.0.3-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:8da9669d359f02c0b91ccc01cac4a67f16afec0dac22c2ad09f46bee0697eba8", size = 181669, upload-time = "2025-09-25T21:32:23.673Z" }, + { url = "https://files.pythonhosted.org/packages/b1/16/95309993f1d3748cd644e02e38b75d50cbc0d9561d21f390a76242ce073f/pyyaml-6.0.3-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:2283a07e2c21a2aa78d9c4442724ec1eb15f5e42a723b99cb3d822d48f5f7ad1", size = 173252, upload-time = "2025-09-25T21:32:25.149Z" }, + { url = "https://files.pythonhosted.org/packages/50/31/b20f376d3f810b9b2371e72ef5adb33879b25edb7a6d072cb7ca0c486398/pyyaml-6.0.3-cp313-cp313-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:ee2922902c45ae8ccada2c5b501ab86c36525b883eff4255313a253a3160861c", size = 767081, upload-time = "2025-09-25T21:32:26.575Z" }, + { url = "https://files.pythonhosted.org/packages/49/1e/a55ca81e949270d5d4432fbbd19dfea5321eda7c41a849d443dc92fd1ff7/pyyaml-6.0.3-cp313-cp313-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:a33284e20b78bd4a18c8c2282d549d10bc8408a2a7ff57653c0cf0b9be0afce5", size = 841159, upload-time = "2025-09-25T21:32:27.727Z" }, + { url = "https://files.pythonhosted.org/packages/74/27/e5b8f34d02d9995b80abcef563ea1f8b56d20134d8f4e5e81733b1feceb2/pyyaml-6.0.3-cp313-cp313-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:0f29edc409a6392443abf94b9cf89ce99889a1dd5376d94316ae5145dfedd5d6", size = 801626, upload-time = "2025-09-25T21:32:28.878Z" }, + { url = "https://files.pythonhosted.org/packages/f9/11/ba845c23988798f40e52ba45f34849aa8a1f2d4af4b798588010792ebad6/pyyaml-6.0.3-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:f7057c9a337546edc7973c0d3ba84ddcdf0daa14533c2065749c9075001090e6", size = 753613, upload-time = "2025-09-25T21:32:30.178Z" }, + { url = "https://files.pythonhosted.org/packages/3d/e0/7966e1a7bfc0a45bf0a7fb6b98ea03fc9b8d84fa7f2229e9659680b69ee3/pyyaml-6.0.3-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:eda16858a3cab07b80edaf74336ece1f986ba330fdb8ee0d6c0d68fe82bc96be", size = 794115, upload-time = "2025-09-25T21:32:31.353Z" }, + { url = "https://files.pythonhosted.org/packages/de/94/980b50a6531b3019e45ddeada0626d45fa85cbe22300844a7983285bed3b/pyyaml-6.0.3-cp313-cp313-win32.whl", hash = "sha256:d0eae10f8159e8fdad514efdc92d74fd8d682c933a6dd088030f3834bc8e6b26", size = 137427, upload-time = "2025-09-25T21:32:32.58Z" }, + { url = "https://files.pythonhosted.org/packages/97/c9/39d5b874e8b28845e4ec2202b5da735d0199dbe5b8fb85f91398814a9a46/pyyaml-6.0.3-cp313-cp313-win_amd64.whl", hash = "sha256:79005a0d97d5ddabfeeea4cf676af11e647e41d81c9a7722a193022accdb6b7c", size = 154090, upload-time = "2025-09-25T21:32:33.659Z" }, + { url = "https://files.pythonhosted.org/packages/73/e8/2bdf3ca2090f68bb3d75b44da7bbc71843b19c9f2b9cb9b0f4ab7a5a4329/pyyaml-6.0.3-cp313-cp313-win_arm64.whl", hash = "sha256:5498cd1645aa724a7c71c8f378eb29ebe23da2fc0d7a08071d89469bf1d2defb", size = 140246, upload-time = "2025-09-25T21:32:34.663Z" }, + { url = "https://files.pythonhosted.org/packages/9d/8c/f4bd7f6465179953d3ac9bc44ac1a8a3e6122cf8ada906b4f96c60172d43/pyyaml-6.0.3-cp314-cp314-macosx_10_13_x86_64.whl", hash = "sha256:8d1fab6bb153a416f9aeb4b8763bc0f22a5586065f86f7664fc23339fc1c1fac", size = 181814, upload-time = "2025-09-25T21:32:35.712Z" }, + { url = "https://files.pythonhosted.org/packages/bd/9c/4d95bb87eb2063d20db7b60faa3840c1b18025517ae857371c4dd55a6b3a/pyyaml-6.0.3-cp314-cp314-macosx_11_0_arm64.whl", hash = "sha256:34d5fcd24b8445fadc33f9cf348c1047101756fd760b4dacb5c3e99755703310", size = 173809, upload-time = "2025-09-25T21:32:36.789Z" }, + { url = "https://files.pythonhosted.org/packages/92/b5/47e807c2623074914e29dabd16cbbdd4bf5e9b2db9f8090fa64411fc5382/pyyaml-6.0.3-cp314-cp314-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:501a031947e3a9025ed4405a168e6ef5ae3126c59f90ce0cd6f2bfc477be31b7", size = 766454, upload-time = "2025-09-25T21:32:37.966Z" }, + { url = "https://files.pythonhosted.org/packages/02/9e/e5e9b168be58564121efb3de6859c452fccde0ab093d8438905899a3a483/pyyaml-6.0.3-cp314-cp314-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:b3bc83488de33889877a0f2543ade9f70c67d66d9ebb4ac959502e12de895788", size = 836355, upload-time = "2025-09-25T21:32:39.178Z" }, + { url = "https://files.pythonhosted.org/packages/88/f9/16491d7ed2a919954993e48aa941b200f38040928474c9e85ea9e64222c3/pyyaml-6.0.3-cp314-cp314-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:c458b6d084f9b935061bc36216e8a69a7e293a2f1e68bf956dcd9e6cbcd143f5", size = 794175, upload-time = "2025-09-25T21:32:40.865Z" }, + { url = "https://files.pythonhosted.org/packages/dd/3f/5989debef34dc6397317802b527dbbafb2b4760878a53d4166579111411e/pyyaml-6.0.3-cp314-cp314-musllinux_1_2_aarch64.whl", hash = "sha256:7c6610def4f163542a622a73fb39f534f8c101d690126992300bf3207eab9764", size = 755228, upload-time = "2025-09-25T21:32:42.084Z" }, + { url = "https://files.pythonhosted.org/packages/d7/ce/af88a49043cd2e265be63d083fc75b27b6ed062f5f9fd6cdc223ad62f03e/pyyaml-6.0.3-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:5190d403f121660ce8d1d2c1bb2ef1bd05b5f68533fc5c2ea899bd15f4399b35", size = 789194, upload-time = "2025-09-25T21:32:43.362Z" }, + { url = "https://files.pythonhosted.org/packages/23/20/bb6982b26a40bb43951265ba29d4c246ef0ff59c9fdcdf0ed04e0687de4d/pyyaml-6.0.3-cp314-cp314-win_amd64.whl", hash = "sha256:4a2e8cebe2ff6ab7d1050ecd59c25d4c8bd7e6f400f5f82b96557ac0abafd0ac", size = 156429, upload-time = "2025-09-25T21:32:57.844Z" }, + { url = "https://files.pythonhosted.org/packages/f4/f4/a4541072bb9422c8a883ab55255f918fa378ecf083f5b85e87fc2b4eda1b/pyyaml-6.0.3-cp314-cp314-win_arm64.whl", hash = "sha256:93dda82c9c22deb0a405ea4dc5f2d0cda384168e466364dec6255b293923b2f3", size = 143912, upload-time = "2025-09-25T21:32:59.247Z" }, + { url = "https://files.pythonhosted.org/packages/7c/f9/07dd09ae774e4616edf6cda684ee78f97777bdd15847253637a6f052a62f/pyyaml-6.0.3-cp314-cp314t-macosx_10_13_x86_64.whl", hash = "sha256:02893d100e99e03eda1c8fd5c441d8c60103fd175728e23e431db1b589cf5ab3", size = 189108, upload-time = "2025-09-25T21:32:44.377Z" }, + { url = "https://files.pythonhosted.org/packages/4e/78/8d08c9fb7ce09ad8c38ad533c1191cf27f7ae1effe5bb9400a46d9437fcf/pyyaml-6.0.3-cp314-cp314t-macosx_11_0_arm64.whl", hash = "sha256:c1ff362665ae507275af2853520967820d9124984e0f7466736aea23d8611fba", size = 183641, upload-time = "2025-09-25T21:32:45.407Z" }, + { url = "https://files.pythonhosted.org/packages/7b/5b/3babb19104a46945cf816d047db2788bcaf8c94527a805610b0289a01c6b/pyyaml-6.0.3-cp314-cp314t-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:6adc77889b628398debc7b65c073bcb99c4a0237b248cacaf3fe8a557563ef6c", size = 831901, upload-time = "2025-09-25T21:32:48.83Z" }, + { url = "https://files.pythonhosted.org/packages/8b/cc/dff0684d8dc44da4d22a13f35f073d558c268780ce3c6ba1b87055bb0b87/pyyaml-6.0.3-cp314-cp314t-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:a80cb027f6b349846a3bf6d73b5e95e782175e52f22108cfa17876aaeff93702", size = 861132, upload-time = "2025-09-25T21:32:50.149Z" }, + { url = "https://files.pythonhosted.org/packages/b1/5e/f77dc6b9036943e285ba76b49e118d9ea929885becb0a29ba8a7c75e29fe/pyyaml-6.0.3-cp314-cp314t-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:00c4bdeba853cc34e7dd471f16b4114f4162dc03e6b7afcc2128711f0eca823c", size = 839261, upload-time = "2025-09-25T21:32:51.808Z" }, + { url = "https://files.pythonhosted.org/packages/ce/88/a9db1376aa2a228197c58b37302f284b5617f56a5d959fd1763fb1675ce6/pyyaml-6.0.3-cp314-cp314t-musllinux_1_2_aarch64.whl", hash = "sha256:66e1674c3ef6f541c35191caae2d429b967b99e02040f5ba928632d9a7f0f065", size = 805272, upload-time = "2025-09-25T21:32:52.941Z" }, + { url = "https://files.pythonhosted.org/packages/da/92/1446574745d74df0c92e6aa4a7b0b3130706a4142b2d1a5869f2eaa423c6/pyyaml-6.0.3-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:16249ee61e95f858e83976573de0f5b2893b3677ba71c9dd36b9cf8be9ac6d65", size = 829923, upload-time = "2025-09-25T21:32:54.537Z" }, + { url = "https://files.pythonhosted.org/packages/f0/7a/1c7270340330e575b92f397352af856a8c06f230aa3e76f86b39d01b416a/pyyaml-6.0.3-cp314-cp314t-win_amd64.whl", hash = "sha256:4ad1906908f2f5ae4e5a8ddfce73c320c2a1429ec52eafd27138b7f1cbe341c9", size = 174062, upload-time = "2025-09-25T21:32:55.767Z" }, + { url = "https://files.pythonhosted.org/packages/f1/12/de94a39c2ef588c7e6455cfbe7343d3b2dc9d6b6b2f40c4c6565744c873d/pyyaml-6.0.3-cp314-cp314t-win_arm64.whl", hash = "sha256:ebc55a14a21cb14062aa4162f906cd962b28e2e9ea38f9b4391244cd8de4ae0b", size = 149341, upload-time = "2025-09-25T21:32:56.828Z" }, + { url = "https://files.pythonhosted.org/packages/9f/62/67fc8e68a75f738c9200422bf65693fb79a4cd0dc5b23310e5202e978090/pyyaml-6.0.3-cp39-cp39-macosx_10_13_x86_64.whl", hash = "sha256:b865addae83924361678b652338317d1bd7e79b1f4596f96b96c77a5a34b34da", size = 184450, upload-time = "2025-09-25T21:33:00.618Z" }, + { url = "https://files.pythonhosted.org/packages/ae/92/861f152ce87c452b11b9d0977952259aa7df792d71c1053365cc7b09cc08/pyyaml-6.0.3-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:c3355370a2c156cffb25e876646f149d5d68f5e0a3ce86a5084dd0b64a994917", size = 174319, upload-time = "2025-09-25T21:33:02.086Z" }, + { url = "https://files.pythonhosted.org/packages/d0/cd/f0cfc8c74f8a030017a2b9c771b7f47e5dd702c3e28e5b2071374bda2948/pyyaml-6.0.3-cp39-cp39-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:3c5677e12444c15717b902a5798264fa7909e41153cdf9ef7ad571b704a63dd9", size = 737631, upload-time = "2025-09-25T21:33:03.25Z" }, + { url = "https://files.pythonhosted.org/packages/ef/b2/18f2bd28cd2055a79a46c9b0895c0b3d987ce40ee471cecf58a1a0199805/pyyaml-6.0.3-cp39-cp39-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:5ed875a24292240029e4483f9d4a4b8a1ae08843b9c54f43fcc11e404532a8a5", size = 836795, upload-time = "2025-09-25T21:33:05.014Z" }, + { url = "https://files.pythonhosted.org/packages/73/b9/793686b2d54b531203c160ef12bec60228a0109c79bae6c1277961026770/pyyaml-6.0.3-cp39-cp39-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:0150219816b6a1fa26fb4699fb7daa9caf09eb1999f3b70fb6e786805e80375a", size = 750767, upload-time = "2025-09-25T21:33:06.398Z" }, + { url = "https://files.pythonhosted.org/packages/a9/86/a137b39a611def2ed78b0e66ce2fe13ee701a07c07aebe55c340ed2a050e/pyyaml-6.0.3-cp39-cp39-musllinux_1_2_aarch64.whl", hash = "sha256:fa160448684b4e94d80416c0fa4aac48967a969efe22931448d853ada8baf926", size = 727982, upload-time = "2025-09-25T21:33:08.708Z" }, + { url = "https://files.pythonhosted.org/packages/dd/62/71c27c94f457cf4418ef8ccc71735324c549f7e3ea9d34aba50874563561/pyyaml-6.0.3-cp39-cp39-musllinux_1_2_x86_64.whl", hash = "sha256:27c0abcb4a5dac13684a37f76e701e054692a9b2d3064b70f5e4eb54810553d7", size = 755677, upload-time = "2025-09-25T21:33:09.876Z" }, + { url = "https://files.pythonhosted.org/packages/29/3d/6f5e0d58bd924fb0d06c3a6bad00effbdae2de5adb5cda5648006ffbd8d3/pyyaml-6.0.3-cp39-cp39-win32.whl", hash = "sha256:1ebe39cb5fc479422b83de611d14e2c0d3bb2a18bbcb01f229ab3cfbd8fee7a0", size = 142592, upload-time = "2025-09-25T21:33:10.983Z" }, + { url = "https://files.pythonhosted.org/packages/f0/0c/25113e0b5e103d7f1490c0e947e303fe4a696c10b501dea7a9f49d4e876c/pyyaml-6.0.3-cp39-cp39-win_amd64.whl", hash = "sha256:2e71d11abed7344e42a8849600193d15b6def118602c4c176f748e4583246007", size = 158777, upload-time = "2025-09-25T21:33:15.55Z" }, +] + +[[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 = "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 = "roman-numerals-py" +version = "4.1.0" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "roman-numerals", marker = "python_full_version >= '3.11'" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/cb/b5/de96fca640f4f656eb79bbee0e79aeec52e3e0e359f8a3e6a0d366378b64/roman_numerals_py-4.1.0.tar.gz", hash = "sha256:f5d7b2b4ca52dd855ef7ab8eb3590f428c0b1ea480736ce32b01fef2a5f8daf9", size = 4274, upload-time = "2025-12-17T18:25:41.153Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/27/2c/daca29684cbe9fd4bc711f8246da3c10adca1ccc4d24436b17572eb2590e/roman_numerals_py-4.1.0-py3-none-any.whl", hash = "sha256:553114c1167141c1283a51743759723ecd05604a1b6b507225e91dc1a6df0780", size = 4547, upload-time = "2025-12-17T18:25:40.136Z" }, +] + +[[package]] +name = "ruff" +version = "0.14.14" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/2e/06/f71e3a86b2df0dfa2d2f72195941cd09b44f87711cb7fa5193732cb9a5fc/ruff-0.14.14.tar.gz", hash = "sha256:2d0f819c9a90205f3a867dbbd0be083bee9912e170fd7d9704cc8ae45824896b", size = 4515732, upload-time = "2026-01-22T22:30:17.527Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/d2/89/20a12e97bc6b9f9f68343952da08a8099c57237aef953a56b82711d55edd/ruff-0.14.14-py3-none-linux_armv6l.whl", hash = "sha256:7cfe36b56e8489dee8fbc777c61959f60ec0f1f11817e8f2415f429552846aed", size = 10467650, upload-time = "2026-01-22T22:30:08.578Z" }, + { url = "https://files.pythonhosted.org/packages/a3/b1/c5de3fd2d5a831fcae21beda5e3589c0ba67eec8202e992388e4b17a6040/ruff-0.14.14-py3-none-macosx_10_12_x86_64.whl", hash = "sha256:6006a0082336e7920b9573ef8a7f52eec837add1265cc74e04ea8a4368cd704c", size = 10883245, upload-time = "2026-01-22T22:30:04.155Z" }, + { url = "https://files.pythonhosted.org/packages/b8/7c/3c1db59a10e7490f8f6f8559d1db8636cbb13dccebf18686f4e3c9d7c772/ruff-0.14.14-py3-none-macosx_11_0_arm64.whl", hash = "sha256:026c1d25996818f0bf498636686199d9bd0d9d6341c9c2c3b62e2a0198b758de", size = 10231273, upload-time = "2026-01-22T22:30:34.642Z" }, + { url = "https://files.pythonhosted.org/packages/a1/6e/5e0e0d9674be0f8581d1f5e0f0a04761203affce3232c1a1189d0e3b4dad/ruff-0.14.14-py3-none-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:f666445819d31210b71e0a6d1c01e24447a20b85458eea25a25fe8142210ae0e", size = 10585753, upload-time = "2026-01-22T22:30:31.781Z" }, + { url = "https://files.pythonhosted.org/packages/23/09/754ab09f46ff1884d422dc26d59ba18b4e5d355be147721bb2518aa2a014/ruff-0.14.14-py3-none-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:3c0f18b922c6d2ff9a5e6c3ee16259adc513ca775bcf82c67ebab7cbd9da5bc8", size = 10286052, upload-time = "2026-01-22T22:30:24.827Z" }, + { url = "https://files.pythonhosted.org/packages/c8/cc/e71f88dd2a12afb5f50733851729d6b571a7c3a35bfdb16c3035132675a0/ruff-0.14.14-py3-none-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:1629e67489c2dea43e8658c3dba659edbfd87361624b4040d1df04c9740ae906", size = 11043637, upload-time = "2026-01-22T22:30:13.239Z" }, + { url = "https://files.pythonhosted.org/packages/67/b2/397245026352494497dac935d7f00f1468c03a23a0c5db6ad8fc49ca3fb2/ruff-0.14.14-py3-none-manylinux_2_17_ppc64.manylinux2014_ppc64.whl", hash = "sha256:27493a2131ea0f899057d49d303e4292b2cae2bb57253c1ed1f256fbcd1da480", size = 12194761, upload-time = "2026-01-22T22:30:22.542Z" }, + { url = "https://files.pythonhosted.org/packages/5b/06/06ef271459f778323112c51b7587ce85230785cd64e91772034ddb88f200/ruff-0.14.14-py3-none-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:01ff589aab3f5b539e35db38425da31a57521efd1e4ad1ae08fc34dbe30bd7df", size = 12005701, upload-time = "2026-01-22T22:30:20.499Z" }, + { url = "https://files.pythonhosted.org/packages/41/d6/99364514541cf811ccc5ac44362f88df66373e9fec1b9d1c4cc830593fe7/ruff-0.14.14-py3-none-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:1cc12d74eef0f29f51775f5b755913eb523546b88e2d733e1d701fe65144e89b", size = 11282455, upload-time = "2026-01-22T22:29:59.679Z" }, + { url = "https://files.pythonhosted.org/packages/ca/71/37daa46f89475f8582b7762ecd2722492df26421714a33e72ccc9a84d7a5/ruff-0.14.14-py3-none-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:bb8481604b7a9e75eff53772496201690ce2687067e038b3cc31aaf16aa0b974", size = 11215882, upload-time = "2026-01-22T22:29:57.032Z" }, + { url = "https://files.pythonhosted.org/packages/2c/10/a31f86169ec91c0705e618443ee74ede0bdd94da0a57b28e72db68b2dbac/ruff-0.14.14-py3-none-manylinux_2_31_riscv64.whl", hash = "sha256:14649acb1cf7b5d2d283ebd2f58d56b75836ed8c6f329664fa91cdea19e76e66", size = 11180549, upload-time = "2026-01-22T22:30:27.175Z" }, + { url = "https://files.pythonhosted.org/packages/fd/1e/c723f20536b5163adf79bdd10c5f093414293cdf567eed9bdb7b83940f3f/ruff-0.14.14-py3-none-musllinux_1_2_aarch64.whl", hash = "sha256:e8058d2145566510790eab4e2fad186002e288dec5e0d343a92fe7b0bc1b3e13", size = 10543416, upload-time = "2026-01-22T22:30:01.964Z" }, + { url = "https://files.pythonhosted.org/packages/3e/34/8a84cea7e42c2d94ba5bde1d7a4fae164d6318f13f933d92da6d7c2041ff/ruff-0.14.14-py3-none-musllinux_1_2_armv7l.whl", hash = "sha256:e651e977a79e4c758eb807f0481d673a67ffe53cfa92209781dfa3a996cf8412", size = 10285491, upload-time = "2026-01-22T22:30:29.51Z" }, + { url = "https://files.pythonhosted.org/packages/55/ef/b7c5ea0be82518906c978e365e56a77f8de7678c8bb6651ccfbdc178c29f/ruff-0.14.14-py3-none-musllinux_1_2_i686.whl", hash = "sha256:cc8b22da8d9d6fdd844a68ae937e2a0adf9b16514e9a97cc60355e2d4b219fc3", size = 10733525, upload-time = "2026-01-22T22:30:06.499Z" }, + { url = "https://files.pythonhosted.org/packages/6a/5b/aaf1dfbcc53a2811f6cc0a1759de24e4b03e02ba8762daabd9b6bd8c59e3/ruff-0.14.14-py3-none-musllinux_1_2_x86_64.whl", hash = "sha256:16bc890fb4cc9781bb05beb5ab4cd51be9e7cb376bf1dd3580512b24eb3fda2b", size = 11315626, upload-time = "2026-01-22T22:30:36.848Z" }, + { url = "https://files.pythonhosted.org/packages/2c/aa/9f89c719c467dfaf8ad799b9bae0df494513fb21d31a6059cb5870e57e74/ruff-0.14.14-py3-none-win32.whl", hash = "sha256:b530c191970b143375b6a68e6f743800b2b786bbcf03a7965b06c4bf04568167", size = 10502442, upload-time = "2026-01-22T22:30:38.93Z" }, + { url = "https://files.pythonhosted.org/packages/87/44/90fa543014c45560cae1fffc63ea059fb3575ee6e1cb654562197e5d16fb/ruff-0.14.14-py3-none-win_amd64.whl", hash = "sha256:3dde1435e6b6fe5b66506c1dff67a421d0b7f6488d466f651c07f4cab3bf20fd", size = 11630486, upload-time = "2026-01-22T22:30:10.852Z" }, + { url = "https://files.pythonhosted.org/packages/9e/6a/40fee331a52339926a92e17ae748827270b288a35ef4a15c9c8f2ec54715/ruff-0.14.14-py3-none-win_arm64.whl", hash = "sha256:56e6981a98b13a32236a72a8da421d7839221fa308b223b9283312312e5ac76c", size = 10920448, upload-time = "2026-01-22T22:30:15.417Z" }, +] + +[[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 = "sphinx" +version = "7.4.7" +source = { registry = "https://pypi.org/simple" } +resolution-markers = [ + "python_full_version < '3.10'", +] +dependencies = [ + { name = "alabaster", version = "0.7.16", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version < '3.10'" }, + { name = "babel", marker = "python_full_version < '3.10'" }, + { name = "colorama", marker = "python_full_version < '3.10' and sys_platform == 'win32'" }, + { name = "docutils", marker = "python_full_version < '3.10'" }, + { name = "imagesize", marker = "python_full_version < '3.10'" }, + { name = "importlib-metadata", marker = "python_full_version < '3.10'" }, + { name = "jinja2", marker = "python_full_version < '3.10'" }, + { name = "packaging", marker = "python_full_version < '3.10'" }, + { name = "pygments", marker = "python_full_version < '3.10'" }, + { name = "requests", marker = "python_full_version < '3.10'" }, + { name = "snowballstemmer", marker = "python_full_version < '3.10'" }, + { name = "sphinxcontrib-applehelp", marker = "python_full_version < '3.10'" }, + { name = "sphinxcontrib-devhelp", marker = "python_full_version < '3.10'" }, + { name = "sphinxcontrib-htmlhelp", marker = "python_full_version < '3.10'" }, + { name = "sphinxcontrib-jsmath", marker = "python_full_version < '3.10'" }, + { name = "sphinxcontrib-qthelp", marker = "python_full_version < '3.10'" }, + { name = "sphinxcontrib-serializinghtml", marker = "python_full_version < '3.10'" }, + { name = "tomli", marker = "python_full_version < '3.10'" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/5b/be/50e50cb4f2eff47df05673d361095cafd95521d2a22521b920c67a372dcb/sphinx-7.4.7.tar.gz", hash = "sha256:242f92a7ea7e6c5b406fdc2615413890ba9f699114a9c09192d7dfead2ee9cfe", size = 8067911, upload-time = "2024-07-20T14:46:56.059Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/0d/ef/153f6803c5d5f8917dbb7f7fcf6d34a871ede3296fa89c2c703f5f8a6c8e/sphinx-7.4.7-py3-none-any.whl", hash = "sha256:c2419e2135d11f1951cd994d6eb18a1835bd8fdd8429f9ca375dc1f3281bd239", size = 3401624, upload-time = "2024-07-20T14:46:52.142Z" }, +] + +[[package]] +name = "sphinx" +version = "8.1.3" +source = { registry = "https://pypi.org/simple" } +resolution-markers = [ + "python_full_version == '3.10.*'", +] +dependencies = [ + { name = "alabaster", version = "1.0.0", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version == '3.10.*'" }, + { name = "babel", marker = "python_full_version == '3.10.*'" }, + { name = "colorama", marker = "python_full_version == '3.10.*' and sys_platform == 'win32'" }, + { name = "docutils", marker = "python_full_version == '3.10.*'" }, + { name = "imagesize", marker = "python_full_version == '3.10.*'" }, + { name = "jinja2", marker = "python_full_version == '3.10.*'" }, + { name = "packaging", marker = "python_full_version == '3.10.*'" }, + { name = "pygments", marker = "python_full_version == '3.10.*'" }, + { name = "requests", marker = "python_full_version == '3.10.*'" }, + { name = "snowballstemmer", marker = "python_full_version == '3.10.*'" }, + { name = "sphinxcontrib-applehelp", marker = "python_full_version == '3.10.*'" }, + { name = "sphinxcontrib-devhelp", marker = "python_full_version == '3.10.*'" }, + { name = "sphinxcontrib-htmlhelp", marker = "python_full_version == '3.10.*'" }, + { name = "sphinxcontrib-jsmath", marker = "python_full_version == '3.10.*'" }, + { name = "sphinxcontrib-qthelp", marker = "python_full_version == '3.10.*'" }, + { name = "sphinxcontrib-serializinghtml", marker = "python_full_version == '3.10.*'" }, + { name = "tomli", marker = "python_full_version == '3.10.*'" }, +] +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 = "8.2.3" +source = { registry = "https://pypi.org/simple" } +resolution-markers = [ + "python_full_version >= '3.12'", + "python_full_version == '3.11.*'", +] +dependencies = [ + { name = "alabaster", version = "1.0.0", source = { registry = "https://pypi.org/simple" }, 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", 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-py", 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/38/ad/4360e50ed56cb483667b8e6dadf2d3fda62359593faabbe749a27c4eaca6/sphinx-8.2.3.tar.gz", hash = "sha256:398ad29dee7f63a75888314e9424d40f52ce5a6a87ae88e7071e80af296ec348", size = 8321876, upload-time = "2025-03-02T22:31:59.658Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/31/53/136e9eca6e0b9dc0e1962e2c908fbea2e5ac000c2a2fbd9a35797958c48b/sphinx-8.2.3-py3-none-any.whl", hash = "sha256:4405915165f13521d875a8c29c8970800a0141c14cc5416a38feca4ea5d9b9c3", size = 3589741, upload-time = "2025-03-02T22:31:56.836Z" }, +] + +[[package]] +name = "sphinx-autobuild" +version = "2024.10.3" +source = { registry = "https://pypi.org/simple" } +resolution-markers = [ + "python_full_version == '3.10.*'", + "python_full_version < '3.10'", +] +dependencies = [ + { name = "colorama", marker = "python_full_version < '3.11'" }, + { name = "sphinx", version = "7.4.7", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version < '3.10'" }, + { name = "sphinx", version = "8.1.3", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version == '3.10.*'" }, + { name = "starlette", version = "0.49.3", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version < '3.10'" }, + { name = "starlette", version = "0.52.1", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version == '3.10.*'" }, + { name = "uvicorn", version = "0.39.0", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version < '3.10'" }, + { name = "uvicorn", version = "0.40.0", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version == '3.10.*'" }, + { name = "watchfiles", marker = "python_full_version < '3.11'" }, + { name = "websockets", version = "15.0.1", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version < '3.10'" }, + { name = "websockets", version = "16.0", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version == '3.10.*'" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/a5/2c/155e1de2c1ba96a72e5dba152c509a8b41e047ee5c2def9e9f0d812f8be7/sphinx_autobuild-2024.10.3.tar.gz", hash = "sha256:248150f8f333e825107b6d4b86113ab28fa51750e5f9ae63b59dc339be951fb1", size = 14023, upload-time = "2024-10-02T23:15:30.172Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/18/c0/eba125db38c84d3c74717008fd3cb5000b68cd7e2cbafd1349c6a38c3d3b/sphinx_autobuild-2024.10.3-py3-none-any.whl", hash = "sha256:158e16c36f9d633e613c9aaf81c19b0fc458ca78b112533b20dafcda430d60fa", size = 11908, upload-time = "2024-10-02T23:15:28.739Z" }, +] + +[[package]] +name = "sphinx-autobuild" +version = "2025.8.25" +source = { registry = "https://pypi.org/simple" } +resolution-markers = [ + "python_full_version >= '3.12'", + "python_full_version == '3.11.*'", +] +dependencies = [ + { name = "colorama", marker = "python_full_version >= '3.11'" }, + { name = "sphinx", version = "8.2.3", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version >= '3.11'" }, + { name = "starlette", version = "0.52.1", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version >= '3.11'" }, + { name = "uvicorn", version = "0.40.0", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version >= '3.11'" }, + { name = "watchfiles", marker = "python_full_version >= '3.11'" }, + { name = "websockets", version = "16.0", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version >= '3.11'" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/e0/3c/a59a3a453d4133777f7ed2e83c80b7dc817d43c74b74298ca0af869662ad/sphinx_autobuild-2025.8.25.tar.gz", hash = "sha256:9cf5aab32853c8c31af572e4fecdc09c997e2b8be5a07daf2a389e270e85b213", size = 15200, upload-time = "2025-08-25T18:44:55.436Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/d7/20/56411b52f917696995f5ad27d2ea7e9492c84a043c5b49a3a3173573cd93/sphinx_autobuild-2025.8.25-py3-none-any.whl", hash = "sha256:b750ac7d5a18603e4665294323fd20f6dcc0a984117026d1986704fa68f0379a", size = 12535, upload-time = "2025-08-25T18:44:54.164Z" }, +] + +[[package]] +name = "sphinx-notfound-page" +version = "1.1.0" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "sphinx", version = "7.4.7", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version < '3.10'" }, + { name = "sphinx", version = "8.1.3", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version == '3.10.*'" }, + { name = "sphinx", version = "8.2.3", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version >= '3.11'" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/6a/b2/67603444a8ee97b4a8ea71b0a9d6bab1727ed65e362c87e02f818ee57b8a/sphinx_notfound_page-1.1.0.tar.gz", hash = "sha256:913e1754370bb3db201d9300d458a8b8b5fb22e9246a816643a819a9ea2b8067", size = 7392, upload-time = "2025-01-28T18:45:02.871Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/cd/d4/019fe439c840a7966012bbb95ccbdd81c5c10271749706793b43beb05145/sphinx_notfound_page-1.1.0-py3-none-any.whl", hash = "sha256:835dc76ff7914577a1f58d80a2c8418fb6138c0932c8da8adce4d9096fbcd389", size = 8167, upload-time = "2025-01-28T18:45:00.465Z" }, +] + +[[package]] +name = "sphinx-tabs" +version = "3.4.7" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "docutils" }, + { name = "pygments" }, + { name = "sphinx", version = "7.4.7", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version < '3.10'" }, + { name = "sphinx", version = "8.1.3", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version == '3.10.*'" }, + { name = "sphinx", version = "8.2.3", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version >= '3.11'" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/6a/53/a9a91995cb365e589f413b77fc75f1c0e9b4ac61bfa8da52a779ad855cc0/sphinx-tabs-3.4.7.tar.gz", hash = "sha256:991ad4a424ff54119799ba1491701aa8130dd43509474aef45a81c42d889784d", size = 15891, upload-time = "2024-10-08T13:37:27.887Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/6b/c6/f47505b564b918a3ba60c1e99232d4942c4a7e44ecaae603e829e3d05dae/sphinx_tabs-3.4.7-py3-none-any.whl", hash = "sha256:c12d7a36fd413b369e9e9967a0a4015781b71a9c393575419834f19204bd1915", size = 9727, upload-time = "2024-10-08T13:37:26.192Z" }, +] + +[[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-log-cabinet" +version = "1.0.1" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "sphinx", version = "7.4.7", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version < '3.10'" }, + { name = "sphinx", version = "8.1.3", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version == '3.10.*'" }, + { name = "sphinx", version = "8.2.3", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version >= '3.11'" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/75/26/0687391e10c605a4d0c7ebe118c57c51ecc687128bcdae5803d9b96def81/sphinxcontrib-log-cabinet-1.0.1.tar.gz", hash = "sha256:103b2e62df4e57abb943bea05ee9c2beb7da922222c8b77314ffd6ab9901c558", size = 4072, upload-time = "2019-07-05T23:22:34.596Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/27/e7/dbfc155c1b4c429a9a8149032a56bfb7bab4efabc656abb24ab4619c715d/sphinxcontrib_log_cabinet-1.0.1-py2.py3-none-any.whl", hash = "sha256:3decc888e8e453d1912cd95d50efb0794a4670a214efa65e71a7de277dcfe2cd", size = 4887, upload-time = "2019-07-05T23:22:32.969Z" }, +] + +[[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 = "starlette" +version = "0.49.3" +source = { registry = "https://pypi.org/simple" } +resolution-markers = [ + "python_full_version < '3.10'", +] +dependencies = [ + { name = "anyio", marker = "python_full_version < '3.10'" }, + { name = "typing-extensions", marker = "python_full_version < '3.10'" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/de/1a/608df0b10b53b0beb96a37854ee05864d182ddd4b1156a22f1ad3860425a/starlette-0.49.3.tar.gz", hash = "sha256:1c14546f299b5901a1ea0e34410575bc33bbd741377a10484a54445588d00284", size = 2655031, upload-time = "2025-11-01T15:12:26.13Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/a3/e0/021c772d6a662f43b63044ab481dc6ac7592447605b5b35a957785363122/starlette-0.49.3-py3-none-any.whl", hash = "sha256:b579b99715fdc2980cf88c8ec96d3bf1ce16f5a8051a7c2b84ef9b1cdecaea2f", size = 74340, upload-time = "2025-11-01T15:12:24.387Z" }, +] + +[[package]] +name = "starlette" +version = "0.52.1" +source = { registry = "https://pypi.org/simple" } +resolution-markers = [ + "python_full_version >= '3.12'", + "python_full_version == '3.11.*'", + "python_full_version == '3.10.*'", +] +dependencies = [ + { name = "anyio", marker = "python_full_version >= '3.10'" }, + { name = "typing-extensions", marker = "python_full_version >= '3.10' and python_full_version < '3.13'" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/c4/68/79977123bb7be889ad680d79a40f339082c1978b5cfcf62c2d8d196873ac/starlette-0.52.1.tar.gz", hash = "sha256:834edd1b0a23167694292e94f597773bc3f89f362be6effee198165a35d62933", size = 2653702, upload-time = "2026-01-18T13:34:11.062Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/81/0d/13d1d239a25cbfb19e740db83143e95c772a1fe10202dda4b76792b114dd/starlette-0.52.1-py3-none-any.whl", hash = "sha256:0029d43eb3d273bc4f83a08720b4912ea4b071087a3b48db01b7c839f7954d74", size = 74272, upload-time = "2026-01-18T13:34:09.188Z" }, +] + +[[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/34/91/7f65f9809f2936e1f4ce6268ae1903074563603b2a2bd969ebbda802744f/tomli-2.4.0-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:84d081fbc252d1b6a982e1870660e7330fb8f90f676f6e78b052ad4e64714bf0", size = 154915, upload-time = "2026-01-11T11:22:06.703Z" }, + { url = "https://files.pythonhosted.org/packages/20/aa/64dd73a5a849c2e8f216b755599c511badde80e91e9bc2271baa7b2cdbb1/tomli-2.4.0-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:9a08144fa4cba33db5255f9b74f0b89888622109bd2776148f2597447f92a94e", size = 149038, upload-time = "2026-01-11T11:22:07.56Z" }, + { url = "https://files.pythonhosted.org/packages/9e/8a/6d38870bd3d52c8d1505ce054469a73f73a0fe62c0eaf5dddf61447e32fa/tomli-2.4.0-cp313-cp313-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:c73add4bb52a206fd0c0723432db123c0c75c280cbd67174dd9d2db228ebb1b4", size = 242245, upload-time = "2026-01-11T11:22:08.344Z" }, + { url = "https://files.pythonhosted.org/packages/59/bb/8002fadefb64ab2669e5b977df3f5e444febea60e717e755b38bb7c41029/tomli-2.4.0-cp313-cp313-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:1fb2945cbe303b1419e2706e711b7113da57b7db31ee378d08712d678a34e51e", size = 250335, upload-time = "2026-01-11T11:22:09.951Z" }, + { url = "https://files.pythonhosted.org/packages/a5/3d/4cdb6f791682b2ea916af2de96121b3cb1284d7c203d97d92d6003e91c8d/tomli-2.4.0-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:bbb1b10aa643d973366dc2cb1ad94f99c1726a02343d43cbc011edbfac579e7c", size = 245962, upload-time = "2026-01-11T11:22:11.27Z" }, + { url = "https://files.pythonhosted.org/packages/f2/4a/5f25789f9a460bd858ba9756ff52d0830d825b458e13f754952dd15fb7bb/tomli-2.4.0-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:4cbcb367d44a1f0c2be408758b43e1ffb5308abe0ea222897d6bfc8e8281ef2f", size = 250396, upload-time = "2026-01-11T11:22:12.325Z" }, + { url = "https://files.pythonhosted.org/packages/aa/2f/b73a36fea58dfa08e8b3a268750e6853a6aac2a349241a905ebd86f3047a/tomli-2.4.0-cp313-cp313-win32.whl", hash = "sha256:7d49c66a7d5e56ac959cb6fc583aff0651094ec071ba9ad43df785abc2320d86", size = 97530, upload-time = "2026-01-11T11:22:13.865Z" }, + { url = "https://files.pythonhosted.org/packages/3b/af/ca18c134b5d75de7e8dc551c5234eaba2e8e951f6b30139599b53de9c187/tomli-2.4.0-cp313-cp313-win_amd64.whl", hash = "sha256:3cf226acb51d8f1c394c1b310e0e0e61fecdd7adcb78d01e294ac297dd2e7f87", size = 108227, upload-time = "2026-01-11T11:22:15.224Z" }, + { url = "https://files.pythonhosted.org/packages/22/c3/b386b832f209fee8073c8138ec50f27b4460db2fdae9ffe022df89a57f9b/tomli-2.4.0-cp313-cp313-win_arm64.whl", hash = "sha256:d20b797a5c1ad80c516e41bc1fb0443ddb5006e9aaa7bda2d71978346aeb9132", size = 94748, upload-time = "2026-01-11T11:22:16.009Z" }, + { url = "https://files.pythonhosted.org/packages/f3/c4/84047a97eb1004418bc10bdbcfebda209fca6338002eba2dc27cc6d13563/tomli-2.4.0-cp314-cp314-macosx_10_15_x86_64.whl", hash = "sha256:26ab906a1eb794cd4e103691daa23d95c6919cc2fa9160000ac02370cc9dd3f6", size = 154725, upload-time = "2026-01-11T11:22:17.269Z" }, + { url = "https://files.pythonhosted.org/packages/a8/5d/d39038e646060b9d76274078cddf146ced86dc2b9e8bbf737ad5983609a0/tomli-2.4.0-cp314-cp314-macosx_11_0_arm64.whl", hash = "sha256:20cedb4ee43278bc4f2fee6cb50daec836959aadaf948db5172e776dd3d993fc", size = 148901, upload-time = "2026-01-11T11:22:18.287Z" }, + { url = "https://files.pythonhosted.org/packages/73/e5/383be1724cb30f4ce44983d249645684a48c435e1cd4f8b5cded8a816d3c/tomli-2.4.0-cp314-cp314-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:39b0b5d1b6dd03684b3fb276407ebed7090bbec989fa55838c98560c01113b66", size = 243375, upload-time = "2026-01-11T11:22:19.154Z" }, + { url = "https://files.pythonhosted.org/packages/31/f0/bea80c17971c8d16d3cc109dc3585b0f2ce1036b5f4a8a183789023574f2/tomli-2.4.0-cp314-cp314-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:a26d7ff68dfdb9f87a016ecfd1e1c2bacbe3108f4e0f8bcd2228ef9a766c787d", size = 250639, upload-time = "2026-01-11T11:22:20.168Z" }, + { url = "https://files.pythonhosted.org/packages/2c/8f/2853c36abbb7608e3f945d8a74e32ed3a74ee3a1f468f1ffc7d1cb3abba6/tomli-2.4.0-cp314-cp314-musllinux_1_2_aarch64.whl", hash = "sha256:20ffd184fb1df76a66e34bd1b36b4a4641bd2b82954befa32fe8163e79f1a702", size = 246897, upload-time = "2026-01-11T11:22:21.544Z" }, + { url = "https://files.pythonhosted.org/packages/49/f0/6c05e3196ed5337b9fe7ea003e95fd3819a840b7a0f2bf5a408ef1dad8ed/tomli-2.4.0-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:75c2f8bbddf170e8effc98f5e9084a8751f8174ea6ccf4fca5398436e0320bc8", size = 254697, upload-time = "2026-01-11T11:22:23.058Z" }, + { url = "https://files.pythonhosted.org/packages/f3/f5/2922ef29c9f2951883525def7429967fc4d8208494e5ab524234f06b688b/tomli-2.4.0-cp314-cp314-win32.whl", hash = "sha256:31d556d079d72db7c584c0627ff3a24c5d3fb4f730221d3444f3efb1b2514776", size = 98567, upload-time = "2026-01-11T11:22:24.033Z" }, + { url = "https://files.pythonhosted.org/packages/7b/31/22b52e2e06dd2a5fdbc3ee73226d763b184ff21fc24e20316a44ccc4d96b/tomli-2.4.0-cp314-cp314-win_amd64.whl", hash = "sha256:43e685b9b2341681907759cf3a04e14d7104b3580f808cfde1dfdb60ada85475", size = 108556, upload-time = "2026-01-11T11:22:25.378Z" }, + { url = "https://files.pythonhosted.org/packages/48/3d/5058dff3255a3d01b705413f64f4306a141a8fd7a251e5a495e3f192a998/tomli-2.4.0-cp314-cp314-win_arm64.whl", hash = "sha256:3d895d56bd3f82ddd6faaff993c275efc2ff38e52322ea264122d72729dca2b2", size = 96014, upload-time = "2026-01-11T11:22:26.138Z" }, + { url = "https://files.pythonhosted.org/packages/b8/4e/75dab8586e268424202d3a1997ef6014919c941b50642a1682df43204c22/tomli-2.4.0-cp314-cp314t-macosx_10_15_x86_64.whl", hash = "sha256:5b5807f3999fb66776dbce568cc9a828544244a8eb84b84b9bafc080c99597b9", size = 163339, upload-time = "2026-01-11T11:22:27.143Z" }, + { url = "https://files.pythonhosted.org/packages/06/e3/b904d9ab1016829a776d97f163f183a48be6a4deb87304d1e0116a349519/tomli-2.4.0-cp314-cp314t-macosx_11_0_arm64.whl", hash = "sha256:c084ad935abe686bd9c898e62a02a19abfc9760b5a79bc29644463eaf2840cb0", size = 159490, upload-time = "2026-01-11T11:22:28.399Z" }, + { url = "https://files.pythonhosted.org/packages/e3/5a/fc3622c8b1ad823e8ea98a35e3c632ee316d48f66f80f9708ceb4f2a0322/tomli-2.4.0-cp314-cp314t-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:0f2e3955efea4d1cfbcb87bc321e00dc08d2bcb737fd1d5e398af111d86db5df", size = 269398, upload-time = "2026-01-11T11:22:29.345Z" }, + { url = "https://files.pythonhosted.org/packages/fd/33/62bd6152c8bdd4c305ad9faca48f51d3acb2df1f8791b1477d46ff86e7f8/tomli-2.4.0-cp314-cp314t-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:0e0fe8a0b8312acf3a88077a0802565cb09ee34107813bba1c7cd591fa6cfc8d", size = 276515, upload-time = "2026-01-11T11:22:30.327Z" }, + { url = "https://files.pythonhosted.org/packages/4b/ff/ae53619499f5235ee4211e62a8d7982ba9e439a0fb4f2f351a93d67c1dd2/tomli-2.4.0-cp314-cp314t-musllinux_1_2_aarch64.whl", hash = "sha256:413540dce94673591859c4c6f794dfeaa845e98bf35d72ed59636f869ef9f86f", size = 273806, upload-time = "2026-01-11T11:22:32.56Z" }, + { url = "https://files.pythonhosted.org/packages/47/71/cbca7787fa68d4d0a9f7072821980b39fbb1b6faeb5f5cf02f4a5559fa28/tomli-2.4.0-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:0dc56fef0e2c1c470aeac5b6ca8cc7b640bb93e92d9803ddaf9ea03e198f5b0b", size = 281340, upload-time = "2026-01-11T11:22:33.505Z" }, + { url = "https://files.pythonhosted.org/packages/f5/00/d595c120963ad42474cf6ee7771ad0d0e8a49d0f01e29576ee9195d9ecdf/tomli-2.4.0-cp314-cp314t-win32.whl", hash = "sha256:d878f2a6707cc9d53a1be1414bbb419e629c3d6e67f69230217bb663e76b5087", size = 108106, upload-time = "2026-01-11T11:22:34.451Z" }, + { url = "https://files.pythonhosted.org/packages/de/69/9aa0c6a505c2f80e519b43764f8b4ba93b5a0bbd2d9a9de6e2b24271b9a5/tomli-2.4.0-cp314-cp314t-win_amd64.whl", hash = "sha256:2add28aacc7425117ff6364fe9e06a183bb0251b03f986df0e78e974047571fd", size = 120504, upload-time = "2026-01-11T11:22:35.764Z" }, + { url = "https://files.pythonhosted.org/packages/b3/9f/f1668c281c58cfae01482f7114a4b88d345e4c140386241a1a24dcc9e7bc/tomli-2.4.0-cp314-cp314t-win_arm64.whl", hash = "sha256:2b1e3b80e1d5e52e40e9b924ec43d81570f0e7d09d11081b797bc4692765a3d4", size = 99561, upload-time = "2026-01-11T11:22:36.624Z" }, + { 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 = "tox" +version = "4.30.3" +source = { registry = "https://pypi.org/simple" } +resolution-markers = [ + "python_full_version < '3.10'", +] +dependencies = [ + { name = "cachetools", marker = "python_full_version < '3.10'" }, + { name = "chardet", marker = "python_full_version < '3.10'" }, + { name = "colorama", marker = "python_full_version < '3.10'" }, + { name = "filelock", version = "3.19.1", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version < '3.10'" }, + { name = "packaging", marker = "python_full_version < '3.10'" }, + { name = "platformdirs", version = "4.4.0", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version < '3.10'" }, + { name = "pluggy", marker = "python_full_version < '3.10'" }, + { name = "pyproject-api", version = "1.9.1", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version < '3.10'" }, + { name = "tomli", marker = "python_full_version < '3.10'" }, + { name = "typing-extensions", marker = "python_full_version < '3.10'" }, + { name = "virtualenv", marker = "python_full_version < '3.10'" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/51/b2/cee55172e5e10ce030b087cd3ac06641e47d08a3dc8d76c17b157dba7558/tox-4.30.3.tar.gz", hash = "sha256:f3dd0735f1cd4e8fbea5a3661b77f517456b5f0031a6256432533900e34b90bf", size = 202799, upload-time = "2025-10-02T16:24:39.974Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/e2/e4/8bb9ce952820df4165eb34610af347665d6cb436898a234db9d84d093ce6/tox-4.30.3-py3-none-any.whl", hash = "sha256:a9f17b4b2d0f74fe0d76207236925a119095011e5c2e661a133115a8061178c9", size = 175512, upload-time = "2025-10-02T16:24:38.209Z" }, +] + +[[package]] +name = "tox" +version = "4.34.1" +source = { registry = "https://pypi.org/simple" } +resolution-markers = [ + "python_full_version >= '3.12'", + "python_full_version == '3.11.*'", + "python_full_version == '3.10.*'", +] +dependencies = [ + { name = "cachetools", marker = "python_full_version >= '3.10'" }, + { name = "chardet", marker = "python_full_version >= '3.10'" }, + { name = "colorama", marker = "python_full_version >= '3.10'" }, + { name = "filelock", version = "3.20.3", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version >= '3.10'" }, + { name = "packaging", marker = "python_full_version >= '3.10'" }, + { name = "platformdirs", version = "4.5.1", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version >= '3.10'" }, + { name = "pluggy", marker = "python_full_version >= '3.10'" }, + { name = "pyproject-api", version = "1.10.0", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version >= '3.10'" }, + { name = "tomli", marker = "python_full_version == '3.10.*'" }, + { name = "typing-extensions", marker = "python_full_version == '3.10.*'" }, + { name = "virtualenv", marker = "python_full_version >= '3.10'" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/5e/9b/5909f40b281ebd37c2f83de5087b9cb8a9a64c33745f334be0aeaedadbbc/tox-4.34.1.tar.gz", hash = "sha256:ef1e82974c2f5ea02954d590ee0b967fad500c3879b264ea19efb9a554f3cc60", size = 205306, upload-time = "2026-01-09T17:42:59.895Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/ec/0f/fe6629e277ce615e53d0a0b65dc23c88b15a402bb7dbf771f17bbd18f1c4/tox-4.34.1-py3-none-any.whl", hash = "sha256:5610d69708bab578d618959b023f8d7d5d3386ed14a2392aeebf9c583615af60", size = 176812, upload-time = "2026-01-09T17:42:58.629Z" }, +] + +[[package]] +name = "tox-uv" +version = "1.28.1" +source = { registry = "https://pypi.org/simple" } +resolution-markers = [ + "python_full_version < '3.10'", +] +dependencies = [ + { name = "packaging", marker = "python_full_version < '3.10'" }, + { name = "tomli", marker = "python_full_version < '3.10'" }, + { name = "tox", version = "4.30.3", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version < '3.10'" }, + { name = "typing-extensions", marker = "python_full_version < '3.10'" }, + { name = "uv", marker = "python_full_version < '3.10'" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/f2/23/5c7f9bb50f25b4e9096a3b38e4b67604d3030388fdb6e645e54226b30cb0/tox_uv-1.28.1.tar.gz", hash = "sha256:fb01a34f49496e51e198196ee73a2be19ecd9cdbdc2508d86b981314c3d1b058", size = 23518, upload-time = "2025-10-09T16:13:45.286Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/f1/54/46abc86d4cf2844d34dbd8e7bd0e4ed226ed6fb6a9a9481a85d4daff28ca/tox_uv-1.28.1-py3-none-any.whl", hash = "sha256:29f64076c57bda643b0c25dcb925011a35bfa57b0a94d3aaf550607d31e9f30a", size = 17363, upload-time = "2025-10-09T16:13:43.793Z" }, +] + +[[package]] +name = "tox-uv" +version = "1.29.0" +source = { registry = "https://pypi.org/simple" } +resolution-markers = [ + "python_full_version >= '3.12'", + "python_full_version == '3.11.*'", + "python_full_version == '3.10.*'", +] +dependencies = [ + { name = "packaging", marker = "python_full_version >= '3.10'" }, + { name = "tomli", marker = "python_full_version == '3.10.*'" }, + { name = "tox", version = "4.34.1", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version >= '3.10'" }, + { name = "uv", marker = "python_full_version >= '3.10'" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/4f/90/06752775b8cfadba8856190f5beae9f552547e0f287e0246677972107375/tox_uv-1.29.0.tar.gz", hash = "sha256:30fa9e6ad507df49d3c6a2f88894256bcf90f18e240a00764da6ecab1db24895", size = 23427, upload-time = "2025-10-09T20:40:27.384Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/5c/17/221d62937c4130b044bb437caac4181e7e13d5536bbede65264db1f0ac9f/tox_uv-1.29.0-py3-none-any.whl", hash = "sha256:b1d251286edeeb4bc4af1e24c8acfdd9404700143c2199ccdbb4ea195f7de6cc", size = 17254, upload-time = "2025-10-09T20:40:25.885Z" }, +] + +[[package]] +name = "types-contextvars" +version = "2.4.7.3" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/6c/20/6a0271fe78050f15eaad21f94a4efebbddcfd0cadac0d35b056e8d32b40f/types-contextvars-2.4.7.3.tar.gz", hash = "sha256:a15a1624c709d04974900ea4f8c4fc2676941bf7d4771a9c9c4ac3daa0e0060d", size = 3166, upload-time = "2023-07-07T09:16:39.067Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/41/5e/770b3dd271a925b4428ee17664536d037695698eb764b4c5ed6fe815169b/types_contextvars-2.4.7.3-py3-none-any.whl", hash = "sha256:bcd8e97a5b58e76d20f5cc161ba39b29b60ac46dcc6edf3e23c1d33f99b34351", size = 2752, upload-time = "2023-07-07T09:16:37.855Z" }, +] + +[[package]] +name = "types-dataclasses" +version = "0.6.6" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/4b/6a/dec8fbc818b1e716cb2d9424f1ea0f6f3b1443460eb6a70d00d9d8527360/types-dataclasses-0.6.6.tar.gz", hash = "sha256:4b5a2fcf8e568d5a1974cd69010e320e1af8251177ec968de7b9bb49aa49f7b9", size = 2884, upload-time = "2022-06-30T09:49:21.449Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/31/85/23ab2bbc280266af5bf22ded4e070946d1694d1721ced90666b649eaa795/types_dataclasses-0.6.6-py3-none-any.whl", hash = "sha256:a0a1ab5324ba30363a15c9daa0f053ae4fff914812a1ebd8ad84a08e5349574d", size = 2868, upload-time = "2022-06-30T09:49:19.977Z" }, +] + +[[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 = "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 = "uv" +version = "0.9.26" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/ff/6a/ef4ea19097ecdfd7df6e608f93874536af045c68fd70aa628c667815c458/uv-0.9.26.tar.gz", hash = "sha256:8b7017a01cc48847a7ae26733383a2456dd060fc50d21d58de5ee14f6b6984d7", size = 3790483, upload-time = "2026-01-15T20:51:33.582Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/fe/e1/5c0b17833d5e3b51a897957348ff8d937a3cdfc5eea5c4a7075d8d7b9870/uv-0.9.26-py3-none-linux_armv6l.whl", hash = "sha256:7dba609e32b7bd13ef81788d580970c6ff3a8874d942755b442cffa8f25dba57", size = 22638031, upload-time = "2026-01-15T20:51:44.187Z" }, + { url = "https://files.pythonhosted.org/packages/a5/8b/68ac5825a615a8697e324f52ac0b92feb47a0ec36a63759c5f2931f0c3a0/uv-0.9.26-py3-none-macosx_10_12_x86_64.whl", hash = "sha256:b815e3b26eeed00e00f831343daba7a9d99c1506883c189453bb4d215f54faac", size = 21507805, upload-time = "2026-01-15T20:50:42.574Z" }, + { url = "https://files.pythonhosted.org/packages/0d/a2/664a338aefe009f6e38e47455ee2f64a21da7ad431dbcaf8b45d8b1a2b7a/uv-0.9.26-py3-none-macosx_11_0_arm64.whl", hash = "sha256:1b012e6c4dfe767f818cbb6f47d02c207c9b0c82fee69a5de6d26ffb26a3ef3c", size = 20249791, upload-time = "2026-01-15T20:50:49.835Z" }, + { url = "https://files.pythonhosted.org/packages/ba/3d/b8186a7dec1346ca4630c674b760517d28bffa813a01965f4b57596bacf3/uv-0.9.26-py3-none-manylinux_2_17_aarch64.manylinux2014_aarch64.musllinux_1_1_aarch64.whl", hash = "sha256:ea296b700d7c4c27acdfd23ffaef2b0ecdd0aa1b58d942c62ee87df3b30f06ac", size = 22039108, upload-time = "2026-01-15T20:51:00.675Z" }, + { url = "https://files.pythonhosted.org/packages/aa/a9/687fd587e7a3c2c826afe72214fb24b7f07b0d8b0b0300e6a53b554180ea/uv-0.9.26-py3-none-manylinux_2_17_armv7l.manylinux2014_armv7l.musllinux_1_1_armv7l.whl", hash = "sha256:1ba860d2988efc27e9c19f8537a2f9fa499a8b7ebe4afbe2d3d323d72f9aee61", size = 22174763, upload-time = "2026-01-15T20:50:46.471Z" }, + { url = "https://files.pythonhosted.org/packages/38/69/7fa03ee7d59e562fca1426436f15a8c107447d41b34e0899e25ee69abfad/uv-0.9.26-py3-none-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:8610bdfc282a681a0a40b90495a478599aa3484c12503ef79ef42cd271fd80fe", size = 22189861, upload-time = "2026-01-15T20:51:15.618Z" }, + { url = "https://files.pythonhosted.org/packages/10/2d/4be446a2ec09f3c428632b00a138750af47c76b0b9f987e9a5b52fef0405/uv-0.9.26-py3-none-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:c4bf700bd071bd595084b9ee0a8d77c6a0a10ca3773d3771346a2599f306bd9c", size = 23005589, upload-time = "2026-01-15T20:50:57.185Z" }, + { url = "https://files.pythonhosted.org/packages/c3/16/860990b812136695a63a8da9fb5f819c3cf18ea37dcf5852e0e1b795ca0d/uv-0.9.26-py3-none-manylinux_2_17_ppc64.manylinux2014_ppc64.whl", hash = "sha256:89a7beea1c692f76a6f8da13beff3cbb43f7123609e48e03517cc0db5c5de87c", size = 24713505, upload-time = "2026-01-15T20:51:04.366Z" }, + { url = "https://files.pythonhosted.org/packages/01/43/5d7f360d551e62d8f8bf6624b8fca9895cea49ebe5fce8891232d7ed2321/uv-0.9.26-py3-none-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:182f5c086c7d03ad447e522b70fa29a0302a70bcfefad4b8cd08496828a0e179", size = 24342500, upload-time = "2026-01-15T20:51:47.863Z" }, + { url = "https://files.pythonhosted.org/packages/9b/9c/2bae010a189e7d8e5dc555edcfd053b11ce96fad2301b919ba0d9dd23659/uv-0.9.26-py3-none-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:5d8c62a501f13425b4b0ce1dd4c6b82f3ce5a5179e2549c55f4bb27cc0eb8ef8", size = 23222578, upload-time = "2026-01-15T20:51:36.85Z" }, + { url = "https://files.pythonhosted.org/packages/38/16/a07593a040fe6403c36f3b0a99b309f295cbfe19a1074dbadb671d5d4ef7/uv-0.9.26-py3-none-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:b7e89798bd3df7dcc4b2b4ac4e2fc11d6b3ff4fe7d764aa3012d664c635e2922", size = 23250201, upload-time = "2026-01-15T20:51:19.117Z" }, + { url = "https://files.pythonhosted.org/packages/23/a0/45893e15ad3ab842db27c1eb3b8605b9b4023baa5d414e67cfa559a0bff0/uv-0.9.26-py3-none-manylinux_2_28_aarch64.whl", hash = "sha256:60a66f1783ec4efc87b7e1f9bd66e8fd2de3e3b30d122b31cb1487f63a3ea8b7", size = 22229160, upload-time = "2026-01-15T20:51:22.931Z" }, + { url = "https://files.pythonhosted.org/packages/fc/c0/20a597a5c253702a223b5e745cf8c16cd5dd053080f896bb10717b3bedec/uv-0.9.26-py3-none-manylinux_2_31_riscv64.whl", hash = "sha256:63c6a1f1187facba1fb45a2fa45396980631a3427ac11b0e3d9aa3ebcf2c73cf", size = 23090730, upload-time = "2026-01-15T20:51:26.611Z" }, + { url = "https://files.pythonhosted.org/packages/40/c9/744537867d9ab593fea108638b57cca1165a0889cfd989981c942b6de9a5/uv-0.9.26-py3-none-musllinux_1_1_i686.whl", hash = "sha256:c6d8650fbc980ccb348b168266143a9bd4deebc86437537caaf8ff2a39b6ea50", size = 22436632, upload-time = "2026-01-15T20:51:12.045Z" }, + { url = "https://files.pythonhosted.org/packages/6b/e2/be683e30262f2cf02dcb41b6c32910a6939517d50ec45f502614d239feb7/uv-0.9.26-py3-none-musllinux_1_1_x86_64.whl", hash = "sha256:25278f9298aa4dade38241a93d036739b0c87278dcfad1ec1f57e803536bfc49", size = 23480064, upload-time = "2026-01-15T20:50:53.333Z" }, + { url = "https://files.pythonhosted.org/packages/50/3e/4a7e6bc5db2beac9c4966f212805f1903d37d233f2e160737f0b24780ada/uv-0.9.26-py3-none-win32.whl", hash = "sha256:10d075e0193e3a0e6c54f830731c4cb965d6f4e11956e84a7bed7ed61d42aa27", size = 21000052, upload-time = "2026-01-15T20:51:40.753Z" }, + { url = "https://files.pythonhosted.org/packages/07/5d/eb80c6eff2a9f7d5cf35ec84fda323b74aa0054145db28baf72d35a7a301/uv-0.9.26-py3-none-win_amd64.whl", hash = "sha256:0315fc321f5644b12118f9928086513363ed9b29d74d99f1539fda1b6b5478ab", size = 23684930, upload-time = "2026-01-15T20:51:08.448Z" }, + { url = "https://files.pythonhosted.org/packages/ed/9d/3b2631931649b1783f5024796ca8ad2b42a01a829b9ce1202d973cc7bce5/uv-0.9.26-py3-none-win_arm64.whl", hash = "sha256:344ff38749b6cd7b7dfdfb382536f168cafe917ae3a5aa78b7a63746ba2a905b", size = 22158123, upload-time = "2026-01-15T20:51:30.939Z" }, +] + +[[package]] +name = "uvicorn" +version = "0.39.0" +source = { registry = "https://pypi.org/simple" } +resolution-markers = [ + "python_full_version < '3.10'", +] +dependencies = [ + { name = "click", version = "8.1.8", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version < '3.10'" }, + { name = "h11", marker = "python_full_version < '3.10'" }, + { name = "typing-extensions", marker = "python_full_version < '3.10'" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/ae/4f/f9fdac7cf6dd79790eb165639b5c452ceeabc7bbabbba4569155470a287d/uvicorn-0.39.0.tar.gz", hash = "sha256:610512b19baa93423d2892d7823741f6d27717b642c8964000d7194dded19302", size = 82001, upload-time = "2025-12-21T13:05:17.973Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/6b/25/db2b1c6c35bf22e17fe5412d2ee5d3fd7a20d07ebc9dac8b58f7db2e23a0/uvicorn-0.39.0-py3-none-any.whl", hash = "sha256:7beec21bd2693562b386285b188a7963b06853c0d006302b3e4cfed950c9929a", size = 68491, upload-time = "2025-12-21T13:05:16.291Z" }, +] + +[[package]] +name = "uvicorn" +version = "0.40.0" +source = { registry = "https://pypi.org/simple" } +resolution-markers = [ + "python_full_version >= '3.12'", + "python_full_version == '3.11.*'", + "python_full_version == '3.10.*'", +] +dependencies = [ + { name = "click", version = "8.3.1", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version >= '3.10'" }, + { name = "h11", marker = "python_full_version >= '3.10'" }, + { name = "typing-extensions", marker = "python_full_version == '3.10.*'" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/c3/d1/8f3c683c9561a4e6689dd3b1d345c815f10f86acd044ee1fb9a4dcd0b8c5/uvicorn-0.40.0.tar.gz", hash = "sha256:839676675e87e73694518b5574fd0f24c9d97b46bea16df7b8c05ea1a51071ea", size = 81761, upload-time = "2025-12-21T14:16:22.45Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/3d/d8/2083a1daa7439a66f3a48589a57d576aa117726762618f6bb09fe3798796/uvicorn-0.40.0-py3-none-any.whl", hash = "sha256:c6c8f55bc8bf13eb6fa9ff87ad62308bbbc33d0b67f84293151efe87e0d5f2ee", size = 68502, upload-time = "2025-12-21T14:16:21.041Z" }, +] + +[[package]] +name = "virtualenv" +version = "20.36.1" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "distlib" }, + { name = "filelock", version = "3.19.1", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version < '3.10'" }, + { name = "filelock", version = "3.20.3", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version >= '3.10'" }, + { name = "platformdirs", version = "4.4.0", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version < '3.10'" }, + { name = "platformdirs", version = "4.5.1", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version >= '3.10'" }, + { name = "typing-extensions", marker = "python_full_version < '3.11'" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/aa/a3/4d310fa5f00863544e1d0f4de93bddec248499ccf97d4791bc3122c9d4f3/virtualenv-20.36.1.tar.gz", hash = "sha256:8befb5c81842c641f8ee658481e42641c68b5eab3521d8e092d18320902466ba", size = 6032239, upload-time = "2026-01-09T18:21:01.296Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/6a/2a/dc2228b2888f51192c7dc766106cd475f1b768c10caaf9727659726f7391/virtualenv-20.36.1-py3-none-any.whl", hash = "sha256:575a8d6b124ef88f6f51d56d656132389f961062a9177016a50e4f507bbcc19f", size = 6008258, upload-time = "2026-01-09T18:20:59.425Z" }, +] + +[[package]] +name = "watchfiles" +version = "1.1.1" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "anyio" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/c2/c9/8869df9b2a2d6c59d79220a4db37679e74f807c559ffe5265e08b227a210/watchfiles-1.1.1.tar.gz", hash = "sha256:a173cb5c16c4f40ab19cecf48a534c409f7ea983ab8fed0741304a1c0a31b3f2", size = 94440, upload-time = "2025-10-14T15:06:21.08Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/a7/1a/206e8cf2dd86fddf939165a57b4df61607a1e0add2785f170a3f616b7d9f/watchfiles-1.1.1-cp310-cp310-macosx_10_12_x86_64.whl", hash = "sha256:eef58232d32daf2ac67f42dea51a2c80f0d03379075d44a587051e63cc2e368c", size = 407318, upload-time = "2025-10-14T15:04:18.753Z" }, + { url = "https://files.pythonhosted.org/packages/b3/0f/abaf5262b9c496b5dad4ed3c0e799cbecb1f8ea512ecb6ddd46646a9fca3/watchfiles-1.1.1-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:03fa0f5237118a0c5e496185cafa92878568b652a2e9a9382a5151b1a0380a43", size = 394478, upload-time = "2025-10-14T15:04:20.297Z" }, + { url = "https://files.pythonhosted.org/packages/b1/04/9cc0ba88697b34b755371f5ace8d3a4d9a15719c07bdc7bd13d7d8c6a341/watchfiles-1.1.1-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:8ca65483439f9c791897f7db49202301deb6e15fe9f8fe2fed555bf986d10c31", size = 449894, upload-time = "2025-10-14T15:04:21.527Z" }, + { url = "https://files.pythonhosted.org/packages/d2/9c/eda4615863cd8621e89aed4df680d8c3ec3da6a4cf1da113c17decd87c7f/watchfiles-1.1.1-cp310-cp310-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:f0ab1c1af0cb38e3f598244c17919fb1a84d1629cc08355b0074b6d7f53138ac", size = 459065, upload-time = "2025-10-14T15:04:22.795Z" }, + { url = "https://files.pythonhosted.org/packages/84/13/f28b3f340157d03cbc8197629bc109d1098764abe1e60874622a0be5c112/watchfiles-1.1.1-cp310-cp310-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:3bc570d6c01c206c46deb6e935a260be44f186a2f05179f52f7fcd2be086a94d", size = 488377, upload-time = "2025-10-14T15:04:24.138Z" }, + { url = "https://files.pythonhosted.org/packages/86/93/cfa597fa9389e122488f7ffdbd6db505b3b915ca7435ecd7542e855898c2/watchfiles-1.1.1-cp310-cp310-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:e84087b432b6ac94778de547e08611266f1f8ffad28c0ee4c82e028b0fc5966d", size = 595837, upload-time = "2025-10-14T15:04:25.057Z" }, + { url = "https://files.pythonhosted.org/packages/57/1e/68c1ed5652b48d89fc24d6af905d88ee4f82fa8bc491e2666004e307ded1/watchfiles-1.1.1-cp310-cp310-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:620bae625f4cb18427b1bb1a2d9426dc0dd5a5ba74c7c2cdb9de405f7b129863", size = 473456, upload-time = "2025-10-14T15:04:26.497Z" }, + { url = "https://files.pythonhosted.org/packages/d5/dc/1a680b7458ffa3b14bb64878112aefc8f2e4f73c5af763cbf0bd43100658/watchfiles-1.1.1-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:544364b2b51a9b0c7000a4b4b02f90e9423d97fbbf7e06689236443ebcad81ab", size = 455614, upload-time = "2025-10-14T15:04:27.539Z" }, + { url = "https://files.pythonhosted.org/packages/61/a5/3d782a666512e01eaa6541a72ebac1d3aae191ff4a31274a66b8dd85760c/watchfiles-1.1.1-cp310-cp310-musllinux_1_1_aarch64.whl", hash = "sha256:bbe1ef33d45bc71cf21364df962af171f96ecaeca06bd9e3d0b583efb12aec82", size = 630690, upload-time = "2025-10-14T15:04:28.495Z" }, + { url = "https://files.pythonhosted.org/packages/9b/73/bb5f38590e34687b2a9c47a244aa4dd50c56a825969c92c9c5fc7387cea1/watchfiles-1.1.1-cp310-cp310-musllinux_1_1_x86_64.whl", hash = "sha256:1a0bb430adb19ef49389e1ad368450193a90038b5b752f4ac089ec6942c4dff4", size = 622459, upload-time = "2025-10-14T15:04:29.491Z" }, + { url = "https://files.pythonhosted.org/packages/f1/ac/c9bb0ec696e07a20bd58af5399aeadaef195fb2c73d26baf55180fe4a942/watchfiles-1.1.1-cp310-cp310-win32.whl", hash = "sha256:3f6d37644155fb5beca5378feb8c1708d5783145f2a0f1c4d5a061a210254844", size = 272663, upload-time = "2025-10-14T15:04:30.435Z" }, + { url = "https://files.pythonhosted.org/packages/11/a0/a60c5a7c2ec59fa062d9a9c61d02e3b6abd94d32aac2d8344c4bdd033326/watchfiles-1.1.1-cp310-cp310-win_amd64.whl", hash = "sha256:a36d8efe0f290835fd0f33da35042a1bb5dc0e83cbc092dcf69bce442579e88e", size = 287453, upload-time = "2025-10-14T15:04:31.53Z" }, + { url = "https://files.pythonhosted.org/packages/1f/f8/2c5f479fb531ce2f0564eda479faecf253d886b1ab3630a39b7bf7362d46/watchfiles-1.1.1-cp311-cp311-macosx_10_12_x86_64.whl", hash = "sha256:f57b396167a2565a4e8b5e56a5a1c537571733992b226f4f1197d79e94cf0ae5", size = 406529, upload-time = "2025-10-14T15:04:32.899Z" }, + { url = "https://files.pythonhosted.org/packages/fe/cd/f515660b1f32f65df671ddf6f85bfaca621aee177712874dc30a97397977/watchfiles-1.1.1-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:421e29339983e1bebc281fab40d812742268ad057db4aee8c4d2bce0af43b741", size = 394384, upload-time = "2025-10-14T15:04:33.761Z" }, + { url = "https://files.pythonhosted.org/packages/7b/c3/28b7dc99733eab43fca2d10f55c86e03bd6ab11ca31b802abac26b23d161/watchfiles-1.1.1-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:6e43d39a741e972bab5d8100b5cdacf69db64e34eb19b6e9af162bccf63c5cc6", size = 448789, upload-time = "2025-10-14T15:04:34.679Z" }, + { url = "https://files.pythonhosted.org/packages/4a/24/33e71113b320030011c8e4316ccca04194bf0cbbaeee207f00cbc7d6b9f5/watchfiles-1.1.1-cp311-cp311-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:f537afb3276d12814082a2e9b242bdcf416c2e8fd9f799a737990a1dbe906e5b", size = 460521, upload-time = "2025-10-14T15:04:35.963Z" }, + { url = "https://files.pythonhosted.org/packages/f4/c3/3c9a55f255aa57b91579ae9e98c88704955fa9dac3e5614fb378291155df/watchfiles-1.1.1-cp311-cp311-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:b2cd9e04277e756a2e2d2543d65d1e2166d6fd4c9b183f8808634fda23f17b14", size = 488722, upload-time = "2025-10-14T15:04:37.091Z" }, + { url = "https://files.pythonhosted.org/packages/49/36/506447b73eb46c120169dc1717fe2eff07c234bb3232a7200b5f5bd816e9/watchfiles-1.1.1-cp311-cp311-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:5f3f58818dc0b07f7d9aa7fe9eb1037aecb9700e63e1f6acfed13e9fef648f5d", size = 596088, upload-time = "2025-10-14T15:04:38.39Z" }, + { url = "https://files.pythonhosted.org/packages/82/ab/5f39e752a9838ec4d52e9b87c1e80f1ee3ccdbe92e183c15b6577ab9de16/watchfiles-1.1.1-cp311-cp311-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:9bb9f66367023ae783551042d31b1d7fd422e8289eedd91f26754a66f44d5cff", size = 472923, upload-time = "2025-10-14T15:04:39.666Z" }, + { url = "https://files.pythonhosted.org/packages/af/b9/a419292f05e302dea372fa7e6fda5178a92998411f8581b9830d28fb9edb/watchfiles-1.1.1-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:aebfd0861a83e6c3d1110b78ad54704486555246e542be3e2bb94195eabb2606", size = 456080, upload-time = "2025-10-14T15:04:40.643Z" }, + { url = "https://files.pythonhosted.org/packages/b0/c3/d5932fd62bde1a30c36e10c409dc5d54506726f08cb3e1d8d0ba5e2bc8db/watchfiles-1.1.1-cp311-cp311-musllinux_1_1_aarch64.whl", hash = "sha256:5fac835b4ab3c6487b5dbad78c4b3724e26bcc468e886f8ba8cc4306f68f6701", size = 629432, upload-time = "2025-10-14T15:04:41.789Z" }, + { url = "https://files.pythonhosted.org/packages/f7/77/16bddd9779fafb795f1a94319dc965209c5641db5bf1edbbccace6d1b3c0/watchfiles-1.1.1-cp311-cp311-musllinux_1_1_x86_64.whl", hash = "sha256:399600947b170270e80134ac854e21b3ccdefa11a9529a3decc1327088180f10", size = 623046, upload-time = "2025-10-14T15:04:42.718Z" }, + { url = "https://files.pythonhosted.org/packages/46/ef/f2ecb9a0f342b4bfad13a2787155c6ee7ce792140eac63a34676a2feeef2/watchfiles-1.1.1-cp311-cp311-win32.whl", hash = "sha256:de6da501c883f58ad50db3a32ad397b09ad29865b5f26f64c24d3e3281685849", size = 271473, upload-time = "2025-10-14T15:04:43.624Z" }, + { url = "https://files.pythonhosted.org/packages/94/bc/f42d71125f19731ea435c3948cad148d31a64fccde3867e5ba4edee901f9/watchfiles-1.1.1-cp311-cp311-win_amd64.whl", hash = "sha256:35c53bd62a0b885bf653ebf6b700d1bf05debb78ad9292cf2a942b23513dc4c4", size = 287598, upload-time = "2025-10-14T15:04:44.516Z" }, + { url = "https://files.pythonhosted.org/packages/57/c9/a30f897351f95bbbfb6abcadafbaca711ce1162f4db95fc908c98a9165f3/watchfiles-1.1.1-cp311-cp311-win_arm64.whl", hash = "sha256:57ca5281a8b5e27593cb7d82c2ac927ad88a96ed406aa446f6344e4328208e9e", size = 277210, upload-time = "2025-10-14T15:04:45.883Z" }, + { url = "https://files.pythonhosted.org/packages/74/d5/f039e7e3c639d9b1d09b07ea412a6806d38123f0508e5f9b48a87b0a76cc/watchfiles-1.1.1-cp312-cp312-macosx_10_12_x86_64.whl", hash = "sha256:8c89f9f2f740a6b7dcc753140dd5e1ab9215966f7a3530d0c0705c83b401bd7d", size = 404745, upload-time = "2025-10-14T15:04:46.731Z" }, + { url = "https://files.pythonhosted.org/packages/a5/96/a881a13aa1349827490dab2d363c8039527060cfcc2c92cc6d13d1b1049e/watchfiles-1.1.1-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:bd404be08018c37350f0d6e34676bd1e2889990117a2b90070b3007f172d0610", size = 391769, upload-time = "2025-10-14T15:04:48.003Z" }, + { url = "https://files.pythonhosted.org/packages/4b/5b/d3b460364aeb8da471c1989238ea0e56bec24b6042a68046adf3d9ddb01c/watchfiles-1.1.1-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:8526e8f916bb5b9a0a777c8317c23ce65de259422bba5b31325a6fa6029d33af", size = 449374, upload-time = "2025-10-14T15:04:49.179Z" }, + { url = "https://files.pythonhosted.org/packages/b9/44/5769cb62d4ed055cb17417c0a109a92f007114a4e07f30812a73a4efdb11/watchfiles-1.1.1-cp312-cp312-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:2edc3553362b1c38d9f06242416a5d8e9fe235c204a4072e988ce2e5bb1f69f6", size = 459485, upload-time = "2025-10-14T15:04:50.155Z" }, + { url = "https://files.pythonhosted.org/packages/19/0c/286b6301ded2eccd4ffd0041a1b726afda999926cf720aab63adb68a1e36/watchfiles-1.1.1-cp312-cp312-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:30f7da3fb3f2844259cba4720c3fc7138eb0f7b659c38f3bfa65084c7fc7abce", size = 488813, upload-time = "2025-10-14T15:04:51.059Z" }, + { url = "https://files.pythonhosted.org/packages/c7/2b/8530ed41112dd4a22f4dcfdb5ccf6a1baad1ff6eed8dc5a5f09e7e8c41c7/watchfiles-1.1.1-cp312-cp312-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:f8979280bdafff686ba5e4d8f97840f929a87ed9cdf133cbbd42f7766774d2aa", size = 594816, upload-time = "2025-10-14T15:04:52.031Z" }, + { url = "https://files.pythonhosted.org/packages/ce/d2/f5f9fb49489f184f18470d4f99f4e862a4b3e9ac2865688eb2099e3d837a/watchfiles-1.1.1-cp312-cp312-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:dcc5c24523771db3a294c77d94771abcfcb82a0e0ee8efd910c37c59ec1b31bb", size = 475186, upload-time = "2025-10-14T15:04:53.064Z" }, + { url = "https://files.pythonhosted.org/packages/cf/68/5707da262a119fb06fbe214d82dd1fe4a6f4af32d2d14de368d0349eb52a/watchfiles-1.1.1-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:1db5d7ae38ff20153d542460752ff397fcf5c96090c1230803713cf3147a6803", size = 456812, upload-time = "2025-10-14T15:04:55.174Z" }, + { url = "https://files.pythonhosted.org/packages/66/ab/3cbb8756323e8f9b6f9acb9ef4ec26d42b2109bce830cc1f3468df20511d/watchfiles-1.1.1-cp312-cp312-musllinux_1_1_aarch64.whl", hash = "sha256:28475ddbde92df1874b6c5c8aaeb24ad5be47a11f87cde5a28ef3835932e3e94", size = 630196, upload-time = "2025-10-14T15:04:56.22Z" }, + { url = "https://files.pythonhosted.org/packages/78/46/7152ec29b8335f80167928944a94955015a345440f524d2dfe63fc2f437b/watchfiles-1.1.1-cp312-cp312-musllinux_1_1_x86_64.whl", hash = "sha256:36193ed342f5b9842edd3532729a2ad55c4160ffcfa3700e0d54be496b70dd43", size = 622657, upload-time = "2025-10-14T15:04:57.521Z" }, + { url = "https://files.pythonhosted.org/packages/0a/bf/95895e78dd75efe9a7f31733607f384b42eb5feb54bd2eb6ed57cc2e94f4/watchfiles-1.1.1-cp312-cp312-win32.whl", hash = "sha256:859e43a1951717cc8de7f4c77674a6d389b106361585951d9e69572823f311d9", size = 272042, upload-time = "2025-10-14T15:04:59.046Z" }, + { url = "https://files.pythonhosted.org/packages/87/0a/90eb755f568de2688cb220171c4191df932232c20946966c27a59c400850/watchfiles-1.1.1-cp312-cp312-win_amd64.whl", hash = "sha256:91d4c9a823a8c987cce8fa2690923b069966dabb196dd8d137ea2cede885fde9", size = 288410, upload-time = "2025-10-14T15:05:00.081Z" }, + { url = "https://files.pythonhosted.org/packages/36/76/f322701530586922fbd6723c4f91ace21364924822a8772c549483abed13/watchfiles-1.1.1-cp312-cp312-win_arm64.whl", hash = "sha256:a625815d4a2bdca61953dbba5a39d60164451ef34c88d751f6c368c3ea73d404", size = 278209, upload-time = "2025-10-14T15:05:01.168Z" }, + { url = "https://files.pythonhosted.org/packages/bb/f4/f750b29225fe77139f7ae5de89d4949f5a99f934c65a1f1c0b248f26f747/watchfiles-1.1.1-cp313-cp313-macosx_10_12_x86_64.whl", hash = "sha256:130e4876309e8686a5e37dba7d5e9bc77e6ed908266996ca26572437a5271e18", size = 404321, upload-time = "2025-10-14T15:05:02.063Z" }, + { url = "https://files.pythonhosted.org/packages/2b/f9/f07a295cde762644aa4c4bb0f88921d2d141af45e735b965fb2e87858328/watchfiles-1.1.1-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:5f3bde70f157f84ece3765b42b4a52c6ac1a50334903c6eaf765362f6ccca88a", size = 391783, upload-time = "2025-10-14T15:05:03.052Z" }, + { url = "https://files.pythonhosted.org/packages/bc/11/fc2502457e0bea39a5c958d86d2cb69e407a4d00b85735ca724bfa6e0d1a/watchfiles-1.1.1-cp313-cp313-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:14e0b1fe858430fc0251737ef3824c54027bedb8c37c38114488b8e131cf8219", size = 449279, upload-time = "2025-10-14T15:05:04.004Z" }, + { url = "https://files.pythonhosted.org/packages/e3/1f/d66bc15ea0b728df3ed96a539c777acfcad0eb78555ad9efcaa1274688f0/watchfiles-1.1.1-cp313-cp313-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:f27db948078f3823a6bb3b465180db8ebecf26dd5dae6f6180bd87383b6b4428", size = 459405, upload-time = "2025-10-14T15:05:04.942Z" }, + { url = "https://files.pythonhosted.org/packages/be/90/9f4a65c0aec3ccf032703e6db02d89a157462fbb2cf20dd415128251cac0/watchfiles-1.1.1-cp313-cp313-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:059098c3a429f62fc98e8ec62b982230ef2c8df68c79e826e37b895bc359a9c0", size = 488976, upload-time = "2025-10-14T15:05:05.905Z" }, + { url = "https://files.pythonhosted.org/packages/37/57/ee347af605d867f712be7029bb94c8c071732a4b44792e3176fa3c612d39/watchfiles-1.1.1-cp313-cp313-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:bfb5862016acc9b869bb57284e6cb35fdf8e22fe59f7548858e2f971d045f150", size = 595506, upload-time = "2025-10-14T15:05:06.906Z" }, + { url = "https://files.pythonhosted.org/packages/a8/78/cc5ab0b86c122047f75e8fc471c67a04dee395daf847d3e59381996c8707/watchfiles-1.1.1-cp313-cp313-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:319b27255aacd9923b8a276bb14d21a5f7ff82564c744235fc5eae58d95422ae", size = 474936, upload-time = "2025-10-14T15:05:07.906Z" }, + { url = "https://files.pythonhosted.org/packages/62/da/def65b170a3815af7bd40a3e7010bf6ab53089ef1b75d05dd5385b87cf08/watchfiles-1.1.1-cp313-cp313-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:c755367e51db90e75b19454b680903631d41f9e3607fbd941d296a020c2d752d", size = 456147, upload-time = "2025-10-14T15:05:09.138Z" }, + { url = "https://files.pythonhosted.org/packages/57/99/da6573ba71166e82d288d4df0839128004c67d2778d3b566c138695f5c0b/watchfiles-1.1.1-cp313-cp313-musllinux_1_1_aarch64.whl", hash = "sha256:c22c776292a23bfc7237a98f791b9ad3144b02116ff10d820829ce62dff46d0b", size = 630007, upload-time = "2025-10-14T15:05:10.117Z" }, + { url = "https://files.pythonhosted.org/packages/a8/51/7439c4dd39511368849eb1e53279cd3454b4a4dbace80bab88feeb83c6b5/watchfiles-1.1.1-cp313-cp313-musllinux_1_1_x86_64.whl", hash = "sha256:3a476189be23c3686bc2f4321dd501cb329c0a0469e77b7b534ee10129ae6374", size = 622280, upload-time = "2025-10-14T15:05:11.146Z" }, + { url = "https://files.pythonhosted.org/packages/95/9c/8ed97d4bba5db6fdcdb2b298d3898f2dd5c20f6b73aee04eabe56c59677e/watchfiles-1.1.1-cp313-cp313-win32.whl", hash = "sha256:bf0a91bfb5574a2f7fc223cf95eeea79abfefa404bf1ea5e339c0c1560ae99a0", size = 272056, upload-time = "2025-10-14T15:05:12.156Z" }, + { url = "https://files.pythonhosted.org/packages/1f/f3/c14e28429f744a260d8ceae18bf58c1d5fa56b50d006a7a9f80e1882cb0d/watchfiles-1.1.1-cp313-cp313-win_amd64.whl", hash = "sha256:52e06553899e11e8074503c8e716d574adeeb7e68913115c4b3653c53f9bae42", size = 288162, upload-time = "2025-10-14T15:05:13.208Z" }, + { url = "https://files.pythonhosted.org/packages/dc/61/fe0e56c40d5cd29523e398d31153218718c5786b5e636d9ae8ae79453d27/watchfiles-1.1.1-cp313-cp313-win_arm64.whl", hash = "sha256:ac3cc5759570cd02662b15fbcd9d917f7ecd47efe0d6b40474eafd246f91ea18", size = 277909, upload-time = "2025-10-14T15:05:14.49Z" }, + { url = "https://files.pythonhosted.org/packages/79/42/e0a7d749626f1e28c7108a99fb9bf524b501bbbeb9b261ceecde644d5a07/watchfiles-1.1.1-cp313-cp313t-macosx_10_12_x86_64.whl", hash = "sha256:563b116874a9a7ce6f96f87cd0b94f7faf92d08d0021e837796f0a14318ef8da", size = 403389, upload-time = "2025-10-14T15:05:15.777Z" }, + { url = "https://files.pythonhosted.org/packages/15/49/08732f90ce0fbbc13913f9f215c689cfc9ced345fb1bcd8829a50007cc8d/watchfiles-1.1.1-cp313-cp313t-macosx_11_0_arm64.whl", hash = "sha256:3ad9fe1dae4ab4212d8c91e80b832425e24f421703b5a42ef2e4a1e215aff051", size = 389964, upload-time = "2025-10-14T15:05:16.85Z" }, + { url = "https://files.pythonhosted.org/packages/27/0d/7c315d4bd5f2538910491a0393c56bf70d333d51bc5b34bee8e68e8cea19/watchfiles-1.1.1-cp313-cp313t-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:ce70f96a46b894b36eba678f153f052967a0d06d5b5a19b336ab0dbbd029f73e", size = 448114, upload-time = "2025-10-14T15:05:17.876Z" }, + { url = "https://files.pythonhosted.org/packages/c3/24/9e096de47a4d11bc4df41e9d1e61776393eac4cb6eb11b3e23315b78b2cc/watchfiles-1.1.1-cp313-cp313t-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:cb467c999c2eff23a6417e58d75e5828716f42ed8289fe6b77a7e5a91036ca70", size = 460264, upload-time = "2025-10-14T15:05:18.962Z" }, + { url = "https://files.pythonhosted.org/packages/cc/0f/e8dea6375f1d3ba5fcb0b3583e2b493e77379834c74fd5a22d66d85d6540/watchfiles-1.1.1-cp313-cp313t-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:836398932192dae4146c8f6f737d74baeac8b70ce14831a239bdb1ca882fc261", size = 487877, upload-time = "2025-10-14T15:05:20.094Z" }, + { url = "https://files.pythonhosted.org/packages/ac/5b/df24cfc6424a12deb41503b64d42fbea6b8cb357ec62ca84a5a3476f654a/watchfiles-1.1.1-cp313-cp313t-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:743185e7372b7bc7c389e1badcc606931a827112fbbd37f14c537320fca08620", size = 595176, upload-time = "2025-10-14T15:05:21.134Z" }, + { url = "https://files.pythonhosted.org/packages/8f/b5/853b6757f7347de4e9b37e8cc3289283fb983cba1ab4d2d7144694871d9c/watchfiles-1.1.1-cp313-cp313t-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:afaeff7696e0ad9f02cbb8f56365ff4686ab205fcf9c4c5b6fdfaaa16549dd04", size = 473577, upload-time = "2025-10-14T15:05:22.306Z" }, + { url = "https://files.pythonhosted.org/packages/e1/f7/0a4467be0a56e80447c8529c9fce5b38eab4f513cb3d9bf82e7392a5696b/watchfiles-1.1.1-cp313-cp313t-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:3f7eb7da0eb23aa2ba036d4f616d46906013a68caf61b7fdbe42fc8b25132e77", size = 455425, upload-time = "2025-10-14T15:05:23.348Z" }, + { url = "https://files.pythonhosted.org/packages/8e/e0/82583485ea00137ddf69bc84a2db88bd92ab4a6e3c405e5fb878ead8d0e7/watchfiles-1.1.1-cp313-cp313t-musllinux_1_1_aarch64.whl", hash = "sha256:831a62658609f0e5c64178211c942ace999517f5770fe9436be4c2faeba0c0ef", size = 628826, upload-time = "2025-10-14T15:05:24.398Z" }, + { url = "https://files.pythonhosted.org/packages/28/9a/a785356fccf9fae84c0cc90570f11702ae9571036fb25932f1242c82191c/watchfiles-1.1.1-cp313-cp313t-musllinux_1_1_x86_64.whl", hash = "sha256:f9a2ae5c91cecc9edd47e041a930490c31c3afb1f5e6d71de3dc671bfaca02bf", size = 622208, upload-time = "2025-10-14T15:05:25.45Z" }, + { url = "https://files.pythonhosted.org/packages/c3/f4/0872229324ef69b2c3edec35e84bd57a1289e7d3fe74588048ed8947a323/watchfiles-1.1.1-cp314-cp314-macosx_10_12_x86_64.whl", hash = "sha256:d1715143123baeeaeadec0528bb7441103979a1d5f6fd0e1f915383fea7ea6d5", size = 404315, upload-time = "2025-10-14T15:05:26.501Z" }, + { url = "https://files.pythonhosted.org/packages/7b/22/16d5331eaed1cb107b873f6ae1b69e9ced582fcf0c59a50cd84f403b1c32/watchfiles-1.1.1-cp314-cp314-macosx_11_0_arm64.whl", hash = "sha256:39574d6370c4579d7f5d0ad940ce5b20db0e4117444e39b6d8f99db5676c52fd", size = 390869, upload-time = "2025-10-14T15:05:27.649Z" }, + { url = "https://files.pythonhosted.org/packages/b2/7e/5643bfff5acb6539b18483128fdc0ef2cccc94a5b8fbda130c823e8ed636/watchfiles-1.1.1-cp314-cp314-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:7365b92c2e69ee952902e8f70f3ba6360d0d596d9299d55d7d386df84b6941fb", size = 449919, upload-time = "2025-10-14T15:05:28.701Z" }, + { url = "https://files.pythonhosted.org/packages/51/2e/c410993ba5025a9f9357c376f48976ef0e1b1aefb73b97a5ae01a5972755/watchfiles-1.1.1-cp314-cp314-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:bfff9740c69c0e4ed32416f013f3c45e2ae42ccedd1167ef2d805c000b6c71a5", size = 460845, upload-time = "2025-10-14T15:05:30.064Z" }, + { url = "https://files.pythonhosted.org/packages/8e/a4/2df3b404469122e8680f0fcd06079317e48db58a2da2950fb45020947734/watchfiles-1.1.1-cp314-cp314-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:b27cf2eb1dda37b2089e3907d8ea92922b673c0c427886d4edc6b94d8dfe5db3", size = 489027, upload-time = "2025-10-14T15:05:31.064Z" }, + { url = "https://files.pythonhosted.org/packages/ea/84/4587ba5b1f267167ee715b7f66e6382cca6938e0a4b870adad93e44747e6/watchfiles-1.1.1-cp314-cp314-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:526e86aced14a65a5b0ec50827c745597c782ff46b571dbfe46192ab9e0b3c33", size = 595615, upload-time = "2025-10-14T15:05:32.074Z" }, + { url = "https://files.pythonhosted.org/packages/6a/0f/c6988c91d06e93cd0bb3d4a808bcf32375ca1904609835c3031799e3ecae/watchfiles-1.1.1-cp314-cp314-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:04e78dd0b6352db95507fd8cb46f39d185cf8c74e4cf1e4fbad1d3df96faf510", size = 474836, upload-time = "2025-10-14T15:05:33.209Z" }, + { url = "https://files.pythonhosted.org/packages/b4/36/ded8aebea91919485b7bbabbd14f5f359326cb5ec218cd67074d1e426d74/watchfiles-1.1.1-cp314-cp314-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:5c85794a4cfa094714fb9c08d4a218375b2b95b8ed1666e8677c349906246c05", size = 455099, upload-time = "2025-10-14T15:05:34.189Z" }, + { url = "https://files.pythonhosted.org/packages/98/e0/8c9bdba88af756a2fce230dd365fab2baf927ba42cd47521ee7498fd5211/watchfiles-1.1.1-cp314-cp314-musllinux_1_1_aarch64.whl", hash = "sha256:74d5012b7630714b66be7b7b7a78855ef7ad58e8650c73afc4c076a1f480a8d6", size = 630626, upload-time = "2025-10-14T15:05:35.216Z" }, + { url = "https://files.pythonhosted.org/packages/2a/84/a95db05354bf2d19e438520d92a8ca475e578c647f78f53197f5a2f17aaf/watchfiles-1.1.1-cp314-cp314-musllinux_1_1_x86_64.whl", hash = "sha256:8fbe85cb3201c7d380d3d0b90e63d520f15d6afe217165d7f98c9c649654db81", size = 622519, upload-time = "2025-10-14T15:05:36.259Z" }, + { url = "https://files.pythonhosted.org/packages/1d/ce/d8acdc8de545de995c339be67711e474c77d643555a9bb74a9334252bd55/watchfiles-1.1.1-cp314-cp314-win32.whl", hash = "sha256:3fa0b59c92278b5a7800d3ee7733da9d096d4aabcfabb9a928918bd276ef9b9b", size = 272078, upload-time = "2025-10-14T15:05:37.63Z" }, + { url = "https://files.pythonhosted.org/packages/c4/c9/a74487f72d0451524be827e8edec251da0cc1fcf111646a511ae752e1a3d/watchfiles-1.1.1-cp314-cp314-win_amd64.whl", hash = "sha256:c2047d0b6cea13b3316bdbafbfa0c4228ae593d995030fda39089d36e64fc03a", size = 287664, upload-time = "2025-10-14T15:05:38.95Z" }, + { url = "https://files.pythonhosted.org/packages/df/b8/8ac000702cdd496cdce998c6f4ee0ca1f15977bba51bdf07d872ebdfc34c/watchfiles-1.1.1-cp314-cp314-win_arm64.whl", hash = "sha256:842178b126593addc05acf6fce960d28bc5fae7afbaa2c6c1b3a7b9460e5be02", size = 277154, upload-time = "2025-10-14T15:05:39.954Z" }, + { url = "https://files.pythonhosted.org/packages/47/a8/e3af2184707c29f0f14b1963c0aace6529f9d1b8582d5b99f31bbf42f59e/watchfiles-1.1.1-cp314-cp314t-macosx_10_12_x86_64.whl", hash = "sha256:88863fbbc1a7312972f1c511f202eb30866370ebb8493aef2812b9ff28156a21", size = 403820, upload-time = "2025-10-14T15:05:40.932Z" }, + { url = "https://files.pythonhosted.org/packages/c0/ec/e47e307c2f4bd75f9f9e8afbe3876679b18e1bcec449beca132a1c5ffb2d/watchfiles-1.1.1-cp314-cp314t-macosx_11_0_arm64.whl", hash = "sha256:55c7475190662e202c08c6c0f4d9e345a29367438cf8e8037f3155e10a88d5a5", size = 390510, upload-time = "2025-10-14T15:05:41.945Z" }, + { url = "https://files.pythonhosted.org/packages/d5/a0/ad235642118090f66e7b2f18fd5c42082418404a79205cdfca50b6309c13/watchfiles-1.1.1-cp314-cp314t-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:3f53fa183d53a1d7a8852277c92b967ae99c2d4dcee2bfacff8868e6e30b15f7", size = 448408, upload-time = "2025-10-14T15:05:43.385Z" }, + { url = "https://files.pythonhosted.org/packages/df/85/97fa10fd5ff3332ae17e7e40e20784e419e28521549780869f1413742e9d/watchfiles-1.1.1-cp314-cp314t-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:6aae418a8b323732fa89721d86f39ec8f092fc2af67f4217a2b07fd3e93c6101", size = 458968, upload-time = "2025-10-14T15:05:44.404Z" }, + { url = "https://files.pythonhosted.org/packages/47/c2/9059c2e8966ea5ce678166617a7f75ecba6164375f3b288e50a40dc6d489/watchfiles-1.1.1-cp314-cp314t-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:f096076119da54a6080e8920cbdaac3dbee667eb91dcc5e5b78840b87415bd44", size = 488096, upload-time = "2025-10-14T15:05:45.398Z" }, + { url = "https://files.pythonhosted.org/packages/94/44/d90a9ec8ac309bc26db808a13e7bfc0e4e78b6fc051078a554e132e80160/watchfiles-1.1.1-cp314-cp314t-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:00485f441d183717038ed2e887a7c868154f216877653121068107b227a2f64c", size = 596040, upload-time = "2025-10-14T15:05:46.502Z" }, + { url = "https://files.pythonhosted.org/packages/95/68/4e3479b20ca305cfc561db3ed207a8a1c745ee32bf24f2026a129d0ddb6e/watchfiles-1.1.1-cp314-cp314t-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:a55f3e9e493158d7bfdb60a1165035f1cf7d320914e7b7ea83fe22c6023b58fc", size = 473847, upload-time = "2025-10-14T15:05:47.484Z" }, + { url = "https://files.pythonhosted.org/packages/4f/55/2af26693fd15165c4ff7857e38330e1b61ab8c37d15dc79118cdba115b7a/watchfiles-1.1.1-cp314-cp314t-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:8c91ed27800188c2ae96d16e3149f199d62f86c7af5f5f4d2c61a3ed8cd3666c", size = 455072, upload-time = "2025-10-14T15:05:48.928Z" }, + { url = "https://files.pythonhosted.org/packages/66/1d/d0d200b10c9311ec25d2273f8aad8c3ef7cc7ea11808022501811208a750/watchfiles-1.1.1-cp314-cp314t-musllinux_1_1_aarch64.whl", hash = "sha256:311ff15a0bae3714ffb603e6ba6dbfba4065ab60865d15a6ec544133bdb21099", size = 629104, upload-time = "2025-10-14T15:05:49.908Z" }, + { url = "https://files.pythonhosted.org/packages/e3/bd/fa9bb053192491b3867ba07d2343d9f2252e00811567d30ae8d0f78136fe/watchfiles-1.1.1-cp314-cp314t-musllinux_1_1_x86_64.whl", hash = "sha256:a916a2932da8f8ab582f242c065f5c81bed3462849ca79ee357dd9551b0e9b01", size = 622112, upload-time = "2025-10-14T15:05:50.941Z" }, + { url = "https://files.pythonhosted.org/packages/a4/68/a7303a15cc797ab04d58f1fea7f67c50bd7f80090dfd7e750e7576e07582/watchfiles-1.1.1-cp39-cp39-macosx_10_12_x86_64.whl", hash = "sha256:c882d69f6903ef6092bedfb7be973d9319940d56b8427ab9187d1ecd73438a70", size = 409220, upload-time = "2025-10-14T15:05:51.917Z" }, + { url = "https://files.pythonhosted.org/packages/99/b8/d1857ce9ac76034c053fa7ef0e0ef92d8bd031e842ea6f5171725d31e88f/watchfiles-1.1.1-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:d6ff426a7cb54f310d51bfe83fe9f2bbe40d540c741dc974ebc30e6aa238f52e", size = 396712, upload-time = "2025-10-14T15:05:53.437Z" }, + { url = "https://files.pythonhosted.org/packages/41/7a/da7ada566f48beaa6a30b13335b49d1f6febaf3a5ddbd1d92163a1002cf4/watchfiles-1.1.1-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:79ff6c6eadf2e3fc0d7786331362e6ef1e51125892c75f1004bd6b52155fb956", size = 451462, upload-time = "2025-10-14T15:05:54.742Z" }, + { url = "https://files.pythonhosted.org/packages/e2/b2/7cb9e0d5445a8d45c4cccd68a590d9e3a453289366b96ff37d1075aaebef/watchfiles-1.1.1-cp39-cp39-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:c1f5210f1b8fc91ead1283c6fd89f70e76fb07283ec738056cf34d51e9c1d62c", size = 460811, upload-time = "2025-10-14T15:05:55.743Z" }, + { url = "https://files.pythonhosted.org/packages/04/9d/b07d4491dde6db6ea6c680fdec452f4be363d65c82004faf2d853f59b76f/watchfiles-1.1.1-cp39-cp39-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:b9c4702f29ca48e023ffd9b7ff6b822acdf47cb1ff44cb490a3f1d5ec8987e9c", size = 490576, upload-time = "2025-10-14T15:05:56.983Z" }, + { url = "https://files.pythonhosted.org/packages/56/03/e64dcab0a1806157db272a61b7891b062f441a30580a581ae72114259472/watchfiles-1.1.1-cp39-cp39-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:acb08650863767cbc58bca4813b92df4d6c648459dcaa3d4155681962b2aa2d3", size = 597726, upload-time = "2025-10-14T15:05:57.986Z" }, + { url = "https://files.pythonhosted.org/packages/5c/8e/a827cf4a8d5f2903a19a934dcf512082eb07675253e154d4cd9367978a58/watchfiles-1.1.1-cp39-cp39-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:08af70fd77eee58549cd69c25055dc344f918d992ff626068242259f98d598a2", size = 474900, upload-time = "2025-10-14T15:05:59.378Z" }, + { url = "https://files.pythonhosted.org/packages/dc/a6/94fed0b346b85b22303a12eee5f431006fae6af70d841cac2f4403245533/watchfiles-1.1.1-cp39-cp39-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:6c3631058c37e4a0ec440bf583bc53cdbd13e5661bb6f465bc1d88ee9a0a4d02", size = 457521, upload-time = "2025-10-14T15:06:00.419Z" }, + { url = "https://files.pythonhosted.org/packages/c4/64/bc3331150e8f3c778d48a4615d4b72b3d2d87868635e6c54bbd924946189/watchfiles-1.1.1-cp39-cp39-musllinux_1_1_aarch64.whl", hash = "sha256:cf57a27fb986c6243d2ee78392c503826056ffe0287e8794503b10fb51b881be", size = 632191, upload-time = "2025-10-14T15:06:01.621Z" }, + { url = "https://files.pythonhosted.org/packages/e4/84/f39e19549c2f3ec97225dcb2ceb9a7bb3c5004ed227aad1f321bf0ff2051/watchfiles-1.1.1-cp39-cp39-musllinux_1_1_x86_64.whl", hash = "sha256:d7e7067c98040d646982daa1f37a33d3544138ea155536c2e0e63e07ff8a7e0f", size = 623923, upload-time = "2025-10-14T15:06:02.671Z" }, + { url = "https://files.pythonhosted.org/packages/0e/24/0759ae15d9a0c9c5fe946bd4cf45ab9e7bad7cfede2c06dc10f59171b29f/watchfiles-1.1.1-cp39-cp39-win32.whl", hash = "sha256:6c9c9262f454d1c4d8aaa7050121eb4f3aea197360553699520767daebf2180b", size = 274010, upload-time = "2025-10-14T15:06:03.779Z" }, + { url = "https://files.pythonhosted.org/packages/7e/3b/eb26cddd4dfa081e2bf6918be3b2fc05ee3b55c1d21331d5562ee0c6aaad/watchfiles-1.1.1-cp39-cp39-win_amd64.whl", hash = "sha256:74472234c8370669850e1c312490f6026d132ca2d396abfad8830b4f1c096957", size = 289090, upload-time = "2025-10-14T15:06:04.821Z" }, + { url = "https://files.pythonhosted.org/packages/ba/4c/a888c91e2e326872fa4705095d64acd8aa2fb9c1f7b9bd0588f33850516c/watchfiles-1.1.1-pp310-pypy310_pp73-macosx_10_12_x86_64.whl", hash = "sha256:17ef139237dfced9da49fb7f2232c86ca9421f666d78c264c7ffca6601d154c3", size = 409611, upload-time = "2025-10-14T15:06:05.809Z" }, + { url = "https://files.pythonhosted.org/packages/1e/c7/5420d1943c8e3ce1a21c0a9330bcf7edafb6aa65d26b21dbb3267c9e8112/watchfiles-1.1.1-pp310-pypy310_pp73-macosx_11_0_arm64.whl", hash = "sha256:672b8adf25b1a0d35c96b5888b7b18699d27d4194bac8beeae75be4b7a3fc9b2", size = 396889, upload-time = "2025-10-14T15:06:07.035Z" }, + { url = "https://files.pythonhosted.org/packages/0c/e5/0072cef3804ce8d3aaddbfe7788aadff6b3d3f98a286fdbee9fd74ca59a7/watchfiles-1.1.1-pp310-pypy310_pp73-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:77a13aea58bc2b90173bc69f2a90de8e282648939a00a602e1dc4ee23e26b66d", size = 451616, upload-time = "2025-10-14T15:06:08.072Z" }, + { url = "https://files.pythonhosted.org/packages/83/4e/b87b71cbdfad81ad7e83358b3e447fedd281b880a03d64a760fe0a11fc2e/watchfiles-1.1.1-pp310-pypy310_pp73-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:0b495de0bb386df6a12b18335a0285dda90260f51bdb505503c02bcd1ce27a8b", size = 458413, upload-time = "2025-10-14T15:06:09.209Z" }, + { url = "https://files.pythonhosted.org/packages/d3/8e/e500f8b0b77be4ff753ac94dc06b33d8f0d839377fee1b78e8c8d8f031bf/watchfiles-1.1.1-pp311-pypy311_pp73-macosx_10_12_x86_64.whl", hash = "sha256:db476ab59b6765134de1d4fe96a1a9c96ddf091683599be0f26147ea1b2e4b88", size = 408250, upload-time = "2025-10-14T15:06:10.264Z" }, + { url = "https://files.pythonhosted.org/packages/bd/95/615e72cd27b85b61eec764a5ca51bd94d40b5adea5ff47567d9ebc4d275a/watchfiles-1.1.1-pp311-pypy311_pp73-macosx_11_0_arm64.whl", hash = "sha256:89eef07eee5e9d1fda06e38822ad167a044153457e6fd997f8a858ab7564a336", size = 396117, upload-time = "2025-10-14T15:06:11.28Z" }, + { url = "https://files.pythonhosted.org/packages/c9/81/e7fe958ce8a7fb5c73cc9fb07f5aeaf755e6aa72498c57d760af760c91f8/watchfiles-1.1.1-pp311-pypy311_pp73-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:ce19e06cbda693e9e7686358af9cd6f5d61312ab8b00488bc36f5aabbaf77e24", size = 450493, upload-time = "2025-10-14T15:06:12.321Z" }, + { url = "https://files.pythonhosted.org/packages/6e/d4/ed38dd3b1767193de971e694aa544356e63353c33a85d948166b5ff58b9e/watchfiles-1.1.1-pp311-pypy311_pp73-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:3e6f39af2eab0118338902798b5aa6664f46ff66bc0280de76fca67a7f262a49", size = 457546, upload-time = "2025-10-14T15:06:13.372Z" }, + { url = "https://files.pythonhosted.org/packages/00/db/38a2c52fdbbfe2fc7ffaaaaaebc927d52b9f4d5139bba3186c19a7463001/watchfiles-1.1.1-pp39-pypy39_pp73-macosx_10_12_x86_64.whl", hash = "sha256:cdab464fee731e0884c35ae3588514a9bcf718d0e2c82169c1c4a85cc19c3c7f", size = 409210, upload-time = "2025-10-14T15:06:14.492Z" }, + { url = "https://files.pythonhosted.org/packages/d1/43/d7e8b71f6c21ff813ee8da1006f89b6c7fff047fb4c8b16ceb5e840599c5/watchfiles-1.1.1-pp39-pypy39_pp73-macosx_11_0_arm64.whl", hash = "sha256:3dbd8cbadd46984f802f6d479b7e3afa86c42d13e8f0f322d669d79722c8ec34", size = 397286, upload-time = "2025-10-14T15:06:16.177Z" }, + { url = "https://files.pythonhosted.org/packages/1f/5d/884074a5269317e75bd0b915644b702b89de73e61a8a7446e2b225f45b1f/watchfiles-1.1.1-pp39-pypy39_pp73-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:5524298e3827105b61951a29c3512deb9578586abf3a7c5da4a8069df247cccc", size = 451768, upload-time = "2025-10-14T15:06:18.266Z" }, + { url = "https://files.pythonhosted.org/packages/17/71/7ffcaa9b5e8961a25026058058c62ec8f604d2a6e8e1e94bee8a09e1593f/watchfiles-1.1.1-pp39-pypy39_pp73-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:4b943d3668d61cfa528eb949577479d3b077fd25fb83c641235437bc0b5bc60e", size = 458561, upload-time = "2025-10-14T15:06:19.323Z" }, +] + +[[package]] +name = "websockets" +version = "15.0.1" +source = { registry = "https://pypi.org/simple" } +resolution-markers = [ + "python_full_version < '3.10'", +] +sdist = { url = "https://files.pythonhosted.org/packages/21/e6/26d09fab466b7ca9c7737474c52be4f76a40301b08362eb2dbc19dcc16c1/websockets-15.0.1.tar.gz", hash = "sha256:82544de02076bafba038ce055ee6412d68da13ab47f0c60cab827346de828dee", size = 177016, upload-time = "2025-03-05T20:03:41.606Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/1e/da/6462a9f510c0c49837bbc9345aca92d767a56c1fb2939e1579df1e1cdcf7/websockets-15.0.1-cp310-cp310-macosx_10_9_universal2.whl", hash = "sha256:d63efaa0cd96cf0c5fe4d581521d9fa87744540d4bc999ae6e08595a1014b45b", size = 175423, upload-time = "2025-03-05T20:01:35.363Z" }, + { url = "https://files.pythonhosted.org/packages/1c/9f/9d11c1a4eb046a9e106483b9ff69bce7ac880443f00e5ce64261b47b07e7/websockets-15.0.1-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:ac60e3b188ec7574cb761b08d50fcedf9d77f1530352db4eef1707fe9dee7205", size = 173080, upload-time = "2025-03-05T20:01:37.304Z" }, + { url = "https://files.pythonhosted.org/packages/d5/4f/b462242432d93ea45f297b6179c7333dd0402b855a912a04e7fc61c0d71f/websockets-15.0.1-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:5756779642579d902eed757b21b0164cd6fe338506a8083eb58af5c372e39d9a", size = 173329, upload-time = "2025-03-05T20:01:39.668Z" }, + { url = "https://files.pythonhosted.org/packages/6e/0c/6afa1f4644d7ed50284ac59cc70ef8abd44ccf7d45850d989ea7310538d0/websockets-15.0.1-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:0fdfe3e2a29e4db3659dbd5bbf04560cea53dd9610273917799f1cde46aa725e", size = 182312, upload-time = "2025-03-05T20:01:41.815Z" }, + { url = "https://files.pythonhosted.org/packages/dd/d4/ffc8bd1350b229ca7a4db2a3e1c482cf87cea1baccd0ef3e72bc720caeec/websockets-15.0.1-cp310-cp310-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:4c2529b320eb9e35af0fa3016c187dffb84a3ecc572bcee7c3ce302bfeba52bf", size = 181319, upload-time = "2025-03-05T20:01:43.967Z" }, + { url = "https://files.pythonhosted.org/packages/97/3a/5323a6bb94917af13bbb34009fac01e55c51dfde354f63692bf2533ffbc2/websockets-15.0.1-cp310-cp310-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:ac1e5c9054fe23226fb11e05a6e630837f074174c4c2f0fe442996112a6de4fb", size = 181631, upload-time = "2025-03-05T20:01:46.104Z" }, + { url = "https://files.pythonhosted.org/packages/a6/cc/1aeb0f7cee59ef065724041bb7ed667b6ab1eeffe5141696cccec2687b66/websockets-15.0.1-cp310-cp310-musllinux_1_2_aarch64.whl", hash = "sha256:5df592cd503496351d6dc14f7cdad49f268d8e618f80dce0cd5a36b93c3fc08d", size = 182016, upload-time = "2025-03-05T20:01:47.603Z" }, + { url = "https://files.pythonhosted.org/packages/79/f9/c86f8f7af208e4161a7f7e02774e9d0a81c632ae76db2ff22549e1718a51/websockets-15.0.1-cp310-cp310-musllinux_1_2_i686.whl", hash = "sha256:0a34631031a8f05657e8e90903e656959234f3a04552259458aac0b0f9ae6fd9", size = 181426, upload-time = "2025-03-05T20:01:48.949Z" }, + { url = "https://files.pythonhosted.org/packages/c7/b9/828b0bc6753db905b91df6ae477c0b14a141090df64fb17f8a9d7e3516cf/websockets-15.0.1-cp310-cp310-musllinux_1_2_x86_64.whl", hash = "sha256:3d00075aa65772e7ce9e990cab3ff1de702aa09be3940d1dc88d5abf1ab8a09c", size = 181360, upload-time = "2025-03-05T20:01:50.938Z" }, + { url = "https://files.pythonhosted.org/packages/89/fb/250f5533ec468ba6327055b7d98b9df056fb1ce623b8b6aaafb30b55d02e/websockets-15.0.1-cp310-cp310-win32.whl", hash = "sha256:1234d4ef35db82f5446dca8e35a7da7964d02c127b095e172e54397fb6a6c256", size = 176388, upload-time = "2025-03-05T20:01:52.213Z" }, + { url = "https://files.pythonhosted.org/packages/1c/46/aca7082012768bb98e5608f01658ff3ac8437e563eca41cf068bd5849a5e/websockets-15.0.1-cp310-cp310-win_amd64.whl", hash = "sha256:39c1fec2c11dc8d89bba6b2bf1556af381611a173ac2b511cf7231622058af41", size = 176830, upload-time = "2025-03-05T20:01:53.922Z" }, + { url = "https://files.pythonhosted.org/packages/9f/32/18fcd5919c293a398db67443acd33fde142f283853076049824fc58e6f75/websockets-15.0.1-cp311-cp311-macosx_10_9_universal2.whl", hash = "sha256:823c248b690b2fd9303ba00c4f66cd5e2d8c3ba4aa968b2779be9532a4dad431", size = 175423, upload-time = "2025-03-05T20:01:56.276Z" }, + { url = "https://files.pythonhosted.org/packages/76/70/ba1ad96b07869275ef42e2ce21f07a5b0148936688c2baf7e4a1f60d5058/websockets-15.0.1-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:678999709e68425ae2593acf2e3ebcbcf2e69885a5ee78f9eb80e6e371f1bf57", size = 173082, upload-time = "2025-03-05T20:01:57.563Z" }, + { url = "https://files.pythonhosted.org/packages/86/f2/10b55821dd40eb696ce4704a87d57774696f9451108cff0d2824c97e0f97/websockets-15.0.1-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:d50fd1ee42388dcfb2b3676132c78116490976f1300da28eb629272d5d93e905", size = 173330, upload-time = "2025-03-05T20:01:59.063Z" }, + { url = "https://files.pythonhosted.org/packages/a5/90/1c37ae8b8a113d3daf1065222b6af61cc44102da95388ac0018fcb7d93d9/websockets-15.0.1-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:d99e5546bf73dbad5bf3547174cd6cb8ba7273062a23808ffea025ecb1cf8562", size = 182878, upload-time = "2025-03-05T20:02:00.305Z" }, + { url = "https://files.pythonhosted.org/packages/8e/8d/96e8e288b2a41dffafb78e8904ea7367ee4f891dafc2ab8d87e2124cb3d3/websockets-15.0.1-cp311-cp311-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:66dd88c918e3287efc22409d426c8f729688d89a0c587c88971a0faa2c2f3792", size = 181883, upload-time = "2025-03-05T20:02:03.148Z" }, + { url = "https://files.pythonhosted.org/packages/93/1f/5d6dbf551766308f6f50f8baf8e9860be6182911e8106da7a7f73785f4c4/websockets-15.0.1-cp311-cp311-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:8dd8327c795b3e3f219760fa603dcae1dcc148172290a8ab15158cf85a953413", size = 182252, upload-time = "2025-03-05T20:02:05.29Z" }, + { url = "https://files.pythonhosted.org/packages/d4/78/2d4fed9123e6620cbf1706c0de8a1632e1a28e7774d94346d7de1bba2ca3/websockets-15.0.1-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:8fdc51055e6ff4adeb88d58a11042ec9a5eae317a0a53d12c062c8a8865909e8", size = 182521, upload-time = "2025-03-05T20:02:07.458Z" }, + { url = "https://files.pythonhosted.org/packages/e7/3b/66d4c1b444dd1a9823c4a81f50231b921bab54eee2f69e70319b4e21f1ca/websockets-15.0.1-cp311-cp311-musllinux_1_2_i686.whl", hash = "sha256:693f0192126df6c2327cce3baa7c06f2a117575e32ab2308f7f8216c29d9e2e3", size = 181958, upload-time = "2025-03-05T20:02:09.842Z" }, + { url = "https://files.pythonhosted.org/packages/08/ff/e9eed2ee5fed6f76fdd6032ca5cd38c57ca9661430bb3d5fb2872dc8703c/websockets-15.0.1-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:54479983bd5fb469c38f2f5c7e3a24f9a4e70594cd68cd1fa6b9340dadaff7cf", size = 181918, upload-time = "2025-03-05T20:02:11.968Z" }, + { url = "https://files.pythonhosted.org/packages/d8/75/994634a49b7e12532be6a42103597b71098fd25900f7437d6055ed39930a/websockets-15.0.1-cp311-cp311-win32.whl", hash = "sha256:16b6c1b3e57799b9d38427dda63edcbe4926352c47cf88588c0be4ace18dac85", size = 176388, upload-time = "2025-03-05T20:02:13.32Z" }, + { url = "https://files.pythonhosted.org/packages/98/93/e36c73f78400a65f5e236cd376713c34182e6663f6889cd45a4a04d8f203/websockets-15.0.1-cp311-cp311-win_amd64.whl", hash = "sha256:27ccee0071a0e75d22cb35849b1db43f2ecd3e161041ac1ee9d2352ddf72f065", size = 176828, upload-time = "2025-03-05T20:02:14.585Z" }, + { url = "https://files.pythonhosted.org/packages/51/6b/4545a0d843594f5d0771e86463606a3988b5a09ca5123136f8a76580dd63/websockets-15.0.1-cp312-cp312-macosx_10_13_universal2.whl", hash = "sha256:3e90baa811a5d73f3ca0bcbf32064d663ed81318ab225ee4f427ad4e26e5aff3", size = 175437, upload-time = "2025-03-05T20:02:16.706Z" }, + { url = "https://files.pythonhosted.org/packages/f4/71/809a0f5f6a06522af902e0f2ea2757f71ead94610010cf570ab5c98e99ed/websockets-15.0.1-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:592f1a9fe869c778694f0aa806ba0374e97648ab57936f092fd9d87f8bc03665", size = 173096, upload-time = "2025-03-05T20:02:18.832Z" }, + { url = "https://files.pythonhosted.org/packages/3d/69/1a681dd6f02180916f116894181eab8b2e25b31e484c5d0eae637ec01f7c/websockets-15.0.1-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:0701bc3cfcb9164d04a14b149fd74be7347a530ad3bbf15ab2c678a2cd3dd9a2", size = 173332, upload-time = "2025-03-05T20:02:20.187Z" }, + { url = "https://files.pythonhosted.org/packages/a6/02/0073b3952f5bce97eafbb35757f8d0d54812b6174ed8dd952aa08429bcc3/websockets-15.0.1-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:e8b56bdcdb4505c8078cb6c7157d9811a85790f2f2b3632c7d1462ab5783d215", size = 183152, upload-time = "2025-03-05T20:02:22.286Z" }, + { url = "https://files.pythonhosted.org/packages/74/45/c205c8480eafd114b428284840da0b1be9ffd0e4f87338dc95dc6ff961a1/websockets-15.0.1-cp312-cp312-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:0af68c55afbd5f07986df82831c7bff04846928ea8d1fd7f30052638788bc9b5", size = 182096, upload-time = "2025-03-05T20:02:24.368Z" }, + { url = "https://files.pythonhosted.org/packages/14/8f/aa61f528fba38578ec553c145857a181384c72b98156f858ca5c8e82d9d3/websockets-15.0.1-cp312-cp312-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:64dee438fed052b52e4f98f76c5790513235efaa1ef7f3f2192c392cd7c91b65", size = 182523, upload-time = "2025-03-05T20:02:25.669Z" }, + { url = "https://files.pythonhosted.org/packages/ec/6d/0267396610add5bc0d0d3e77f546d4cd287200804fe02323797de77dbce9/websockets-15.0.1-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:d5f6b181bb38171a8ad1d6aa58a67a6aa9d4b38d0f8c5f496b9e42561dfc62fe", size = 182790, upload-time = "2025-03-05T20:02:26.99Z" }, + { url = "https://files.pythonhosted.org/packages/02/05/c68c5adbf679cf610ae2f74a9b871ae84564462955d991178f95a1ddb7dd/websockets-15.0.1-cp312-cp312-musllinux_1_2_i686.whl", hash = "sha256:5d54b09eba2bada6011aea5375542a157637b91029687eb4fdb2dab11059c1b4", size = 182165, upload-time = "2025-03-05T20:02:30.291Z" }, + { url = "https://files.pythonhosted.org/packages/29/93/bb672df7b2f5faac89761cb5fa34f5cec45a4026c383a4b5761c6cea5c16/websockets-15.0.1-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:3be571a8b5afed347da347bfcf27ba12b069d9d7f42cb8c7028b5e98bbb12597", size = 182160, upload-time = "2025-03-05T20:02:31.634Z" }, + { url = "https://files.pythonhosted.org/packages/ff/83/de1f7709376dc3ca9b7eeb4b9a07b4526b14876b6d372a4dc62312bebee0/websockets-15.0.1-cp312-cp312-win32.whl", hash = "sha256:c338ffa0520bdb12fbc527265235639fb76e7bc7faafbb93f6ba80d9c06578a9", size = 176395, upload-time = "2025-03-05T20:02:33.017Z" }, + { url = "https://files.pythonhosted.org/packages/7d/71/abf2ebc3bbfa40f391ce1428c7168fb20582d0ff57019b69ea20fa698043/websockets-15.0.1-cp312-cp312-win_amd64.whl", hash = "sha256:fcd5cf9e305d7b8338754470cf69cf81f420459dbae8a3b40cee57417f4614a7", size = 176841, upload-time = "2025-03-05T20:02:34.498Z" }, + { url = "https://files.pythonhosted.org/packages/cb/9f/51f0cf64471a9d2b4d0fc6c534f323b664e7095640c34562f5182e5a7195/websockets-15.0.1-cp313-cp313-macosx_10_13_universal2.whl", hash = "sha256:ee443ef070bb3b6ed74514f5efaa37a252af57c90eb33b956d35c8e9c10a1931", size = 175440, upload-time = "2025-03-05T20:02:36.695Z" }, + { url = "https://files.pythonhosted.org/packages/8a/05/aa116ec9943c718905997412c5989f7ed671bc0188ee2ba89520e8765d7b/websockets-15.0.1-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:5a939de6b7b4e18ca683218320fc67ea886038265fd1ed30173f5ce3f8e85675", size = 173098, upload-time = "2025-03-05T20:02:37.985Z" }, + { url = "https://files.pythonhosted.org/packages/ff/0b/33cef55ff24f2d92924923c99926dcce78e7bd922d649467f0eda8368923/websockets-15.0.1-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:746ee8dba912cd6fc889a8147168991d50ed70447bf18bcda7039f7d2e3d9151", size = 173329, upload-time = "2025-03-05T20:02:39.298Z" }, + { url = "https://files.pythonhosted.org/packages/31/1d/063b25dcc01faa8fada1469bdf769de3768b7044eac9d41f734fd7b6ad6d/websockets-15.0.1-cp313-cp313-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:595b6c3969023ecf9041b2936ac3827e4623bfa3ccf007575f04c5a6aa318c22", size = 183111, upload-time = "2025-03-05T20:02:40.595Z" }, + { url = "https://files.pythonhosted.org/packages/93/53/9a87ee494a51bf63e4ec9241c1ccc4f7c2f45fff85d5bde2ff74fcb68b9e/websockets-15.0.1-cp313-cp313-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:3c714d2fc58b5ca3e285461a4cc0c9a66bd0e24c5da9911e30158286c9b5be7f", size = 182054, upload-time = "2025-03-05T20:02:41.926Z" }, + { url = "https://files.pythonhosted.org/packages/ff/b2/83a6ddf56cdcbad4e3d841fcc55d6ba7d19aeb89c50f24dd7e859ec0805f/websockets-15.0.1-cp313-cp313-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:0f3c1e2ab208db911594ae5b4f79addeb3501604a165019dd221c0bdcabe4db8", size = 182496, upload-time = "2025-03-05T20:02:43.304Z" }, + { url = "https://files.pythonhosted.org/packages/98/41/e7038944ed0abf34c45aa4635ba28136f06052e08fc2168520bb8b25149f/websockets-15.0.1-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:229cf1d3ca6c1804400b0a9790dc66528e08a6a1feec0d5040e8b9eb14422375", size = 182829, upload-time = "2025-03-05T20:02:48.812Z" }, + { url = "https://files.pythonhosted.org/packages/e0/17/de15b6158680c7623c6ef0db361da965ab25d813ae54fcfeae2e5b9ef910/websockets-15.0.1-cp313-cp313-musllinux_1_2_i686.whl", hash = "sha256:756c56e867a90fb00177d530dca4b097dd753cde348448a1012ed6c5131f8b7d", size = 182217, upload-time = "2025-03-05T20:02:50.14Z" }, + { url = "https://files.pythonhosted.org/packages/33/2b/1f168cb6041853eef0362fb9554c3824367c5560cbdaad89ac40f8c2edfc/websockets-15.0.1-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:558d023b3df0bffe50a04e710bc87742de35060580a293c2a984299ed83bc4e4", size = 182195, upload-time = "2025-03-05T20:02:51.561Z" }, + { url = "https://files.pythonhosted.org/packages/86/eb/20b6cdf273913d0ad05a6a14aed4b9a85591c18a987a3d47f20fa13dcc47/websockets-15.0.1-cp313-cp313-win32.whl", hash = "sha256:ba9e56e8ceeeedb2e080147ba85ffcd5cd0711b89576b83784d8605a7df455fa", size = 176393, upload-time = "2025-03-05T20:02:53.814Z" }, + { url = "https://files.pythonhosted.org/packages/1b/6c/c65773d6cab416a64d191d6ee8a8b1c68a09970ea6909d16965d26bfed1e/websockets-15.0.1-cp313-cp313-win_amd64.whl", hash = "sha256:e09473f095a819042ecb2ab9465aee615bd9c2028e4ef7d933600a8401c79561", size = 176837, upload-time = "2025-03-05T20:02:55.237Z" }, + { url = "https://files.pythonhosted.org/packages/36/db/3fff0bcbe339a6fa6a3b9e3fbc2bfb321ec2f4cd233692272c5a8d6cf801/websockets-15.0.1-cp39-cp39-macosx_10_9_universal2.whl", hash = "sha256:5f4c04ead5aed67c8a1a20491d54cdfba5884507a48dd798ecaf13c74c4489f5", size = 175424, upload-time = "2025-03-05T20:02:56.505Z" }, + { url = "https://files.pythonhosted.org/packages/46/e6/519054c2f477def4165b0ec060ad664ed174e140b0d1cbb9fafa4a54f6db/websockets-15.0.1-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:abdc0c6c8c648b4805c5eacd131910d2a7f6455dfd3becab248ef108e89ab16a", size = 173077, upload-time = "2025-03-05T20:02:58.37Z" }, + { url = "https://files.pythonhosted.org/packages/1a/21/c0712e382df64c93a0d16449ecbf87b647163485ca1cc3f6cbadb36d2b03/websockets-15.0.1-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:a625e06551975f4b7ea7102bc43895b90742746797e2e14b70ed61c43a90f09b", size = 173324, upload-time = "2025-03-05T20:02:59.773Z" }, + { url = "https://files.pythonhosted.org/packages/1c/cb/51ba82e59b3a664df54beed8ad95517c1b4dc1a913730e7a7db778f21291/websockets-15.0.1-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:d591f8de75824cbb7acad4e05d2d710484f15f29d4a915092675ad3456f11770", size = 182094, upload-time = "2025-03-05T20:03:01.827Z" }, + { url = "https://files.pythonhosted.org/packages/fb/0f/bf3788c03fec679bcdaef787518dbe60d12fe5615a544a6d4cf82f045193/websockets-15.0.1-cp39-cp39-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:47819cea040f31d670cc8d324bb6435c6f133b8c7a19ec3d61634e62f8d8f9eb", size = 181094, upload-time = "2025-03-05T20:03:03.123Z" }, + { url = "https://files.pythonhosted.org/packages/5e/da/9fb8c21edbc719b66763a571afbaf206cb6d3736d28255a46fc2fe20f902/websockets-15.0.1-cp39-cp39-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:ac017dd64572e5c3bd01939121e4d16cf30e5d7e110a119399cf3133b63ad054", size = 181397, upload-time = "2025-03-05T20:03:04.443Z" }, + { url = "https://files.pythonhosted.org/packages/2e/65/65f379525a2719e91d9d90c38fe8b8bc62bd3c702ac651b7278609b696c4/websockets-15.0.1-cp39-cp39-musllinux_1_2_aarch64.whl", hash = "sha256:4a9fac8e469d04ce6c25bb2610dc535235bd4aa14996b4e6dbebf5e007eba5ee", size = 181794, upload-time = "2025-03-05T20:03:06.708Z" }, + { url = "https://files.pythonhosted.org/packages/d9/26/31ac2d08f8e9304d81a1a7ed2851c0300f636019a57cbaa91342015c72cc/websockets-15.0.1-cp39-cp39-musllinux_1_2_i686.whl", hash = "sha256:363c6f671b761efcb30608d24925a382497c12c506b51661883c3e22337265ed", size = 181194, upload-time = "2025-03-05T20:03:08.844Z" }, + { url = "https://files.pythonhosted.org/packages/98/72/1090de20d6c91994cd4b357c3f75a4f25ee231b63e03adea89671cc12a3f/websockets-15.0.1-cp39-cp39-musllinux_1_2_x86_64.whl", hash = "sha256:2034693ad3097d5355bfdacfffcbd3ef5694f9718ab7f29c29689a9eae841880", size = 181164, upload-time = "2025-03-05T20:03:10.242Z" }, + { url = "https://files.pythonhosted.org/packages/2d/37/098f2e1c103ae8ed79b0e77f08d83b0ec0b241cf4b7f2f10edd0126472e1/websockets-15.0.1-cp39-cp39-win32.whl", hash = "sha256:3b1ac0d3e594bf121308112697cf4b32be538fb1444468fb0a6ae4feebc83411", size = 176381, upload-time = "2025-03-05T20:03:12.77Z" }, + { url = "https://files.pythonhosted.org/packages/75/8b/a32978a3ab42cebb2ebdd5b05df0696a09f4d436ce69def11893afa301f0/websockets-15.0.1-cp39-cp39-win_amd64.whl", hash = "sha256:b7643a03db5c95c799b89b31c036d5f27eeb4d259c798e878d6937d71832b1e4", size = 176841, upload-time = "2025-03-05T20:03:14.367Z" }, + { url = "https://files.pythonhosted.org/packages/02/9e/d40f779fa16f74d3468357197af8d6ad07e7c5a27ea1ca74ceb38986f77a/websockets-15.0.1-pp310-pypy310_pp73-macosx_10_15_x86_64.whl", hash = "sha256:0c9e74d766f2818bb95f84c25be4dea09841ac0f734d1966f415e4edfc4ef1c3", size = 173109, upload-time = "2025-03-05T20:03:17.769Z" }, + { url = "https://files.pythonhosted.org/packages/bc/cd/5b887b8585a593073fd92f7c23ecd3985cd2c3175025a91b0d69b0551372/websockets-15.0.1-pp310-pypy310_pp73-macosx_11_0_arm64.whl", hash = "sha256:1009ee0c7739c08a0cd59de430d6de452a55e42d6b522de7aa15e6f67db0b8e1", size = 173343, upload-time = "2025-03-05T20:03:19.094Z" }, + { url = "https://files.pythonhosted.org/packages/fe/ae/d34f7556890341e900a95acf4886833646306269f899d58ad62f588bf410/websockets-15.0.1-pp310-pypy310_pp73-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:76d1f20b1c7a2fa82367e04982e708723ba0e7b8d43aa643d3dcd404d74f1475", size = 174599, upload-time = "2025-03-05T20:03:21.1Z" }, + { url = "https://files.pythonhosted.org/packages/71/e6/5fd43993a87db364ec60fc1d608273a1a465c0caba69176dd160e197ce42/websockets-15.0.1-pp310-pypy310_pp73-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:f29d80eb9a9263b8d109135351caf568cc3f80b9928bccde535c235de55c22d9", size = 174207, upload-time = "2025-03-05T20:03:23.221Z" }, + { url = "https://files.pythonhosted.org/packages/2b/fb/c492d6daa5ec067c2988ac80c61359ace5c4c674c532985ac5a123436cec/websockets-15.0.1-pp310-pypy310_pp73-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:b359ed09954d7c18bbc1680f380c7301f92c60bf924171629c5db97febb12f04", size = 174155, upload-time = "2025-03-05T20:03:25.321Z" }, + { url = "https://files.pythonhosted.org/packages/68/a1/dcb68430b1d00b698ae7a7e0194433bce4f07ded185f0ee5fb21e2a2e91e/websockets-15.0.1-pp310-pypy310_pp73-win_amd64.whl", hash = "sha256:cad21560da69f4ce7658ca2cb83138fb4cf695a2ba3e475e0559e05991aa8122", size = 176884, upload-time = "2025-03-05T20:03:27.934Z" }, + { url = "https://files.pythonhosted.org/packages/b7/48/4b67623bac4d79beb3a6bb27b803ba75c1bdedc06bd827e465803690a4b2/websockets-15.0.1-pp39-pypy39_pp73-macosx_10_15_x86_64.whl", hash = "sha256:7f493881579c90fc262d9cdbaa05a6b54b3811c2f300766748db79f098db9940", size = 173106, upload-time = "2025-03-05T20:03:29.404Z" }, + { url = "https://files.pythonhosted.org/packages/ed/f0/adb07514a49fe5728192764e04295be78859e4a537ab8fcc518a3dbb3281/websockets-15.0.1-pp39-pypy39_pp73-macosx_11_0_arm64.whl", hash = "sha256:47b099e1f4fbc95b701b6e85768e1fcdaf1630f3cbe4765fa216596f12310e2e", size = 173339, upload-time = "2025-03-05T20:03:30.755Z" }, + { url = "https://files.pythonhosted.org/packages/87/28/bd23c6344b18fb43df40d0700f6d3fffcd7cef14a6995b4f976978b52e62/websockets-15.0.1-pp39-pypy39_pp73-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:67f2b6de947f8c757db2db9c71527933ad0019737ec374a8a6be9a956786aaf9", size = 174597, upload-time = "2025-03-05T20:03:32.247Z" }, + { url = "https://files.pythonhosted.org/packages/6d/79/ca288495863d0f23a60f546f0905ae8f3ed467ad87f8b6aceb65f4c013e4/websockets-15.0.1-pp39-pypy39_pp73-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:d08eb4c2b7d6c41da6ca0600c077e93f5adcfd979cd777d747e9ee624556da4b", size = 174205, upload-time = "2025-03-05T20:03:33.731Z" }, + { url = "https://files.pythonhosted.org/packages/04/e4/120ff3180b0872b1fe6637f6f995bcb009fb5c87d597c1fc21456f50c848/websockets-15.0.1-pp39-pypy39_pp73-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:4b826973a4a2ae47ba357e4e82fa44a463b8f168e1ca775ac64521442b19e87f", size = 174150, upload-time = "2025-03-05T20:03:35.757Z" }, + { url = "https://files.pythonhosted.org/packages/cb/c3/30e2f9c539b8da8b1d76f64012f3b19253271a63413b2d3adb94b143407f/websockets-15.0.1-pp39-pypy39_pp73-win_amd64.whl", hash = "sha256:21c1fa28a6a7e3cbdc171c694398b6df4744613ce9b36b1a498e816787e28123", size = 176877, upload-time = "2025-03-05T20:03:37.199Z" }, + { url = "https://files.pythonhosted.org/packages/fa/a8/5b41e0da817d64113292ab1f8247140aac61cbf6cfd085d6a0fa77f4984f/websockets-15.0.1-py3-none-any.whl", hash = "sha256:f7a866fbc1e97b5c617ee4116daaa09b722101d4a3c170c787450ba409f9736f", size = 169743, upload-time = "2025-03-05T20:03:39.41Z" }, +] + +[[package]] +name = "websockets" +version = "16.0" +source = { registry = "https://pypi.org/simple" } +resolution-markers = [ + "python_full_version >= '3.12'", + "python_full_version == '3.11.*'", + "python_full_version == '3.10.*'", +] +sdist = { url = "https://files.pythonhosted.org/packages/04/24/4b2031d72e840ce4c1ccb255f693b15c334757fc50023e4db9537080b8c4/websockets-16.0.tar.gz", hash = "sha256:5f6261a5e56e8d5c42a4497b364ea24d94d9563e8fbd44e78ac40879c60179b5", size = 179346, upload-time = "2026-01-10T09:23:47.181Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/20/74/221f58decd852f4b59cc3354cccaf87e8ef695fede361d03dc9a7396573b/websockets-16.0-cp310-cp310-macosx_10_9_universal2.whl", hash = "sha256:04cdd5d2d1dacbad0a7bf36ccbcd3ccd5a30ee188f2560b7a62a30d14107b31a", size = 177343, upload-time = "2026-01-10T09:22:21.28Z" }, + { url = "https://files.pythonhosted.org/packages/19/0f/22ef6107ee52ab7f0b710d55d36f5a5d3ef19e8a205541a6d7ffa7994e5a/websockets-16.0-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:8ff32bb86522a9e5e31439a58addbb0166f0204d64066fb955265c4e214160f0", size = 175021, upload-time = "2026-01-10T09:22:22.696Z" }, + { url = "https://files.pythonhosted.org/packages/10/40/904a4cb30d9b61c0e278899bf36342e9b0208eb3c470324a9ecbaac2a30f/websockets-16.0-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:583b7c42688636f930688d712885cf1531326ee05effd982028212ccc13e5957", size = 175320, upload-time = "2026-01-10T09:22:23.94Z" }, + { url = "https://files.pythonhosted.org/packages/9d/2f/4b3ca7e106bc608744b1cdae041e005e446124bebb037b18799c2d356864/websockets-16.0-cp310-cp310-manylinux1_x86_64.manylinux_2_28_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:7d837379b647c0c4c2355c2499723f82f1635fd2c26510e1f587d89bc2199e72", size = 183815, upload-time = "2026-01-10T09:22:25.469Z" }, + { url = "https://files.pythonhosted.org/packages/86/26/d40eaa2a46d4302becec8d15b0fc5e45bdde05191e7628405a19cf491ccd/websockets-16.0-cp310-cp310-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:df57afc692e517a85e65b72e165356ed1df12386ecb879ad5693be08fac65dde", size = 185054, upload-time = "2026-01-10T09:22:27.101Z" }, + { url = "https://files.pythonhosted.org/packages/b0/ba/6500a0efc94f7373ee8fefa8c271acdfd4dca8bd49a90d4be7ccabfc397e/websockets-16.0-cp310-cp310-musllinux_1_2_aarch64.whl", hash = "sha256:2b9f1e0d69bc60a4a87349d50c09a037a2607918746f07de04df9e43252c77a3", size = 184565, upload-time = "2026-01-10T09:22:28.293Z" }, + { url = "https://files.pythonhosted.org/packages/04/b4/96bf2cee7c8d8102389374a2616200574f5f01128d1082f44102140344cc/websockets-16.0-cp310-cp310-musllinux_1_2_x86_64.whl", hash = "sha256:335c23addf3d5e6a8633f9f8eda77efad001671e80b95c491dd0924587ece0b3", size = 183848, upload-time = "2026-01-10T09:22:30.394Z" }, + { url = "https://files.pythonhosted.org/packages/02/8e/81f40fb00fd125357814e8c3025738fc4ffc3da4b6b4a4472a82ba304b41/websockets-16.0-cp310-cp310-win32.whl", hash = "sha256:37b31c1623c6605e4c00d466c9d633f9b812ea430c11c8a278774a1fde1acfa9", size = 178249, upload-time = "2026-01-10T09:22:32.083Z" }, + { url = "https://files.pythonhosted.org/packages/b4/5f/7e40efe8df57db9b91c88a43690ac66f7b7aa73a11aa6a66b927e44f26fa/websockets-16.0-cp310-cp310-win_amd64.whl", hash = "sha256:8e1dab317b6e77424356e11e99a432b7cb2f3ec8c5ab4dabbcee6add48f72b35", size = 178685, upload-time = "2026-01-10T09:22:33.345Z" }, + { url = "https://files.pythonhosted.org/packages/f2/db/de907251b4ff46ae804ad0409809504153b3f30984daf82a1d84a9875830/websockets-16.0-cp311-cp311-macosx_10_9_universal2.whl", hash = "sha256:31a52addea25187bde0797a97d6fc3d2f92b6f72a9370792d65a6e84615ac8a8", size = 177340, upload-time = "2026-01-10T09:22:34.539Z" }, + { url = "https://files.pythonhosted.org/packages/f3/fa/abe89019d8d8815c8781e90d697dec52523fb8ebe308bf11664e8de1877e/websockets-16.0-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:417b28978cdccab24f46400586d128366313e8a96312e4b9362a4af504f3bbad", size = 175022, upload-time = "2026-01-10T09:22:36.332Z" }, + { url = "https://files.pythonhosted.org/packages/58/5d/88ea17ed1ded2079358b40d31d48abe90a73c9e5819dbcde1606e991e2ad/websockets-16.0-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:af80d74d4edfa3cb9ed973a0a5ba2b2a549371f8a741e0800cb07becdd20f23d", size = 175319, upload-time = "2026-01-10T09:22:37.602Z" }, + { url = "https://files.pythonhosted.org/packages/d2/ae/0ee92b33087a33632f37a635e11e1d99d429d3d323329675a6022312aac2/websockets-16.0-cp311-cp311-manylinux1_x86_64.manylinux_2_28_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:08d7af67b64d29823fed316505a89b86705f2b7981c07848fb5e3ea3020c1abe", size = 184631, upload-time = "2026-01-10T09:22:38.789Z" }, + { url = "https://files.pythonhosted.org/packages/c8/c5/27178df583b6c5b31b29f526ba2da5e2f864ecc79c99dae630a85d68c304/websockets-16.0-cp311-cp311-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:7be95cfb0a4dae143eaed2bcba8ac23f4892d8971311f1b06f3c6b78952ee70b", size = 185870, upload-time = "2026-01-10T09:22:39.893Z" }, + { url = "https://files.pythonhosted.org/packages/87/05/536652aa84ddc1c018dbb7e2c4cbcd0db884580bf8e95aece7593fde526f/websockets-16.0-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:d6297ce39ce5c2e6feb13c1a996a2ded3b6832155fcfc920265c76f24c7cceb5", size = 185361, upload-time = "2026-01-10T09:22:41.016Z" }, + { url = "https://files.pythonhosted.org/packages/6d/e2/d5332c90da12b1e01f06fb1b85c50cfc489783076547415bf9f0a659ec19/websockets-16.0-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:1c1b30e4f497b0b354057f3467f56244c603a79c0d1dafce1d16c283c25f6e64", size = 184615, upload-time = "2026-01-10T09:22:42.442Z" }, + { url = "https://files.pythonhosted.org/packages/77/fb/d3f9576691cae9253b51555f841bc6600bf0a983a461c79500ace5a5b364/websockets-16.0-cp311-cp311-win32.whl", hash = "sha256:5f451484aeb5cafee1ccf789b1b66f535409d038c56966d6101740c1614b86c6", size = 178246, upload-time = "2026-01-10T09:22:43.654Z" }, + { url = "https://files.pythonhosted.org/packages/54/67/eaff76b3dbaf18dcddabc3b8c1dba50b483761cccff67793897945b37408/websockets-16.0-cp311-cp311-win_amd64.whl", hash = "sha256:8d7f0659570eefb578dacde98e24fb60af35350193e4f56e11190787bee77dac", size = 178684, upload-time = "2026-01-10T09:22:44.941Z" }, + { url = "https://files.pythonhosted.org/packages/84/7b/bac442e6b96c9d25092695578dda82403c77936104b5682307bd4deb1ad4/websockets-16.0-cp312-cp312-macosx_10_13_universal2.whl", hash = "sha256:71c989cbf3254fbd5e84d3bff31e4da39c43f884e64f2551d14bb3c186230f00", size = 177365, upload-time = "2026-01-10T09:22:46.787Z" }, + { url = "https://files.pythonhosted.org/packages/b0/fe/136ccece61bd690d9c1f715baaeefd953bb2360134de73519d5df19d29ca/websockets-16.0-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:8b6e209ffee39ff1b6d0fa7bfef6de950c60dfb91b8fcead17da4ee539121a79", size = 175038, upload-time = "2026-01-10T09:22:47.999Z" }, + { url = "https://files.pythonhosted.org/packages/40/1e/9771421ac2286eaab95b8575b0cb701ae3663abf8b5e1f64f1fd90d0a673/websockets-16.0-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:86890e837d61574c92a97496d590968b23c2ef0aeb8a9bc9421d174cd378ae39", size = 175328, upload-time = "2026-01-10T09:22:49.809Z" }, + { url = "https://files.pythonhosted.org/packages/18/29/71729b4671f21e1eaa5d6573031ab810ad2936c8175f03f97f3ff164c802/websockets-16.0-cp312-cp312-manylinux1_x86_64.manylinux_2_28_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:9b5aca38b67492ef518a8ab76851862488a478602229112c4b0d58d63a7a4d5c", size = 184915, upload-time = "2026-01-10T09:22:51.071Z" }, + { url = "https://files.pythonhosted.org/packages/97/bb/21c36b7dbbafc85d2d480cd65df02a1dc93bf76d97147605a8e27ff9409d/websockets-16.0-cp312-cp312-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:e0334872c0a37b606418ac52f6ab9cfd17317ac26365f7f65e203e2d0d0d359f", size = 186152, upload-time = "2026-01-10T09:22:52.224Z" }, + { url = "https://files.pythonhosted.org/packages/4a/34/9bf8df0c0cf88fa7bfe36678dc7b02970c9a7d5e065a3099292db87b1be2/websockets-16.0-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:a0b31e0b424cc6b5a04b8838bbaec1688834b2383256688cf47eb97412531da1", size = 185583, upload-time = "2026-01-10T09:22:53.443Z" }, + { url = "https://files.pythonhosted.org/packages/47/88/4dd516068e1a3d6ab3c7c183288404cd424a9a02d585efbac226cb61ff2d/websockets-16.0-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:485c49116d0af10ac698623c513c1cc01c9446c058a4e61e3bf6c19dff7335a2", size = 184880, upload-time = "2026-01-10T09:22:55.033Z" }, + { url = "https://files.pythonhosted.org/packages/91/d6/7d4553ad4bf1c0421e1ebd4b18de5d9098383b5caa1d937b63df8d04b565/websockets-16.0-cp312-cp312-win32.whl", hash = "sha256:eaded469f5e5b7294e2bdca0ab06becb6756ea86894a47806456089298813c89", size = 178261, upload-time = "2026-01-10T09:22:56.251Z" }, + { url = "https://files.pythonhosted.org/packages/c3/f0/f3a17365441ed1c27f850a80b2bc680a0fa9505d733fe152fdf5e98c1c0b/websockets-16.0-cp312-cp312-win_amd64.whl", hash = "sha256:5569417dc80977fc8c2d43a86f78e0a5a22fee17565d78621b6bb264a115d4ea", size = 178693, upload-time = "2026-01-10T09:22:57.478Z" }, + { url = "https://files.pythonhosted.org/packages/cc/9c/baa8456050d1c1b08dd0ec7346026668cbc6f145ab4e314d707bb845bf0d/websockets-16.0-cp313-cp313-macosx_10_13_universal2.whl", hash = "sha256:878b336ac47938b474c8f982ac2f7266a540adc3fa4ad74ae96fea9823a02cc9", size = 177364, upload-time = "2026-01-10T09:22:59.333Z" }, + { url = "https://files.pythonhosted.org/packages/7e/0c/8811fc53e9bcff68fe7de2bcbe75116a8d959ac699a3200f4847a8925210/websockets-16.0-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:52a0fec0e6c8d9a784c2c78276a48a2bdf099e4ccc2a4cad53b27718dbfd0230", size = 175039, upload-time = "2026-01-10T09:23:01.171Z" }, + { url = "https://files.pythonhosted.org/packages/aa/82/39a5f910cb99ec0b59e482971238c845af9220d3ab9fa76dd9162cda9d62/websockets-16.0-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:e6578ed5b6981005df1860a56e3617f14a6c307e6a71b4fff8c48fdc50f3ed2c", size = 175323, upload-time = "2026-01-10T09:23:02.341Z" }, + { url = "https://files.pythonhosted.org/packages/bd/28/0a25ee5342eb5d5f297d992a77e56892ecb65e7854c7898fb7d35e9b33bd/websockets-16.0-cp313-cp313-manylinux1_x86_64.manylinux_2_28_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:95724e638f0f9c350bb1c2b0a7ad0e83d9cc0c9259f3ea94e40d7b02a2179ae5", size = 184975, upload-time = "2026-01-10T09:23:03.756Z" }, + { url = "https://files.pythonhosted.org/packages/f9/66/27ea52741752f5107c2e41fda05e8395a682a1e11c4e592a809a90c6a506/websockets-16.0-cp313-cp313-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:c0204dc62a89dc9d50d682412c10b3542d748260d743500a85c13cd1ee4bde82", size = 186203, upload-time = "2026-01-10T09:23:05.01Z" }, + { url = "https://files.pythonhosted.org/packages/37/e5/8e32857371406a757816a2b471939d51c463509be73fa538216ea52b792a/websockets-16.0-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:52ac480f44d32970d66763115edea932f1c5b1312de36df06d6b219f6741eed8", size = 185653, upload-time = "2026-01-10T09:23:06.301Z" }, + { url = "https://files.pythonhosted.org/packages/9b/67/f926bac29882894669368dc73f4da900fcdf47955d0a0185d60103df5737/websockets-16.0-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:6e5a82b677f8f6f59e8dfc34ec06ca6b5b48bc4fcda346acd093694cc2c24d8f", size = 184920, upload-time = "2026-01-10T09:23:07.492Z" }, + { url = "https://files.pythonhosted.org/packages/3c/a1/3d6ccdcd125b0a42a311bcd15a7f705d688f73b2a22d8cf1c0875d35d34a/websockets-16.0-cp313-cp313-win32.whl", hash = "sha256:abf050a199613f64c886ea10f38b47770a65154dc37181bfaff70c160f45315a", size = 178255, upload-time = "2026-01-10T09:23:09.245Z" }, + { url = "https://files.pythonhosted.org/packages/6b/ae/90366304d7c2ce80f9b826096a9e9048b4bb760e44d3b873bb272cba696b/websockets-16.0-cp313-cp313-win_amd64.whl", hash = "sha256:3425ac5cf448801335d6fdc7ae1eb22072055417a96cc6b31b3861f455fbc156", size = 178689, upload-time = "2026-01-10T09:23:10.483Z" }, + { url = "https://files.pythonhosted.org/packages/f3/1d/e88022630271f5bd349ed82417136281931e558d628dd52c4d8621b4a0b2/websockets-16.0-cp314-cp314-macosx_10_15_universal2.whl", hash = "sha256:8cc451a50f2aee53042ac52d2d053d08bf89bcb31ae799cb4487587661c038a0", size = 177406, upload-time = "2026-01-10T09:23:12.178Z" }, + { url = "https://files.pythonhosted.org/packages/f2/78/e63be1bf0724eeb4616efb1ae1c9044f7c3953b7957799abb5915bffd38e/websockets-16.0-cp314-cp314-macosx_10_15_x86_64.whl", hash = "sha256:daa3b6ff70a9241cf6c7fc9e949d41232d9d7d26fd3522b1ad2b4d62487e9904", size = 175085, upload-time = "2026-01-10T09:23:13.511Z" }, + { url = "https://files.pythonhosted.org/packages/bb/f4/d3c9220d818ee955ae390cf319a7c7a467beceb24f05ee7aaaa2414345ba/websockets-16.0-cp314-cp314-macosx_11_0_arm64.whl", hash = "sha256:fd3cb4adb94a2a6e2b7c0d8d05cb94e6f1c81a0cf9dc2694fb65c7e8d94c42e4", size = 175328, upload-time = "2026-01-10T09:23:14.727Z" }, + { url = "https://files.pythonhosted.org/packages/63/bc/d3e208028de777087e6fb2b122051a6ff7bbcca0d6df9d9c2bf1dd869ae9/websockets-16.0-cp314-cp314-manylinux1_x86_64.manylinux_2_28_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:781caf5e8eee67f663126490c2f96f40906594cb86b408a703630f95550a8c3e", size = 185044, upload-time = "2026-01-10T09:23:15.939Z" }, + { url = "https://files.pythonhosted.org/packages/ad/6e/9a0927ac24bd33a0a9af834d89e0abc7cfd8e13bed17a86407a66773cc0e/websockets-16.0-cp314-cp314-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:caab51a72c51973ca21fa8a18bd8165e1a0183f1ac7066a182ff27107b71e1a4", size = 186279, upload-time = "2026-01-10T09:23:17.148Z" }, + { url = "https://files.pythonhosted.org/packages/b9/ca/bf1c68440d7a868180e11be653c85959502efd3a709323230314fda6e0b3/websockets-16.0-cp314-cp314-musllinux_1_2_aarch64.whl", hash = "sha256:19c4dc84098e523fd63711e563077d39e90ec6702aff4b5d9e344a60cb3c0cb1", size = 185711, upload-time = "2026-01-10T09:23:18.372Z" }, + { url = "https://files.pythonhosted.org/packages/c4/f8/fdc34643a989561f217bb477cbc47a3a07212cbda91c0e4389c43c296ebf/websockets-16.0-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:a5e18a238a2b2249c9a9235466b90e96ae4795672598a58772dd806edc7ac6d3", size = 184982, upload-time = "2026-01-10T09:23:19.652Z" }, + { url = "https://files.pythonhosted.org/packages/dd/d1/574fa27e233764dbac9c52730d63fcf2823b16f0856b3329fc6268d6ae4f/websockets-16.0-cp314-cp314-win32.whl", hash = "sha256:a069d734c4a043182729edd3e9f247c3b2a4035415a9172fd0f1b71658a320a8", size = 177915, upload-time = "2026-01-10T09:23:21.458Z" }, + { url = "https://files.pythonhosted.org/packages/8a/f1/ae6b937bf3126b5134ce1f482365fde31a357c784ac51852978768b5eff4/websockets-16.0-cp314-cp314-win_amd64.whl", hash = "sha256:c0ee0e63f23914732c6d7e0cce24915c48f3f1512ec1d079ed01fc629dab269d", size = 178381, upload-time = "2026-01-10T09:23:22.715Z" }, + { url = "https://files.pythonhosted.org/packages/06/9b/f791d1db48403e1f0a27577a6beb37afae94254a8c6f08be4a23e4930bc0/websockets-16.0-cp314-cp314t-macosx_10_15_universal2.whl", hash = "sha256:a35539cacc3febb22b8f4d4a99cc79b104226a756aa7400adc722e83b0d03244", size = 177737, upload-time = "2026-01-10T09:23:24.523Z" }, + { url = "https://files.pythonhosted.org/packages/bd/40/53ad02341fa33b3ce489023f635367a4ac98b73570102ad2cdd770dacc9a/websockets-16.0-cp314-cp314t-macosx_10_15_x86_64.whl", hash = "sha256:b784ca5de850f4ce93ec85d3269d24d4c82f22b7212023c974c401d4980ebc5e", size = 175268, upload-time = "2026-01-10T09:23:25.781Z" }, + { url = "https://files.pythonhosted.org/packages/74/9b/6158d4e459b984f949dcbbb0c5d270154c7618e11c01029b9bbd1bb4c4f9/websockets-16.0-cp314-cp314t-macosx_11_0_arm64.whl", hash = "sha256:569d01a4e7fba956c5ae4fc988f0d4e187900f5497ce46339c996dbf24f17641", size = 175486, upload-time = "2026-01-10T09:23:27.033Z" }, + { url = "https://files.pythonhosted.org/packages/e5/2d/7583b30208b639c8090206f95073646c2c9ffd66f44df967981a64f849ad/websockets-16.0-cp314-cp314t-manylinux1_x86_64.manylinux_2_28_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:50f23cdd8343b984957e4077839841146f67a3d31ab0d00e6b824e74c5b2f6e8", size = 185331, upload-time = "2026-01-10T09:23:28.259Z" }, + { url = "https://files.pythonhosted.org/packages/45/b0/cce3784eb519b7b5ad680d14b9673a31ab8dcb7aad8b64d81709d2430aa8/websockets-16.0-cp314-cp314t-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:152284a83a00c59b759697b7f9e9cddf4e3c7861dd0d964b472b70f78f89e80e", size = 186501, upload-time = "2026-01-10T09:23:29.449Z" }, + { url = "https://files.pythonhosted.org/packages/19/60/b8ebe4c7e89fb5f6cdf080623c9d92789a53636950f7abacfc33fe2b3135/websockets-16.0-cp314-cp314t-musllinux_1_2_aarch64.whl", hash = "sha256:bc59589ab64b0022385f429b94697348a6a234e8ce22544e3681b2e9331b5944", size = 186062, upload-time = "2026-01-10T09:23:31.368Z" }, + { url = "https://files.pythonhosted.org/packages/88/a8/a080593f89b0138b6cba1b28f8df5673b5506f72879322288b031337c0b8/websockets-16.0-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:32da954ffa2814258030e5a57bc73a3635463238e797c7375dc8091327434206", size = 185356, upload-time = "2026-01-10T09:23:32.627Z" }, + { url = "https://files.pythonhosted.org/packages/c2/b6/b9afed2afadddaf5ebb2afa801abf4b0868f42f8539bfe4b071b5266c9fe/websockets-16.0-cp314-cp314t-win32.whl", hash = "sha256:5a4b4cc550cb665dd8a47f868c8d04c8230f857363ad3c9caf7a0c3bf8c61ca6", size = 178085, upload-time = "2026-01-10T09:23:33.816Z" }, + { url = "https://files.pythonhosted.org/packages/9f/3e/28135a24e384493fa804216b79a6a6759a38cc4ff59118787b9fb693df93/websockets-16.0-cp314-cp314t-win_amd64.whl", hash = "sha256:b14dc141ed6d2dde437cddb216004bcac6a1df0935d79656387bd41632ba0bbd", size = 178531, upload-time = "2026-01-10T09:23:35.016Z" }, + { url = "https://files.pythonhosted.org/packages/72/07/c98a68571dcf256e74f1f816b8cc5eae6eb2d3d5cfa44d37f801619d9166/websockets-16.0-pp311-pypy311_pp73-macosx_10_15_x86_64.whl", hash = "sha256:349f83cd6c9a415428ee1005cadb5c2c56f4389bc06a9af16103c3bc3dcc8b7d", size = 174947, upload-time = "2026-01-10T09:23:36.166Z" }, + { url = "https://files.pythonhosted.org/packages/7e/52/93e166a81e0305b33fe416338be92ae863563fe7bce446b0f687b9df5aea/websockets-16.0-pp311-pypy311_pp73-macosx_11_0_arm64.whl", hash = "sha256:4a1aba3340a8dca8db6eb5a7986157f52eb9e436b74813764241981ca4888f03", size = 175260, upload-time = "2026-01-10T09:23:37.409Z" }, + { url = "https://files.pythonhosted.org/packages/56/0c/2dbf513bafd24889d33de2ff0368190a0e69f37bcfa19009ef819fe4d507/websockets-16.0-pp311-pypy311_pp73-manylinux1_x86_64.manylinux_2_28_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:f4a32d1bd841d4bcbffdcb3d2ce50c09c3909fbead375ab28d0181af89fd04da", size = 176071, upload-time = "2026-01-10T09:23:39.158Z" }, + { url = "https://files.pythonhosted.org/packages/a5/8f/aea9c71cc92bf9b6cc0f7f70df8f0b420636b6c96ef4feee1e16f80f75dd/websockets-16.0-pp311-pypy311_pp73-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:0298d07ee155e2e9fda5be8a9042200dd2e3bb0b8a38482156576f863a9d457c", size = 176968, upload-time = "2026-01-10T09:23:41.031Z" }, + { url = "https://files.pythonhosted.org/packages/9a/3f/f70e03f40ffc9a30d817eef7da1be72ee4956ba8d7255c399a01b135902a/websockets-16.0-pp311-pypy311_pp73-win_amd64.whl", hash = "sha256:a653aea902e0324b52f1613332ddf50b00c06fdaf7e92624fbf8c77c78fa5767", size = 178735, upload-time = "2026-01-10T09:23:42.259Z" }, + { url = "https://files.pythonhosted.org/packages/6f/28/258ebab549c2bf3e64d2b0217b973467394a9cea8c42f70418ca2c5d0d2e/websockets-16.0-py3-none-any.whl", hash = "sha256:1637db62fad1dc833276dded54215f2c7fa46912301a24bd94d45d46a011ceec", size = 171598, upload-time = "2026-01-10T09:23:45.395Z" }, +] + +[[package]] +name = "werkzeug" +version = "3.1.5" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "markupsafe" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/5a/70/1469ef1d3542ae7c2c7b72bd5e3a4e6ee69d7978fa8a3af05a38eca5becf/werkzeug-3.1.5.tar.gz", hash = "sha256:6a548b0e88955dd07ccb25539d7d0cc97417ee9e179677d22c7041c8f078ce67", size = 864754, upload-time = "2026-01-08T17:49:23.247Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/ad/e4/8d97cca767bcc1be76d16fb76951608305561c6e056811587f36cb1316a8/werkzeug-3.1.5-py3-none-any.whl", hash = "sha256:5111e36e91086ece91f93268bb39b4a35c1e6f1feac762c9c822ded0a4e322dc", size = 225025, upload-time = "2026-01-08T17:49:21.859Z" }, +] + +[[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" }, +] diff --git a/working-docs/implementation/lock-file-cascade.md b/working-docs/implementation/lock-file-cascade.md index 8c8c67c0..f6fbbdba 100644 --- a/working-docs/implementation/lock-file-cascade.md +++ b/working-docs/implementation/lock-file-cascade.md @@ -8,6 +8,12 @@ SPDX-License-Identifier: CC0-1.0 # Lock/pin format priority cascade -- implementation notes +This is implementation reference for maintainers -- what the code does +and why. For the user-facing behavior (what shows up in a generated +SBOM, which lock file wins, which commands use lock files at all), see +[docs/dependency-sources.md](../../docs/dependency-sources.md) instead; +this page assumes that one as background and doesn't restate it. + See also: [poetry-support.md](poetry-support.md)'s "`poetry.lock` transitive dependencies" section and [pep751-pylock-support.md](pep751-pylock-support.md) for the two formats this cascade was generalized from; @@ -34,10 +40,13 @@ new format's would-be bespoke function with one shared, ordered cascade. ```python _LockExtractor = Callable[[Path], list[str]] -_LOCK_SOURCES: list[tuple[str, _LockExtractor, str]] = [ +_LOCK_SOURCES: list[tuple[str, _LockExtractor | None, str | None]] = [ ("pylock.toml", extract_pylock_dependencies, "resolved_lockfile"), - # uv.lock, pdm.lock, Pipfile.lock, requirements.txt land here as - # their own extractors ship -- see roadmap.md. + ("uv.lock", extract_uv_lock_dependencies, "resolved_lockfile"), + ("poetry.lock", None, None), + ("pdm.lock", extract_pdm_lock_dependencies, "resolved_lockfile"), + # Pipfile.lock, requirements.txt land here as their own extractors + # ship -- see roadmap.md. ] @@ -45,29 +54,33 @@ def apply_locked_dependencies(metadata: ProjectMetadata, project_dir: Path) -> N ... ``` -Each entry pairs a source filename, an extractor (`project_dir -> list[str]` +Each entry pairs a source name, an extractor (`project_dir -> list[str]` of exact-pin PEP 508 strings, empty when absent/unusable -- the same signature convention `_poetry_lock.py`/`_pylock.py` already established), and a provenance `Method` tag. `apply_locked_dependencies()` tries each -entry in priority order (highest first) and applies the first non-empty -result, in place, onto `metadata.locked_dependencies` and -`metadata.provenance["locked_dependencies"]`. - -**`poetry.lock` is not in this table.** It stays exactly where it -shipped, gated inside `_try_read_poetry()`'s `include_locked_dependencies` -build-stage flag, since `poetry.lock` only ever makes sense alongside a -`[tool.poetry]` table -- which requires `pyproject.toml` to exist -regardless, so it needs no `read_project()`-level generalization. The -cascade runs *after* `_try_read_poetry()` in `read_pyproject()` -(indirectly, via `read_project()` -- see below), so a higher-priority -cascade entry can still override an already-set `poetry.lock` result. +extractor-bearing entry in priority order (highest first) and applies +the first non-empty result, in place, onto `metadata.locked_dependencies` +and `metadata.provenance["locked_dependencies"]`. + +**`poetry.lock` has no extractor here (`None`, `None`), but it *is* in +the table.** It's still applied earlier, gated inside +`_try_read_poetry()`'s `include_locked_dependencies` build-stage flag, +since `poetry.lock` only ever makes sense alongside a `[tool.poetry]` +table -- which requires `pyproject.toml` to exist regardless, so it +needs no `read_project()`-level generalization of its own. What changed +once a format *below* `poetry.lock` in the priority order (`pdm.lock`) +joined the cascade: `poetry.lock` needed a fixed rank in the *same* +list, not just an informal "runs before this cascade" note -- see the +next section for why. ## Priority order -Highest to lowest, per `working-docs/design/roadmap.md`'s "Remaining -lock formats" item and `lock-files.md`'s phase reasoning -(build-backend-agnostic and universal beats tool-specific; a real -resolver lock beats a merely-pinned file): +Same order [docs/dependency-sources.md](../../docs/dependency-sources.md) +documents for users, restated here as the exact rank list +`_LOCK_SOURCES` must match. Highest to lowest, per +`working-docs/design/roadmap.md`'s "Remaining lock formats" item and +`lock-files.md`'s phase reasoning (build-backend-agnostic and universal +beats tool-specific; a real resolver lock beats a merely-pinned file): 1. `pylock.toml` (PEP 751) -- the interoperability standard. 2. `uv.lock` @@ -78,6 +91,107 @@ resolver lock beats a merely-pinned file): line is an exact `==` pin (see that format's own implementation notes once it lands). +## Why `poetry.lock` needs a fixed rank, not just "runs first" + +Caught while adding `pdm.lock` (rank 4, below `poetry.lock` at rank 3): +the original cascade loop applied the *first* extractor-bearing entry +that returned non-empty data, full stop -- correct as long as every +entry in `_LOCK_SOURCES` outranks `poetry.lock` (true for `pylock.toml` +and `uv.lock`, ranks 1-2), but silently wrong the moment an entry ranks +*below* it. Without a fix, a project with both `poetry.lock` and +`pdm.lock` present would have `pdm.lock` unconditionally clobber +`poetry.lock`'s already-applied result, even though `pdm.lock` is +supposed to lose that comparison. + +The fix: `poetry.lock` is a real entry in `_LOCK_SOURCES` (extractor +`None`, since it's applied elsewhere), so its rank is looked up the same +way as everything else instead of being assumed. `apply_locked_dependencies()` +first resolves the rank of whatever source (if any) already populated +`metadata.provenance["locked_dependencies"]` -- today that can only be +`poetry.lock`, via `_try_read_poetry()`, which runs before this cascade +-- then, walking `_LOCK_SOURCES` in order, stops (`break`) the moment it +reaches an entry ranked *below* that already-set source, since nothing +from there on could legitimately win. `tests/extract/test_pdm_lock.py::test_read_project_pdm_lock_never_overrides_poetry_lock` +is the regression test for this; `test_read_project_uv_lock_still_overrides_pdm_lock` +confirms the higher-ranked entries' behavior didn't change. + +**Any future format ranked below `poetry.lock` (`Pipfile.lock`, pinned +`requirements.txt`) needs no extra code for this** -- the same generic +rank check covers them once they're added to `_LOCK_SOURCES` at their +documented position. Only a format that would need to be inserted +*around* an existing entry (unlikely, given the order above is already +settled) would need to re-verify this logic. + +## Per-format extraction notes + +Most formats' extractors are a simple flat scan (`_pylock.py`, +`_poetry_lock.py`): every locked entry is either included or excluded, +independently of the others. `_uv_lock.py` is the one exception so far, +because `uv.lock` resolves *every* Python-version/platform combination +its `resolution-markers` cover in one file -- its top-level `[[package]]` +table is a flat union across all of them, so the same package name can +legitimately appear more than once, at different versions, restricted +to different marker conditions (e.g. one entry for +`python_full_version < '3.10'`, another for `>= '3.10'`). Since this +cascade (like every sibling extractor) doesn't evaluate markers against +a real environment, `_uv_lock.py`: + +1. Identifies the project's own package entry (via a `source.editable`/ + `source.virtual` marker -- how uv distinguishes "this is the local + project" from a PyPI download) instead of scanning every + `[[package]]` entry directly. +2. Reads only that entry's own `dependencies` list (main/runtime -- + `optional-dependencies`/`dev-dependencies` are extras and dev + groups, excluded the same way `poetry.lock`'s non-`main` groups are). +3. Resolves each referenced name against the flat table only when + exactly one candidate exists for that name; an ambiguous + (multiple-version) or marker-conditional (inline `version` on the + dependency reference itself) name is skipped with a `WARNING:`, not + guessed. See `tests/fixtures/real-world-locks/README.md`'s `flask` + entry for a real fixture exercising this (its `click` dependency is + deliberately absent from `locked_dependencies`). + +A future format that shares this same "multiple resolutions in one +file" shape should follow this same pattern rather than inventing a new +one. + +`_pdm_lock.py` hits a *milder* version of the same "same name, more than +one entry" shape, but for a different, harmless reason: PDM records a +separate `[[package]]` entry per requested extra variant of a package +(e.g. a bare `httpx` entry alongside one with `extras = ["socks"]`), +always agreeing on `version` -- unlike `uv.lock`'s genuinely conflicting +duplicates. It reuses `index_packages_by_name()` (see the next section) +to group entries by name, then only treats a name as ambiguous (skip, +`WARNING:`) when its entries actually *disagree* on `version`; entries +that agree are collapsed to one `name==version`, not two. + +## Sharing code across formats (`_lock_common.py`) + +Two steps turned out to be identical across every extractor, not just +similar in spirit: + +- **Loading the lock file.** "Try to read/parse it; absent -> empty + result, silently; malformed -> empty result, with a `WARNING:`" was + copy-pasted verbatim into `_poetry_lock.py`, `_pylock.py`, and + `_uv_lock.py` before being factored into + `pitloom.extract._lock_common.load_lock_toml()`, which all four + extractors (including `_pdm_lock.py`) now call instead. +- **Grouping a flat package list by name.** First written for + `_uv_lock.py`'s ambiguity check, then reused as-is by `_pdm_lock.py`'s + own (milder) version of the same check -- see above. Lives as + `pitloom.extract._lock_common.index_packages_by_name()`. + +What's deliberately **not** shared: the per-entry validation shape +(what counts as "malformed", which keys mark a non-registry source, +what the `groups`/`dependencies` filtering looks like). Each format's +own field names and conventions differ enough (`poetry.lock`'s +`source.type` vs `pylock.toml`'s top-level `vcs`/`directory`/`archive` +keys vs `uv.lock`'s nested `source.{key}` vs `pdm.lock`'s flat +`git`/`path` keys) that forcing one shared function across all of them +would hurt clarity more than it would save -- consistent *wording* +across their `WARNING:` messages matters more here than a single shared +implementation, per this repo's message-style convention. + ## Where the cascade is called from -- `read_project()`, not `read_pyproject()` This is the one deliberate divergence from `pylock.toml`'s original @@ -116,11 +230,11 @@ uniformly regardless of which metadata source won. `poetry.lock`, or a previous cascade winner -- though only one cascade entry ever wins per call), the resulting string gets a trailing `| Note: supersedes `, e.g. `"Source: pylock.toml | Method: - resolved_lockfile | Note: supersedes poetry.lock"`. This means a - reader of the *generated SBOM* -- not only Pitloom's own stderr at - generation time -- can see that more than one lock source existed and - which one Pitloom trusted, per this repo's "no silent deviations" - principle applied to the artifact itself. + resolved_lockfile | Note: supersedes poetry.lock"` -- per this repo's + "no silent deviations" principle applied to the artifact itself, not + only to the run that produced it. See + [docs/dependency-sources.md](../../docs/dependency-sources.md#how-to-tell-which-source-was-used) + for how a user reads this in a generated SBOM. ## Document UUID seeding @@ -146,9 +260,17 @@ unaffected -- purely additive. in its own `src/pitloom/extract/_.py`, following `_pylock.py`'s shape: exact-pin PEP 508 strings, empty list when absent/unusable, `WARNING:` (never a silent drop) for anything - malformed or non-registry-sourced. + malformed or non-registry-sourced. Use + `_lock_common.load_lock_toml()` to load the file, and (if the format + can resolve the same name more than once, the way `uv.lock`/`pdm.lock` + can) `_lock_common.index_packages_by_name()` to group entries before + deciding whether that's ambiguous. 2. Add one entry to `_LOCK_SOURCES` in `_locked_dependencies.py`, at the - priority position from the table above. + priority position from the table above -- **including if it ranks + below `poetry.lock`** (`Pipfile.lock` and pinned `requirements.txt` + both do). No extra code is needed for that case: the rank check in + `apply_locked_dependencies()` already treats every entry in + `_LOCK_SOURCES` (poetry.lock's placeholder included) uniformly. 3. No changes needed anywhere else -- `read_project()`'s wiring, provenance formatting, the override note, and UUID seeding are all already generic across every entry in the table. From 1df753a082957be5dc59c2629fe79364b2b43fbc Mon Sep 17 00:00:00 2001 From: Arthit Suriyawongkul Date: Fri, 4 Sep 2026 17:51:12 +0700 Subject: [PATCH 04/35] Fix bugs in uv.lock and pdm.lock support Signed-off-by: Arthit Suriyawongkul --- src/pitloom/extract/_lock_common.py | 41 ++++++++- src/pitloom/extract/_locked_dependencies.py | 3 +- src/pitloom/extract/_pdm_lock.py | 17 +++- src/pitloom/extract/_pylock.py | 6 +- src/pitloom/extract/_pyproject.py | 37 +++++++- src/pitloom/extract/_uv_lock.py | 88 ++++++++++++++---- src/pitloom/extract/project.py | 33 ++++--- .../assemble/test_deps_locked_dependencies.py | 2 +- tests/extract/test_lock_common.py | 26 +++++- tests/extract/test_pdm_lock.py | 2 +- tests/extract/test_project.py | 24 +++++ tests/extract/test_pyproject.py | 6 +- tests/extract/test_uv_lock.py | 92 ++++++++++++++++++- working-docs/design/lock-files.md | 6 +- working-docs/design/roadmap.md | 33 ++++--- .../implementation/lock-file-cascade.md | 4 +- 16 files changed, 351 insertions(+), 69 deletions(-) diff --git a/src/pitloom/extract/_lock_common.py b/src/pitloom/extract/_lock_common.py index 7bf66beb..2ebd9afa 100644 --- a/src/pitloom/extract/_lock_common.py +++ b/src/pitloom/extract/_lock_common.py @@ -22,6 +22,7 @@ from __future__ import annotations import logging +from collections.abc import Iterable, Mapping from pathlib import Path from typing import Any @@ -29,7 +30,26 @@ log = logging.getLogger(__name__) -__all__ = ["index_packages_by_name", "load_lock_toml"] +__all__ = [ + "POETRY_LOCK_SOURCE_NAME", + "find_first_present_key", + "index_packages_by_name", + "load_lock_toml", +] + +#: The literal ``Source:`` name written into +#: ``metadata.provenance["locked_dependencies"]`` for a ``poetry.lock`` +#: result (by :func:`pitloom.extract._pyproject._try_read_poetry`) and +#: read back out of that same string (by +#: :func:`pitloom.extract._locked_dependencies.apply_locked_dependencies`, +#: to look up ``poetry.lock``'s fixed rank when deciding whether a +#: cascade entry may override it). A single shared constant instead of +#: two independently-typed string literals -- editing one without the +#: other would silently break that rank lookup (it would just stop +#: matching, not raise), the "pattern hand-copied across 3+ call sites +#: drifts" problem CLAUDE.md warns about, here between a producer and a +#: consumer rather than three siblings. +POETRY_LOCK_SOURCE_NAME = "poetry.lock" def load_lock_toml(lock_path: Path) -> dict[str, Any] | None: @@ -75,3 +95,22 @@ def index_packages_by_name(packages: list[Any]) -> dict[str, list[dict[str, Any] if isinstance(name, str) and name: by_name.setdefault(name, []).append(pkg) return by_name + + +def find_first_present_key( + mapping: Mapping[str, Any], keys: Iterable[str] +) -> str | None: + """Return the first of *keys* (in order) that's a key of *mapping*, + or ``None`` if none are. + + Every non-registry-source check (``poetry.lock``'s ``source.type`` + values, ``pylock.toml``'s top-level ``vcs``/``directory``/``archive`` + keys, ``uv.lock``'s nested ``source.{key}``, ``pdm.lock``'s flat + ``git``/``path``/``url`` keys) reduces to this same "which + non-registry marker, if any, is present" lookup once the caller has + the right mapping and key tuple for its own format -- factored out so + a shared key list update (e.g. adding a newly-noticed key like + ``"url"``) can be a one-line change in one format's own key tuple + without also re-deriving this lookup itself at each call site. + """ + return next((key for key in keys if key in mapping), None) diff --git a/src/pitloom/extract/_locked_dependencies.py b/src/pitloom/extract/_locked_dependencies.py index d57733d4..030c644f 100644 --- a/src/pitloom/extract/_locked_dependencies.py +++ b/src/pitloom/extract/_locked_dependencies.py @@ -36,6 +36,7 @@ from pitloom.assemble.spdx3._provenance_encoders import parse_provenance_value from pitloom.core.project import ProjectMetadata +from pitloom.extract._lock_common import POETRY_LOCK_SOURCE_NAME from pitloom.extract._pdm_lock import extract_pdm_lock_dependencies from pitloom.extract._pylock import extract_pylock_dependencies from pitloom.extract._uv_lock import extract_uv_lock_dependencies @@ -60,7 +61,7 @@ _LOCK_SOURCES: list[tuple[str, _LockExtractor | None, str | None]] = [ ("pylock.toml", extract_pylock_dependencies, "resolved_lockfile"), ("uv.lock", extract_uv_lock_dependencies, "resolved_lockfile"), - ("poetry.lock", None, None), + (POETRY_LOCK_SOURCE_NAME, None, None), ("pdm.lock", extract_pdm_lock_dependencies, "resolved_lockfile"), ] diff --git a/src/pitloom/extract/_pdm_lock.py b/src/pitloom/extract/_pdm_lock.py index eb347661..ebb3f5f8 100644 --- a/src/pitloom/extract/_pdm_lock.py +++ b/src/pitloom/extract/_pdm_lock.py @@ -42,7 +42,11 @@ from pathlib import Path from typing import Any -from pitloom.extract._lock_common import index_packages_by_name, load_lock_toml +from pitloom.extract._lock_common import ( + find_first_present_key, + index_packages_by_name, + load_lock_toml, +) log = logging.getLogger(__name__) @@ -54,8 +58,13 @@ #: ``pdm.lock`` keys, present directly on a ``[[package]]`` table (no #: nested ``source`` table, unlike ``uv.lock``), that mark a package as -#: not resolvable to a meaningful PyPI version pin. -_NON_REGISTRY_KEYS = ("git", "path") +#: not resolvable to a meaningful PyPI version pin. ``url`` records a +#: direct file-server/URL-sourced package (PDM's ``static_urls`` lock +#: strategy, or a plain ``pdm add ``) -- confirmed absent from +#: every ordinary registry-resolved entry in this repo's two real +#: pdm.lock fixtures, so including it here doesn't risk excluding a +#: normal package. +_NON_REGISTRY_KEYS = ("git", "url", "path") def _default_group_package_or_none(pkg: Any) -> dict[str, Any] | None: @@ -84,7 +93,7 @@ def _default_group_package_or_none(pkg: Any) -> dict[str, Any] | None: if not isinstance(groups, list) or _DEFAULT_GROUP not in groups: return None - non_registry_key = next((key for key in _NON_REGISTRY_KEYS if key in pkg), None) + non_registry_key = find_first_present_key(pkg, _NON_REGISTRY_KEYS) if non_registry_key is not None: log.warning( "Skipping pdm.lock entry %r: %s-sourced dependencies cannot be " diff --git a/src/pitloom/extract/_pylock.py b/src/pitloom/extract/_pylock.py index 428253be..38792c29 100644 --- a/src/pitloom/extract/_pylock.py +++ b/src/pitloom/extract/_pylock.py @@ -31,7 +31,7 @@ from pathlib import Path from typing import Any -from pitloom.extract._lock_common import load_lock_toml +from pitloom.extract._lock_common import find_first_present_key, load_lock_toml log = logging.getLogger(__name__) @@ -113,9 +113,7 @@ def _pinned_dep_for_package(pkg: Any) -> str | None: name, ) return None - non_registry_source = next( - (key for key in _NON_REGISTRY_SOURCE_KEYS if key in pkg), None - ) + non_registry_source = find_first_present_key(pkg, _NON_REGISTRY_SOURCE_KEYS) if non_registry_source is not None: log.warning( "Skipping pylock.toml entry %r: %s-sourced dependencies cannot " diff --git a/src/pitloom/extract/_pyproject.py b/src/pitloom/extract/_pyproject.py index 2b07270d..e8385c87 100644 --- a/src/pitloom/extract/_pyproject.py +++ b/src/pitloom/extract/_pyproject.py @@ -31,6 +31,7 @@ detect_license_for_project, resolve_license_concluded, ) +from pitloom.extract._lock_common import POETRY_LOCK_SOURCE_NAME from pitloom.extract._poetry import extract_poetry_metadata from pitloom.extract._poetry_lock import extract_poetry_lock_dependencies from pitloom.extract._pyproject_dynamic import prepare_dynamic_version @@ -44,9 +45,15 @@ def _read_pyproject_fallback( pyproject_path: Path, name: str, pitloom_config: PitloomConfig, + *, + include_locked_dependencies: bool, ) -> tuple[ProjectMetadata, PitloomConfig]: """Handle fallback when [project] section is absent or missing a name.""" - poetry_meta = _try_read_poetry(data, pyproject_path.parent) + poetry_meta = _try_read_poetry( + data, + pyproject_path.parent, + include_locked_dependencies=include_locked_dependencies, + ) if poetry_meta is not None: return poetry_meta, pitloom_config license_name, license_prov = detect_license_for_project(pyproject_path.parent) @@ -165,11 +172,21 @@ def _parse_standard_metadata_with_retry( # pylint: disable-next=too-many-locals -def read_pyproject(pyproject_path: Path) -> tuple[ProjectMetadata, PitloomConfig]: +def read_pyproject( + pyproject_path: Path, + *, + include_locked_dependencies: bool = True, +) -> tuple[ProjectMetadata, PitloomConfig]: """Read project metadata from a ``pyproject.toml`` file. Parses the ``[project]`` section via ``pyproject-metadata``, resolves dynamic versions, and reads Pitloom-specific settings from ``[tool.pitloom]``. + + ``include_locked_dependencies`` is forwarded to :func:`_try_read_poetry` + -- see its own docstring for why a build-stage caller must pass + ``False``. Defaults to ``True`` so every existing call site (the + source-stage path via :func:`pitloom.extract.project.read_project`) + keeps its current behaviour unless it explicitly opts out. """ if not pyproject_path.exists(): raise FileNotFoundError(f"pyproject.toml not found at {pyproject_path}") @@ -181,7 +198,13 @@ def read_pyproject(pyproject_path: Path) -> tuple[ProjectMetadata, PitloomConfig name: str = (project_data.get("name") or "").strip() if not project_data or not name: - return _read_pyproject_fallback(data, pyproject_path, name, pitloom_config) + return _read_pyproject_fallback( + data, + pyproject_path, + name, + pitloom_config, + include_locked_dependencies=include_locked_dependencies, + ) data, dynamic_fields, version_source, description_source = prepare_dynamic_version( data, project_data, pyproject_path @@ -232,7 +255,11 @@ def read_pyproject(pyproject_path: Path) -> tuple[ProjectMetadata, PitloomConfig ) # Fill any remaining gaps from [tool.poetry] (project fields win). - poetry_meta = _try_read_poetry(data, pyproject_path.parent) + poetry_meta = _try_read_poetry( + data, + pyproject_path.parent, + include_locked_dependencies=include_locked_dependencies, + ) if poetry_meta is not None: metadata = merge_project_metadata(metadata, poetry_meta) @@ -452,6 +479,6 @@ def _try_read_poetry( if locked_dependencies: metadata.locked_dependencies = locked_dependencies metadata.provenance["locked_dependencies"] = ( - "Source: poetry.lock | Method: resolved_lockfile" + f"Source: {POETRY_LOCK_SOURCE_NAME} | Method: resolved_lockfile" ) return metadata diff --git a/src/pitloom/extract/_uv_lock.py b/src/pitloom/extract/_uv_lock.py index 80381ad0..2c4128b5 100644 --- a/src/pitloom/extract/_uv_lock.py +++ b/src/pitloom/extract/_uv_lock.py @@ -47,7 +47,13 @@ from pathlib import Path from typing import Any -from pitloom.extract._lock_common import index_packages_by_name, load_lock_toml +from packaging.utils import canonicalize_name + +from pitloom.extract._lock_common import ( + find_first_present_key, + index_packages_by_name, + load_lock_toml, +) log = logging.getLogger(__name__) @@ -56,25 +62,62 @@ #: uv.lock ``source`` keys that mark a package as not resolvable to a #: meaningful PyPI version pin -- mirrors ``poetry.lock``'s #: ``directory``/``file``/``git``/``url`` skip and ``pylock.toml``'s -#: ``vcs``/``directory``/``archive`` skip. -_NON_REGISTRY_SOURCE_KEYS = ("git", "path", "directory", "editable", "virtual") +#: ``vcs``/``directory``/``archive`` skip. ``url`` is uv's direct +#: remote-wheel/sdist source (per uv's docs: source types are +#: Index/Git/URL/Path/Directory/Editable/Virtual) -- without it, a +#: url-sourced package would be emitted as an ordinary registry pin. +_NON_REGISTRY_SOURCE_KEYS = ("git", "url", "path", "directory", "editable", "virtual") #: ``source`` keys identifying the project's own package entry (a local #: root/workspace member, not a PyPI download). _ROOT_SOURCE_KEYS = ("editable", "virtual") -def _find_root_package(packages: list[Any]) -> dict[str, Any] | None: - """Return the first ``[[package]]`` entry that is the project's own +def _find_root_package( + packages: list[Any], expected_name: str | None +) -> dict[str, Any] | None: + """Return the ``[[package]]`` entry that is the project's own (identified by an ``editable``/``virtual`` ``source``), or ``None`` - if none is found.""" - for pkg in packages: - if not isinstance(pkg, dict): - continue - source = pkg.get("source") - if isinstance(source, dict) and any(key in source for key in _ROOT_SOURCE_KEYS): - return pkg - return None + if none is found. + + A shared ``uv.lock`` (a uv workspace) can list more than one such + entry -- one per local workspace member. When *expected_name* (the + calling project's own declared name, from its ``pyproject.toml``) is + given, it's used to pick the matching entry among candidates rather + than blindly taking the first one, which would silently attribute a + *different* workspace member's dependencies to this project. Falls + back to the sole candidate when there's exactly one and none named + *expected_name* matched (e.g. the name is unreadable, or differs + only in normalization); with more than one candidate and no match, + returns ``None`` rather than guess. + """ + candidates = [ + pkg + for pkg in packages + if isinstance(pkg, dict) + and isinstance(pkg.get("source"), dict) + and any(key in pkg["source"] for key in _ROOT_SOURCE_KEYS) + ] + if not candidates: + return None + + if expected_name is not None: + expected = canonicalize_name(expected_name) + for pkg in candidates: + name = pkg.get("name") + if isinstance(name, str) and canonicalize_name(name) == expected: + return pkg + + if len(candidates) > 1: + log.warning( + "%d candidate local/workspace package entries found in " + "uv.lock but none named %r -- can't determine which is this " + "project's own; ignoring uv.lock", + len(candidates), + expected_name, + ) + return None + return candidates[0] def _pinned_dep_for_root_dependency( @@ -136,9 +179,7 @@ def _pinned_dep_for_package(pkg: dict[str, Any]) -> str | None: name = pkg["name"] source = pkg.get("source") if isinstance(source, dict): - non_registry_source = next( - (key for key in _NON_REGISTRY_SOURCE_KEYS if key in source), None - ) + non_registry_source = find_first_present_key(source, _NON_REGISTRY_SOURCE_KEYS) if non_registry_source is not None: log.warning( "Skipping uv.lock entry %r: %s-sourced dependencies cannot " @@ -157,6 +198,19 @@ def _pinned_dep_for_package(pkg: dict[str, Any]) -> str | None: return f"{name}=={version}" +def _expected_project_name(project_dir: Path) -> str | None: + """Read the bare ``[project].name`` from *project_dir*'s + ``pyproject.toml``, or ``None`` if it's absent/unreadable -- used + only to disambiguate a shared uv workspace lock's multiple local + package entries, not as a metadata-resolution path in its own right + (that's :func:`pitloom.extract._pyproject.read_pyproject`'s job).""" + data = load_lock_toml(project_dir / "pyproject.toml") + if data is None: + return None + name = data.get("project", {}).get("name") + return name if isinstance(name, str) and name else None + + def extract_uv_lock_dependencies(project_dir: Path) -> list[str]: """Read ``uv.lock`` next to ``pyproject.toml`` and return the project's own main/runtime dependencies as exact-pin PEP 508 @@ -180,7 +234,7 @@ def extract_uv_lock_dependencies(project_dir: Path) -> list[str]: ) return [] - root = _find_root_package(packages) + root = _find_root_package(packages, _expected_project_name(project_dir)) if root is None: log.warning( "%s: no project package found (no 'editable'/'virtual' " diff --git a/src/pitloom/extract/project.py b/src/pitloom/extract/project.py index 0806f33d..d6a255f4 100644 --- a/src/pitloom/extract/project.py +++ b/src/pitloom/extract/project.py @@ -56,20 +56,27 @@ def read_project( some lock formats (``Pipfile.lock``, pinned ``requirements.txt``) pair with a bare ``setup.py`` in real projects, never ``pyproject.toml``. - ``include_locked_dependencies``, mirroring - :func:`pitloom.extract._pyproject._try_read_poetry`'s - ``poetry.lock``-specific flag of the same name, lets a build-stage or - config-only caller (e.g. ``embed-wheel``, or a shared CLI helper that - only wants ``[tool.pitloom]`` settings and discards the metadata) - explicitly opt out -- source-stage lock/pin data must never leak into - a build-stage SBOM, and skipping the cascade here also skips its file - I/O for a caller that would discard the result anyway. + ``include_locked_dependencies`` lets a build-stage or config-only + caller (e.g. ``embed-wheel``, or a shared CLI helper that only wants + ``[tool.pitloom]`` settings and discards the metadata) explicitly opt + out of *every* lock/pin source -- source-stage lock/pin data must + never leak into a build-stage SBOM, and skipping this also skips its + file I/O for a caller that would discard the result anyway. It's + forwarded to :func:`pitloom.extract._pyproject.read_pyproject` (which + forwards it again to + :func:`pitloom.extract._pyproject._try_read_poetry` for + ``poetry.lock``, gated by that function's own identically-named + parameter) *and* used directly here to gate + :func:`pitloom.extract._locked_dependencies.apply_locked_dependencies` + for every other format -- one flag controls both, not two + independently-set ones that happen to share a name. Args: project_path: Project root directory or sdist archive path. - include_locked_dependencies: Whether to overlay a sibling lock/pin - file's resolved dependencies (default ``True``). Pass - ``False`` from any build-stage or metadata-discarding caller. + include_locked_dependencies: Whether to read any lock/pin file's + resolved dependencies at all -- ``poetry.lock`` included + (default ``True``). Pass ``False`` from any build-stage or + metadata-discarding caller. Returns: A 3-tuple of: @@ -99,7 +106,9 @@ def read_project( config_path: Path | None pyproject_path = project_path / "pyproject.toml" if pyproject_path.exists(): - metadata, pitloom_config = read_pyproject(pyproject_path) + metadata, pitloom_config = read_pyproject( + pyproject_path, include_locked_dependencies=include_locked_dependencies + ) if not metadata.name and (setup_cfg.exists() or setup_py.exists()): # pyproject.toml exists but resolved no usable metadata -- # no [project] table (e.g. a custom/legacy build backend diff --git a/tests/assemble/test_deps_locked_dependencies.py b/tests/assemble/test_deps_locked_dependencies.py index ed7a0d69..6d9a404a 100644 --- a/tests/assemble/test_deps_locked_dependencies.py +++ b/tests/assemble/test_deps_locked_dependencies.py @@ -227,7 +227,7 @@ def test_locked_dependencies_same_content_different_provenance_changes_doc_uuid( def test_locked_dependencies_provenance_omitted_matches_empty_string() -> None: """Omitting ``locked_dependencies_provenance`` (every pre-existing call site) must produce the same UUID as every caller that predates - this parameter -- purely additive, no behavior change for callers + this parameter -- purely additive, no behaviour change for callers that don't know about it.""" omitted = compute_doc_uuid( "pkg", "1.0.0", ["requests>=2.0"], locked_dependencies=["idna==3.7"] diff --git a/tests/extract/test_lock_common.py b/tests/extract/test_lock_common.py index ab62cfb2..b02f78dd 100644 --- a/tests/extract/test_lock_common.py +++ b/tests/extract/test_lock_common.py @@ -14,7 +14,11 @@ import pytest -from pitloom.extract._lock_common import index_packages_by_name, load_lock_toml +from pitloom.extract._lock_common import ( + find_first_present_key, + index_packages_by_name, + load_lock_toml, +) def test_load_lock_toml_missing_file_returns_none() -> None: @@ -79,3 +83,23 @@ def test_index_packages_by_name_ignores_entries_with_missing_or_bad_name() -> No def test_index_packages_by_name_empty_list_returns_empty_dict() -> None: assert not index_packages_by_name([]) + + +def test_find_first_present_key_returns_first_match_in_key_order() -> None: + """Order is determined by *keys*, not by the mapping's own key + order -- callers rely on this to report a stable, predictable + non-registry-source name even when the mapping has multiple such + keys (shouldn't normally happen, but the tie-break must be + deterministic).""" + mapping = {"path": "x", "git": "y"} + + assert find_first_present_key(mapping, ("git", "path")) == "git" + assert find_first_present_key(mapping, ("path", "git")) == "path" + + +def test_find_first_present_key_returns_none_when_no_key_present() -> None: + assert find_first_present_key({"registry": "x"}, ("git", "path")) is None + + +def test_find_first_present_key_empty_mapping_returns_none() -> None: + assert find_first_present_key({}, ("git", "path")) is None diff --git a/tests/extract/test_pdm_lock.py b/tests/extract/test_pdm_lock.py index 585d74fd..5128fa1f 100644 --- a/tests/extract/test_pdm_lock.py +++ b/tests/extract/test_pdm_lock.py @@ -156,7 +156,7 @@ def test_missing_version_skipped_and_warns(caplog: pytest.LogCaptureFixture) -> assert "missing" in caplog.text.lower() -@pytest.mark.parametrize("source_key", ["git", "path"]) +@pytest.mark.parametrize("source_key", ["git", "url", "path"]) def test_non_registry_sourced_package_excluded( source_key: str, caplog: pytest.LogCaptureFixture ) -> None: diff --git a/tests/extract/test_project.py b/tests/extract/test_project.py index 7af1d354..b687268a 100644 --- a/tests/extract/test_project.py +++ b/tests/extract/test_project.py @@ -223,3 +223,27 @@ def test_read_project_include_locked_dependencies_false_skips_cascade( assert metadata.locked_dependencies == [] assert "locked_dependencies" not in metadata.provenance + + +def test_read_project_include_locked_dependencies_false_also_skips_poetry_lock( + tmp_path: Path, +) -> None: + """Regression: `include_locked_dependencies=False` used to only gate + the pylock.toml/uv.lock/pdm.lock cascade -- `poetry.lock` was still + read and attached to `locked_dependencies` regardless, since + `read_pyproject()` never forwarded the flag to `_try_read_poetry()`. + One flag must gate every lock source, `poetry.lock` included.""" + (tmp_path / "pyproject.toml").write_text( + '[tool.poetry]\nname = "pkg"\nversion = "1.0.0"\n', encoding="utf-8" + ) + (tmp_path / "poetry.lock").write_text( + '[[package]]\nname = "requests"\nversion = "2.31.0"\ngroups = ["main"]\n', + encoding="utf-8", + ) + + metadata, _pitloom_config, _config_path = read_project( + tmp_path, include_locked_dependencies=False + ) + + assert metadata.locked_dependencies == [] + assert "locked_dependencies" not in metadata.provenance diff --git a/tests/extract/test_pyproject.py b/tests/extract/test_pyproject.py index ef5d091b..9cfc810b 100644 --- a/tests/extract/test_pyproject.py +++ b/tests/extract/test_pyproject.py @@ -120,7 +120,11 @@ def test_read_pyproject_fallback_records_name_provenance() -> None: pyproject_path = tmp_path / "pyproject.toml" pyproject_path.write_text("[build-system]\n") metadata, _config = _read_pyproject_fallback( - {}, pyproject_path, "mypkg", PitloomConfig() + {}, + pyproject_path, + "mypkg", + PitloomConfig(), + include_locked_dependencies=True, ) assert metadata.name == "mypkg" assert metadata.provenance["name"] == "Source: pyproject.toml | Field: project.name" diff --git a/tests/extract/test_uv_lock.py b/tests/extract/test_uv_lock.py index 0e0e3427..cd55baa8 100644 --- a/tests/extract/test_uv_lock.py +++ b/tests/extract/test_uv_lock.py @@ -244,13 +244,24 @@ def test_ambiguous_multi_version_dependency_skipped_and_warns( @pytest.mark.parametrize( - "source_key", ["git", "path", "directory", "editable", "virtual"] + "source_key", ["git", "url", "path", "directory", "editable", "virtual"] ) def test_non_registry_sourced_dependency_excluded( source_key: str, caplog: pytest.LogCaptureFixture ) -> None: with tempfile.TemporaryDirectory() as tmp: tmp_path = Path(tmp) + # A real pyproject.toml (matching _ROOT_HEADER's "demo") is + # needed here specifically for the "editable"/"virtual" + # source_key cases: without it, "local-dep" (also + # editable/virtual-sourced by this test's own parametrization) + # would be a second candidate root package indistinguishable + # from "demo", and _find_root_package() would correctly refuse + # to guess between them -- unrelated to what this test checks + # (that a non-registry-sourced *dependency* is excluded). + (tmp_path / "pyproject.toml").write_text( + '[project]\nname = "demo"\nversion = "1.0.0"\n', encoding="utf-8" + ) _write_lock( tmp_path, _ROOT_HEADER + 'dependencies = [{ name = "local-dep" }]\n\n' @@ -300,7 +311,7 @@ def test_dependency_with_no_source_table_still_included() -> None: def test_find_root_package_returns_none_for_empty_list() -> None: - assert _find_root_package([]) is None + assert _find_root_package([], None) is None def test_find_root_package_ignores_malformed_entries() -> None: @@ -314,7 +325,52 @@ def test_find_root_package_ignores_malformed_entries() -> None: {"name": "requests", "version": "2.31.0"}, ] - assert _find_root_package(packages) is None + assert _find_root_package(packages, None) is None + + +def test_find_root_package_single_candidate_used_even_without_name_match() -> None: + """With exactly one editable/virtual candidate, it's used even when + it doesn't match `expected_name` (or `expected_name` is unavailable) + -- there's no ambiguity about *which* entry, only whether the name + happens to match, so guessing wrong here isn't the workspace-mixup + risk multiple candidates pose.""" + packages: list[object] = [ + {"name": "actual-name", "source": {"editable": "."}}, + ] + + assert _find_root_package(packages, "different-name") == packages[0] + assert _find_root_package(packages, None) == packages[0] + + +def test_find_root_package_prefers_name_match_among_multiple_candidates() -> None: + packages: list[object] = [ + {"name": "pkg-a", "source": {"editable": "."}}, + {"name": "pkg-b", "source": {"editable": "."}}, + ] + + assert _find_root_package(packages, "pkg-b") == packages[1] + assert _find_root_package(packages, "Pkg_B") == packages[1] # canonicalized + + +def test_find_root_package_multiple_candidates_no_name_match_returns_none_and_warns( + caplog: pytest.LogCaptureFixture, +) -> None: + """A shared uv workspace lock listing more than one local member, + where none matches the project actually being scanned, must not + silently attribute the wrong member's dependencies -- this is the + regression case: picking `packages[0]` unconditionally here would + misattribute `pkg-a`'s (or `pkg-b`'s) dependencies to `pkg-c`.""" + packages: list[object] = [ + {"name": "pkg-a", "source": {"editable": "."}}, + {"name": "pkg-b", "source": {"editable": "."}}, + ] + + with caplog.at_level(logging.WARNING): + result = _find_root_package(packages, "pkg-c") + + assert result is None + assert "2 candidate" in caplog.text + assert "pkg-c" in caplog.text def test_pinned_dep_for_package_returns_none_when_source_not_a_dict() -> None: @@ -408,6 +464,36 @@ def test_read_project_pylock_takes_priority_over_uv_lock() -> None: ) +def test_read_project_uv_workspace_picks_matching_member_by_name() -> None: + """Regression: a shared uv.lock listing more than one local + workspace member must resolve the *scanned* project's own + dependencies, identified by matching `pyproject.toml`'s declared + name -- not whichever editable entry happens to be listed first in + the lock file.""" + with tempfile.TemporaryDirectory() as tmp: + tmp_path = Path(tmp) + (tmp_path / "pyproject.toml").write_text( + '[project]\nname = "pkg-b"\nversion = "1.0.0"\n', encoding="utf-8" + ) + _write_lock( + tmp_path, + '[[package]]\nname = "pkg-a"\nversion = "1.0.0"\n' + 'source = { editable = "." }\n' + 'dependencies = [{ name = "requests" }]\n\n' + '[[package]]\nname = "pkg-b"\nversion = "1.0.0"\n' + 'source = { editable = "." }\n' + 'dependencies = [{ name = "httpx" }]\n\n' + '[[package]]\nname = "requests"\nversion = "2.31.0"\n' + 'source = { registry = "https://pypi.org/simple" }\n\n' + '[[package]]\nname = "httpx"\nversion = "0.27.0"\n' + 'source = { registry = "https://pypi.org/simple" }\n', + ) + + metadata, _config, _path = read_project(tmp_path) + + assert metadata.locked_dependencies == ["httpx==0.27.0"] + + # --- real-world fixtures --------------------------------------------------- diff --git a/working-docs/design/lock-files.md b/working-docs/design/lock-files.md index 2144c4b4..dd83f856 100644 --- a/working-docs/design/lock-files.md +++ b/working-docs/design/lock-files.md @@ -1,6 +1,6 @@ --- Created: 2026-08-31 -Last-Modified: 2026-09-02 +Last-Modified: 2026-09-04 SPDX-FileCopyrightText: 2026-present Arthit Suriyawongkul SPDX-FileType: DOCUMENTATION SPDX-License-Identifier: CC0-1.0 @@ -66,12 +66,12 @@ simply by asking users to run `[tool] export --format pylock`. | --- | --- | --- | | **1: The Universal Core** | `pylock.toml` (PEP 751) | **Done (2026-09-02)** -- see the "See also" note above. The official Python interoperability standard. Universal fallback. | | | `pyproject.toml` | Standard project metadata (PEP 621) to define the root SBOM component. | -| | `uv.lock` | The dominant lock file for modern, high-performance ML inference stacks (vLLM, FastAPI). | +| | `uv.lock` | **Done (2026-09-04)** -- see `working-docs/implementation/lock-file-cascade.md`. The dominant lock file for modern, high-performance ML inference stacks (vLLM, FastAPI). | | | `requirements.txt` | Ubiquitous in ML research Dockerfiles, PyTorch deployments, and Hugging Face spaces. | | **2: AI/ML Native Binary** | `pixi.lock` | Essential for AI: natively resolves both Python packages and system-level C/C++ CUDA/Conda binaries. | | | `conda-lock.yml` | Maps Conda data science packages alongside PyPI wheels. | | **3: Corporate Standards** | `poetry.lock` | **Done (2026-08-31)** -- see the "See also" note above. Massive legacy and enterprise footprint in Data Engineering (Airflow, dbt). | -| **4: Legacy & Niche** | `pdm.lock` | PDM leads PEP standard compliance, but `pylock.toml` export handles most PDM use cases. | +| **4: Legacy & Niche** | `pdm.lock` | **Done (2026-09-04)** -- see `working-docs/implementation/lock-file-cascade.md`. PDM leads PEP standard compliance, but `pylock.toml` export handles most PDM use cases. | | | `Pipfile.lock` | Largely legacy tooling. Low priority. | --- diff --git a/working-docs/design/roadmap.md b/working-docs/design/roadmap.md index b71a7cdf..cedc7e66 100644 --- a/working-docs/design/roadmap.md +++ b/working-docs/design/roadmap.md @@ -164,23 +164,30 @@ table in [non-hatchling-file-discovery.md](non-hatchling-file-discovery.md)); See [pep751-pylock-support.md](../implementation/pep751-pylock-support.md) and [lock-file-cascade.md](../implementation/lock-file-cascade.md) for the shared priority mechanism across all lock formats. +- [x] **`uv.lock`** -- done (2026-09-04): reads a sibling `uv.lock`'s + resolved main/runtime dependencies, ranked below `pylock.toml` and + above `poetry.lock` in the shared priority cascade. See + [lock-file-cascade.md](../implementation/lock-file-cascade.md). +- [x] **`pdm.lock`** -- done (2026-09-04): reads a sibling `pdm.lock`'s + resolved `default`-group dependencies, ranked below `poetry.lock`. + See [lock-file-cascade.md](../implementation/lock-file-cascade.md). - [ ] **Remaining lock formats as a resolved-dependency source** - (`uv.lock`, `pixi.lock`, `conda-lock.yml`, `pdm.lock`, `Pipfile.lock`, - pinned `requirements.txt`) -- `loom project` still records only the - declared version specifier from `pyproject.toml [project] dependencies` + (`pixi.lock`, `conda-lock.yml`, `Pipfile.lock`, pinned + `requirements.txt`) -- `loom project` still records only the declared + version specifier from `pyproject.toml [project] dependencies` (`normalize_dependency_specifier`, `src/pitloom/extract/_pyproject.py:220`, - e.g. `requests>=2.0`) for a project with none of `poetry.lock`/ - `pylock.toml` present, never a concrete resolved version. Parsing one + e.g. `requests>=2.0`) for a project with none of the four already-shipped + lock formats present, never a concrete resolved version. Parsing one when present would let a Source SBOM carry the actual pinned version a build will use, not just the declared range -- closer to what CISA's - Source SBOM guidance expects. The `poetry.lock`/`pylock.toml` cases - above establish the pattern (additive transitive-only edges, - `completeness` tagging, source-stage-only scoping, `pylock.toml` - overriding `poetry.lock` when both are present); each further format - added needs its own slot in that same priority order and a provenance - `method` tag. See [lock-files.md](./lock-files.md) for the broader - multi-format extraction-priority roadmap (`uv.lock`, `pixi.lock`, - `conda-lock.yml`, `pdm.lock`, `Pipfile.lock`) this item now defers to. + Source SBOM guidance expects. `pylock.toml`/`uv.lock`/`poetry.lock`/ + `pdm.lock` establish the pattern (additive transitive-only edges, + `completeness` tagging, source-stage-only scoping, one shared priority + cascade); each further format added needs its own slot in that same + priority order and a provenance `method` tag. See + [lock-files.md](./lock-files.md) for the broader multi-format + extraction-priority roadmap (`pixi.lock`, `conda-lock.yml`, + `Pipfile.lock`) this item now defers to. ### PEP 770 / embed-wheel diff --git a/working-docs/implementation/lock-file-cascade.md b/working-docs/implementation/lock-file-cascade.md index f6fbbdba..92697330 100644 --- a/working-docs/implementation/lock-file-cascade.md +++ b/working-docs/implementation/lock-file-cascade.md @@ -9,7 +9,7 @@ SPDX-License-Identifier: CC0-1.0 # Lock/pin format priority cascade -- implementation notes This is implementation reference for maintainers -- what the code does -and why. For the user-facing behavior (what shows up in a generated +and why. For the user-facing behaviour (what shows up in a generated SBOM, which lock file wins, which commands use lock files at all), see [docs/dependency-sources.md](../../docs/dependency-sources.md) instead; this page assumes that one as background and doesn't restate it. @@ -113,7 +113,7 @@ first resolves the rank of whatever source (if any) already populated reaches an entry ranked *below* that already-set source, since nothing from there on could legitimately win. `tests/extract/test_pdm_lock.py::test_read_project_pdm_lock_never_overrides_poetry_lock` is the regression test for this; `test_read_project_uv_lock_still_overrides_pdm_lock` -confirms the higher-ranked entries' behavior didn't change. +confirms the higher-ranked entries' behaviour didn't change. **Any future format ranked below `poetry.lock` (`Pipfile.lock`, pinned `requirements.txt`) needs no extra code for this** -- the same generic From dadd0fa2886206afe3a3b4f178236dbec29c7282 Mon Sep 17 00:00:00 2001 From: Arthit Suriyawongkul Date: Fri, 4 Sep 2026 23:59:26 +0700 Subject: [PATCH 05/35] Prevent clash from malform [project] Signed-off-by: Arthit Suriyawongkul --- src/pitloom/extract/_lock_common.py | 37 ++- src/pitloom/extract/_locked_dependencies.py | 56 +++- src/pitloom/extract/_pdm_lock.py | 3 +- src/pitloom/extract/_poetry_lock.py | 4 +- src/pitloom/extract/_pylock.py | 8 +- src/pitloom/extract/_uv_lock.py | 26 +- src/pitloom/extract/project.py | 27 +- tests/extract/test_poetry_lock.py | 16 ++ tests/extract/test_project.py | 37 +++ tests/extract/test_uv_lock.py | 258 +----------------- tests/extract/test_uv_lock_integration.py | 191 +++++++++++++ tests/extract/test_uv_lock_root_package.py | 95 +++++++ .../implementation/lock-file-cascade.md | 16 +- 13 files changed, 497 insertions(+), 277 deletions(-) create mode 100644 tests/extract/test_uv_lock_integration.py create mode 100644 tests/extract/test_uv_lock_root_package.py diff --git a/src/pitloom/extract/_lock_common.py b/src/pitloom/extract/_lock_common.py index 2ebd9afa..8e8f7d35 100644 --- a/src/pitloom/extract/_lock_common.py +++ b/src/pitloom/extract/_lock_common.py @@ -34,6 +34,7 @@ "POETRY_LOCK_SOURCE_NAME", "find_first_present_key", "index_packages_by_name", + "is_usable_version", "load_lock_toml", ] @@ -97,20 +98,38 @@ def index_packages_by_name(packages: list[Any]) -> dict[str, list[dict[str, Any] return by_name +def is_usable_version(version: Any) -> bool: + """Return whether *version* is a non-empty string -- the "can this + become a real ``name==version`` pin" check every lock/pin extractor + (``poetry.lock``, ``pylock.toml``, ``uv.lock``, ``pdm.lock``) applies + to a ``[[package]]`` entry's ``version`` field before using it. + Factored out once four independent copies of ``not + isinstance(version, str) or not version`` existed, per this repo's + "a pattern hand-copied across 3+ call sites drifts" convention -- + each call site still logs its own ``WARNING:`` when this returns + ``False``, since the message wording (which field, which format) is + genuinely format-specific. + """ + return isinstance(version, str) and bool(version) + + def find_first_present_key( mapping: Mapping[str, Any], keys: Iterable[str] ) -> str | None: """Return the first of *keys* (in order) that's a key of *mapping*, or ``None`` if none are. - Every non-registry-source check (``poetry.lock``'s ``source.type`` - values, ``pylock.toml``'s top-level ``vcs``/``directory``/``archive`` - keys, ``uv.lock``'s nested ``source.{key}``, ``pdm.lock``'s flat - ``git``/``path``/``url`` keys) reduces to this same "which - non-registry marker, if any, is present" lookup once the caller has - the right mapping and key tuple for its own format -- factored out so - a shared key list update (e.g. adding a newly-noticed key like - ``"url"``) can be a one-line change in one format's own key tuple - without also re-deriving this lookup itself at each call site. + Every *key-presence* non-registry-source check (``pylock.toml``'s + top-level ``vcs``/``directory``/``archive`` keys, ``uv.lock``'s + nested ``source.{key}``, ``pdm.lock``'s flat ``git``/``path``/``url`` + keys) reduces to this same "which non-registry marker, if any, is + present" lookup once the caller has the right mapping and key tuple + for its own format -- factored out so a shared key list update (e.g. + adding a newly-noticed key like ``"url"``) can be a one-line change + in one format's own key tuple without also re-deriving this lookup + itself at each call site. ``poetry.lock``'s own non-registry check is + a different shape (single-field *value* membership on + ``source.type``, not presence of any of several keys) and doesn't + use this helper. """ return next((key for key in keys if key in mapping), None) diff --git a/src/pitloom/extract/_locked_dependencies.py b/src/pitloom/extract/_locked_dependencies.py index 030c644f..4339a79a 100644 --- a/src/pitloom/extract/_locked_dependencies.py +++ b/src/pitloom/extract/_locked_dependencies.py @@ -45,7 +45,23 @@ __all__ = ["apply_locked_dependencies"] -_LockExtractor = Callable[[Path], list[str]] +_LockExtractor = Callable[[Path, str | None], list[str]] + + +def _ignore_expected_name(extractor: Callable[[Path], list[str]]) -> _LockExtractor: + """Adapt a single-argument extractor to :data:`_LockExtractor`'s + uniform ``(project_dir, expected_name)`` shape. + + Only ``uv.lock``'s extractor actually needs *expected_name* (to + disambiguate a shared workspace lock's multiple local package + entries -- see :func:`pitloom.extract._uv_lock.extract_uv_lock_dependencies`). + Rather than widen every extractor's own signature with a parameter + only one format uses, this adapter localizes the cascade's uniform- + call requirement to this one module, keeping each format's own + extractor signature as simple as its actual needs. + """ + return lambda project_dir, _expected_name: extractor(project_dir) + #: Full priority order (highest first) across every lock/pin source, #: including ``poetry.lock`` even though it has no extractor here (see @@ -59,10 +75,18 @@ #: for why this order was chosen (build-backend-agnostic and universal #: beats tool-specific; a real resolver lock beats a merely-pinned file). _LOCK_SOURCES: list[tuple[str, _LockExtractor | None, str | None]] = [ - ("pylock.toml", extract_pylock_dependencies, "resolved_lockfile"), + ( + "pylock.toml", + _ignore_expected_name(extract_pylock_dependencies), + "resolved_lockfile", + ), ("uv.lock", extract_uv_lock_dependencies, "resolved_lockfile"), (POETRY_LOCK_SOURCE_NAME, None, None), - ("pdm.lock", extract_pdm_lock_dependencies, "resolved_lockfile"), + ( + "pdm.lock", + _ignore_expected_name(extract_pdm_lock_dependencies), + "resolved_lockfile", + ), ] @@ -80,6 +104,16 @@ def apply_locked_dependencies(metadata: ProjectMetadata, project_dir: Path) -> N silently clobber a higher-priority result just because it happens to run later in this function's own loop. + Every extractor is called uniformly as ``extractor(project_dir, + metadata.name)`` -- ``metadata.name`` is already fully resolved by + the time this runs (see :func:`pitloom.extract.project.read_project`), + so passing it lets an extractor that needs it (currently only + ``uv.lock``'s workspace-root disambiguation) skip re-reading and + re-parsing ``pyproject.toml`` a second time just for that. Extractors + that don't need it (``pylock.toml``, ``pdm.lock``) keep their + simpler single-``project_dir`` signature and are wrapped with + :func:`_ignore_expected_name` in :data:`_LOCK_SOURCES` above instead. + If *metadata* already carries a ``locked_dependencies`` result and a higher-or-equal-priority source here wins, that source replaces it and a ``WARNING:`` names the override -- and, per this repo's "no @@ -100,6 +134,20 @@ def apply_locked_dependencies(metadata: ProjectMetadata, project_dir: Path) -> N ), None, ) + if previous is not None and previous_rank is None: + # previous_source doesn't match any _LOCK_SOURCES entry -- a + # provenance-string source name has drifted from this table (a + # bug, not a real absence of a prior result). Without this, + # every remaining rank's override-guard below would silently + # never fire, letting even the lowest-priority format overwrite + # an unrecognized-but-real prior result with no warning at all. + log.warning( + "%s: previously-resolved locked_dependencies source %r doesn't " + "match any known lock source -- can't rank it, so any " + "cascade-tried format may override it", + project_dir, + previous_source, + ) for rank, (source_name, extractor, method) in enumerate(_LOCK_SOURCES): if extractor is None: @@ -109,7 +157,7 @@ def apply_locked_dependencies(metadata: ProjectMetadata, project_dir: Path) -> N # none of them can win, so stop instead of scanning further. break - dependencies = extractor(project_dir) + dependencies = extractor(project_dir, metadata.name) if not dependencies: continue diff --git a/src/pitloom/extract/_pdm_lock.py b/src/pitloom/extract/_pdm_lock.py index ebb3f5f8..64e87fd0 100644 --- a/src/pitloom/extract/_pdm_lock.py +++ b/src/pitloom/extract/_pdm_lock.py @@ -45,6 +45,7 @@ from pitloom.extract._lock_common import ( find_first_present_key, index_packages_by_name, + is_usable_version, load_lock_toml, ) @@ -104,7 +105,7 @@ def _default_group_package_or_none(pkg: Any) -> dict[str, Any] | None: return None version = pkg.get("version") - if not isinstance(version, str) or not version: + if not is_usable_version(version): log.warning( "Skipping pdm.lock entry %r: missing or non-string 'version'", name, diff --git a/src/pitloom/extract/_poetry_lock.py b/src/pitloom/extract/_poetry_lock.py index 6e53f409..cc2de5da 100644 --- a/src/pitloom/extract/_poetry_lock.py +++ b/src/pitloom/extract/_poetry_lock.py @@ -27,7 +27,7 @@ from pathlib import Path from typing import Any -from pitloom.extract._lock_common import load_lock_toml +from pitloom.extract._lock_common import is_usable_version, load_lock_toml log = logging.getLogger(__name__) @@ -94,7 +94,7 @@ def _pinned_dep_for_package(pkg: Any) -> str | None: return None name = pkg.get("name") version = pkg.get("version") - if not isinstance(name, str) or not name or not isinstance(version, str): + if not isinstance(name, str) or not name or not is_usable_version(version): log.warning( "Skipping malformed poetry.lock [[package]] entry: missing or " "non-string 'name'/'version' (name=%r, version=%r)", diff --git a/src/pitloom/extract/_pylock.py b/src/pitloom/extract/_pylock.py index 38792c29..d0f215b4 100644 --- a/src/pitloom/extract/_pylock.py +++ b/src/pitloom/extract/_pylock.py @@ -31,7 +31,11 @@ from pathlib import Path from typing import Any -from pitloom.extract._lock_common import find_first_present_key, load_lock_toml +from pitloom.extract._lock_common import ( + find_first_present_key, + is_usable_version, + load_lock_toml, +) log = logging.getLogger(__name__) @@ -123,7 +127,7 @@ def _pinned_dep_for_package(pkg: Any) -> str | None: ) return None version = pkg.get("version") - if not isinstance(version, str) or not version: + if not is_usable_version(version): log.warning( "Skipping pylock.toml entry %r: missing or non-string 'version'", name, diff --git a/src/pitloom/extract/_uv_lock.py b/src/pitloom/extract/_uv_lock.py index 2c4128b5..532377c2 100644 --- a/src/pitloom/extract/_uv_lock.py +++ b/src/pitloom/extract/_uv_lock.py @@ -52,6 +52,7 @@ from pitloom.extract._lock_common import ( find_first_present_key, index_packages_by_name, + is_usable_version, load_lock_toml, ) @@ -96,7 +97,7 @@ def _find_root_package( for pkg in packages if isinstance(pkg, dict) and isinstance(pkg.get("source"), dict) - and any(key in pkg["source"] for key in _ROOT_SOURCE_KEYS) + and find_first_present_key(pkg["source"], _ROOT_SOURCE_KEYS) is not None ] if not candidates: return None @@ -189,7 +190,7 @@ def _pinned_dep_for_package(pkg: dict[str, Any]) -> str | None: ) return None version = pkg.get("version") - if not isinstance(version, str) or not version: + if not is_usable_version(version): log.warning( "Skipping uv.lock entry %r: missing or non-string 'version'", name, @@ -207,15 +208,28 @@ def _expected_project_name(project_dir: Path) -> str | None: data = load_lock_toml(project_dir / "pyproject.toml") if data is None: return None - name = data.get("project", {}).get("name") + project_table = data.get("project", {}) + if not isinstance(project_table, dict): + return None + name = project_table.get("name") return name if isinstance(name, str) and name else None -def extract_uv_lock_dependencies(project_dir: Path) -> list[str]: +def extract_uv_lock_dependencies( + project_dir: Path, expected_name: str | None = None +) -> list[str]: """Read ``uv.lock`` next to ``pyproject.toml`` and return the project's own main/runtime dependencies as exact-pin PEP 508 strings. + *expected_name* disambiguates a shared uv workspace lock's multiple + local package entries (see :func:`_find_root_package`) -- pass the + caller's already-resolved :attr:`~pitloom.core.project.ProjectMetadata.name` + (``apply_locked_dependencies()`` always does) to avoid re-parsing + ``pyproject.toml`` a second time just for this. When omitted (e.g. a + caller invoking this extractor directly, outside the cascade), falls + back to reading it via :func:`_expected_project_name`. + Returns an empty list when no ``uv.lock`` is present, it can't be parsed, or the project's own package entry can't be identified -- this is optional enrichment, never a requirement. @@ -234,7 +248,9 @@ def extract_uv_lock_dependencies(project_dir: Path) -> list[str]: ) return [] - root = _find_root_package(packages, _expected_project_name(project_dir)) + if expected_name is None: + expected_name = _expected_project_name(project_dir) + root = _find_root_package(packages, expected_name) if root is None: log.warning( "%s: no project package found (no 'editable'/'virtual' " diff --git a/src/pitloom/extract/project.py b/src/pitloom/extract/project.py index d6a255f4..e3e12923 100644 --- a/src/pitloom/extract/project.py +++ b/src/pitloom/extract/project.py @@ -17,7 +17,7 @@ from pathlib import Path from pitloom.core.config import PitloomConfig -from pitloom.core.project import ProjectMetadata +from pitloom.core.project import ProjectMetadata, merge_project_metadata from pitloom.extract._locked_dependencies import apply_locked_dependencies from pitloom.extract._pyproject import read_pyproject from pitloom.extract._sdist import read_sdist @@ -125,7 +125,32 @@ def read_project( "instead of an empty pyproject.toml-only result", pyproject_path, ) + # read_pyproject() may have already resolved locked_dependencies + # from poetry.lock -- via _try_read_poetry()'s own "[tool.poetry] + # couldn't be parsed, but still apply poetry.lock's resolved + # dependencies" path, the exact case that leads here (an empty + # name with real poetry.lock data already attached). Never + # silently drop a real result just because metadata itself had + # to come from setup.cfg/setup.py instead: without this, a + # lower-priority lock/pin format could also silently win the + # cascade below in poetry.lock's place, since + # apply_locked_dependencies() would see no prior result at all + # on the replaced metadata. + # + # merge_project_metadata() (not a hand-rolled per-field + # carry-over) does this generically for every ProjectMetadata + # field, `name` always primary's (read_setuptools()'s -- the + # real resolved name) and `locked_dependencies`/`provenance` + # falling back to secondary's (the pre-swap `metadata`) when + # primary's own is empty -- the same mechanism already used + # for the analogous [tool.pitloom]-config carry-over just + # below, generalized so a future field needing the same + # treatment doesn't need a third hand-copied carry-over here. + pre_setuptools_metadata = metadata metadata, setuptools_pitloom_config = read_setuptools(project_path) + metadata = merge_project_metadata( + primary=metadata, secondary=pre_setuptools_metadata + ) # [tool.pitloom] always lives in pyproject.toml, never # setup.cfg/setup.py -- keep the one read_pyproject() already # resolved from the real pyproject.toml unless it's untouched diff --git a/tests/extract/test_poetry_lock.py b/tests/extract/test_poetry_lock.py index 37e2b6f7..9eb87671 100644 --- a/tests/extract/test_poetry_lock.py +++ b/tests/extract/test_poetry_lock.py @@ -148,6 +148,22 @@ def test_malformed_package_entry_warns(caplog: pytest.LogCaptureFixture) -> None assert "malformed" in caplog.text.lower() +def test_malformed_package_entry_empty_version_skipped() -> None: + """Regression: an entry with ``version = ""`` (a string, but empty) + used to pass the ``isinstance(version, str)`` check and produce an + invalid ``name==`` pin -- parity with ``_pylock.py``/``_uv_lock.py``/ + ``_pdm_lock.py``, which all also reject an empty-string version.""" + with tempfile.TemporaryDirectory() as tmp: + tmp_path = Path(tmp) + _write_lock( + tmp_path, + '[[package]]\nname = "broken"\nversion = ""\n\n' + '[[package]]\nname = "complete-pkg"\nversion = "2.0.0"\n', + ) + + assert extract_poetry_lock_dependencies(tmp_path) == ["complete-pkg==2.0.0"] + + def test_package_table_not_a_list_warns(caplog: pytest.LogCaptureFixture) -> None: """Regression: a malformed top-level ``package`` key (not a list) used to degrade to an empty list with zero logging.""" diff --git a/tests/extract/test_project.py b/tests/extract/test_project.py index b687268a..71234dd6 100644 --- a/tests/extract/test_project.py +++ b/tests/extract/test_project.py @@ -128,6 +128,43 @@ def test_read_project_fallback_still_applies_lock_cascade(tmp_path: Path) -> Non ) +def test_read_project_fallback_preserves_already_resolved_poetry_lock( + tmp_path: Path, +) -> None: + """Regression: `_try_read_poetry()` can resolve `poetry.lock`'s data + onto a name-less `ProjectMetadata` when `[tool.poetry]` itself fails + to parse (its own "skipping Poetry gap-fill, but still applying + poetry.lock's resolved dependencies" path). That empty name then + triggers this same setup.cfg/setup.py fallback branch, which used to + replace `metadata` wholesale via `read_setuptools()` -- silently + discarding poetry.lock's already-resolved `locked_dependencies` and + letting a lower-priority format (here `pdm.lock`) win the cascade in + its place with no "supersedes" note. The prior result must survive + the metadata swap.""" + (tmp_path / "pyproject.toml").write_text( + '[tool.poetry]\nversion = "1.0.0"\n', encoding="utf-8" + ) + (tmp_path / "poetry.lock").write_text( + '[[package]]\nname = "requests"\nversion = "2.31.0"\ngroups = ["main"]\n', + encoding="utf-8", + ) + (tmp_path / "setup.cfg").write_text( + "[metadata]\nname = real-pkg\nversion = 1.2.3\n", encoding="utf-8" + ) + (tmp_path / "pdm.lock").write_text( + '[[package]]\nname = "httpx"\nversion = "0.28.1"\ngroups = ["default"]\n', + encoding="utf-8", + ) + + metadata, _pitloom_config, _config_path = read_project(tmp_path) + + assert metadata.name == "real-pkg" + assert metadata.locked_dependencies == ["requests==2.31.0"] + assert metadata.provenance["locked_dependencies"] == ( + "Source: poetry.lock | Method: resolved_lockfile" + ) + + def test_read_project_build_system_only_pyproject_no_setuptools_fallback( tmp_path: Path, ) -> None: diff --git a/tests/extract/test_uv_lock.py b/tests/extract/test_uv_lock.py index cd55baa8..014e13e3 100644 --- a/tests/extract/test_uv_lock.py +++ b/tests/extract/test_uv_lock.py @@ -4,12 +4,14 @@ # SPDX-License-Identifier: Apache-2.0 """Tests for ``uv.lock`` resolved-dependency parsing -(:mod:`pitloom.extract._uv_lock`) and its overlay onto -``ProjectMetadata.locked_dependencies`` via ``read_project()``'s lock -cascade (:mod:`pitloom.extract._locked_dependencies`). - -See also: test_pylock.py/test_poetry_lock.py for the sibling lock -extractors this module's tests mirror in shape; +(:mod:`pitloom.extract._uv_lock`)'s core extraction correctness -- +malformed/missing input handling, dependency-reference resolution, and +the marker-ambiguity skip policy. + +See also: test_uv_lock_root_package.py (root/workspace-member package +selection), test_uv_lock_integration.py (``read_project()`` cascade +wiring and real-world fixtures), test_pylock.py/test_poetry_lock.py for +the sibling lock extractors this module's tests mirror in shape, and test_locked_dependencies.py for the cascade mechanism's own tests. """ @@ -19,12 +21,7 @@ import pytest -from pitloom.extract._uv_lock import ( - _find_root_package, - _pinned_dep_for_package, - extract_uv_lock_dependencies, -) -from pitloom.extract.project import read_project +from pitloom.extract._uv_lock import extract_uv_lock_dependencies _LOCK_HEADER = 'version = 1\nrevision = 1\nrequires-python = ">=3.10"\n' @@ -34,8 +31,6 @@ '[[package]]\nname = "demo"\nversion = "1.0.0"\nsource = { editable = "." }\n' ) -REAL_WORLD_LOCKS = Path(__file__).parent.parent / "fixtures" / "real-world-locks" / "uv" - def _write_lock(tmp_dir: Path, body: str = "") -> None: (tmp_dir / "uv.lock").write_text(_LOCK_HEADER + body, encoding="utf-8") @@ -308,238 +303,3 @@ def test_dependency_with_no_source_table_still_included() -> None: ) assert extract_uv_lock_dependencies(tmp_path) == ["no-source==1.2.3"] - - -def test_find_root_package_returns_none_for_empty_list() -> None: - assert _find_root_package([], None) is None - - -def test_find_root_package_ignores_malformed_entries() -> None: - """A malformed top-level `[[package]]` entry (not a table) is - silently skipped while searching for the root package -- see - test_lock_common.py for the equivalent `index_packages_by_name()` - coverage this and `_uv_lock.py`'s own extraction share.""" - packages: list[object] = [ - "not-a-dict", - {"version": "1.0.0"}, # missing name, still not editable/virtual - {"name": "requests", "version": "2.31.0"}, - ] - - assert _find_root_package(packages, None) is None - - -def test_find_root_package_single_candidate_used_even_without_name_match() -> None: - """With exactly one editable/virtual candidate, it's used even when - it doesn't match `expected_name` (or `expected_name` is unavailable) - -- there's no ambiguity about *which* entry, only whether the name - happens to match, so guessing wrong here isn't the workspace-mixup - risk multiple candidates pose.""" - packages: list[object] = [ - {"name": "actual-name", "source": {"editable": "."}}, - ] - - assert _find_root_package(packages, "different-name") == packages[0] - assert _find_root_package(packages, None) == packages[0] - - -def test_find_root_package_prefers_name_match_among_multiple_candidates() -> None: - packages: list[object] = [ - {"name": "pkg-a", "source": {"editable": "."}}, - {"name": "pkg-b", "source": {"editable": "."}}, - ] - - assert _find_root_package(packages, "pkg-b") == packages[1] - assert _find_root_package(packages, "Pkg_B") == packages[1] # canonicalized - - -def test_find_root_package_multiple_candidates_no_name_match_returns_none_and_warns( - caplog: pytest.LogCaptureFixture, -) -> None: - """A shared uv workspace lock listing more than one local member, - where none matches the project actually being scanned, must not - silently attribute the wrong member's dependencies -- this is the - regression case: picking `packages[0]` unconditionally here would - misattribute `pkg-a`'s (or `pkg-b`'s) dependencies to `pkg-c`.""" - packages: list[object] = [ - {"name": "pkg-a", "source": {"editable": "."}}, - {"name": "pkg-b", "source": {"editable": "."}}, - ] - - with caplog.at_level(logging.WARNING): - result = _find_root_package(packages, "pkg-c") - - assert result is None - assert "2 candidate" in caplog.text - assert "pkg-c" in caplog.text - - -def test_pinned_dep_for_package_returns_none_when_source_not_a_dict() -> None: - """Defensive: a `source` value that isn't a table (malformed) is - skipped by the source-key check, not a crash -- version resolution - still proceeds normally.""" - assert ( - _pinned_dep_for_package( - {"name": "odd-pkg", "version": "1.0.0", "source": "not-a-table"} - ) - == "odd-pkg==1.0.0" - ) - - -# --- read_project() cascade integration ----------------------------------- - - -def test_read_project_populates_locked_dependencies() -> None: - with tempfile.TemporaryDirectory() as tmp: - tmp_path = Path(tmp) - (tmp_path / "pyproject.toml").write_text( - '[project]\nname = "demo"\nversion = "1.0.0"\n', encoding="utf-8" - ) - _write_lock( - tmp_path, - _ROOT_HEADER + 'dependencies = [{ name = "requests" }]\n\n' - '[[package]]\nname = "requests"\nversion = "2.31.0"\n' - 'source = { registry = "https://pypi.org/simple" }\n', - ) - - metadata, _config, _path = read_project(tmp_path) - - assert metadata.locked_dependencies == ["requests==2.31.0"] - assert metadata.provenance["locked_dependencies"] == ( - "Source: uv.lock | Method: resolved_lockfile" - ) - - -def test_read_project_uv_lock_takes_priority_over_poetry_lock( - caplog: pytest.LogCaptureFixture, -) -> None: - with tempfile.TemporaryDirectory() as tmp: - tmp_path = Path(tmp) - (tmp_path / "pyproject.toml").write_text( - '[tool.poetry]\nname = "demo"\nversion = "1.0.0"\n', encoding="utf-8" - ) - (tmp_path / "poetry.lock").write_text( - '[[package]]\nname = "requests"\nversion = "2.31.0"\ngroups = ["main"]\n', - encoding="utf-8", - ) - _write_lock( - tmp_path, - _ROOT_HEADER + 'dependencies = [{ name = "httpx" }]\n\n' - '[[package]]\nname = "httpx"\nversion = "0.27.0"\n' - 'source = { registry = "https://pypi.org/simple" }\n', - ) - - with caplog.at_level(logging.WARNING): - metadata, _config, _path = read_project(tmp_path) - - assert metadata.locked_dependencies == ["httpx==0.27.0"] - assert metadata.provenance["locked_dependencies"] == ( - "Source: uv.lock | Method: resolved_lockfile | Note: supersedes poetry.lock" - ) - - -def test_read_project_pylock_takes_priority_over_uv_lock() -> None: - """pylock.toml (PEP 751) outranks uv.lock in the cascade.""" - with tempfile.TemporaryDirectory() as tmp: - tmp_path = Path(tmp) - (tmp_path / "pyproject.toml").write_text( - '[project]\nname = "demo"\nversion = "1.0.0"\n', encoding="utf-8" - ) - (tmp_path / "pylock.toml").write_text( - 'lock-version = "1.0"\ncreated-by = "test"\n' - '[[packages]]\nname = "httpx"\nversion = "0.27.0"\n', - encoding="utf-8", - ) - _write_lock( - tmp_path, - _ROOT_HEADER + 'dependencies = [{ name = "requests" }]\n\n' - '[[package]]\nname = "requests"\nversion = "2.31.0"\n' - 'source = { registry = "https://pypi.org/simple" }\n', - ) - - metadata, _config, _path = read_project(tmp_path) - - assert metadata.locked_dependencies == ["httpx==0.27.0"] - assert metadata.provenance["locked_dependencies"] == ( - "Source: pylock.toml | Method: resolved_lockfile" - ) - - -def test_read_project_uv_workspace_picks_matching_member_by_name() -> None: - """Regression: a shared uv.lock listing more than one local - workspace member must resolve the *scanned* project's own - dependencies, identified by matching `pyproject.toml`'s declared - name -- not whichever editable entry happens to be listed first in - the lock file.""" - with tempfile.TemporaryDirectory() as tmp: - tmp_path = Path(tmp) - (tmp_path / "pyproject.toml").write_text( - '[project]\nname = "pkg-b"\nversion = "1.0.0"\n', encoding="utf-8" - ) - _write_lock( - tmp_path, - '[[package]]\nname = "pkg-a"\nversion = "1.0.0"\n' - 'source = { editable = "." }\n' - 'dependencies = [{ name = "requests" }]\n\n' - '[[package]]\nname = "pkg-b"\nversion = "1.0.0"\n' - 'source = { editable = "." }\n' - 'dependencies = [{ name = "httpx" }]\n\n' - '[[package]]\nname = "requests"\nversion = "2.31.0"\n' - 'source = { registry = "https://pypi.org/simple" }\n\n' - '[[package]]\nname = "httpx"\nversion = "0.27.0"\n' - 'source = { registry = "https://pypi.org/simple" }\n', - ) - - metadata, _config, _path = read_project(tmp_path) - - assert metadata.locked_dependencies == ["httpx==0.27.0"] - - -# --- real-world fixtures --------------------------------------------------- - - -def test_real_world_flask() -> None: - """`pallets/flask` -- `uv.lock` ships in the PyPI sdist itself (the - only fixture where that's true, per real-world-locks/README.md). - Has multiple marker-conditional duplicate names (e.g. `click`), - exercising the ambiguity-skip path against real data.""" - metadata, _config, _path = read_project(REAL_WORLD_LOCKS / "flask-3.1.3") - - assert metadata.name == "Flask" - names = {dep.split("==", maxsplit=1)[0] for dep in metadata.locked_dependencies} - assert names == { - "blinker", - "importlib-metadata", - "itsdangerous", - "jinja2", - "markupsafe", - "werkzeug", - } - assert "click" not in names # ambiguous (ships two marker-conditional versions) - assert metadata.provenance["locked_dependencies"] == ( - "Source: uv.lock | Method: resolved_lockfile" - ) - - -def test_real_world_fastapi_cli() -> None: - metadata, _config, _path = read_project(REAL_WORLD_LOCKS / "fastapi-cli-0.0.32") - - assert metadata.name == "fastapi-cli" - names = {dep.split("==", maxsplit=1)[0] for dep in metadata.locked_dependencies} - assert names == {"rich-toolkit", "tomli", "typer", "uvicorn"} - - -def test_real_world_abi3audit() -> None: - metadata, _config, _path = read_project(REAL_WORLD_LOCKS / "abi3audit-0.0.26") - - assert metadata.name == "abi3audit" - names = {dep.split("==", maxsplit=1)[0] for dep in metadata.locked_dependencies} - assert names == { - "abi3info", - "kaitaistruct", - "packaging", - "pefile", - "pyelftools", - "requests", - "requests-cache", - "rich", - } diff --git a/tests/extract/test_uv_lock_integration.py b/tests/extract/test_uv_lock_integration.py new file mode 100644 index 00000000..007e49ed --- /dev/null +++ b/tests/extract/test_uv_lock_integration.py @@ -0,0 +1,191 @@ +# SPDX-FileContributor: Arthit Suriyawongkul +# SPDX-FileCopyrightText: 2026-present Arthit Suriyawongkul +# SPDX-FileType: SOURCE +# SPDX-License-Identifier: Apache-2.0 + +"""Tests for ``uv.lock``'s overlay onto +``ProjectMetadata.locked_dependencies`` via ``read_project()``'s lock +cascade (:mod:`pitloom.extract._locked_dependencies`), and real-world +fixture coverage. + +See also: test_uv_lock.py (extraction correctness this module's tests +were split from -- see that module's own docstring for the split +rationale) and test_uv_lock_root_package.py (root/workspace-member +selection unit tests). +""" + +import logging +import tempfile +from pathlib import Path + +import pytest + +from pitloom.extract.project import read_project + +_LOCK_HEADER = 'version = 1\nrevision = 1\nrequires-python = ">=3.10"\n' + +#: A minimal root/project package entry -- every test that needs one +#: root dependency composes this with its own `dependencies` block. +_ROOT_HEADER = ( + '[[package]]\nname = "demo"\nversion = "1.0.0"\nsource = { editable = "." }\n' +) + +REAL_WORLD_LOCKS = Path(__file__).parent.parent / "fixtures" / "real-world-locks" / "uv" + + +def _write_lock(tmp_dir: Path, body: str = "") -> None: + (tmp_dir / "uv.lock").write_text(_LOCK_HEADER + body, encoding="utf-8") + + +def test_read_project_populates_locked_dependencies() -> None: + with tempfile.TemporaryDirectory() as tmp: + tmp_path = Path(tmp) + (tmp_path / "pyproject.toml").write_text( + '[project]\nname = "demo"\nversion = "1.0.0"\n', encoding="utf-8" + ) + _write_lock( + tmp_path, + _ROOT_HEADER + 'dependencies = [{ name = "requests" }]\n\n' + '[[package]]\nname = "requests"\nversion = "2.31.0"\n' + 'source = { registry = "https://pypi.org/simple" }\n', + ) + + metadata, _config, _path = read_project(tmp_path) + + assert metadata.locked_dependencies == ["requests==2.31.0"] + assert metadata.provenance["locked_dependencies"] == ( + "Source: uv.lock | Method: resolved_lockfile" + ) + + +def test_read_project_uv_lock_takes_priority_over_poetry_lock( + caplog: pytest.LogCaptureFixture, +) -> None: + with tempfile.TemporaryDirectory() as tmp: + tmp_path = Path(tmp) + (tmp_path / "pyproject.toml").write_text( + '[tool.poetry]\nname = "demo"\nversion = "1.0.0"\n', encoding="utf-8" + ) + (tmp_path / "poetry.lock").write_text( + '[[package]]\nname = "requests"\nversion = "2.31.0"\ngroups = ["main"]\n', + encoding="utf-8", + ) + _write_lock( + tmp_path, + _ROOT_HEADER + 'dependencies = [{ name = "httpx" }]\n\n' + '[[package]]\nname = "httpx"\nversion = "0.27.0"\n' + 'source = { registry = "https://pypi.org/simple" }\n', + ) + + with caplog.at_level(logging.WARNING): + metadata, _config, _path = read_project(tmp_path) + + assert metadata.locked_dependencies == ["httpx==0.27.0"] + assert metadata.provenance["locked_dependencies"] == ( + "Source: uv.lock | Method: resolved_lockfile | Note: supersedes poetry.lock" + ) + + +def test_read_project_pylock_takes_priority_over_uv_lock() -> None: + """pylock.toml (PEP 751) outranks uv.lock in the cascade.""" + with tempfile.TemporaryDirectory() as tmp: + tmp_path = Path(tmp) + (tmp_path / "pyproject.toml").write_text( + '[project]\nname = "demo"\nversion = "1.0.0"\n', encoding="utf-8" + ) + (tmp_path / "pylock.toml").write_text( + 'lock-version = "1.0"\ncreated-by = "test"\n' + '[[packages]]\nname = "httpx"\nversion = "0.27.0"\n', + encoding="utf-8", + ) + _write_lock( + tmp_path, + _ROOT_HEADER + 'dependencies = [{ name = "requests" }]\n\n' + '[[package]]\nname = "requests"\nversion = "2.31.0"\n' + 'source = { registry = "https://pypi.org/simple" }\n', + ) + + metadata, _config, _path = read_project(tmp_path) + + assert metadata.locked_dependencies == ["httpx==0.27.0"] + assert metadata.provenance["locked_dependencies"] == ( + "Source: pylock.toml | Method: resolved_lockfile" + ) + + +def test_read_project_uv_workspace_picks_matching_member_by_name() -> None: + """Regression: a shared uv.lock listing more than one local + workspace member must resolve the *scanned* project's own + dependencies, identified by matching `pyproject.toml`'s declared + name -- not whichever editable entry happens to be listed first in + the lock file.""" + with tempfile.TemporaryDirectory() as tmp: + tmp_path = Path(tmp) + (tmp_path / "pyproject.toml").write_text( + '[project]\nname = "pkg-b"\nversion = "1.0.0"\n', encoding="utf-8" + ) + _write_lock( + tmp_path, + '[[package]]\nname = "pkg-a"\nversion = "1.0.0"\n' + 'source = { editable = "." }\n' + 'dependencies = [{ name = "requests" }]\n\n' + '[[package]]\nname = "pkg-b"\nversion = "1.0.0"\n' + 'source = { editable = "." }\n' + 'dependencies = [{ name = "httpx" }]\n\n' + '[[package]]\nname = "requests"\nversion = "2.31.0"\n' + 'source = { registry = "https://pypi.org/simple" }\n\n' + '[[package]]\nname = "httpx"\nversion = "0.27.0"\n' + 'source = { registry = "https://pypi.org/simple" }\n', + ) + + metadata, _config, _path = read_project(tmp_path) + + assert metadata.locked_dependencies == ["httpx==0.27.0"] + + +def test_real_world_flask() -> None: + """`pallets/flask` -- `uv.lock` ships in the PyPI sdist itself (the + only fixture where that's true, per real-world-locks/README.md). + Has multiple marker-conditional duplicate names (e.g. `click`), + exercising the ambiguity-skip path against real data.""" + metadata, _config, _path = read_project(REAL_WORLD_LOCKS / "flask-3.1.3") + + assert metadata.name == "Flask" + names = {dep.split("==", maxsplit=1)[0] for dep in metadata.locked_dependencies} + assert names == { + "blinker", + "importlib-metadata", + "itsdangerous", + "jinja2", + "markupsafe", + "werkzeug", + } + assert "click" not in names # ambiguous (ships two marker-conditional versions) + assert metadata.provenance["locked_dependencies"] == ( + "Source: uv.lock | Method: resolved_lockfile" + ) + + +def test_real_world_fastapi_cli() -> None: + metadata, _config, _path = read_project(REAL_WORLD_LOCKS / "fastapi-cli-0.0.32") + + assert metadata.name == "fastapi-cli" + names = {dep.split("==", maxsplit=1)[0] for dep in metadata.locked_dependencies} + assert names == {"rich-toolkit", "tomli", "typer", "uvicorn"} + + +def test_real_world_abi3audit() -> None: + metadata, _config, _path = read_project(REAL_WORLD_LOCKS / "abi3audit-0.0.26") + + assert metadata.name == "abi3audit" + names = {dep.split("==", maxsplit=1)[0] for dep in metadata.locked_dependencies} + assert names == { + "abi3info", + "kaitaistruct", + "packaging", + "pefile", + "pyelftools", + "requests", + "requests-cache", + "rich", + } diff --git a/tests/extract/test_uv_lock_root_package.py b/tests/extract/test_uv_lock_root_package.py new file mode 100644 index 00000000..cb5fbc24 --- /dev/null +++ b/tests/extract/test_uv_lock_root_package.py @@ -0,0 +1,95 @@ +# SPDX-FileContributor: Arthit Suriyawongkul +# SPDX-FileCopyrightText: 2026-present Arthit Suriyawongkul +# SPDX-FileType: SOURCE +# SPDX-License-Identifier: Apache-2.0 + +"""Tests for ``uv.lock``'s root/workspace-member package selection +(:func:`pitloom.extract._uv_lock._find_root_package`) and single-entry +pin resolution (:func:`pitloom.extract._uv_lock._pinned_dep_for_package`). + +See also: test_uv_lock.py (extraction correctness this module's tests +were split from -- see that module's own docstring for the split +rationale) and test_uv_lock_integration.py (the workspace-disambiguation +regression exercised through the full ``read_project()`` cascade). +""" + +import logging + +import pytest + +from pitloom.extract._uv_lock import _find_root_package, _pinned_dep_for_package + + +def test_find_root_package_returns_none_for_empty_list() -> None: + assert _find_root_package([], None) is None + + +def test_find_root_package_ignores_malformed_entries() -> None: + """A malformed top-level `[[package]]` entry (not a table) is + silently skipped while searching for the root package -- see + test_lock_common.py for the equivalent `index_packages_by_name()` + coverage this and `_uv_lock.py`'s own extraction share.""" + packages: list[object] = [ + "not-a-dict", + {"version": "1.0.0"}, # missing name, still not editable/virtual + {"name": "requests", "version": "2.31.0"}, + ] + + assert _find_root_package(packages, None) is None + + +def test_find_root_package_single_candidate_used_even_without_name_match() -> None: + """With exactly one editable/virtual candidate, it's used even when + it doesn't match `expected_name` (or `expected_name` is unavailable) + -- there's no ambiguity about *which* entry, only whether the name + happens to match, so guessing wrong here isn't the workspace-mixup + risk multiple candidates pose.""" + packages: list[object] = [ + {"name": "actual-name", "source": {"editable": "."}}, + ] + + assert _find_root_package(packages, "different-name") == packages[0] + assert _find_root_package(packages, None) == packages[0] + + +def test_find_root_package_prefers_name_match_among_multiple_candidates() -> None: + packages: list[object] = [ + {"name": "pkg-a", "source": {"editable": "."}}, + {"name": "pkg-b", "source": {"editable": "."}}, + ] + + assert _find_root_package(packages, "pkg-b") == packages[1] + assert _find_root_package(packages, "Pkg_B") == packages[1] # canonicalized + + +def test_find_root_package_multiple_candidates_no_name_match_returns_none_and_warns( + caplog: pytest.LogCaptureFixture, +) -> None: + """A shared uv workspace lock listing more than one local member, + where none matches the project actually being scanned, must not + silently attribute the wrong member's dependencies -- this is the + regression case: picking `packages[0]` unconditionally here would + misattribute `pkg-a`'s (or `pkg-b`'s) dependencies to `pkg-c`.""" + packages: list[object] = [ + {"name": "pkg-a", "source": {"editable": "."}}, + {"name": "pkg-b", "source": {"editable": "."}}, + ] + + with caplog.at_level(logging.WARNING): + result = _find_root_package(packages, "pkg-c") + + assert result is None + assert "2 candidate" in caplog.text + assert "pkg-c" in caplog.text + + +def test_pinned_dep_for_package_returns_none_when_source_not_a_dict() -> None: + """Defensive: a `source` value that isn't a table (malformed) is + skipped by the source-key check, not a crash -- version resolution + still proceeds normally.""" + assert ( + _pinned_dep_for_package( + {"name": "odd-pkg", "version": "1.0.0", "source": "not-a-table"} + ) + == "odd-pkg==1.0.0" + ) diff --git a/working-docs/implementation/lock-file-cascade.md b/working-docs/implementation/lock-file-cascade.md index 92697330..91381a61 100644 --- a/working-docs/implementation/lock-file-cascade.md +++ b/working-docs/implementation/lock-file-cascade.md @@ -38,7 +38,7 @@ new format's would-be bespoke function with one shared, ordered cascade. ## The cascade ```python -_LockExtractor = Callable[[Path], list[str]] +_LockExtractor = Callable[[Path, str | None], list[str]] _LOCK_SOURCES: list[tuple[str, _LockExtractor | None, str | None]] = [ ("pylock.toml", extract_pylock_dependencies, "resolved_lockfile"), @@ -54,9 +54,17 @@ def apply_locked_dependencies(metadata: ProjectMetadata, project_dir: Path) -> N ... ``` -Each entry pairs a source name, an extractor (`project_dir -> list[str]` -of exact-pin PEP 508 strings, empty when absent/unusable -- the same -signature convention `_poetry_lock.py`/`_pylock.py` already established), +Each entry pairs a source name, an extractor matching the uniform +`_LockExtractor` shape (`(project_dir, expected_name) -> list[str]` of +exact-pin PEP 508 strings, empty when absent/unusable), and a +provenance `Method` tag. Only `uv.lock`'s own extractor uses +*expected_name* (to disambiguate a shared workspace lock's multiple +local package entries without re-reading `pyproject.toml` a second +time); `pylock.toml`'s and `pdm.lock`'s extractors keep their simpler, +single-`project_dir` signature and are wrapped with +`_ignore_expected_name()` when registered in `_LOCK_SOURCES` below, +rather than widening every format's own signature for a need only one +of them has, and a provenance `Method` tag. `apply_locked_dependencies()` tries each extractor-bearing entry in priority order (highest first) and applies the first non-empty result, in place, onto `metadata.locked_dependencies` From 13ce345f3c38214773529047f77c6917a041d218 Mon Sep 17 00:00:00 2001 From: Arthit Suriyawongkul Date: Sat, 5 Sep 2026 01:04:22 +0700 Subject: [PATCH 06/35] Add Piplock.file support Signed-off-by: Arthit Suriyawongkul --- CHANGELOG.md | 5 +- docs/cli.md | 5 +- docs/dependency-sources.md | 7 +- src/pitloom/extract/_lock_common.py | 61 +- src/pitloom/extract/_locked_dependencies.py | 6 + src/pitloom/extract/_pdm_lock.py | 8 +- src/pitloom/extract/_pipfile_lock.py | 162 ++++ src/pitloom/extract/_poetry_lock.py | 13 +- src/pitloom/extract/_pylock.py | 8 +- src/pitloom/extract/_uv_lock.py | 8 +- tests/extract/test_pipfile_lock.py | 388 +++++++++ tests/fixtures/real-world-locks/README.md | 18 +- .../pipfile/requests-html-0.10.0/Pipfile.lock | 589 ++++++++++++++ .../pipfile/requests-html-0.10.0/setup.py | 108 +++ .../pipfile/responder-2.0.0/Pipfile.lock | 755 ++++++++++++++++++ .../pipfile/responder-2.0.0/setup.py | 140 ++++ working-docs/design/lock-files.md | 2 +- working-docs/design/roadmap.md | 29 +- .../implementation/lock-file-cascade.md | 137 +++- 19 files changed, 2359 insertions(+), 90 deletions(-) create mode 100644 src/pitloom/extract/_pipfile_lock.py create mode 100644 tests/extract/test_pipfile_lock.py create mode 100644 tests/fixtures/real-world-locks/pipfile/requests-html-0.10.0/Pipfile.lock create mode 100644 tests/fixtures/real-world-locks/pipfile/requests-html-0.10.0/setup.py create mode 100644 tests/fixtures/real-world-locks/pipfile/responder-2.0.0/Pipfile.lock create mode 100644 tests/fixtures/real-world-locks/pipfile/responder-2.0.0/setup.py diff --git a/CHANGELOG.md b/CHANGELOG.md index e8ca4d37..1c968cc5 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -42,8 +42,9 @@ and this project adheres to file gets a `software_File` element at the real wheel's `.dist-info/licenses/` path and a `hasDeclaredLicense` relationship ([#207]) - Add resolved-dependency parsing for `loom project`/`loom generate` - from `pylock.toml` (PEP 751), `uv.lock`, and `pdm.lock`, more planned - -- see [Dependency sources and precedence](docs/dependency-sources.md) + from `pylock.toml` (PEP 751), `uv.lock`, `pdm.lock`, and + `Pipfile.lock`, more planned -- see [Dependency sources and + precedence](docs/dependency-sources.md) ### Fixed diff --git a/docs/cli.md b/docs/cli.md index 171d88bd..3c9641de 100644 --- a/docs/cli.md +++ b/docs/cli.md @@ -66,8 +66,9 @@ loom project /path/to/project -o sbom.spdx3.json > Project-level metadata (name, version, dependencies, license, > authors) is read independently and unaffected either way. -If a lock file (`pylock.toml`, `uv.lock`, `poetry.lock`, or `pdm.lock`) -is present next to `pyproject.toml`, its resolved transitive +If a lock file (`pylock.toml`, `uv.lock`, `poetry.lock`, `pdm.lock`, or +`Pipfile.lock`) is present next to `pyproject.toml` (or `setup.py`, for +`Pipfile.lock`), its resolved transitive dependencies are added to the Source SBOM's dependency list too -- see [Dependency sources and precedence](dependency-sources.md) for which one wins when more than one is present, and what counts as "resolved" diff --git a/docs/dependency-sources.md b/docs/dependency-sources.md index 300723cf..f954fb64 100644 --- a/docs/dependency-sources.md +++ b/docs/dependency-sources.md @@ -1,6 +1,6 @@ --- Created: 2026-09-04 -Last-Modified: 2026-09-04 +Last-Modified: 2026-09-05 SPDX-FileCopyrightText: 2026-present Arthit Suriyawongkul SPDX-FileType: DOCUMENTATION SPDX-License-Identifier: CC0-1.0 @@ -41,9 +41,10 @@ exactly-pinned entries. | 2 | uv | `uv.lock` | Your project's own main/runtime dependencies (not `optional-dependencies` extras or `dev-dependencies` groups). A dependency pinned to more than one version for different Python versions is skipped, not guessed at -- see below. | | 3 | Poetry | `poetry.lock` | Packages in the `main` dependency group only (not `[tool.poetry.group.*]` dev/extra groups). | | 4 | PDM | `pdm.lock` | Packages in the `default` dependency group only. | +| 5 | Pipenv | `Pipfile.lock` | Packages in the `default` section only (not `develop`). A package whose resolved `version` isn't a single exact `==` pin is skipped, not guessed at. | -Support for `Pipfile.lock` (Pipenv) and a fully pinned `requirements.txt` -is planned, ranked below the formats above. +Support for a fully pinned `requirements.txt` is planned, ranked below +the formats above. **Only the single highest-priority lock file present is used.** If more than one lock file exists in the same project directory (uncommon, but diff --git a/src/pitloom/extract/_lock_common.py b/src/pitloom/extract/_lock_common.py index 8e8f7d35..e7cc8c48 100644 --- a/src/pitloom/extract/_lock_common.py +++ b/src/pitloom/extract/_lock_common.py @@ -5,8 +5,8 @@ """Shared helpers for lock/pin file extractors (:mod:`pitloom.extract._poetry_lock`, :mod:`pitloom.extract._pylock`, -:mod:`pitloom.extract._uv_lock`, :mod:`pitloom.extract._pdm_lock`, and -future formats registered in +:mod:`pitloom.extract._uv_lock`, :mod:`pitloom.extract._pdm_lock`, +:mod:`pitloom.extract._pipfile_lock`, and future formats registered in :mod:`pitloom.extract._locked_dependencies`). Factored out once the same two steps -- "load the lock file, handling @@ -21,6 +21,7 @@ from __future__ import annotations +import json import logging from collections.abc import Iterable, Mapping from pathlib import Path @@ -35,7 +36,9 @@ "find_first_present_key", "index_packages_by_name", "is_usable_version", + "load_lock_json", "load_lock_toml", + "warn_non_registry_source", ] #: The literal ``Source:`` name written into @@ -69,6 +72,39 @@ def load_lock_toml(lock_path: Path) -> dict[str, Any] | None: return None +def load_lock_json(lock_path: Path) -> dict[str, Any] | None: + """Read and parse *lock_path* as JSON, returning ``None`` (after a + ``WARNING:`` for a parse/read or shape failure, silently for a + simply-absent file) instead of raising -- the JSON-format + counterpart of :func:`load_lock_toml`, for ``Pipfile.lock`` (JSON, + unlike every other lock/pin format this module serves, which are + TOML). + + Unlike TOML (whose grammar guarantees a table at the document root, + so this can't happen to :func:`load_lock_toml`), JSON's top level + can legally be an array, string, number, or ``null`` -- rejected + here with a ``WARNING:`` so every caller can rely on this function's + declared ``dict[str, Any] | None`` return type without its own + defensive `isinstance` check. + """ + try: + with open(lock_path, encoding="utf-8") as f: + data = json.load(f) + except FileNotFoundError: + return None + except (OSError, json.JSONDecodeError) as exc: + log.warning("Failed to parse %s: %s", lock_path, exc) + return None + if not isinstance(data, dict): + log.warning( + "%s: top-level JSON value is %s, expected an object", + lock_path, + type(data).__name__, + ) + return None + return data + + def index_packages_by_name(packages: list[Any]) -> dict[str, list[dict[str, Any]]]: """Group every well-formed entry of *packages* (a lock format's flat ``[[package]]``-style list) by its ``name`` field, preserving file @@ -113,6 +149,27 @@ def is_usable_version(version: Any) -> bool: return isinstance(version, str) and bool(version) +def warn_non_registry_source(lock_file: str, name: str, source_key: str) -> None: + """Log the standard ``WARNING:`` for a non-registry-sourced entry + (VCS, local path, archive/URL -- anything a bare ``name==version`` + pin can't represent), naming *lock_file* (e.g. ``"uv.lock"``), + *name* (the package), and *source_key* (which non-registry marker + was found). + + The exact wording was hand-copied identically into every extractor + (`_poetry_lock.py`, `_pylock.py`, `_uv_lock.py`, `_pdm_lock.py`, + `_pipfile_lock.py`) before being factored out here, per this repo's + "a pattern hand-copied across 3+ call sites drifts" convention. + """ + log.warning( + "Skipping %s entry %r: %s-sourced dependencies cannot be " + "represented as a PEP 508 specifier", + lock_file, + name, + source_key, + ) + + def find_first_present_key( mapping: Mapping[str, Any], keys: Iterable[str] ) -> str | None: diff --git a/src/pitloom/extract/_locked_dependencies.py b/src/pitloom/extract/_locked_dependencies.py index 4339a79a..b5bd2c41 100644 --- a/src/pitloom/extract/_locked_dependencies.py +++ b/src/pitloom/extract/_locked_dependencies.py @@ -38,6 +38,7 @@ from pitloom.core.project import ProjectMetadata from pitloom.extract._lock_common import POETRY_LOCK_SOURCE_NAME from pitloom.extract._pdm_lock import extract_pdm_lock_dependencies +from pitloom.extract._pipfile_lock import extract_pipfile_lock_dependencies from pitloom.extract._pylock import extract_pylock_dependencies from pitloom.extract._uv_lock import extract_uv_lock_dependencies @@ -87,6 +88,11 @@ def _ignore_expected_name(extractor: Callable[[Path], list[str]]) -> _LockExtrac _ignore_expected_name(extract_pdm_lock_dependencies), "resolved_lockfile", ), + ( + "Pipfile.lock", + _ignore_expected_name(extract_pipfile_lock_dependencies), + "resolved_lockfile", + ), ] diff --git a/src/pitloom/extract/_pdm_lock.py b/src/pitloom/extract/_pdm_lock.py index 64e87fd0..5719decf 100644 --- a/src/pitloom/extract/_pdm_lock.py +++ b/src/pitloom/extract/_pdm_lock.py @@ -47,6 +47,7 @@ index_packages_by_name, is_usable_version, load_lock_toml, + warn_non_registry_source, ) log = logging.getLogger(__name__) @@ -96,12 +97,7 @@ def _default_group_package_or_none(pkg: Any) -> dict[str, Any] | None: non_registry_key = find_first_present_key(pkg, _NON_REGISTRY_KEYS) if non_registry_key is not None: - log.warning( - "Skipping pdm.lock entry %r: %s-sourced dependencies cannot be " - "represented as a PEP 508 specifier", - name, - non_registry_key, - ) + warn_non_registry_source("pdm.lock", name, non_registry_key) return None version = pkg.get("version") diff --git a/src/pitloom/extract/_pipfile_lock.py b/src/pitloom/extract/_pipfile_lock.py new file mode 100644 index 00000000..e6b88059 --- /dev/null +++ b/src/pitloom/extract/_pipfile_lock.py @@ -0,0 +1,162 @@ +# SPDX-FileContributor: Arthit Suriyawongkul +# SPDX-FileCopyrightText: 2026-present Arthit Suriyawongkul +# SPDX-FileType: SOURCE +# SPDX-License-Identifier: Apache-2.0 + +"""Extractor for resolved dependencies from a Pipenv ``Pipfile.lock``. + +See also: :mod:`pitloom.extract._poetry_lock` (the sibling lock +extractor this module mirrors in shape -- same ``main``/``default``-group +filtering, same source-stage-only scoping, same ``name==version`` +output, same "no silent deviations" warning policy) and +:mod:`pitloom.extract._locked_dependencies` (the cascade module that +calls this extractor and overlays its output onto +``ProjectMetadata.locked_dependencies``, in priority order against every +other lock format). + +``Pipfile.lock`` is source-stage-only, the same class as every sibling +lock format: appropriate for ``loom project``/``loom generate``, never +for ``loom wheel``/``embed-wheel`` (the real wheel's own metadata is +ground truth and never consults a lock) or ``loom env`` (live +introspection of what's actually installed is strictly more +authoritative than a lock that may be stale relative to it). + +Unlike every other lock format this repo parses, ``Pipfile.lock`` is +**JSON**, not TOML (:func:`pitloom.extract._lock_common.load_lock_json`, +not :func:`pitloom.extract._lock_common.load_lock_toml`). Its top level +has two package-name-keyed objects, ``"default"`` (main/runtime +dependencies) and ``"develop"`` (dev dependencies) -- only ``"default"`` +is included here, mirroring ``poetry.lock``'s ``main``-group-only +policy. Each entry's own ``"version"`` field is already a PEP 440 +specifier string (typically ``"==x.y.z"``, since ``pipenv lock`` +resolves to an exact pin) rather than a bare version number the way +every other format's ``version`` field is -- this extractor validates +it's a single exact ``==`` specifier with no wildcard before using it, +not a range, a prefix-match specifier like ``"==x.y.*"``, or a +malformed string coerced into looking like one. +""" + +from __future__ import annotations + +import logging +from pathlib import Path +from typing import Any + +from packaging.specifiers import InvalidSpecifier, SpecifierSet + +from pitloom.extract._lock_common import ( + find_first_present_key, + is_usable_version, + load_lock_json, + warn_non_registry_source, +) + +log = logging.getLogger(__name__) + +__all__ = ["extract_pipfile_lock_dependencies"] + +#: ``Pipfile.lock`` per-package keys that mark it as not resolvable to a +#: meaningful PyPI version pin -- a VCS (Git, Mercurial, Bazaar, +#: Subversion -- pip's/``requirementslib``'s full VCS backend list), +#: local-path, or archive/URL source, mirroring every sibling format's +#: own non-registry-source skip. +_NON_REGISTRY_KEYS = ("git", "hg", "bzr", "svn", "path", "file", "editable") + + +def extract_pipfile_lock_dependencies(project_dir: Path) -> list[str]: + """Read ``Pipfile.lock`` next to ``Pipfile``/``setup.py`` and return + its resolved ``"default"``-section packages as exact-pin PEP 508 + strings. + + Returns an empty list when no ``Pipfile.lock`` is present, or when + it can't be parsed -- this is optional enrichment, never a + requirement. + """ + lock_path = project_dir / "Pipfile.lock" + data = load_lock_json(lock_path) + if data is None: + return [] + + default_section = data.get("default", {}) + if not isinstance(default_section, dict): + log.warning( + "%s: top-level 'default' key is %s, expected a table -- " + "ignoring Pipfile.lock", + lock_path, + type(default_section).__name__, + ) + return [] + + dependencies: list[str] = [] + for name, entry in default_section.items(): + dep = _pinned_dep_for_package(name, entry) + if dep is not None: + dependencies.append(dep) + return dependencies + + +def _pinned_dep_for_package(name: Any, entry: Any) -> str | None: + """Return ``name==version`` for one ``"default"``-section entry, or + ``None`` when it's malformed, non-registry-sourced, or its + ``version`` isn't a single exact ``==`` specifier.""" + if not isinstance(name, str) or not name: + log.warning( + "Skipping malformed Pipfile.lock entry: non-string or empty " + "package name (name=%r)", + name, + ) + return None + if not isinstance(entry, dict): + log.warning( + "Skipping malformed Pipfile.lock entry %r: expected a table, got %s", + name, + type(entry).__name__, + ) + return None + non_registry_key = find_first_present_key(entry, _NON_REGISTRY_KEYS) + if non_registry_key is not None: + warn_non_registry_source("Pipfile.lock", name, non_registry_key) + return None + pinned_version = _exact_pinned_version(name, entry.get("version")) + if pinned_version is None: + return None + return f"{name}=={pinned_version}" + + +def _exact_pinned_version(name: str, version: Any) -> str | None: + """Return the bare version string when *version* is a single exact + ``==`` PEP 440 specifier with no wildcard (e.g. ``"==2.31.0"`` -> + ``"2.31.0"``), or ``None`` (with a ``WARNING:``) when it's missing, + unparseable, or anything looser than one exact pin -- including a + prefix-match specifier like ``"==2.31.*"``, which + ``packaging.specifiers.Specifier`` also reports as operator ``"=="`` + but which pins a *range* of versions, not one exact release. + """ + if not is_usable_version(version): + log.warning( + "Skipping Pipfile.lock entry %r: missing or non-string 'version'", + name, + ) + return None + try: + specifiers = list(SpecifierSet(version)) + except InvalidSpecifier: + log.warning( + "Skipping Pipfile.lock entry %r: %r isn't a valid PEP 440 specifier", + name, + version, + ) + return None + if ( + len(specifiers) != 1 + or specifiers[0].operator != "==" + or "*" in specifiers[0].version + ): + log.warning( + "Skipping Pipfile.lock entry %r: 'version' %r isn't a single " + "exact '==' pin", + name, + version, + ) + return None + return specifiers[0].version diff --git a/src/pitloom/extract/_poetry_lock.py b/src/pitloom/extract/_poetry_lock.py index cc2de5da..2227d7ec 100644 --- a/src/pitloom/extract/_poetry_lock.py +++ b/src/pitloom/extract/_poetry_lock.py @@ -27,7 +27,11 @@ from pathlib import Path from typing import Any -from pitloom.extract._lock_common import is_usable_version, load_lock_toml +from pitloom.extract._lock_common import ( + is_usable_version, + load_lock_toml, + warn_non_registry_source, +) log = logging.getLogger(__name__) @@ -108,11 +112,6 @@ def _pinned_dep_for_package(pkg: Any) -> str | None: source = pkg.get("source") source_type = source.get("type") if isinstance(source, dict) else None if source_type in _NON_PEP508_SOURCE_TYPES: - log.warning( - "Skipping poetry.lock entry %r: %s-sourced dependencies cannot " - "be represented as a PEP 508 specifier", - name, - source_type, - ) + warn_non_registry_source("poetry.lock", name, source_type) return None return f"{name}=={version}" diff --git a/src/pitloom/extract/_pylock.py b/src/pitloom/extract/_pylock.py index d0f215b4..91845a8b 100644 --- a/src/pitloom/extract/_pylock.py +++ b/src/pitloom/extract/_pylock.py @@ -35,6 +35,7 @@ find_first_present_key, is_usable_version, load_lock_toml, + warn_non_registry_source, ) log = logging.getLogger(__name__) @@ -119,12 +120,7 @@ def _pinned_dep_for_package(pkg: Any) -> str | None: return None non_registry_source = find_first_present_key(pkg, _NON_REGISTRY_SOURCE_KEYS) if non_registry_source is not None: - log.warning( - "Skipping pylock.toml entry %r: %s-sourced dependencies cannot " - "be represented as a PEP 508 specifier", - name, - non_registry_source, - ) + warn_non_registry_source("pylock.toml", name, non_registry_source) return None version = pkg.get("version") if not is_usable_version(version): diff --git a/src/pitloom/extract/_uv_lock.py b/src/pitloom/extract/_uv_lock.py index 532377c2..af6b1fee 100644 --- a/src/pitloom/extract/_uv_lock.py +++ b/src/pitloom/extract/_uv_lock.py @@ -54,6 +54,7 @@ index_packages_by_name, is_usable_version, load_lock_toml, + warn_non_registry_source, ) log = logging.getLogger(__name__) @@ -182,12 +183,7 @@ def _pinned_dep_for_package(pkg: dict[str, Any]) -> str | None: if isinstance(source, dict): non_registry_source = find_first_present_key(source, _NON_REGISTRY_SOURCE_KEYS) if non_registry_source is not None: - log.warning( - "Skipping uv.lock entry %r: %s-sourced dependencies cannot " - "be represented as a PEP 508 specifier", - name, - non_registry_source, - ) + warn_non_registry_source("uv.lock", name, non_registry_source) return None version = pkg.get("version") if not is_usable_version(version): diff --git a/tests/extract/test_pipfile_lock.py b/tests/extract/test_pipfile_lock.py new file mode 100644 index 00000000..fd7fb84a --- /dev/null +++ b/tests/extract/test_pipfile_lock.py @@ -0,0 +1,388 @@ +# SPDX-FileContributor: Arthit Suriyawongkul +# SPDX-FileCopyrightText: 2026-present Arthit Suriyawongkul +# SPDX-FileType: SOURCE +# SPDX-License-Identifier: Apache-2.0 + +"""Tests for ``Pipfile.lock`` resolved-dependency parsing +(:mod:`pitloom.extract._pipfile_lock`) and its overlay onto +``ProjectMetadata.locked_dependencies`` via ``read_project()``'s lock +cascade (:mod:`pitloom.extract._locked_dependencies`). + +See also: test_poetry_lock.py/test_pylock.py/test_uv_lock.py for the +sibling lock extractors this module's tests mirror in shape; +test_locked_dependencies.py for the cascade mechanism's own tests. +""" + +import json +import logging +import tempfile +from pathlib import Path + +import pytest + +from pitloom.extract._pipfile_lock import extract_pipfile_lock_dependencies +from pitloom.extract.project import read_project + +REAL_WORLD_LOCKS = ( + Path(__file__).parent.parent / "fixtures" / "real-world-locks" / "pipfile" +) + + +def _write_lock(tmp_dir: Path, data: dict[str, object]) -> None: + (tmp_dir / "Pipfile.lock").write_text(json.dumps(data), encoding="utf-8") + + +def test_no_lock_file_returns_empty_list() -> None: + with tempfile.TemporaryDirectory() as tmp: + assert not extract_pipfile_lock_dependencies(Path(tmp)) + + +def test_malformed_json_returns_empty_list_and_warns( + caplog: pytest.LogCaptureFixture, +) -> None: + with tempfile.TemporaryDirectory() as tmp: + tmp_path = Path(tmp) + (tmp_path / "Pipfile.lock").write_text("{not valid json", encoding="utf-8") + + with caplog.at_level(logging.WARNING): + result = extract_pipfile_lock_dependencies(tmp_path) + + assert not result + assert "Failed to parse" in caplog.text + + +def test_default_section_not_a_dict_returns_empty_list_and_warns( + caplog: pytest.LogCaptureFixture, +) -> None: + with tempfile.TemporaryDirectory() as tmp: + tmp_path = Path(tmp) + _write_lock(tmp_path, {"default": ["not-a-dict"]}) + + with caplog.at_level(logging.WARNING): + result = extract_pipfile_lock_dependencies(tmp_path) + + assert not result + assert "expected a table" in caplog.text + + +def test_no_default_section_returns_empty_list() -> None: + """A Pipfile.lock with no `default` key at all (unusual but not + invalid) is treated as zero runtime dependencies, not an error.""" + with tempfile.TemporaryDirectory() as tmp: + tmp_path = Path(tmp) + _write_lock(tmp_path, {"develop": {"pytest": {"version": "==8.0.0"}}}) + + assert not extract_pipfile_lock_dependencies(tmp_path) + + +def test_simple_dependency_resolved() -> None: + with tempfile.TemporaryDirectory() as tmp: + tmp_path = Path(tmp) + _write_lock( + tmp_path, + { + "default": { + "requests": {"version": "==2.31.0", "index": "pypi"}, + }, + "develop": { + "pytest": {"version": "==8.0.0"}, + }, + }, + ) + + assert extract_pipfile_lock_dependencies(tmp_path) == ["requests==2.31.0"] + + +def test_develop_section_excluded() -> None: + """Only `default` (main/runtime) entries are included -- `develop` + (dev-only) entries are excluded, mirroring poetry.lock's + main-group-only policy.""" + with tempfile.TemporaryDirectory() as tmp: + tmp_path = Path(tmp) + _write_lock( + tmp_path, + { + "default": {"requests": {"version": "==2.31.0"}}, + "develop": {"pytest": {"version": "==8.0.0"}}, + }, + ) + + result = extract_pipfile_lock_dependencies(tmp_path) + + assert result == ["requests==2.31.0"] + assert "pytest" not in " ".join(result) + + +def test_malformed_entry_not_a_dict_skipped_and_warns( + caplog: pytest.LogCaptureFixture, +) -> None: + with tempfile.TemporaryDirectory() as tmp: + tmp_path = Path(tmp) + _write_lock( + tmp_path, + { + "default": { + "broken": "not-a-table", + "requests": {"version": "==2.31.0"}, + } + }, + ) + + with caplog.at_level(logging.WARNING): + result = extract_pipfile_lock_dependencies(tmp_path) + + assert result == ["requests==2.31.0"] + assert "malformed" in caplog.text.lower() + + +def test_missing_version_skipped_and_warns( + caplog: pytest.LogCaptureFixture, +) -> None: + with tempfile.TemporaryDirectory() as tmp: + tmp_path = Path(tmp) + _write_lock( + tmp_path, + { + "default": { + "no-version": {"index": "pypi"}, + "requests": {"version": "==2.31.0"}, + } + }, + ) + + with caplog.at_level(logging.WARNING): + result = extract_pipfile_lock_dependencies(tmp_path) + + assert result == ["requests==2.31.0"] + assert "missing" in caplog.text.lower() + + +@pytest.mark.parametrize( + "non_registry_key", ["git", "hg", "bzr", "svn", "path", "file", "editable"] +) +def test_non_registry_sourced_dependency_excluded( + non_registry_key: str, caplog: pytest.LogCaptureFixture +) -> None: + with tempfile.TemporaryDirectory() as tmp: + tmp_path = Path(tmp) + _write_lock( + tmp_path, + { + "default": { + "local-dep": {non_registry_key: "some-value"}, + "requests": {"version": "==2.31.0"}, + } + }, + ) + + with caplog.at_level(logging.WARNING): + result = extract_pipfile_lock_dependencies(tmp_path) + + assert result == ["requests==2.31.0"] + assert "local-dep" in caplog.text + + +def test_invalid_specifier_skipped_and_warns( + caplog: pytest.LogCaptureFixture, +) -> None: + with tempfile.TemporaryDirectory() as tmp: + tmp_path = Path(tmp) + _write_lock( + tmp_path, + { + "default": { + "broken-version": {"version": "not-a-specifier"}, + "requests": {"version": "==2.31.0"}, + } + }, + ) + + with caplog.at_level(logging.WARNING): + result = extract_pipfile_lock_dependencies(tmp_path) + + assert result == ["requests==2.31.0"] + assert "valid PEP 440 specifier" in caplog.text + + +def test_prefix_match_specifier_skipped_and_warns( + caplog: pytest.LogCaptureFixture, +) -> None: + """Regression: `packaging.specifiers.Specifier("==2.31.*").operator` + is also `"=="`, so a naive `operator == "=="` check would wrongly + accept a prefix-match specifier (pinning a *range* of versions) as + if it were a single exact pin.""" + with tempfile.TemporaryDirectory() as tmp: + tmp_path = Path(tmp) + _write_lock( + tmp_path, + { + "default": { + "wildcard": {"version": "==2.31.*"}, + "requests": {"version": "==2.31.0"}, + } + }, + ) + + with caplog.at_level(logging.WARNING): + result = extract_pipfile_lock_dependencies(tmp_path) + + assert result == ["requests==2.31.0"] + assert "isn't a single exact" in caplog.text + + +def test_non_dict_json_top_level_returns_empty_list_and_warns( + caplog: pytest.LogCaptureFixture, +) -> None: + """Regression: unlike TOML (whose grammar guarantees a table at the + document root), a `Pipfile.lock` containing valid but non-object + JSON (e.g. a bare array) used to crash extraction with + `AttributeError` on `data.get(...)` instead of degrading gracefully.""" + with tempfile.TemporaryDirectory() as tmp: + tmp_path = Path(tmp) + (tmp_path / "Pipfile.lock").write_text("[]", encoding="utf-8") + + with caplog.at_level(logging.WARNING): + result = extract_pipfile_lock_dependencies(tmp_path) + + assert not result + assert "expected an object" in caplog.text + + +@pytest.mark.parametrize("version", [">=2.31.0", "==2.31.0,!=2.31.1", "!=2.31.0"]) +def test_non_exact_pin_skipped_and_warns( + version: str, caplog: pytest.LogCaptureFixture +) -> None: + """A `version` that isn't a single exact `==` specifier (a range, or + an excluded-version specifier) is skipped, not coerced -- pipenv + lock output is expected to always resolve to an exact pin, so this + is a defensive "don't guess" path, same policy as every other + format's ambiguous-version skip.""" + with tempfile.TemporaryDirectory() as tmp: + tmp_path = Path(tmp) + _write_lock( + tmp_path, + { + "default": { + "ranged": {"version": version}, + "requests": {"version": "==2.31.0"}, + } + }, + ) + + with caplog.at_level(logging.WARNING): + result = extract_pipfile_lock_dependencies(tmp_path) + + assert result == ["requests==2.31.0"] + assert "exact" in caplog.text.lower() + + +def test_missing_or_empty_name_skipped_and_warns( + caplog: pytest.LogCaptureFixture, +) -> None: + """A malformed top-level key (e.g. JSON's own coercion couldn't + produce a non-string here in practice, but an empty string is + possible and must not silently pass through) is skipped.""" + with tempfile.TemporaryDirectory() as tmp: + tmp_path = Path(tmp) + _write_lock( + tmp_path, + { + "default": { + "": {"version": "==1.0.0"}, + "requests": {"version": "==2.31.0"}, + } + }, + ) + + with caplog.at_level(logging.WARNING): + result = extract_pipfile_lock_dependencies(tmp_path) + + assert result == ["requests==2.31.0"] + assert "malformed" in caplog.text.lower() + + +# --- read_project() cascade integration ------------------------------- + + +def test_read_project_populates_locked_dependencies_from_setup_py_only() -> None: + """Regression: Pipfile.lock predates PEP 621 almost entirely -- + every real project pairs it with a bare setup.py, never + pyproject.toml. The cascade must reach it via read_project()'s + setup.py-only dispatch path, not only the pyproject.toml one.""" + with tempfile.TemporaryDirectory() as tmp: + tmp_path = Path(tmp) + (tmp_path / "setup.py").write_text( + "from setuptools import setup\nsetup(name='demo', version='1.0.0')\n", + encoding="utf-8", + ) + _write_lock(tmp_path, {"default": {"requests": {"version": "==2.31.0"}}}) + + metadata, _config, _path = read_project(tmp_path) + + assert metadata.locked_dependencies == ["requests==2.31.0"] + assert metadata.provenance["locked_dependencies"] == ( + "Source: Pipfile.lock | Method: resolved_lockfile" + ) + + +def test_read_project_pdm_lock_takes_priority_over_pipfile_lock() -> None: + with tempfile.TemporaryDirectory() as tmp: + tmp_path = Path(tmp) + (tmp_path / "pyproject.toml").write_text( + '[project]\nname = "demo"\nversion = "1.0.0"\n', encoding="utf-8" + ) + (tmp_path / "pdm.lock").write_text( + '[[package]]\nname = "httpx"\nversion = "0.27.0"\ngroups = ["default"]\n', + encoding="utf-8", + ) + _write_lock(tmp_path, {"default": {"requests": {"version": "==2.31.0"}}}) + + metadata, _config, _path = read_project(tmp_path) + + assert metadata.locked_dependencies == ["httpx==0.27.0"] + assert metadata.provenance["locked_dependencies"] == ( + "Source: pdm.lock | Method: resolved_lockfile" + ) + + +# --- real-world fixtures ------------------------------------------------- + + +def test_real_world_requests_html() -> None: + """`psf/requests-html` -- real, unmodified `Pipfile.lock` from the + matching GitHub tag, read directly via the extractor rather than + `read_project()`: `requests-html`'s `setup.py` declares `name`/ + `version` via module-level constants (`NAME = 'requests-html'`, + `setup(name=NAME, ...)`), which `_setuptools_py.py`'s literal-only + AST resolution can't follow -- a known, separate, pre-existing gap + (see the `pyyaml` entry in `real-world-projects/README.md`) that + makes `read_setup_py()` raise `ValueError` and, with no `setup.cfg` + fallback either, `read_project()` raise `FileNotFoundError` entirely + for this fixture. That's this fixture's own known limitation, not + something for the lock-cascade extractor to work around -- so this + test exercises `extract_pipfile_lock_dependencies()` directly + against the real fixture data instead.""" + dependencies = extract_pipfile_lock_dependencies( + REAL_WORLD_LOCKS / "requests-html-0.10.0" + ) + + names = {dep.split("==", maxsplit=1)[0] for dep in dependencies} + assert "requests" in names + assert "beautifulsoup4" in names + + +def test_real_world_responder() -> None: + """`kennethreitz/responder` -- also has a self-referential editable + `path`-sourced entry (`responder` itself) in its own `default` + section, exercising the non-registry-source skip against real data. + Same `setup.py`-constant limitation as `requests-html` above applies + here too, so this also calls the extractor directly.""" + from pitloom.extract._pipfile_lock import extract_pipfile_lock_dependencies + + dependencies = extract_pipfile_lock_dependencies( + REAL_WORLD_LOCKS / "responder-2.0.0" + ) + + names = {dep.split("==", maxsplit=1)[0] for dep in dependencies} + assert "requests" in names + assert "responder" not in names # self-referential, editable/path-sourced diff --git a/tests/fixtures/real-world-locks/README.md b/tests/fixtures/real-world-locks/README.md index ebced17a..a1026a84 100644 --- a/tests/fixtures/real-world-locks/README.md +++ b/tests/fixtures/real-world-locks/README.md @@ -72,6 +72,15 @@ artifact. `real-world-projects/README.md`) -- so `metadata.name` won't resolve for either. Tests against these two fixtures assert on `locked_dependencies`/`provenance`, not `metadata.name`. +- **`responder`'s `Pipfile.lock` has a self-referential entry.** Its + `default` section includes `"responder": {"editable": true, "path": + "."}` -- the package's own local checkout, resolved into its own lock + file by `pipenv`. `extract_pipfile_lock_dependencies()`'s non-registry- + source skip excludes it (no `version` to pin against, `editable`/`path` + present) the same way it would any other local-path dependency -- + `requests-html`'s equivalent self-entry lives in `develop` instead, so + it's already excluded by the `default`-only filter and doesn't + exercise this path. - **`pipenv`'s `pylock.toml` fixture reuses a version already vendored elsewhere.** `pypa/pipenv` `2026.8.0` is the same release already vendored as a full sdist in @@ -126,8 +135,9 @@ artifact. | `uv.lock` | [pypa/abi3audit](https://github.com/pypa/abi3audit) | 0.0.26 | MIT | GitHub tag `v0.0.26` | GitHub tag `v0.0.26` | | `pdm.lock` | [pdm-project/pdm](https://github.com/pdm-project/pdm) | 2.29.0 | MIT | GitHub tag `2.29.0` | GitHub tag `2.29.0` | | `pdm.lock` | [frostming/unearth](https://github.com/frostming/unearth) | 0.18.3 | MIT | GitHub tag `0.18.3` | GitHub tag `0.18.3` | +| `Pipfile.lock` | [psf/requests-html](https://github.com/psf/requests-html) | 0.10.0 | MIT | GitHub tag `v0.10.0` (`setup.py`) | GitHub tag `v0.10.0` | +| `Pipfile.lock` | [kennethreitz/responder](https://github.com/kennethreitz/responder) | 2.0.0 | Apache-2.0 | GitHub tag `v2.0.0` (`setup.py`) | GitHub tag `v2.0.0` | -`Pipfile.lock` and pinned `requirements.txt` fixtures land in their own -follow-up changes, alongside each format's own extractor -- see -`working-docs/design/roadmap.md`'s "Remaining lock formats as a -resolved-dependency source" item. +Pinned `requirements.txt` fixtures land in their own follow-up change, +alongside its extractor -- see `working-docs/design/roadmap.md`'s +"Remaining lock formats as a resolved-dependency source" item. diff --git a/tests/fixtures/real-world-locks/pipfile/requests-html-0.10.0/Pipfile.lock b/tests/fixtures/real-world-locks/pipfile/requests-html-0.10.0/Pipfile.lock new file mode 100644 index 00000000..00dddf77 --- /dev/null +++ b/tests/fixtures/real-world-locks/pipfile/requests-html-0.10.0/Pipfile.lock @@ -0,0 +1,589 @@ +{ + "_meta": { + "hash": { + "sha256": "3753380e39283963e51d1e76589d409c847896a862df2ee4a38af664bb7312a6" + }, + "pipfile-spec": 6, + "requires": {}, + "sources": [ + { + "name": "pypi", + "url": "https://pypi.python.org/simple", + "verify_ssl": true + } + ] + }, + "default": { + "beautifulsoup4": { + "hashes": [ + "sha256:11a9a27b7d3bddc6d86f59fb76afb70e921a25ac2d6cc55b40d072bd68435a76", + "sha256:7015e76bf32f1f574636c4288399a6de66ce08fb7b2457f628a8d70c0fbabb11", + "sha256:808b6ac932dccb0a4126558f7dfdcf41710dd44a4ef497a0bb59a77f9f078e89" + ], + "version": "==4.6.0" + }, + "bs4": { + "hashes": [ + "sha256:36ecea1fd7cc5c0c6e4a1ff075df26d50da647b75376626cc186e2212886dd3a" + ], + "index": "pypi", + "version": "==0.0.1" + }, + "certifi": { + "hashes": [ + "sha256:14131608ad2fd56836d33a71ee60fa1c82bc9d2c8d98b7bdbc631fe1b3cd1296", + "sha256:edbc3f203427eef571f79a7692bb160a2b0f7ccaa31953e99bd17e307cf63f7d" + ], + "version": "==2018.1.18" + }, + "chardet": { + "hashes": [ + "sha256:84ab92ed1c4d4f16916e05906b6b75a6c0fb5db821cc65e70cbd64a3e2a5eaae", + "sha256:fc323ffcaeaed0e0a02bf4d117757b98aed530d9ed4531e3e15460124c106691" + ], + "version": "==3.0.4" + }, + "cssselect": { + "hashes": [ + "sha256:066d8bc5229af09617e24b3ca4d52f1f9092d9e061931f4184cd572885c23204", + "sha256:3b5103e8789da9e936a68d993b70df732d06b8bb9a337a05ed4eb52c17ef7206" + ], + "version": "==1.0.3" + }, + "fake-useragent": { + "hashes": [ + "sha256:cc9b9ddcebc708b3deac846f5fccb16e37c02ee47435a4ec7132271dd96aec8c" + ], + "index": "pypi", + "version": "==0.1.10" + }, + "idna": { + "hashes": [ + "sha256:2c6a5de3089009e3da7c5dde64a141dbc8551d5b7f6cf4ed7c2568d0cc520a8f", + "sha256:8c7309c718f94b3a625cb648ace320157ad16ff131ae0af362c9f21b80ef6ec4" + ], + "version": "==2.6" + }, + "lxml": { + "hashes": [ + "sha256:0aa44ffdeaaf6ba45d61980bb2c07e87d4dcac7a8b5b9d458124bc1adcda5233", + "sha256:0af9c9267b1257319d49e9c1e9abbf92a99f965bee3c4733e0f0f7578985182d", + "sha256:0cddc6cde79e1932efc71d9974a4418184ad0b8ca46c633ad772b2c5eaf36b3c", + "sha256:124a9d529eec5e10f307eb237df3efc43dd1fb7ebdb5da5e480c4ed372648b6b", + "sha256:1d1e45584353e4d563685874707fc8c85cdd11b0ef3b79d77bb38046134d68a9", + "sha256:2812bc45a7f53f366217b76a1c53e6728fbfa7f7524d16a321ea8f7131428bd1", + "sha256:29697224b2df76edf7c2de9bcd90a26dd28fe85c5fd7f0171cae84f8383b227e", + "sha256:36ffb216e2f361a5a0a7e219aea6cd44da11c64061baed273944aae21223186c", + "sha256:4626d699551f66687e5f7e7f9b79bfce611e12edebfb9fec276e2df8ec46541e", + "sha256:4c21d7304d37715e6aed756e4d0c374c99c9bb1fa8d64f546b95474b17ac23de", + "sha256:57be98177ce784495dff53f40620995ad0a56456246ed9d51977e595de58e12e", + "sha256:62bfcd0629991e1c1257ffd28df2ab31a5c44da4c06823c26ec0f472723a84ca", + "sha256:71ac6dac6835de75aaf531cae9ffa447dae0783ba1f43bf6eaccfad3680a5b9c", + "sha256:7769ac9203ebe6d8db16904c54d57d77360fcc1926ed7afaa86b04050e4afa5b", + "sha256:7d96fbb5f23a62300aa9bef7d286cd61aca8902357619c8708c0290aba5df73f", + "sha256:88583c6565c9299f617238a500f1a47510bac54daff7872d6a343f13361b659e", + "sha256:8f52c4c8f1cf15419193026e731f34a3260a3ce7977b875ba1eb2517b8a3f660", + "sha256:95b82fdfdaac71640b281da6b9a2c3700177ba5190a786881b184de744ad55de", + "sha256:988d55112f196e12341b7c5138841c2b4f21f871eaa8f138c6ac4c46f28899f9", + "sha256:9e08918b744b89d30750eca8598f37ae75b16202870db678fde970d85afed3e3", + "sha256:b46f31e806f6884bd1053ad1d78ecaca6d1bc5dd94a1b783a6ff0bb4b3a60962", + "sha256:c18f316cad969111b1ff9e84c82fbc9ae6f25f35701118182d384585940cdf80", + "sha256:cef79715f2335bfc1ef7082bcb8b2bac87271431653455221a9127fde146208c", + "sha256:cf63f590090404c52f179b7ceacb7cd549de3a1697bcfe2f79be180b2801d109", + "sha256:d06260e6102b2f18dbee3736185cd6a2e1c88c0fad782bf8e9d7a7a1b24e02b0", + "sha256:d0dc3e5737adcc9a23fd3d3d3072b887fefb48143309563f412ef7b0ebdfdb30", + "sha256:dd98d4f88ce0abda2b02c1542d1de22dd342023f3ba09874bd95841283f29433", + "sha256:f04b184984c23e0caac3c55eac2fe2dbb88726a5a1b35e23715eff6f29a4705c" + ], + "version": "==4.2.0" + }, + "parse": { + "hashes": [ + "sha256:8048dde3f5ca07ad7ac7350460952d83b63eaacecdac1b37f45fd74870d849d2" + ], + "index": "pypi", + "version": "==1.8.2" + }, + "pyee": { + "hashes": [ + "sha256:47f8fa96d6dee61c82001831e1fbba55f3f808003a322d0e6653aa01c59f6b9e", + "sha256:4ec22817297b7024f89721cc34f790ee2767c5b5ca44284c565ee643abafbe32" + ], + "version": "==5.0.0" + }, + "pyppeteer": { + "hashes": [ + "sha256:4e0409fb30bb717296432d5548f6a3407d78d9efcf7a17f308fdb42d43607d9f" + ], + "index": "pypi", + "version": "==0.0.14" + }, + "pyquery": { + "hashes": [ + "sha256:07987c2ed2aed5cba29ff18af95e56e9eb04a2249f42ce47bddfb37f487229a3", + "sha256:4771db76bd14352eba006463656aef990a0147a0eeaf094725097acfa90442bf" + ], + "index": "pypi", + "version": "==1.4.0" + }, + "requests": { + "hashes": [ + "sha256:6a1b267aa90cac58ac3a765d067950e7dbbf75b1da07e895d1f594193a40a38b", + "sha256:9c443e7324ba5b85070c4a818ade28bfabedf16ea10206da1132edaa6dda237e" + ], + "index": "pypi", + "version": "==2.18.4" + }, + "rfc3986": { + "hashes": [ + "sha256:632b8fcd2ac37f24334316227f909be4f9d0738cbf409404cff6fa5f69a24093", + "sha256:8458571c4c57e1cf23593ad860bb601b6a604df6217f829c2bc70dc4b5af941b" + ], + "index": "pypi", + "version": "==1.1.0" + }, + "six": { + "hashes": [ + "sha256:70e8a77beed4562e7f14fe23a786b54f6296e34344c23bc42f07b15018ff98e9", + "sha256:832dc0e10feb1aa2c68dcc57dbb658f1c7e65b9b61af69048abc87a2db00a0eb" + ], + "version": "==1.11.0" + }, + "urllib3": { + "hashes": [ + "sha256:06330f386d6e4b195fbfc736b297f58c5a892e4440e54d294d7004e3a9bbea1b", + "sha256:cc44da8e1145637334317feebd728bd869a35285b93cbb4cca2577da7e62db4f" + ], + "version": "==1.22" + }, + "w3lib": { + "hashes": [ + "sha256:55994787e93b411c2d659068b51b9998d9d0c05e0df188e6daf8f45836e1ea38", + "sha256:aaf7362464532b1036ab0092e2eee78e8fd7b56787baa9ed4967457b083d011b" + ], + "index": "pypi", + "version": "==1.19.0" + }, + "websockets": { + "hashes": [ + "sha256:0c31bc832d529dc7583d324eb6c836a4f362032a1902723c112cf57883488d8c", + "sha256:1f3e5a52cab6daa3d432c7b0de0a14109be39d2bfaad033ee5de4a3d3e11dcdf", + "sha256:341824d8c9ad53fc43cca3fa9407f294125fa258592f7676640396501448e57e", + "sha256:367ff945bc0950ad9634591e2afe50bf2222bc4fad1088a386c4bb700888026e", + "sha256:3859ca16c229ddb0fa21c5090e4efcb037c08ce69b0c1dfed6122c3f98cd0c22", + "sha256:3d425ae081fb4ba1eef9ecf30472ffd79f8e868297ccc7a47993c96dbf2a819c", + "sha256:64896a6b3368c959b8096b655e46f03dfa65b96745249f374bd6a35705cc3489", + "sha256:6df87698022aef2596bffdfecc96d656db59c8d719708c8a471daa815ee61656", + "sha256:80188abdadd23edaaea05ce761dc9a2e1df31a74a0533967f0dcd9560c85add0", + "sha256:d1a0572b6edb22c9208e3e5381064e09d287d2a915f90233fef994ee7a14a935", + "sha256:da4d4fbe059b0453e726d6d993760065d69b823a27efc3040402a6fcfe6a1ed9", + "sha256:da7610a017f5343fdf765f4e0eb6fd0dfd08264ca1565212b110836d9367fc9c", + "sha256:ebdd4f18fe7e3bea9bd3bf446b0f4117739478caa2c76e4f0fb72cc45b03cbd7", + "sha256:f5192da704535a7cbf76d6e99c1ec4af7e8d1288252bf5a2385d414509ded0cf", + "sha256:fd81af8cf3e69f9a97f3a6c0623a0527de0f922c2df725f00cd7646d478af632", + "sha256:fecf51c13195c416c22422353b306dddb9c752e4b80b21e0fa1fccbe38246677" + ], + "version": "==4.0.1" + } + }, + "develop": { + "alabaster": { + "hashes": [ + "sha256:2eef172f44e8d301d25aff8068fddd65f767a3f04b5f15b0f4922f113aa1c732", + "sha256:37cdcb9e9954ed60912ebc1ca12a9d12178c26637abdf124e3cde2341c257fe0" + ], + "version": "==0.7.10" + }, + "attrs": { + "hashes": [ + "sha256:1c7960ccfd6a005cd9f7ba884e6316b5e430a3f1a6c37c5f87d8b43f83b54ec9", + "sha256:a17a9573a6f475c99b551c0e0a812707ddda1ec9653bed04c13841404ed6f450" + ], + "version": "==17.4.0" + }, + "babel": { + "hashes": [ + "sha256:8ce4cb6fdd4393edd323227cba3a077bceb2a6ce5201c902c65e730046f41f14", + "sha256:ad209a68d7162c4cff4b29cdebe3dec4cef75492df501b0049a9433c96ce6f80" + ], + "version": "==2.5.3" + }, + "beautifulsoup4": { + "hashes": [ + "sha256:11a9a27b7d3bddc6d86f59fb76afb70e921a25ac2d6cc55b40d072bd68435a76", + "sha256:7015e76bf32f1f574636c4288399a6de66ce08fb7b2457f628a8d70c0fbabb11", + "sha256:808b6ac932dccb0a4126558f7dfdcf41710dd44a4ef497a0bb59a77f9f078e89" + ], + "version": "==4.6.0" + }, + "bs4": { + "hashes": [ + "sha256:36ecea1fd7cc5c0c6e4a1ff075df26d50da647b75376626cc186e2212886dd3a" + ], + "index": "pypi", + "version": "==0.0.1" + }, + "certifi": { + "hashes": [ + "sha256:14131608ad2fd56836d33a71ee60fa1c82bc9d2c8d98b7bdbc631fe1b3cd1296", + "sha256:edbc3f203427eef571f79a7692bb160a2b0f7ccaa31953e99bd17e307cf63f7d" + ], + "version": "==2018.1.18" + }, + "chardet": { + "hashes": [ + "sha256:84ab92ed1c4d4f16916e05906b6b75a6c0fb5db821cc65e70cbd64a3e2a5eaae", + "sha256:fc323ffcaeaed0e0a02bf4d117757b98aed530d9ed4531e3e15460124c106691" + ], + "version": "==3.0.4" + }, + "cssselect": { + "hashes": [ + "sha256:066d8bc5229af09617e24b3ca4d52f1f9092d9e061931f4184cd572885c23204", + "sha256:3b5103e8789da9e936a68d993b70df732d06b8bb9a337a05ed4eb52c17ef7206" + ], + "version": "==1.0.3" + }, + "docutils": { + "hashes": [ + "sha256:02aec4bd92ab067f6ff27a38a38a41173bf01bed8f89157768c1573f53e474a6", + "sha256:51e64ef2ebfb29cae1faa133b3710143496eca21c530f3f71424d77687764274", + "sha256:7a4bd47eaf6596e1295ecb11361139febe29b084a87bf005bf899f9a42edc3c6" + ], + "version": "==0.14" + }, + "e1839a8": { + "editable": true, + "path": "." + }, + "fake-useragent": { + "hashes": [ + "sha256:cc9b9ddcebc708b3deac846f5fccb16e37c02ee47435a4ec7132271dd96aec8c" + ], + "index": "pypi", + "version": "==0.1.10" + }, + "idna": { + "hashes": [ + "sha256:2c6a5de3089009e3da7c5dde64a141dbc8551d5b7f6cf4ed7c2568d0cc520a8f", + "sha256:8c7309c718f94b3a625cb648ace320157ad16ff131ae0af362c9f21b80ef6ec4" + ], + "version": "==2.6" + }, + "imagesize": { + "hashes": [ + "sha256:3620cc0cadba3f7475f9940d22431fc4d407269f1be59ec9b8edcca26440cf18", + "sha256:5b326e4678b6925158ccc66a9fa3122b6106d7c876ee32d7de6ce59385b96315" + ], + "version": "==1.0.0" + }, + "jinja2": { + "hashes": [ + "sha256:74c935a1b8bb9a3947c50a54766a969d4846290e1e788ea44c1392163723c3bd", + "sha256:f84be1bb0040caca4cea721fcbbbbd61f9be9464ca236387158b0feea01914a4" + ], + "version": "==2.10" + }, + "lxml": { + "hashes": [ + "sha256:0aa44ffdeaaf6ba45d61980bb2c07e87d4dcac7a8b5b9d458124bc1adcda5233", + "sha256:0af9c9267b1257319d49e9c1e9abbf92a99f965bee3c4733e0f0f7578985182d", + "sha256:0cddc6cde79e1932efc71d9974a4418184ad0b8ca46c633ad772b2c5eaf36b3c", + "sha256:124a9d529eec5e10f307eb237df3efc43dd1fb7ebdb5da5e480c4ed372648b6b", + "sha256:1d1e45584353e4d563685874707fc8c85cdd11b0ef3b79d77bb38046134d68a9", + "sha256:2812bc45a7f53f366217b76a1c53e6728fbfa7f7524d16a321ea8f7131428bd1", + "sha256:29697224b2df76edf7c2de9bcd90a26dd28fe85c5fd7f0171cae84f8383b227e", + "sha256:36ffb216e2f361a5a0a7e219aea6cd44da11c64061baed273944aae21223186c", + "sha256:4626d699551f66687e5f7e7f9b79bfce611e12edebfb9fec276e2df8ec46541e", + "sha256:4c21d7304d37715e6aed756e4d0c374c99c9bb1fa8d64f546b95474b17ac23de", + "sha256:57be98177ce784495dff53f40620995ad0a56456246ed9d51977e595de58e12e", + "sha256:62bfcd0629991e1c1257ffd28df2ab31a5c44da4c06823c26ec0f472723a84ca", + "sha256:71ac6dac6835de75aaf531cae9ffa447dae0783ba1f43bf6eaccfad3680a5b9c", + "sha256:7769ac9203ebe6d8db16904c54d57d77360fcc1926ed7afaa86b04050e4afa5b", + "sha256:7d96fbb5f23a62300aa9bef7d286cd61aca8902357619c8708c0290aba5df73f", + "sha256:88583c6565c9299f617238a500f1a47510bac54daff7872d6a343f13361b659e", + "sha256:8f52c4c8f1cf15419193026e731f34a3260a3ce7977b875ba1eb2517b8a3f660", + "sha256:95b82fdfdaac71640b281da6b9a2c3700177ba5190a786881b184de744ad55de", + "sha256:988d55112f196e12341b7c5138841c2b4f21f871eaa8f138c6ac4c46f28899f9", + "sha256:9e08918b744b89d30750eca8598f37ae75b16202870db678fde970d85afed3e3", + "sha256:b46f31e806f6884bd1053ad1d78ecaca6d1bc5dd94a1b783a6ff0bb4b3a60962", + "sha256:c18f316cad969111b1ff9e84c82fbc9ae6f25f35701118182d384585940cdf80", + "sha256:cef79715f2335bfc1ef7082bcb8b2bac87271431653455221a9127fde146208c", + "sha256:cf63f590090404c52f179b7ceacb7cd549de3a1697bcfe2f79be180b2801d109", + "sha256:d06260e6102b2f18dbee3736185cd6a2e1c88c0fad782bf8e9d7a7a1b24e02b0", + "sha256:d0dc3e5737adcc9a23fd3d3d3072b887fefb48143309563f412ef7b0ebdfdb30", + "sha256:dd98d4f88ce0abda2b02c1542d1de22dd342023f3ba09874bd95841283f29433", + "sha256:f04b184984c23e0caac3c55eac2fe2dbb88726a5a1b35e23715eff6f29a4705c" + ], + "version": "==4.2.0" + }, + "markupsafe": { + "hashes": [ + "sha256:a6be69091dac236ea9c6bc7d012beab42010fa914c459791d627dad4910eb665" + ], + "version": "==1.0" + }, + "mypy": { + "hashes": [ + "sha256:83d798f66323f2de6191d66d9ae5ab234e4ee5b400010e19c58d75d308049f25", + "sha256:884f18f3a40cfcf24cdd5860b84958cfb35e6563e439c5adc1503878df221dc3" + ], + "index": "pypi", + "version": "==0.570" + }, + "packaging": { + "hashes": [ + "sha256:e9215d2d2535d3ae866c3d6efc77d5b24a0192cce0ff20e42896cc0664f889c0", + "sha256:f019b770dd64e585a99714f1fd5e01c7a8f11b45635aa953fd41c689a657375b" + ], + "version": "==17.1" + }, + "parse": { + "hashes": [ + "sha256:8048dde3f5ca07ad7ac7350460952d83b63eaacecdac1b37f45fd74870d849d2" + ], + "index": "pypi", + "version": "==1.8.2" + }, + "pkginfo": { + "hashes": [ + "sha256:5878d542a4b3f237e359926384f1dde4e099c9f5525d236b1840cf704fa8d474", + "sha256:a39076cb3eb34c333a0dd390b568e9e1e881c7bf2cc0aee12120636816f55aee" + ], + "version": "==1.4.2" + }, + "pluggy": { + "hashes": [ + "sha256:7f8ae7f5bdf75671a718d2daf0a64b7885f74510bcd98b1a0bb420eb9a9d0cff" + ], + "version": "==0.6.0" + }, + "psutil": { + "hashes": [ + "sha256:230eeb3aeb077814f3a2cd036ddb6e0f571960d327298cc914c02385c3e02a63", + "sha256:4152ae231709e3e8b80e26b6da20dc965a1a589959c48af1ed024eca6473f60d", + "sha256:779ec7e7621758ca11a8d99a1064996454b3570154277cc21342a01148a49c28", + "sha256:82a06785db8eeb637b349006cc28a92e40cd190fefae9875246d18d0de7ccac8", + "sha256:8a15d773203a1277e57b1d11a7ccdf70804744ef4a9518a87ab8436995c31a4b", + "sha256:94d4e63189f2593960e73acaaf96be235dd8a455fe2bcb37d8ad6f0e87f61556", + "sha256:a3286556d4d2f341108db65d8e20d0cd3fcb9a91741cb5eb496832d7daf2a97c", + "sha256:c91eee73eea00df5e62c741b380b7e5b6fdd553891bee5669817a3a38d036f13", + "sha256:e2467e9312c2fa191687b89ff4bc2ad8843be4af6fb4dc95a7cc5f7d7a327b18" + ], + "index": "pypi", + "version": "==5.4.3" + }, + "py": { + "hashes": [ + "sha256:8cca5c229d225f8c1e3085be4fcf306090b00850fefad892f9d96c7b6e2f310f", + "sha256:ca18943e28235417756316bfada6cd96b23ce60dd532642690dcfdaba988a76d" + ], + "version": "==1.5.2" + }, + "pyee": { + "hashes": [ + "sha256:47f8fa96d6dee61c82001831e1fbba55f3f808003a322d0e6653aa01c59f6b9e", + "sha256:4ec22817297b7024f89721cc34f790ee2767c5b5ca44284c565ee643abafbe32" + ], + "version": "==5.0.0" + }, + "pygments": { + "hashes": [ + "sha256:78f3f434bcc5d6ee09020f92ba487f95ba50f1e3ef83ae96b9d5ffa1bab25c5d", + "sha256:dbae1046def0efb574852fab9e90209b23f556367b5a320c0bcb871c77c3e8cc" + ], + "version": "==2.2.0" + }, + "pyparsing": { + "hashes": [ + "sha256:0832bcf47acd283788593e7a0f542407bd9550a55a8a8435214a1960e04bcb04", + "sha256:281683241b25fe9b80ec9d66017485f6deff1af5cde372469134b56ca8447a07", + "sha256:8f1e18d3fd36c6795bb7e02a39fd05c611ffc2596c1e0d995d34d67630426c18", + "sha256:9e8143a3e15c13713506886badd96ca4b579a87fbdf49e550dbfc057d6cb218e", + "sha256:b8b3117ed9bdf45e14dcc89345ce638ec7e0e29b2b579fa1ecf32ce45ebac8a5", + "sha256:e4d45427c6e20a59bf4f88c639dcc03ce30d193112047f94012102f235853a58", + "sha256:fee43f17a9c4087e7ed1605bd6df994c6173c1e977d7ade7b651292fab2bd010" + ], + "version": "==2.2.0" + }, + "pyppeteer": { + "hashes": [ + "sha256:4e0409fb30bb717296432d5548f6a3407d78d9efcf7a17f308fdb42d43607d9f" + ], + "index": "pypi", + "version": "==0.0.14" + }, + "pyquery": { + "hashes": [ + "sha256:07987c2ed2aed5cba29ff18af95e56e9eb04a2249f42ce47bddfb37f487229a3", + "sha256:4771db76bd14352eba006463656aef990a0147a0eeaf094725097acfa90442bf" + ], + "index": "pypi", + "version": "==1.4.0" + }, + "pytest": { + "hashes": [ + "sha256:062027955bccbc04d2fcd5d79690947e018ba31abe4c90b2c6721abec734261b", + "sha256:117bad36c1a787e1a8a659df35de53ba05f9f3398fb9e4ac17e80ad5903eb8c5" + ], + "index": "pypi", + "version": "==3.4.2" + }, + "pytest-asyncio": { + "hashes": [ + "sha256:286b50773e996c80d894b95afaf45df6952408a67a59979ca9839f94693ec7fd", + "sha256:f32804bb58a66e13a3eda11f8942a71b1b6a30466b0d2ffe9214787aab0e172e" + ], + "index": "pypi", + "version": "==0.8.0" + }, + "pytz": { + "hashes": [ + "sha256:07edfc3d4d2705a20a6e99d97f0c4b61c800b8232dc1c04d87e8554f130148dd", + "sha256:3a47ff71597f821cd84a162e71593004286e5be07a340fd462f0d33a760782b5", + "sha256:410bcd1d6409026fbaa65d9ed33bf6dd8b1e94a499e32168acfc7b332e4095c0", + "sha256:5bd55c744e6feaa4d599a6cbd8228b4f8f9ba96de2c38d56f08e534b3c9edf0d", + "sha256:61242a9abc626379574a166dc0e96a66cd7c3b27fc10868003fa210be4bff1c9", + "sha256:887ab5e5b32e4d0c86efddd3d055c1f363cbaa583beb8da5e22d2fa2f64d51ef", + "sha256:ba18e6a243b3625513d85239b3e49055a2f0318466e0b8a92b8fb8ca7ccdf55f", + "sha256:ed6509d9af298b7995d69a440e2822288f2eca1681b8cce37673dbb10091e5fe", + "sha256:f93ddcdd6342f94cea379c73cddb5724e0d6d0a1c91c9bdef364dc0368ba4fda" + ], + "version": "==2018.3" + }, + "requests": { + "hashes": [ + "sha256:6a1b267aa90cac58ac3a765d067950e7dbbf75b1da07e895d1f594193a40a38b", + "sha256:9c443e7324ba5b85070c4a818ade28bfabedf16ea10206da1132edaa6dda237e" + ], + "index": "pypi", + "version": "==2.18.4" + }, + "requests-file": { + "hashes": [ + "sha256:75c175eed739270aec3c5279ffd74e6527dada275c5c0d76b5817e9c86bb7dea", + "sha256:8f04aa6201bacda0567e7ac7f677f1499b0fc76b22140c54bc06edf1ba92e2fa" + ], + "index": "pypi", + "version": "==1.4.3" + }, + "requests-toolbelt": { + "hashes": [ + "sha256:42c9c170abc2cacb78b8ab23ac957945c7716249206f90874651971a4acff237", + "sha256:f6a531936c6fa4c6cfce1b9c10d5c4f498d16528d2a54a22ca00011205a187b5" + ], + "version": "==0.8.0" + }, + "six": { + "hashes": [ + "sha256:70e8a77beed4562e7f14fe23a786b54f6296e34344c23bc42f07b15018ff98e9", + "sha256:832dc0e10feb1aa2c68dcc57dbb658f1c7e65b9b61af69048abc87a2db00a0eb" + ], + "version": "==1.11.0" + }, + "snowballstemmer": { + "hashes": [ + "sha256:919f26a68b2c17a7634da993d91339e288964f93c274f1343e3bbbe2096e1128", + "sha256:9f3bcd3c401c3e862ec0ebe6d2c069ebc012ce142cce209c098ccb5b09136e89" + ], + "version": "==1.2.1" + }, + "sphinx": { + "hashes": [ + "sha256:41ae26acc6130ccf6ed47e5cca73742b80d55a134f0ab897c479bba8d3640b8e", + "sha256:da987de5fcca21a4acc7f67a86a363039e67ac3e8827161e61b91deb131c0ee8" + ], + "index": "pypi", + "version": "==1.7.1" + }, + "sphinxcontrib-websupport": { + "hashes": [ + "sha256:7a85961326aa3a400cd4ad3c816d70ed6f7c740acd7ce5d78cd0a67825072eb9", + "sha256:f4932e95869599b89bf4f80fc3989132d83c9faa5bf633e7b5e0c25dffb75da2" + ], + "version": "==1.0.1" + }, + "tqdm": { + "hashes": [ + "sha256:05e991ecb0f874046ddcb374396a626afd046fb4d31f73633ea752b844458a7a", + "sha256:2aea9f81fdf127048667e0ba22f5fc10ebc879fb838dc52dcf055242037ec1f7" + ], + "version": "==4.19.8" + }, + "twine": { + "hashes": [ + "sha256:08eb132bbaec40c6d25b358f546ec1dc96ebd2638a86eea68769d9e67fe2b129", + "sha256:2fd9a4d9ff0bcacf41fdc40c8cb0cfaef1f1859457c9653fd1b92237cc4e9f25" + ], + "index": "pypi", + "version": "==1.11.0" + }, + "typed-ast": { + "hashes": [ + "sha256:0948004fa228ae071054f5208840a1e88747a357ec1101c17217bfe99b299d58", + "sha256:25d8feefe27eb0303b73545416b13d108c6067b846b543738a25ff304824ed9a", + "sha256:29464a177d56e4e055b5f7b629935af7f49c196be47528cc94e0a7bf83fbc2b9", + "sha256:2e214b72168ea0275efd6c884b114ab42e316de3ffa125b267e732ed2abda892", + "sha256:3e0d5e48e3a23e9a4d1a9f698e32a542a4a288c871d33ed8df1b092a40f3a0f9", + "sha256:519425deca5c2b2bdac49f77b2c5625781abbaf9a809d727d3a5596b30bb4ded", + "sha256:57fe287f0cdd9ceaf69e7b71a2e94a24b5d268b35df251a88fef5cc241bf73aa", + "sha256:668d0cec391d9aed1c6a388b0d5b97cd22e6073eaa5fbaa6d2946603b4871efe", + "sha256:68ba70684990f59497680ff90d18e756a47bf4863c604098f10de9716b2c0bdd", + "sha256:6de012d2b166fe7a4cdf505eee3aaa12192f7ba365beeefaca4ec10e31241a85", + "sha256:79b91ebe5a28d349b6d0d323023350133e927b4de5b651a8aa2db69c761420c6", + "sha256:8550177fa5d4c1f09b5e5f524411c44633c80ec69b24e0e98906dd761941ca46", + "sha256:a8034021801bc0440f2e027c354b4eafd95891b573e12ff0418dec385c76785c", + "sha256:bc978ac17468fe868ee589c795d06777f75496b1ed576d308002c8a5756fb9ea", + "sha256:c05b41bc1deade9f90ddc5d988fe506208019ebba9f2578c622516fd201f5863", + "sha256:c9b060bd1e5a26ab6e8267fd46fc9e02b54eb15fffb16d112d4c7b1c12987559", + "sha256:edb04bdd45bfd76c8292c4d9654568efaedf76fe78eb246dde69bdb13b2dad87", + "sha256:f19f2a4f547505fe9072e15f6f4ae714af51b5a681a97f187971f50c283193b6" + ], + "version": "==1.1.0" + }, + "urllib3": { + "hashes": [ + "sha256:06330f386d6e4b195fbfc736b297f58c5a892e4440e54d294d7004e3a9bbea1b", + "sha256:cc44da8e1145637334317feebd728bd869a35285b93cbb4cca2577da7e62db4f" + ], + "version": "==1.22" + }, + "w3lib": { + "hashes": [ + "sha256:55994787e93b411c2d659068b51b9998d9d0c05e0df188e6daf8f45836e1ea38", + "sha256:aaf7362464532b1036ab0092e2eee78e8fd7b56787baa9ed4967457b083d011b" + ], + "index": "pypi", + "version": "==1.19.0" + }, + "websockets": { + "hashes": [ + "sha256:0c31bc832d529dc7583d324eb6c836a4f362032a1902723c112cf57883488d8c", + "sha256:1f3e5a52cab6daa3d432c7b0de0a14109be39d2bfaad033ee5de4a3d3e11dcdf", + "sha256:341824d8c9ad53fc43cca3fa9407f294125fa258592f7676640396501448e57e", + "sha256:367ff945bc0950ad9634591e2afe50bf2222bc4fad1088a386c4bb700888026e", + "sha256:3859ca16c229ddb0fa21c5090e4efcb037c08ce69b0c1dfed6122c3f98cd0c22", + "sha256:3d425ae081fb4ba1eef9ecf30472ffd79f8e868297ccc7a47993c96dbf2a819c", + "sha256:64896a6b3368c959b8096b655e46f03dfa65b96745249f374bd6a35705cc3489", + "sha256:6df87698022aef2596bffdfecc96d656db59c8d719708c8a471daa815ee61656", + "sha256:80188abdadd23edaaea05ce761dc9a2e1df31a74a0533967f0dcd9560c85add0", + "sha256:d1a0572b6edb22c9208e3e5381064e09d287d2a915f90233fef994ee7a14a935", + "sha256:da4d4fbe059b0453e726d6d993760065d69b823a27efc3040402a6fcfe6a1ed9", + "sha256:da7610a017f5343fdf765f4e0eb6fd0dfd08264ca1565212b110836d9367fc9c", + "sha256:ebdd4f18fe7e3bea9bd3bf446b0f4117739478caa2c76e4f0fb72cc45b03cbd7", + "sha256:f5192da704535a7cbf76d6e99c1ec4af7e8d1288252bf5a2385d414509ded0cf", + "sha256:fd81af8cf3e69f9a97f3a6c0623a0527de0f922c2df725f00cd7646d478af632", + "sha256:fecf51c13195c416c22422353b306dddb9c752e4b80b21e0fa1fccbe38246677" + ], + "version": "==4.0.1" + }, + "white": { + "hashes": [ + "sha256:45e2c7f54de1facc60bf0a726b480cdc43422aad57c3a0bc5ba54cb536696683", + "sha256:bca98066256cfff6fb85ec36b95cc5913c888c170a8407c340786972b06c6f8f" + ], + "index": "pypi", + "version": "==0.1.2" + } + } +} diff --git a/tests/fixtures/real-world-locks/pipfile/requests-html-0.10.0/setup.py b/tests/fixtures/real-world-locks/pipfile/requests-html-0.10.0/setup.py new file mode 100644 index 00000000..899d7006 --- /dev/null +++ b/tests/fixtures/real-world-locks/pipfile/requests-html-0.10.0/setup.py @@ -0,0 +1,108 @@ +#!/usr/bin/env python +# -*- coding: utf-8 -*- + +# Note: To use the 'upload' functionality of this file, you must: +# $ pip install twine + +import io +import os +import sys +from shutil import rmtree + +from setuptools import setup, Command + +# Package meta-data. +NAME = 'requests-html' +DESCRIPTION = 'HTML Parsing for Humans.' +URL = 'https://github.com/kennethreitz/requests-html' +EMAIL = 'me@kennethreitz.org' +AUTHOR = 'Kenneth Reitz' +VERSION = '0.10.0' + +# What packages are required for this module to be executed? +REQUIRED = [ + 'requests', 'pyquery', 'fake-useragent', 'parse', 'bs4', 'w3lib', 'pyppeteer>=0.0.14' +] + +# The rest you shouldn't have to touch too much :) +# ------------------------------------------------ +# Except, perhaps the License and Trove Classifiers! +# If you do change the License, remember to change the Trove Classifier for that! + +here = os.path.abspath(os.path.dirname(__file__)) + +# Import the README and use it as the long-description. +# Note: this will only work if 'README.rst' is present in your MANIFEST.in file! +with io.open(os.path.join(here, 'README.rst'), encoding='utf-8') as f: + long_description = '\n' + f.read() + +class UploadCommand(Command): + """Support setup.py upload.""" + + description = 'Build and publish the package.' + user_options = [] + + @staticmethod + def status(s): + """Prints things in bold.""" + print('\033[1m{0}\033[0m'.format(s)) + + def initialize_options(self): + pass + + def finalize_options(self): + pass + + def run(self): + try: + self.status('Removing previous builds…') + rmtree(os.path.join(here, 'dist')) + except OSError: + pass + + self.status('Building Source and Wheel (universal) distribution…') + os.system('{0} setup.py sdist bdist_wheel --universal'.format(sys.executable)) + + self.status('Uploading the package to PyPi via Twine…') + os.system('twine upload dist/*') + + self.status('Publishing git tags…') + os.system('git tag v{0}'.format(VERSION)) + os.system('git push --tags') + + sys.exit() + + +# Where the magic happens: +setup( + name=NAME, + version=VERSION, + description=DESCRIPTION, + long_description=long_description, + author=AUTHOR, + author_email=EMAIL, + url=URL, + python_requires='>=3.6.0', + # If your package is a single module, use this instead of 'packages': + py_modules=['requests_html'], + + # entry_points={ + # 'console_scripts': ['mycli=mymodule:cli'], + # }, + install_requires=REQUIRED, + include_package_data=True, + license='MIT', + classifiers=[ + # Trove classifiers + # Full list: https://pypi.python.org/pypi?%3Aaction=list_classifiers + 'License :: OSI Approved :: MIT License', + 'Programming Language :: Python', + 'Programming Language :: Python :: 3.6', + 'Programming Language :: Python :: Implementation :: CPython', + 'Programming Language :: Python :: Implementation :: PyPy' + ], + # $ setup.py publish support. + cmdclass={ + 'upload': UploadCommand, + }, +) diff --git a/tests/fixtures/real-world-locks/pipfile/responder-2.0.0/Pipfile.lock b/tests/fixtures/real-world-locks/pipfile/responder-2.0.0/Pipfile.lock new file mode 100644 index 00000000..ecde5034 --- /dev/null +++ b/tests/fixtures/real-world-locks/pipfile/responder-2.0.0/Pipfile.lock @@ -0,0 +1,755 @@ +{ + "_meta": { + "hash": { + "sha256": "ea12c0d556a3ca0848b0eba291a11a5ea98a701f0885c2d030b2aeb1e5b9c15f" + }, + "pipfile-spec": 6, + "requires": {}, + "sources": [ + { + "name": "pypi", + "url": "https://pypi.org/simple", + "verify_ssl": true + } + ] + }, + "default": { + "aiofiles": { + "hashes": [ + "sha256:021ea0ba314a86027c166ecc4b4c07f2d40fc0f4b3a950d1868a0f2571c2bbee", + "sha256:1e644c2573f953664368de28d2aa4c89dfd64550429d0c27c4680ccd3aa4985d" + ], + "version": "==0.4.0" + }, + "aniso8601": { + "hashes": [ + "sha256:513d2b6637b7853806ae79ffaca6f3e8754bdd547048f5ccc1420aec4b714f1e", + "sha256:d10a4bf949f619f719b227ef5386e31f49a2b6d453004b21f02661ccc8670c7b" + ], + "version": "==7.0.0" + }, + "apispec": { + "hashes": [ + "sha256:5fdaa1173b32515cc83f9d413a49a6c37fafc2b87f6b40e95923d3e85f0942c5", + "sha256:9e88c51517a6515612e818459f61c1bc06c00f2313e5187828bdbabaa7461473" + ], + "version": "==3.0.0" + }, + "apistar": { + "hashes": [ + "sha256:8da0d3f15748c8ed6e68914ba5b8f6dd5dff5afbe137950d07103575df0bce73" + ], + "version": "==0.7.2" + }, + "certifi": { + "hashes": [ + "sha256:e4f3620cfea4f83eedc95b24abd9cd56f3c4b146dd0177e83a21b4eb49e21e50", + "sha256:fd7c7c74727ddcf00e9acd26bba8da604ffec95bf1c2144e67aff7a8b50e6cef" + ], + "version": "==2019.9.11" + }, + "chardet": { + "hashes": [ + "sha256:84ab92ed1c4d4f16916e05906b6b75a6c0fb5db821cc65e70cbd64a3e2a5eaae", + "sha256:fc323ffcaeaed0e0a02bf4d117757b98aed530d9ed4531e3e15460124c106691" + ], + "version": "==3.0.4" + }, + "click": { + "hashes": [ + "sha256:2335065e6395b9e67ca716de5f7526736bfa6ceead690adf616d925bdc622b13", + "sha256:5b94b49521f6456670fdb30cd82a4eca9412788a93fa6dd6df72c94d5a8ff2d7" + ], + "version": "==7.0" + }, + "docopt": { + "hashes": [ + "sha256:49b3a825280bd66b3aa83585ef59c4a8c82f2c8a522dbe754a8bc8d08c85c491" + ], + "version": "==0.6.2" + }, + "graphene": { + "hashes": [ + "sha256:09165f03e1591b76bf57b133482db9be6dac72c74b0a628d3c93182af9c5a896", + "sha256:2cbe6d4ef15cfc7b7805e0760a0e5b80747161ce1b0f990dfdc0d2cf497c12f9" + ], + "version": "==2.1.8" + }, + "graphql-core": { + "hashes": [ + "sha256:1488f2a5c2272dc9ba66e3042a6d1c30cea0db4c80bd1e911c6791ad6187d91b", + "sha256:da64c472d720da4537a2e8de8ba859210b62841bd47a9be65ca35177f62fe0e4" + ], + "version": "==2.2.1" + }, + "graphql-relay": { + "hashes": [ + "sha256:0e94201af4089e1f81f07d7bd8f84799768e39d70fa1ea16d1df505b46cc6335", + "sha256:75aa0758971e252964cb94068a4decd472d2a8295229f02189e3cbca1f10dbb5", + "sha256:7fa74661246e826ef939ee92e768f698df167a7617361ab399901eaebf80dce6" + ], + "version": "==2.0.0" + }, + "graphql-server-core": { + "hashes": [ + "sha256:e5f82add4b3d5580aa1f1e7d9f00e944ad3abe1b65eb337e611d6a77cc20f231" + ], + "version": "==1.1.1" + }, + "h11": { + "hashes": [ + "sha256:acca6a44cb52a32ab442b1779adf0875c443c689e9e028f8d831a3769f9c5208", + "sha256:f2b1ca39bfed357d1f19ac732913d5f9faa54a5062eca7d2ec3a916cfb7ae4c7" + ], + "version": "==0.8.1" + }, + "httptools": { + "hashes": [ + "sha256:e00cbd7ba01ff748e494248183abc6e153f49181169d8a3d41bb49132ca01dfc" + ], + "version": "==0.0.13" + }, + "idna": { + "hashes": [ + "sha256:c357b3f628cf53ae2c4c05627ecc484553142ca23264e593d327bcde5e9c3407", + "sha256:ea8b7f6188e6fa117537c3df7da9fc686d485087abf6ac197f9c46432f7e4a3c" + ], + "version": "==2.8" + }, + "jinja2": { + "hashes": [ + "sha256:74320bb91f31270f9551d46522e33af46a80c3d619f4a4bf42b3164d30b5911f", + "sha256:9fe95f19286cfefaa917656583d020be14e7859c6b0252588391e47db34527de" + ], + "version": "==2.10.3" + }, + "markupsafe": { + "hashes": [ + "sha256:00bc623926325b26bb9605ae9eae8a215691f33cae5df11ca5424f06f2d1f473", + "sha256:09027a7803a62ca78792ad89403b1b7a73a01c8cb65909cd876f7fcebd79b161", + "sha256:09c4b7f37d6c648cb13f9230d847adf22f8171b1ccc4d5682398e77f40309235", + "sha256:1027c282dad077d0bae18be6794e6b6b8c91d58ed8a8d89a89d59693b9131db5", + "sha256:24982cc2533820871eba85ba648cd53d8623687ff11cbb805be4ff7b4c971aff", + "sha256:29872e92839765e546828bb7754a68c418d927cd064fd4708fab9fe9c8bb116b", + "sha256:43a55c2930bbc139570ac2452adf3d70cdbb3cfe5912c71cdce1c2c6bbd9c5d1", + "sha256:46c99d2de99945ec5cb54f23c8cd5689f6d7177305ebff350a58ce5f8de1669e", + "sha256:500d4957e52ddc3351cabf489e79c91c17f6e0899158447047588650b5e69183", + "sha256:535f6fc4d397c1563d08b88e485c3496cf5784e927af890fb3c3aac7f933ec66", + "sha256:62fe6c95e3ec8a7fad637b7f3d372c15ec1caa01ab47926cfdf7a75b40e0eac1", + "sha256:6dd73240d2af64df90aa7c4e7481e23825ea70af4b4922f8ede5b9e35f78a3b1", + "sha256:717ba8fe3ae9cc0006d7c451f0bb265ee07739daf76355d06366154ee68d221e", + "sha256:79855e1c5b8da654cf486b830bd42c06e8780cea587384cf6545b7d9ac013a0b", + "sha256:7c1699dfe0cf8ff607dbdcc1e9b9af1755371f92a68f706051cc8c37d447c905", + "sha256:88e5fcfb52ee7b911e8bb6d6aa2fd21fbecc674eadd44118a9cc3863f938e735", + "sha256:8defac2f2ccd6805ebf65f5eeb132adcf2ab57aa11fdf4c0dd5169a004710e7d", + "sha256:98c7086708b163d425c67c7a91bad6e466bb99d797aa64f965e9d25c12111a5e", + "sha256:9add70b36c5666a2ed02b43b335fe19002ee5235efd4b8a89bfcf9005bebac0d", + "sha256:9bf40443012702a1d2070043cb6291650a0841ece432556f784f004937f0f32c", + "sha256:ade5e387d2ad0d7ebf59146cc00c8044acbd863725f887353a10df825fc8ae21", + "sha256:b00c1de48212e4cc9603895652c5c410df699856a2853135b3967591e4beebc2", + "sha256:b1282f8c00509d99fef04d8ba936b156d419be841854fe901d8ae224c59f0be5", + "sha256:b2051432115498d3562c084a49bba65d97cf251f5a331c64a12ee7e04dacc51b", + "sha256:ba59edeaa2fc6114428f1637ffff42da1e311e29382d81b339c1817d37ec93c6", + "sha256:c8716a48d94b06bb3b2524c2b77e055fb313aeb4ea620c8dd03a105574ba704f", + "sha256:cd5df75523866410809ca100dc9681e301e3c27567cf498077e8551b6d20e42f", + "sha256:e249096428b3ae81b08327a63a485ad0878de3fb939049038579ac0ef61e17e7" + ], + "version": "==1.1.1" + }, + "marshmallow": { + "hashes": [ + "sha256:077b4612f5d3b9333b736fdc6b963d2b46d409070f44ff3e6c4109645c673e83", + "sha256:9a2f3e8ea5f530a9664e882d7d04b58650f46190178b2264c72b7d20399d28f0" + ], + "version": "==3.2.1" + }, + "promise": { + "hashes": [ + "sha256:2ebbfc10b7abf6354403ed785fe4f04b9dfd421eb1a474ac8d187022228332af", + "sha256:348f5f6c3edd4fd47c9cd65aed03ac1b31136d375aa63871a57d3e444c85655c" + ], + "version": "==2.2.1" + }, + "python-multipart": { + "hashes": [ + "sha256:f7bb5f611fc600d15fa47b3974c8aa16e93724513b49b5f95c81e6624c83fa43" + ], + "version": "==0.0.5" + }, + "pyyaml": { + "hashes": [ + "sha256:0113bc0ec2ad727182326b61326afa3d1d8280ae1122493553fd6f4397f33df9", + "sha256:01adf0b6c6f61bd11af6e10ca52b7d4057dd0be0343eb9283c878cf3af56aee4", + "sha256:5124373960b0b3f4aa7df1707e63e9f109b5263eca5976c66e08b1c552d4eaf8", + "sha256:5ca4f10adbddae56d824b2c09668e91219bb178a1eee1faa56af6f99f11bf696", + "sha256:7907be34ffa3c5a32b60b95f4d95ea25361c951383a894fec31be7252b2b6f34", + "sha256:7ec9b2a4ed5cad025c2278a1e6a19c011c80a3caaac804fd2d329e9cc2c287c9", + "sha256:87ae4c829bb25b9fe99cf71fbb2140c448f534e24c998cc60f39ae4f94396a73", + "sha256:9de9919becc9cc2ff03637872a440195ac4241c80536632fffeb6a1e25a74299", + "sha256:a5a85b10e450c66b49f98846937e8cfca1db3127a9d5d1e31ca45c3d0bef4c5b", + "sha256:b0997827b4f6a7c286c01c5f60384d218dca4ed7d9efa945c3e1aa623d5709ae", + "sha256:b631ef96d3222e62861443cc89d6563ba3eeb816eeb96b2629345ab795e53681", + "sha256:bf47c0607522fdbca6c9e817a6e81b08491de50f3766a7a0e6a5be7905961b41", + "sha256:f81025eddd0327c7d4cfe9b62cf33190e1e736cc6e97502b3ec425f574b3e7a8" + ], + "version": "==5.1.2" + }, + "requests": { + "hashes": [ + "sha256:11e007a8a2aa0323f5a921e9e6a2d7e4e67d9877e85773fba9ba6419025cbeb4", + "sha256:9cf5292fcd0f598c671cfc1e0d7d1a7f13bb8085e9a590f48c010551dc6c4b31" + ], + "version": "==2.22.0" + }, + "requests-toolbelt": { + "hashes": [ + "sha256:380606e1d10dc85c3bd47bf5a6095f815ec007be7a8b69c878507068df059e6f", + "sha256:968089d4584ad4ad7c171454f0a5c6dac23971e9472521ea3b6d49d610aa6fc0" + ], + "version": "==0.9.1" + }, + "responder": { + "editable": true, + "path": "." + }, + "rfc3986": { + "hashes": [ + "sha256:0344d0bd428126ce554e7ca2b61787b6a28d2bbd19fc70ed2dd85efe31176405", + "sha256:df4eba676077cefb86450c8f60121b9ae04b94f65f85b69f3f731af0516b7b18" + ], + "version": "==1.3.2" + }, + "rx": { + "hashes": [ + "sha256:13a1d8d9e252625c173dc795471e614eadfe1cf40ffc684e08b8fff0d9748c23", + "sha256:7357592bc7e881a95e0c2013b73326f704953301ab551fbc8133a6fadab84105" + ], + "version": "==1.6.1" + }, + "six": { + "hashes": [ + "sha256:3350809f0555b11f552448330d0b52d5f24c91a322ea4a15ef22629740f3761c", + "sha256:d16a0141ec1a18405cd4ce8b4613101da75da0e9a7aec5bdd4fa804d0e0eba73" + ], + "version": "==1.12.0" + }, + "starlette": { + "hashes": [ + "sha256:e41ef52e711a82ef95c195674e5d8d41c75c6b1d6f5a275637eedd4cc2150a7f" + ], + "version": "==0.12.10" + }, + "typesystem": { + "hashes": [ + "sha256:ba2bd10f1c5844d08dd8841e777bdee55bfca569bf21cb96cd0f91e0a4f66cd8" + ], + "version": "==0.2.4" + }, + "urllib3": { + "hashes": [ + "sha256:3de946ffbed6e6746608990594d08faac602528ac7015ac28d33cee6a45b7398", + "sha256:9a107b99a5393caf59c7aa3c1249c16e6879447533d0887f4336dde834c7be86" + ], + "version": "==1.25.6" + }, + "uvicorn": { + "hashes": [ + "sha256:8aa44f9d9c3082ef693950387ea25d376e32944df6d4071dbd8edc3c25a40c74" + ], + "version": "==0.8.6" + }, + "uvloop": { + "hashes": [ + "sha256:0fcd894f6fc3226a962ee7ad895c4f52e3f5c3c55098e21efb17c071849a0573", + "sha256:2f31de1742c059c96cb76b91c5275b22b22b965c886ee1fced093fa27dde9e64", + "sha256:459e4649fcd5ff719523de33964aa284898e55df62761e7773d088823ccbd3e0", + "sha256:67867aafd6e0bc2c30a079603a85d83b94f23c5593b3cc08ec7e58ac18bf48e5", + "sha256:8c200457e6847f28d8bb91c5e5039d301716f5f2fce25646f5fb3fd65eda4a26", + "sha256:958906b9ca39eb158414fbb7d6b8ef1b7aee4db5c8e8e5d00fcbb69a1ce9dca7", + "sha256:ac1dca3d8f3ef52806059e81042ee397ac939e5a86c8a3cea55d6b087db66115", + "sha256:b284c22d8938866318e3b9d178142b8be316c52d16fcfe1560685a686718a021", + "sha256:c48692bf4587ce281d641087658eca275a5ad3b63c78297bbded96570ae9ce8f", + "sha256:fefc3b2b947c99737c348887db2c32e539160dcbeb7af9aa6b53db7a283538fe" + ], + "version": "==0.12.2" + }, + "websockets": { + "hashes": [ + "sha256:04b42a1b57096ffa5627d6a78ea1ff7fad3bc2c0331ffc17bc32a4024da7fea0", + "sha256:08e3c3e0535befa4f0c4443824496c03ecc25062debbcf895874f8a0b4c97c9f", + "sha256:10d89d4326045bf5e15e83e9867c85d686b612822e4d8f149cf4840aab5f46e0", + "sha256:232fac8a1978fc1dead4b1c2fa27c7756750fb393eb4ac52f6bc87ba7242b2fa", + "sha256:4bf4c8097440eff22bc78ec76fe2a865a6e658b6977a504679aaf08f02c121da", + "sha256:51642ea3a00772d1e48fb0c492f0d3ae3b6474f34d20eca005a83f8c9c06c561", + "sha256:55d86102282a636e195dad68aaaf85b81d0bef449d7e2ef2ff79ac450bb25d53", + "sha256:564d2675682bd497b59907d2205031acbf7d3fadf8c763b689b9ede20300b215", + "sha256:5d13bf5197a92149dc0badcc2b699267ff65a867029f465accfca8abab95f412", + "sha256:5eda665f6789edb9b57b57a159b9c55482cbe5b046d7db458948370554b16439", + "sha256:5edb2524d4032be4564c65dc4f9d01e79fe8fad5f966e5b552f4e5164fef0885", + "sha256:79691794288bc51e2a3b8de2bc0272ca8355d0b8503077ea57c0716e840ebaef", + "sha256:7fcc8681e9981b9b511cdee7c580d5b005f3bb86b65bde2188e04a29f1d63317", + "sha256:8e447e05ec88b1b408a4c9cde85aa6f4b04f06aa874b9f0b8e8319faf51b1fee", + "sha256:90ea6b3e7787620bb295a4ae050d2811c807d65b1486749414f78cfd6fb61489", + "sha256:9e13239952694b8b831088431d15f771beace10edfcf9ef230cefea14f18508f", + "sha256:d40f081187f7b54d7a99d8a5c782eaa4edc335a057aa54c85059272ed826dc09", + "sha256:e1df1a58ed2468c7b7ce9a2f9752a32ad08eac2bcd56318625c3647c2cd2da6f", + "sha256:e98d0cec437097f09c7834a11c69d79fe6241729b23f656cfc227e93294fc242", + "sha256:f8d59627702d2ff27cb495ca1abdea8bd8d581de425c56e93bff6517134e0a9b", + "sha256:fc30cdf2e949a2225b012a7911d1d031df3d23e99b7eda7dfc982dc4a860dae9" + ], + "version": "==7.0" + }, + "whitenoise": { + "hashes": [ + "sha256:22f79cf8f1f509639330f93886acaece8ec5ac5e9600c3b981d33c34e8a42dfd", + "sha256:6dfea214b7c12efd689007abf9afa87a426586e9dbc051873ad2c8e535e2a1ac" + ], + "version": "==4.1.4" + } + }, + "develop": { + "alabaster": { + "hashes": [ + "sha256:446438bdcca0e05bd45ea2de1668c1d9b032e1a9154c2c259092d77031ddd359", + "sha256:a661d72d58e6ea8a57f7a86e37d86716863ee5e92788398526d58b26a4e4dc02" + ], + "version": "==0.7.12" + }, + "appdirs": { + "hashes": [ + "sha256:9e5896d1372858f8dd3344faf4e5014d21849c756c8d5701f78f8a103b372d92", + "sha256:d8b24664561d0d34ddfaec54636d502d7cea6e29c3eaf68f3df6180863e2166e" + ], + "version": "==1.4.3" + }, + "atomicwrites": { + "hashes": [ + "sha256:03472c30eb2c5d1ba9227e4c2ca66ab8287fbfbbda3888aa93dc2e28fc6811b4", + "sha256:75a9445bac02d8d058d5e1fe689654ba5a6556a1dfd8ce6ec55a0ed79866cfa6" + ], + "version": "==1.3.0" + }, + "attrs": { + "hashes": [ + "sha256:08a96c641c3a74e44eb59afb61a24f2cb9f4d7188748e76ba4bb5edfa3cb7d1c", + "sha256:f7b7ce16570fe9965acd6d30101a28f62fb4a7f9e926b3bbc9b61f8b04247e72" + ], + "version": "==19.3.0" + }, + "babel": { + "hashes": [ + "sha256:af92e6106cb7c55286b25b38ad7695f8b4efb36a90ba483d7f7a6628c46158ab", + "sha256:e86135ae101e31e2c8ec20a4e0c5220f4eed12487d5cf3f78be7e98d3a57fc28" + ], + "version": "==2.7.0" + }, + "black": { + "hashes": [ + "sha256:09a9dcb7c46ed496a9850b76e4e825d6049ecd38b611f1224857a79bd985a8cf", + "sha256:68950ffd4d9169716bcb8719a56c07a2f4485354fec061cdd5910aa07369731c" + ], + "index": "pypi", + "version": "==19.3b0" + }, + "bleach": { + "hashes": [ + "sha256:213336e49e102af26d9cde77dd2d0397afabc5a6bf2fed985dc35b5d1e285a16", + "sha256:3fdf7f77adcf649c9911387df51254b813185e32b2c6619f690b593a617e19fa" + ], + "version": "==3.1.0" + }, + "certifi": { + "hashes": [ + "sha256:e4f3620cfea4f83eedc95b24abd9cd56f3c4b146dd0177e83a21b4eb49e21e50", + "sha256:fd7c7c74727ddcf00e9acd26bba8da604ffec95bf1c2144e67aff7a8b50e6cef" + ], + "version": "==2019.9.11" + }, + "chardet": { + "hashes": [ + "sha256:84ab92ed1c4d4f16916e05906b6b75a6c0fb5db821cc65e70cbd64a3e2a5eaae", + "sha256:fc323ffcaeaed0e0a02bf4d117757b98aed530d9ed4531e3e15460124c106691" + ], + "version": "==3.0.4" + }, + "click": { + "hashes": [ + "sha256:2335065e6395b9e67ca716de5f7526736bfa6ceead690adf616d925bdc622b13", + "sha256:5b94b49521f6456670fdb30cd82a4eca9412788a93fa6dd6df72c94d5a8ff2d7" + ], + "version": "==7.0" + }, + "coverage": { + "hashes": [ + "sha256:17a417c691de3fc88de027832267313e5ed2b2ea3956745b562c4c389e44d05b", + "sha256:24307e67ebd9dc06fcbab9b7fef87412a97746c1baabb04ed8a93d5c2ccfe5ba", + "sha256:2a5d44a9d8426bd3699123864e63f008dc8dea9df22d5216a141a25d4670f22c", + "sha256:3726b8f5461e103a40e380f52b4b4ccdf2eda55d5d72f037cee43627992b4462", + "sha256:39dd15bbc4880a64399e180925bbc21c0c316a3065f6455d2512039f5cb59b94", + "sha256:3bb121f5dd156aab4fba2ebad6b0ad605bc5dc305931140dc614b101aa9d81ed", + "sha256:3bfdea9226eaed97736c973a7d6d0bbf9e1c1f1c7391c8e9c2bb2d0dbae49156", + "sha256:43be906a16239c1aa9f3742e3e6b0a5dd24781a13ce401f063262e9b4e93b69f", + "sha256:4a54cac1b39b2925041a41bcd1f191898fe401618627d7c3abf127c32a1c6dd1", + "sha256:4e58d65b90d6f26b3ccca7cf0fe573ef847347b8734af596a087a21eebb681f5", + "sha256:50229727d9baf0cd7f5ee6b194bf9dea708e9a20823d93f9e04d710b0a60e757", + "sha256:5141cdb010e9cd6939e37b8c2769d535cb535d80ef94f927c8a306f2e05a4736", + "sha256:748ba2b950425b9aef9d1bde2d6af7023585505016bd634e578f76ada4a30465", + "sha256:75e635bc6730c88b04421b25a0afc47b9b80efc1ed57630839196eb475722e50", + "sha256:78556f51dbfb33f18794eee29a4a8542fd2e301aa0d072653930793974dced03", + "sha256:7de17133509210ecc256535bab2f9a5547f3016c44f984fe12b4c10d81a4623f", + "sha256:83bf376555898fe2dc50d111a34b0152b504e454ed1e13cdcda6e5d50ba0ed5b", + "sha256:87730b5e4c3a42674fe8f0ecbb0d556c59c7e12b11a65c2178f2787252a80dfd", + "sha256:9bb7819c020c20c6200764879f0b10b323d6d4719aa7b0ae316c9e35730f9e2d", + "sha256:9c825788acb13d49ac20455433f3b862029aa497e97faba8c998555a042a6b91", + "sha256:b2bb4941c8838fc9ea2fca3c52e6dd865d39bbbc014bde249161bf8fcccf2152", + "sha256:c1b44c6c680f137910cb0f5481a2ae9899787ca7019f110a3708d9e99df941be", + "sha256:c52c2bc67bd3ff8db685f7c5f03e34a95bddd58a535630161f28d1c485d61e22", + "sha256:d6845e46338695c571759be1c770b013c477111e785b26151ec9feb6cd063543", + "sha256:e292b32dfc80d9f271af2d52df95455248322156e764763c4bfb2385b2e33533" + ], + "version": "==5.0a8" + }, + "docutils": { + "hashes": [ + "sha256:6c4f696463b79f1fb8ba0c594b63840ebd41f059e92b31957c46b74a4599b6d0", + "sha256:9e4d7ecfc600058e07ba661411a2b7de2fd0fafa17d1a7f7361cd47b1175c827", + "sha256:a2aeea129088da402665e92e0b25b04b073c04b2dce4ab65caaa38b7ce2e1a99" + ], + "version": "==0.15.2" + }, + "entrypoints": { + "hashes": [ + "sha256:589f874b313739ad35be6e0cd7efde2a4e9b6fea91edcc34e58ecbb8dbe56d19", + "sha256:c70dd71abe5a8c85e55e12c19bd91ccfeec11a6e99044204511f9ed547d48451" + ], + "version": "==0.3" + }, + "flake8": { + "hashes": [ + "sha256:19241c1cbc971b9962473e4438a2ca19749a7dd002dd1a946eaba171b4114548", + "sha256:8e9dfa3cecb2400b3738a42c54c3043e821682b9c840b0448c0503f781130696" + ], + "index": "pypi", + "version": "==3.7.8" + }, + "flask": { + "hashes": [ + "sha256:13f9f196f330c7c2c5d7a5cf91af894110ca0215ac051b5844701f2bfd934d52", + "sha256:45eb5a6fd193d6cf7e0cf5d8a5b31f83d5faae0293695626f539a823e93b13f6" + ], + "index": "pypi", + "version": "==1.1.1" + }, + "idna": { + "hashes": [ + "sha256:c357b3f628cf53ae2c4c05627ecc484553142ca23264e593d327bcde5e9c3407", + "sha256:ea8b7f6188e6fa117537c3df7da9fc686d485087abf6ac197f9c46432f7e4a3c" + ], + "version": "==2.8" + }, + "imagesize": { + "hashes": [ + "sha256:3f349de3eb99145973fefb7dbe38554414e5c30abd0c8e4b970a7c9d09f3a1d8", + "sha256:f3832918bc3c66617f92e35f5d70729187676313caa60c187eb0f28b8fe5e3b5" + ], + "version": "==1.1.0" + }, + "importlib-metadata": { + "hashes": [ + "sha256:aa18d7378b00b40847790e7c27e11673d7fed219354109d0e7b9e5b25dc3ad26", + "sha256:d5f18a79777f3aa179c145737780282e27b508fc8fd688cb17c7a813e8bd39af" + ], + "markers": "python_version < '3.8'", + "version": "==0.23" + }, + "itsdangerous": { + "hashes": [ + "sha256:321b033d07f2a4136d3ec762eac9f16a10ccd60f53c0c91af90217ace7ba1f19", + "sha256:b12271b2047cb23eeb98c8b5622e2e5c5e9abd9784a153e9d8ef9cb4dd09d749" + ], + "version": "==1.1.0" + }, + "jinja2": { + "hashes": [ + "sha256:74320bb91f31270f9551d46522e33af46a80c3d619f4a4bf42b3164d30b5911f", + "sha256:9fe95f19286cfefaa917656583d020be14e7859c6b0252588391e47db34527de" + ], + "version": "==2.10.3" + }, + "markupsafe": { + "hashes": [ + "sha256:00bc623926325b26bb9605ae9eae8a215691f33cae5df11ca5424f06f2d1f473", + "sha256:09027a7803a62ca78792ad89403b1b7a73a01c8cb65909cd876f7fcebd79b161", + "sha256:09c4b7f37d6c648cb13f9230d847adf22f8171b1ccc4d5682398e77f40309235", + "sha256:1027c282dad077d0bae18be6794e6b6b8c91d58ed8a8d89a89d59693b9131db5", + "sha256:24982cc2533820871eba85ba648cd53d8623687ff11cbb805be4ff7b4c971aff", + "sha256:29872e92839765e546828bb7754a68c418d927cd064fd4708fab9fe9c8bb116b", + "sha256:43a55c2930bbc139570ac2452adf3d70cdbb3cfe5912c71cdce1c2c6bbd9c5d1", + "sha256:46c99d2de99945ec5cb54f23c8cd5689f6d7177305ebff350a58ce5f8de1669e", + "sha256:500d4957e52ddc3351cabf489e79c91c17f6e0899158447047588650b5e69183", + "sha256:535f6fc4d397c1563d08b88e485c3496cf5784e927af890fb3c3aac7f933ec66", + "sha256:62fe6c95e3ec8a7fad637b7f3d372c15ec1caa01ab47926cfdf7a75b40e0eac1", + "sha256:6dd73240d2af64df90aa7c4e7481e23825ea70af4b4922f8ede5b9e35f78a3b1", + "sha256:717ba8fe3ae9cc0006d7c451f0bb265ee07739daf76355d06366154ee68d221e", + "sha256:79855e1c5b8da654cf486b830bd42c06e8780cea587384cf6545b7d9ac013a0b", + "sha256:7c1699dfe0cf8ff607dbdcc1e9b9af1755371f92a68f706051cc8c37d447c905", + "sha256:88e5fcfb52ee7b911e8bb6d6aa2fd21fbecc674eadd44118a9cc3863f938e735", + "sha256:8defac2f2ccd6805ebf65f5eeb132adcf2ab57aa11fdf4c0dd5169a004710e7d", + "sha256:98c7086708b163d425c67c7a91bad6e466bb99d797aa64f965e9d25c12111a5e", + "sha256:9add70b36c5666a2ed02b43b335fe19002ee5235efd4b8a89bfcf9005bebac0d", + "sha256:9bf40443012702a1d2070043cb6291650a0841ece432556f784f004937f0f32c", + "sha256:ade5e387d2ad0d7ebf59146cc00c8044acbd863725f887353a10df825fc8ae21", + "sha256:b00c1de48212e4cc9603895652c5c410df699856a2853135b3967591e4beebc2", + "sha256:b1282f8c00509d99fef04d8ba936b156d419be841854fe901d8ae224c59f0be5", + "sha256:b2051432115498d3562c084a49bba65d97cf251f5a331c64a12ee7e04dacc51b", + "sha256:ba59edeaa2fc6114428f1637ffff42da1e311e29382d81b339c1817d37ec93c6", + "sha256:c8716a48d94b06bb3b2524c2b77e055fb313aeb4ea620c8dd03a105574ba704f", + "sha256:cd5df75523866410809ca100dc9681e301e3c27567cf498077e8551b6d20e42f", + "sha256:e249096428b3ae81b08327a63a485ad0878de3fb939049038579ac0ef61e17e7" + ], + "version": "==1.1.1" + }, + "marshmallow": { + "hashes": [ + "sha256:077b4612f5d3b9333b736fdc6b963d2b46d409070f44ff3e6c4109645c673e83", + "sha256:9a2f3e8ea5f530a9664e882d7d04b58650f46190178b2264c72b7d20399d28f0" + ], + "version": "==3.2.1" + }, + "mccabe": { + "hashes": [ + "sha256:ab8a6258860da4b6677da4bd2fe5dc2c659cff31b3ee4f7f5d64e79735b80d42", + "sha256:dd8d182285a0fe56bace7f45b5e7d1a6ebcbf524e8f3bd87eb0f125271b8831f" + ], + "version": "==0.6.1" + }, + "more-itertools": { + "hashes": [ + "sha256:409cd48d4db7052af495b09dec721011634af3753ae1ef92d2b32f73a745f832", + "sha256:92b8c4b06dac4f0611c0729b2f2ede52b2e1bac1ab48f089c7ddc12e26bb60c4" + ], + "version": "==7.2.0" + }, + "packaging": { + "hashes": [ + "sha256:28b924174df7a2fa32c1953825ff29c61e2f5e082343165438812f00d3a7fc47", + "sha256:d9551545c6d761f3def1677baf08ab2a3ca17c56879e70fecba2fc4dde4ed108" + ], + "version": "==19.2" + }, + "pkginfo": { + "hashes": [ + "sha256:7424f2c8511c186cd5424bbf31045b77435b37a8d604990b79d4e70d741148bb", + "sha256:a6d9e40ca61ad3ebd0b72fbadd4fba16e4c0e4df0428c041e01e06eb6ee71f32" + ], + "version": "==1.5.0.1" + }, + "pluggy": { + "hashes": [ + "sha256:0db4b7601aae1d35b4a033282da476845aa19185c1e6964b25cf324b5e4ec3e6", + "sha256:fa5fa1622fa6dd5c030e9cad086fa19ef6a0cf6d7a2d12318e10cb49d6d68f34" + ], + "version": "==0.13.0" + }, + "py": { + "hashes": [ + "sha256:64f65755aee5b381cea27766a3a147c3f15b9b6b9ac88676de66ba2ae36793fa", + "sha256:dc639b046a6e2cff5bbe40194ad65936d6ba360b52b3c3fe1d08a82dd50b5e53" + ], + "version": "==1.8.0" + }, + "pycodestyle": { + "hashes": [ + "sha256:95a2219d12372f05704562a14ec30bc76b05a5b297b21a5dfe3f6fac3491ae56", + "sha256:e40a936c9a450ad81df37f549d676d127b1b66000a6c500caa2b085bc0ca976c" + ], + "version": "==2.5.0" + }, + "pyflakes": { + "hashes": [ + "sha256:17dbeb2e3f4d772725c777fabc446d5634d1038f234e77343108ce445ea69ce0", + "sha256:d976835886f8c5b31d47970ed689944a0262b5f3afa00a5a7b4dc81e5449f8a2" + ], + "version": "==2.1.1" + }, + "pygments": { + "hashes": [ + "sha256:71e430bc85c88a430f000ac1d9b331d2407f681d6f6aec95e8bcfbc3df5b0127", + "sha256:881c4c157e45f30af185c1ffe8d549d48ac9127433f2c380c24b84572ad66297" + ], + "version": "==2.4.2" + }, + "pyparsing": { + "hashes": [ + "sha256:6f98a7b9397e206d78cc01df10131398f1c8b8510a2f4d97d9abd82e1aacdd80", + "sha256:d9338df12903bbf5d65a0e4e87c2161968b10d2e489652bb47001d82a9b028b4" + ], + "version": "==2.4.2" + }, + "pytest": { + "hashes": [ + "sha256:7e4800063ccfc306a53c461442526c5571e1462f61583506ce97e4da6a1d88c8", + "sha256:ca563435f4941d0cb34767301c27bc65c510cb82e90b9ecf9cb52dc2c63caaa0" + ], + "index": "pypi", + "version": "==5.2.1" + }, + "pytest-cov": { + "hashes": [ + "sha256:cc6742d8bac45070217169f5f72ceee1e0e55b0221f54bcf24845972d3a47f2b", + "sha256:cdbdef4f870408ebdbfeb44e63e07eb18bb4619fae852f6e760645fa36172626" + ], + "index": "pypi", + "version": "==2.8.1" + }, + "pytz": { + "hashes": [ + "sha256:1c557d7d0e871de1f5ccd5833f60fb2550652da6be2693c1e02300743d21500d", + "sha256:b02c06db6cf09c12dd25137e563b31700d3b80fcc4ad23abb7a315f2789819be" + ], + "version": "==2019.3" + }, + "readme-renderer": { + "hashes": [ + "sha256:bb16f55b259f27f75f640acf5e00cf897845a8b3e4731b5c1a436e4b8529202f", + "sha256:c8532b79afc0375a85f10433eca157d6b50f7d6990f337fa498c96cd4bfc203d" + ], + "version": "==24.0" + }, + "requests": { + "hashes": [ + "sha256:11e007a8a2aa0323f5a921e9e6a2d7e4e67d9877e85773fba9ba6419025cbeb4", + "sha256:9cf5292fcd0f598c671cfc1e0d7d1a7f13bb8085e9a590f48c010551dc6c4b31" + ], + "version": "==2.22.0" + }, + "requests-toolbelt": { + "hashes": [ + "sha256:380606e1d10dc85c3bd47bf5a6095f815ec007be7a8b69c878507068df059e6f", + "sha256:968089d4584ad4ad7c171454f0a5c6dac23971e9472521ea3b6d49d610aa6fc0" + ], + "version": "==0.9.1" + }, + "six": { + "hashes": [ + "sha256:3350809f0555b11f552448330d0b52d5f24c91a322ea4a15ef22629740f3761c", + "sha256:d16a0141ec1a18405cd4ce8b4613101da75da0e9a7aec5bdd4fa804d0e0eba73" + ], + "version": "==1.12.0" + }, + "snowballstemmer": { + "hashes": [ + "sha256:209f257d7533fdb3cb73bdbd24f436239ca3b2fa67d56f6ff88e86be08cc5ef0", + "sha256:df3bac3df4c2c01363f3dd2cfa78cce2840a79b9f1c2d2de9ce8d31683992f52" + ], + "version": "==2.0.0" + }, + "sphinx": { + "hashes": [ + "sha256:0d586b0f8c2fc3cc6559c5e8fd6124628110514fda0e5d7c82e682d749d2e845", + "sha256:839a3ed6f6b092bb60f492024489cc9e6991360fb9f52ed6361acd510d261069" + ], + "index": "pypi", + "version": "==2.2.0" + }, + "sphinxcontrib-applehelp": { + "hashes": [ + "sha256:edaa0ab2b2bc74403149cb0209d6775c96de797dfd5b5e2a71981309efab3897", + "sha256:fb8dee85af95e5c30c91f10e7eb3c8967308518e0f7488a2828ef7bc191d0d5d" + ], + "version": "==1.0.1" + }, + "sphinxcontrib-devhelp": { + "hashes": [ + "sha256:6c64b077937330a9128a4da74586e8c2130262f014689b4b89e2d08ee7294a34", + "sha256:9512ecb00a2b0821a146736b39f7aeb90759834b07e81e8cc23a9c70bacb9981" + ], + "version": "==1.0.1" + }, + "sphinxcontrib-htmlhelp": { + "hashes": [ + "sha256:4670f99f8951bd78cd4ad2ab962f798f5618b17675c35c5ac3b2132a14ea8422", + "sha256:d4fd39a65a625c9df86d7fa8a2d9f3cd8299a3a4b15db63b50aac9e161d8eff7" + ], + "version": "==1.0.2" + }, + "sphinxcontrib-jsmath": { + "hashes": [ + "sha256:2ec2eaebfb78f3f2078e73666b1415417a116cc848b72e5172e596c871103178", + "sha256:a9925e4a4587247ed2191a22df5f6970656cb8ca2bd6284309578f2153e0c4b8" + ], + "version": "==1.0.1" + }, + "sphinxcontrib-qthelp": { + "hashes": [ + "sha256:513049b93031beb1f57d4daea74068a4feb77aa5630f856fcff2e50de14e9a20", + "sha256:79465ce11ae5694ff165becda529a600c754f4bc459778778c7017374d4d406f" + ], + "version": "==1.0.2" + }, + "sphinxcontrib-serializinghtml": { + "hashes": [ + "sha256:c0efb33f8052c04fd7a26c0a07f1678e8512e0faec19f4aa8f2473a8b81d5227", + "sha256:db6615af393650bf1151a6cd39120c29abaf93cc60db8c48eb2dddbfdc3a9768" + ], + "version": "==1.1.3" + }, + "toml": { + "hashes": [ + "sha256:229f81c57791a41d65e399fc06bf0848bab550a9dfd5ed66df18ce5f05e73d5c", + "sha256:235682dd292d5899d361a811df37e04a8828a5b1da3115886b73cf81ebc9100e" + ], + "version": "==0.10.0" + }, + "tqdm": { + "hashes": [ + "sha256:abc25d0ce2397d070ef07d8c7e706aede7920da163c64997585d42d3537ece3d", + "sha256:dd3fcca8488bb1d416aa7469d2f277902f26260c45aa86b667b074cd44b3b115" + ], + "version": "==4.36.1" + }, + "twine": { + "hashes": [ + "sha256:5319dd3e02ac73fcddcd94f035b9631589ab5d23e1f4699d57365199d85261e1", + "sha256:9fe7091715c7576df166df8ef6654e61bada39571783f2fd415bdcba867c6993" + ], + "index": "pypi", + "version": "==2.0.0" + }, + "urllib3": { + "hashes": [ + "sha256:3de946ffbed6e6746608990594d08faac602528ac7015ac28d33cee6a45b7398", + "sha256:9a107b99a5393caf59c7aa3c1249c16e6879447533d0887f4336dde834c7be86" + ], + "version": "==1.25.6" + }, + "wcwidth": { + "hashes": [ + "sha256:3df37372226d6e63e1b1e1eda15c594bca98a22d33a23832a90998faa96bc65e", + "sha256:f4ebe71925af7b40a864553f761ed559b43544f8f71746c2d756c7fe788ade7c" + ], + "version": "==0.1.7" + }, + "webencodings": { + "hashes": [ + "sha256:a0af1213f3c2226497a97e2b3aa01a7e4bee4f403f95be16fc9acd2947514a78", + "sha256:b36a1c245f2d304965eb4e0a82848379241dc04b865afcc4aab16748587e1923" + ], + "version": "==0.5.1" + }, + "werkzeug": { + "hashes": [ + "sha256:7280924747b5733b246fe23972186c6b348f9ae29724135a6dfc1e53cea433e7", + "sha256:e5f4a1f98b52b18a93da705a7458e55afb26f32bff83ff5d19189f92462d65c4" + ], + "version": "==0.16.0" + }, + "zipp": { + "hashes": [ + "sha256:3718b1cbcd963c7d4c5511a8240812904164b7f381b647143a89d3b98f9bcd8e", + "sha256:f06903e9f1f43b12d371004b4ac7b06ab39a44adc747266928ae6debfa7b3335" + ], + "version": "==0.6.0" + } + } +} diff --git a/tests/fixtures/real-world-locks/pipfile/responder-2.0.0/setup.py b/tests/fixtures/real-world-locks/pipfile/responder-2.0.0/setup.py new file mode 100644 index 00000000..1d5dcd33 --- /dev/null +++ b/tests/fixtures/real-world-locks/pipfile/responder-2.0.0/setup.py @@ -0,0 +1,140 @@ +#!/usr/bin/env python +# -*- coding: utf-8 -*- +import codecs +import os +import sys +from shutil import rmtree + +from setuptools import find_packages, setup, Command + +here = os.path.abspath(os.path.dirname(__file__)) + +with codecs.open(os.path.join(here, "README.md"), encoding="utf-8") as f: + long_description = "\n" + f.read() + +about = {} + +with open(os.path.join(here, "responder", "__version__.py")) as f: + exec(f.read(), about) + +if sys.argv[-1] == "publish": + os.system("python setup.py sdist bdist_wheel upload") + sys.exit() + +required = [ + "starlette==0.12.*", + "uvicorn>=0.7, <0.9", + "aiofiles", + "pyyaml", + "requests", + "graphene<3.0", + "graphql-server-core>=1.1", + "jinja2", + "uvloop; sys_platform != 'win32' and sys_platform != 'cygwin' and sys_platform != 'cli'", + "rfc3986", + "python-multipart", + "chardet", + "apispec>=1.0.0b1", + "marshmallow", + "whitenoise", + "docopt", + "requests-toolbelt", + "apistar", +] + + +# https://pypi.python.org/pypi/stdeb/0.8.5#quickstart-2-just-tell-me-the-fastest-way-to-make-a-deb +class DebCommand(Command): + """Support for setup.py deb""" + + description = "Build and publish the .deb package." + user_options = [] + + @staticmethod + def status(s): + """Prints things in bold.""" + print("\033[1m{0}\033[0m".format(s)) + + def initialize_options(self): + pass + + def finalize_options(self): + pass + + def run(self): + try: + self.status("Removing previous builds…") + rmtree(os.path.join(here, "deb_dist")) + except FileNotFoundError: + pass + self.status(u"Creating debian manifest…") + os.system( + "python setup.py --command-packages=stdeb.command sdist_dsc -z artful --package3=pipenv --depends3=python3-virtualenv-clone" + ) + self.status(u"Building .deb…") + os.chdir("deb_dist/pipenv-{0}".format(about["__version__"])) + os.system("dpkg-buildpackage -rfakeroot -uc -us") + + +class UploadCommand(Command): + """Support setup.py publish.""" + + description = "Build and publish the package." + user_options = [] + + @staticmethod + def status(s): + """Prints things in bold.""" + print("\033[1m{0}\033[0m".format(s)) + + def initialize_options(self): + pass + + def finalize_options(self): + pass + + def run(self): + try: + self.status("Removing previous builds…") + rmtree(os.path.join(here, "dist")) + except FileNotFoundError: + pass + self.status("Building Source distribution…") + os.system("{0} setup.py sdist bdist_wheel".format(sys.executable)) + self.status("Uploading the package to PyPI via Twine…") + os.system("twine upload dist/*") + self.status("Pushing git tags…") + os.system("git tag v{0}".format(about["__version__"])) + os.system("git push --tags") + sys.exit() + + +setup( + name="responder", + version=about["__version__"], + description="A sorta familiar HTTP framework.", + long_description=long_description, + long_description_content_type="text/markdown", + author="Kenneth Reitz", + author_email="me@kennethreitz.org", + url="https://github.com/kennethreitz/responder", + packages=find_packages(exclude=["tests"]), + entry_points={"console_scripts": ["responder=responder.cli:cli"]}, + package_data={}, + python_requires=">=3.6", + setup_requires=[], + install_requires=required, + extras_require={}, + include_package_data=True, + license="Apache 2.0", + classifiers=[ + "License :: OSI Approved :: Apache Software License", + "Programming Language :: Python", + "Programming Language :: Python :: 3", + "Programming Language :: Python :: 3.6", + "Programming Language :: Python :: 3.7", + "Programming Language :: Python :: Implementation :: CPython", + "Programming Language :: Python :: Implementation :: PyPy", + ], + cmdclass={"upload": UploadCommand, "deb": DebCommand}, +) diff --git a/working-docs/design/lock-files.md b/working-docs/design/lock-files.md index dd83f856..9fa801eb 100644 --- a/working-docs/design/lock-files.md +++ b/working-docs/design/lock-files.md @@ -72,7 +72,7 @@ simply by asking users to run `[tool] export --format pylock`. | | `conda-lock.yml` | Maps Conda data science packages alongside PyPI wheels. | | **3: Corporate Standards** | `poetry.lock` | **Done (2026-08-31)** -- see the "See also" note above. Massive legacy and enterprise footprint in Data Engineering (Airflow, dbt). | | **4: Legacy & Niche** | `pdm.lock` | **Done (2026-09-04)** -- see `working-docs/implementation/lock-file-cascade.md`. PDM leads PEP standard compliance, but `pylock.toml` export handles most PDM use cases. | -| | `Pipfile.lock` | Largely legacy tooling. Low priority. | +| | `Pipfile.lock` | **Done (2026-09-05)** -- see `working-docs/implementation/lock-file-cascade.md`. Largely legacy tooling. | --- diff --git a/working-docs/design/roadmap.md b/working-docs/design/roadmap.md index cedc7e66..933bde48 100644 --- a/working-docs/design/roadmap.md +++ b/working-docs/design/roadmap.md @@ -171,23 +171,30 @@ table in [non-hatchling-file-discovery.md](non-hatchling-file-discovery.md)); - [x] **`pdm.lock`** -- done (2026-09-04): reads a sibling `pdm.lock`'s resolved `default`-group dependencies, ranked below `poetry.lock`. See [lock-file-cascade.md](../implementation/lock-file-cascade.md). +- [x] **`Pipfile.lock`** -- done (2026-09-05): reads a sibling + `Pipfile.lock`'s resolved `default`-section dependencies (JSON, not + TOML -- the one format that isn't), ranked below `pdm.lock`, lowest + cascade priority. Reached via `read_project()`'s `setup.py`-only + dispatch path, not just the `pyproject.toml` one, since `Pipfile.lock` + predates PEP 621 almost entirely in real projects. See + [lock-file-cascade.md](../implementation/lock-file-cascade.md). - [ ] **Remaining lock formats as a resolved-dependency source** - (`pixi.lock`, `conda-lock.yml`, `Pipfile.lock`, pinned - `requirements.txt`) -- `loom project` still records only the declared - version specifier from `pyproject.toml [project] dependencies` + (`pixi.lock`, `conda-lock.yml`, pinned `requirements.txt`) -- `loom + project` still records only the declared version specifier from + `pyproject.toml [project] dependencies` (`normalize_dependency_specifier`, `src/pitloom/extract/_pyproject.py:220`, - e.g. `requests>=2.0`) for a project with none of the four already-shipped + e.g. `requests>=2.0`) for a project with none of the five already-shipped lock formats present, never a concrete resolved version. Parsing one when present would let a Source SBOM carry the actual pinned version a build will use, not just the declared range -- closer to what CISA's Source SBOM guidance expects. `pylock.toml`/`uv.lock`/`poetry.lock`/ - `pdm.lock` establish the pattern (additive transitive-only edges, - `completeness` tagging, source-stage-only scoping, one shared priority - cascade); each further format added needs its own slot in that same - priority order and a provenance `method` tag. See - [lock-files.md](./lock-files.md) for the broader multi-format - extraction-priority roadmap (`pixi.lock`, `conda-lock.yml`, - `Pipfile.lock`) this item now defers to. + `pdm.lock`/`Pipfile.lock` establish the pattern (additive + transitive-only edges, `completeness` tagging, source-stage-only + scoping, one shared priority cascade); each further format added + needs its own slot in that same priority order and a provenance + `method` tag. See [lock-files.md](./lock-files.md) for the broader + multi-format extraction-priority roadmap (`pixi.lock`, + `conda-lock.yml`) this item now defers to. ### PEP 770 / embed-wheel diff --git a/working-docs/implementation/lock-file-cascade.md b/working-docs/implementation/lock-file-cascade.md index 91381a61..c38002ad 100644 --- a/working-docs/implementation/lock-file-cascade.md +++ b/working-docs/implementation/lock-file-cascade.md @@ -1,6 +1,6 @@ --- Created: 2026-09-04 -Last-Modified: 2026-09-04 +Last-Modified: 2026-09-05 SPDX-FileCopyrightText: 2026-present Arthit Suriyawongkul SPDX-FileType: DOCUMENTATION SPDX-License-Identifier: CC0-1.0 @@ -41,17 +41,29 @@ new format's would-be bespoke function with one shared, ordered cascade. _LockExtractor = Callable[[Path, str | None], list[str]] _LOCK_SOURCES: list[tuple[str, _LockExtractor | None, str | None]] = [ - ("pylock.toml", extract_pylock_dependencies, "resolved_lockfile"), + ( + "pylock.toml", + _ignore_expected_name(extract_pylock_dependencies), + "resolved_lockfile", + ), ("uv.lock", extract_uv_lock_dependencies, "resolved_lockfile"), ("poetry.lock", None, None), - ("pdm.lock", extract_pdm_lock_dependencies, "resolved_lockfile"), - # Pipfile.lock, requirements.txt land here as their own extractors - # ship -- see roadmap.md. + ( + "pdm.lock", + _ignore_expected_name(extract_pdm_lock_dependencies), + "resolved_lockfile", + ), + ( + "Pipfile.lock", + _ignore_expected_name(extract_pipfile_lock_dependencies), + "resolved_lockfile", + ), + # pinned requirements.txt lands here as its own extractor ships -- + # see roadmap.md. ] -def apply_locked_dependencies(metadata: ProjectMetadata, project_dir: Path) -> None: - ... +def apply_locked_dependencies(metadata: ProjectMetadata, project_dir: Path) -> None: ... ``` Each entry pairs a source name, an extractor matching the uniform @@ -60,15 +72,14 @@ exact-pin PEP 508 strings, empty when absent/unusable), and a provenance `Method` tag. Only `uv.lock`'s own extractor uses *expected_name* (to disambiguate a shared workspace lock's multiple local package entries without re-reading `pyproject.toml` a second -time); `pylock.toml`'s and `pdm.lock`'s extractors keep their simpler, -single-`project_dir` signature and are wrapped with -`_ignore_expected_name()` when registered in `_LOCK_SOURCES` below, +time); `pylock.toml`'s, `pdm.lock`'s, and `Pipfile.lock`'s extractors +keep their simpler, single-`project_dir` signature and are wrapped with +`_ignore_expected_name()` when registered in `_LOCK_SOURCES` above, rather than widening every format's own signature for a need only one -of them has, -and a provenance `Method` tag. `apply_locked_dependencies()` tries each -extractor-bearing entry in priority order (highest first) and applies -the first non-empty result, in place, onto `metadata.locked_dependencies` -and `metadata.provenance["locked_dependencies"]`. +of them has. `apply_locked_dependencies()` tries each extractor-bearing +entry in priority order (highest first) and applies the first non-empty +result, in place, onto `metadata.locked_dependencies` and +`metadata.provenance["locked_dependencies"]`. **`poetry.lock` has no extractor here (`None`, `None`), but it *is* in the table.** It's still applied earlier, gated inside @@ -94,7 +105,7 @@ beats tool-specific; a real resolver lock beats a merely-pinned file): 2. `uv.lock` 3. `poetry.lock` (via `_try_read_poetry()`, not this cascade -- see above) 4. `pdm.lock` -5. `Pipfile.lock` +5. `Pipfile.lock` -- JSON, not TOML; see its own notes below. 6. pinned `requirements.txt` -- weakest signal; only usable when every line is an exact `==` pin (see that format's own implementation notes once it lands). @@ -123,12 +134,13 @@ from there on could legitimately win. `tests/extract/test_pdm_lock.py::test_read is the regression test for this; `test_read_project_uv_lock_still_overrides_pdm_lock` confirms the higher-ranked entries' behaviour didn't change. -**Any future format ranked below `poetry.lock` (`Pipfile.lock`, pinned -`requirements.txt`) needs no extra code for this** -- the same generic -rank check covers them once they're added to `_LOCK_SOURCES` at their -documented position. Only a format that would need to be inserted -*around* an existing entry (unlikely, given the order above is already -settled) would need to re-verify this logic. +**Any format ranked below `poetry.lock` needs no extra code for this** +-- the same generic rank check covers it once it's added to +`_LOCK_SOURCES` at its documented position; `pdm.lock` and +`Pipfile.lock` both confirmed this when they landed, and pinned +`requirements.txt` (rank 6, lowest) will too. Only a format that would +need to be inserted *around* an existing entry (unlikely, given the +order above is already settled) would need to re-verify this logic. ## Per-format extraction notes @@ -173,6 +185,21 @@ to group entries by name, then only treats a name as ambiguous (skip, `WARNING:`) when its entries actually *disagree* on `version`; entries that agree are collapsed to one `name==version`, not two. +`_pipfile_lock.py` is the one format in the cascade that's **JSON, not +TOML** -- `pitloom.extract._lock_common.load_lock_json()` is its +counterpart to `load_lock_toml()`, same absent/malformed-file contract. +Its `version` field is also shaped differently from every sibling +format: a full PEP 440 specifier string (typically `"==2.31.0"`, since +`pipenv lock` always resolves to an exact pin) rather than a bare +version number, so the extractor parses it with +`packaging.specifiers.SpecifierSet` and requires exactly one `==` +specifier -- anything looser (a range, an excluded-version specifier, or +an unparseable string) is skipped with a `WARNING:`, the same +"don't guess" policy as `uv.lock`'s marker-ambiguity skip. It has no +`groups`-style per-package tag the way `poetry.lock`/`pdm.lock` do; +instead the whole top level splits into `"default"` (included) and +`"develop"` (excluded) sections. + ## Sharing code across formats (`_lock_common.py`) Two steps turned out to be identical across every extractor, not just @@ -182,20 +209,42 @@ similar in spirit: result, silently; malformed -> empty result, with a `WARNING:`" was copy-pasted verbatim into `_poetry_lock.py`, `_pylock.py`, and `_uv_lock.py` before being factored into - `pitloom.extract._lock_common.load_lock_toml()`, which all four - extractors (including `_pdm_lock.py`) now call instead. + `pitloom.extract._lock_common.load_lock_toml()`, which every + TOML-based extractor (`_poetry_lock.py`, `_pylock.py`, `_uv_lock.py`, + `_pdm_lock.py`) now calls instead. `_pipfile_lock.py`'s JSON format + needed the same contract but couldn't reuse a TOML-specific parser, so + it's `load_lock_json()` beside it -- same absent/malformed-file + behaviour (including rejecting a non-object JSON top level with a + `WARNING:`, the one shape TOML's grammar rules out for + `load_lock_toml()` but JSON doesn't), different underlying + `json`/`tomllib` call. - **Grouping a flat package list by name.** First written for `_uv_lock.py`'s ambiguity check, then reused as-is by `_pdm_lock.py`'s own (milder) version of the same check -- see above. Lives as `pitloom.extract._lock_common.index_packages_by_name()`. - -What's deliberately **not** shared: the per-entry validation shape -(what counts as "malformed", which keys mark a non-registry source, -what the `groups`/`dependencies` filtering looks like). Each format's -own field names and conventions differ enough (`poetry.lock`'s -`source.type` vs `pylock.toml`'s top-level `vcs`/`directory`/`archive` -keys vs `uv.lock`'s nested `source.{key}` vs `pdm.lock`'s flat -`git`/`path` keys) that forcing one shared function across all of them +- **Validating a `version` field is a non-empty string.** `not + isinstance(version, str) or not version` existed independently in all + five extractors before being factored into + `pitloom.extract._lock_common.is_usable_version()`. `_pipfile_lock.py` + calls it too, as the first of two checks -- its own `version` + validation is strictly larger (a full PEP 440 exact-`==`-specifier + parse on top), not a replacement for the shared non-empty-string check. +- **The non-registry-source `WARNING:` message.** `"Skipping entry %r: %s-sourced dependencies cannot be represented as a + PEP 508 specifier"` was copy-pasted, wording-identical, into all five + extractors before being factored into + `pitloom.extract._lock_common.warn_non_registry_source(lock_file, + name, source_key)`. Each extractor still does its own lookup of + *which* key triggered it (see below) and only calls this once it has + the answer. + +What's deliberately **not** shared: the per-entry lookup for which key +marks a non-registry source, and what the `groups`/`dependencies` +filtering looks like. Each format's own field names and conventions +differ enough (`poetry.lock`'s `source.type` vs `pylock.toml`'s +top-level `vcs`/`directory`/`archive` keys vs `uv.lock`'s nested +`source.{key}` vs `pdm.lock`'s flat `git`/`path` keys) that forcing one +shared function across all of them would hurt clarity more than it would save -- consistent *wording* across their `WARNING:` messages matters more here than a single shared implementation, per this repo's message-style convention. @@ -214,7 +263,9 @@ project checked while sourcing test fixtures for this cascade (`requests-html`, `responder` pre-`v3.0.0`) is `setup.py`-only, no `pyproject.toml` -- so a cascade wired only inside `read_pyproject()` would never run for the realistic case those two formats actually show -up in. +up in. Confirmed once `Pipfile.lock` actually landed: +`tests/extract/test_pipfile_lock.py::test_read_project_populates_locked_dependencies_from_setup_py_only` +exercises exactly this path against a `setup.py`-only project directory. `apply_locked_dependencies()` is called once, right before each of `read_project()`'s three directory-based `return` statements (the @@ -248,8 +299,9 @@ uniformly regardless of which metadata source won. `compute_doc_uuid()` (`src/pitloom/core/models.py`) folds `locked_dependencies` (the resolved dependency *content*) into its seed, -but originally not *which source produced it*. With six lock/pin -formats now cascading instead of two, two different formats resolving +but originally not *which source produced it*. With five lock/pin +formats now cascading instead of two (a sixth, pinned +`requirements.txt`, still to come), two different formats resolving to an identical dependency set for a small project became a real, checkable collision risk: two runs -- one with only `poetry.lock` present, one with only `pylock.toml` present -- that happen to resolve @@ -268,15 +320,20 @@ unaffected -- purely additive. in its own `src/pitloom/extract/_.py`, following `_pylock.py`'s shape: exact-pin PEP 508 strings, empty list when absent/unusable, `WARNING:` (never a silent drop) for anything - malformed or non-registry-sourced. Use - `_lock_common.load_lock_toml()` to load the file, and (if the format + malformed or non-registry-sourced. Use `_lock_common.load_lock_toml()` + to load the file (or `_lock_common.load_lock_json()` for a JSON-format + lock file -- `_pipfile_lock.py` is the precedent), and (if the format can resolve the same name more than once, the way `uv.lock`/`pdm.lock` can) `_lock_common.index_packages_by_name()` to group entries before - deciding whether that's ambiguous. + deciding whether that's ambiguous. Only add a second parameter to the + extractor itself if it genuinely needs `expected_name` for + disambiguation the way `uv.lock` does (see the cascade code block + above) -- otherwise keep the simpler single-`project_dir` signature + and let `_ignore_expected_name()` wrap it when registered. 2. Add one entry to `_LOCK_SOURCES` in `_locked_dependencies.py`, at the priority position from the table above -- **including if it ranks - below `poetry.lock`** (`Pipfile.lock` and pinned `requirements.txt` - both do). No extra code is needed for that case: the rank check in + below `poetry.lock`** (pinned `requirements.txt`, rank 6, does). + No extra code is needed for that case: the rank check in `apply_locked_dependencies()` already treats every entry in `_LOCK_SOURCES` (poetry.lock's placeholder included) uniformly. 3. No changes needed anywhere else -- `read_project()`'s wiring, From 54e92ecc9b5280dcb594a46e2ca5e58ba38f0c66 Mon Sep 17 00:00:00 2001 From: Arthit Suriyawongkul Date: Sat, 5 Sep 2026 01:59:18 +0700 Subject: [PATCH 07/35] Add pinned requiremenst.txt support Signed-off-by: Arthit Suriyawongkul --- CHANGELOG.md | 4 +- docs/cli.md | 9 +- docs/dependency-sources.md | 18 +- docs/resources.md | 7 +- src/pitloom/extract/_lock_common.py | 93 +- src/pitloom/extract/_locked_dependencies.py | 6 + src/pitloom/extract/_pdm_lock.py | 28 +- src/pitloom/extract/_pipfile_lock.py | 12 +- src/pitloom/extract/_requirements_txt.py | 220 +++++ tests/extract/test_lock_common.py | 27 + tests/extract/test_pdm_lock.py | 39 +- tests/extract/test_requirements_txt.py | 432 +++++++++ tests/fixtures/real-world-locks/README.md | 26 +- .../home-assistant-core-2026.9.0/LICENSE.md | 201 ++++ .../homeassistant/backports/LICENSE.Python | 279 ++++++ .../pyproject.toml | 883 ++++++++++++++++++ .../requirements.txt | 63 ++ working-docs/design/lock-files.md | 2 +- working-docs/design/roadmap.md | 30 +- .../implementation/lock-file-cascade.md | 143 ++- 20 files changed, 2417 insertions(+), 105 deletions(-) create mode 100644 src/pitloom/extract/_requirements_txt.py create mode 100644 tests/extract/test_requirements_txt.py create mode 100644 tests/fixtures/real-world-locks/requirements/home-assistant-core-2026.9.0/LICENSE.md create mode 100644 tests/fixtures/real-world-locks/requirements/home-assistant-core-2026.9.0/homeassistant/backports/LICENSE.Python create mode 100644 tests/fixtures/real-world-locks/requirements/home-assistant-core-2026.9.0/pyproject.toml create mode 100644 tests/fixtures/real-world-locks/requirements/home-assistant-core-2026.9.0/requirements.txt diff --git a/CHANGELOG.md b/CHANGELOG.md index 1c968cc5..eaeeea75 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -42,8 +42,8 @@ and this project adheres to file gets a `software_File` element at the real wheel's `.dist-info/licenses/` path and a `hasDeclaredLicense` relationship ([#207]) - Add resolved-dependency parsing for `loom project`/`loom generate` - from `pylock.toml` (PEP 751), `uv.lock`, `pdm.lock`, and - `Pipfile.lock`, more planned -- see [Dependency sources and + from `pylock.toml` (PEP 751), `uv.lock`, `pdm.lock`, `Pipfile.lock`, + and a fully pinned `requirements.txt` -- see [Dependency sources and precedence](docs/dependency-sources.md) ### Fixed diff --git a/docs/cli.md b/docs/cli.md index 3c9641de..98a29d1f 100644 --- a/docs/cli.md +++ b/docs/cli.md @@ -66,10 +66,11 @@ loom project /path/to/project -o sbom.spdx3.json > Project-level metadata (name, version, dependencies, license, > authors) is read independently and unaffected either way. -If a lock file (`pylock.toml`, `uv.lock`, `poetry.lock`, `pdm.lock`, or -`Pipfile.lock`) is present next to `pyproject.toml` (or `setup.py`, for -`Pipfile.lock`), its resolved transitive -dependencies are added to the Source SBOM's dependency list too -- see +If a lock file (`pylock.toml`, `uv.lock`, `poetry.lock`, `pdm.lock`, +`Pipfile.lock`, or a fully pinned `requirements.txt`) is present next +to `pyproject.toml` (or `setup.py`, for `Pipfile.lock`/`requirements.txt`), +its resolved transitive dependencies are added to the Source SBOM's +dependency list too -- see [Dependency sources and precedence](dependency-sources.md) for which one wins when more than one is present, and what counts as "resolved" for each. diff --git a/docs/dependency-sources.md b/docs/dependency-sources.md index f954fb64..0c1d5a9e 100644 --- a/docs/dependency-sources.md +++ b/docs/dependency-sources.md @@ -42,9 +42,21 @@ exactly-pinned entries. | 3 | Poetry | `poetry.lock` | Packages in the `main` dependency group only (not `[tool.poetry.group.*]` dev/extra groups). | | 4 | PDM | `pdm.lock` | Packages in the `default` dependency group only. | | 5 | Pipenv | `Pipfile.lock` | Packages in the `default` section only (not `develop`). A package whose resolved `version` isn't a single exact `==` pin is skipped, not guessed at. | - -Support for a fully pinned `requirements.txt` is planned, ranked below -the formats above. +| 6 (lowest) | -- | pinned `requirements.txt` | Not a real lock file -- only used when *every* line in the file is already an exact `==` pin. If even one line is unpinned, ranged, a pip option (`-e`, `-r`, `--hash`, ...), a URL-based requirement (even one that looks like it points at a tagged release), or one package name is pinned to two conflicting versions, the **whole file** is skipped, not just that line -- see below. | + +`requirements.txt`'s entry is tagged `Method: pinned_requirements` in +its provenance annotation (see "How to tell which source was used" +below), not `Method: resolved_lockfile` like every format above it -- +it's the one source here that isn't a real lock file, just a list of +lines that happen to already be fully pinned. + +**A URL-based `requirements.txt` line is never treated as a version +pin, even when the URL looks like it points at a tagged release** (e.g. +`name @ https://github.com/org/repo/archive/refs/tags/v2.31.0.zip`). A +git tag or release filename is an arbitrary string with no guaranteed +relationship to the package's real, normalized version -- Pitloom +doesn't fetch the URL to check, so a line like that disqualifies the +whole file the same as an unpinned or ranged one would. **Only the single highest-priority lock file present is used.** If more than one lock file exists in the same project directory (uncommon, but diff --git a/docs/resources.md b/docs/resources.md index 6859f726..23c7411e 100644 --- a/docs/resources.md +++ b/docs/resources.md @@ -1,6 +1,6 @@ --- Created: 2026-03-26 -Last-Modified: 2026-09-04 +Last-Modified: 2026-09-05 SPDX-FileCopyrightText: 2026-present Arthit Suriyawongkul SPDX-FileType: DOCUMENTATION SPDX-License-Identifier: CC0-1.0 @@ -52,6 +52,10 @@ each with a short note on what SBOM metadata it feeds: - [PEP 639][pep-639] – Improving License Clarity with Better Package Metadata: SPDX license expression and `license-files` bundling → the SBOM's declared-license and license-file elements. +- [PEP 751][pep-751] – A file format to record Python dependencies for + installation reproducibility: `pylock.toml`, the highest-priority + resolved-dependency source in the lock-file cascade (see + [Dependency sources](dependency-sources.md)). - [PEP 770][pep-770] – Improving measurability of Python packages with Software Bill-of-Materials: defines `.dist-info/sboms/`, where Pitloom embeds/locates a wheel's own SBOM. @@ -67,6 +71,7 @@ each with a short note on what SBOM metadata it feeds: [pep-518]: https://peps.python.org/pep-0518/ [pep-621]: https://peps.python.org/pep-0621/ [pep-639]: https://peps.python.org/pep-0639/ +[pep-751]: https://peps.python.org/pep-0751/ [pep-770]: https://peps.python.org/pep-0770/ [packaging-peps]: https://peps.python.org/topic/packaging/ diff --git a/src/pitloom/extract/_lock_common.py b/src/pitloom/extract/_lock_common.py index e7cc8c48..7753ef82 100644 --- a/src/pitloom/extract/_lock_common.py +++ b/src/pitloom/extract/_lock_common.py @@ -6,17 +6,14 @@ """Shared helpers for lock/pin file extractors (:mod:`pitloom.extract._poetry_lock`, :mod:`pitloom.extract._pylock`, :mod:`pitloom.extract._uv_lock`, :mod:`pitloom.extract._pdm_lock`, -:mod:`pitloom.extract._pipfile_lock`, and future formats registered in +:mod:`pitloom.extract._pipfile_lock`, :mod:`pitloom.extract._requirements_txt`, +and future formats registered in :mod:`pitloom.extract._locked_dependencies`). -Factored out once the same two steps -- "load the lock file, handling -absence/parse errors the same way every format does" and "group a -lock's flat package-entry list by name, to detect a name resolved to -more than one version" -- started being hand-copied into each new -extractor. Per this repo's "a pattern hand-copied across 3+ call sites -drifts" convention, this module is the one place both now live; only -extraction logic genuinely specific to one format (its own field names, -its own group/source-key conventions) stays in that format's own module. +Every extraction step genuinely specific to one format (its own field +names, its own group/source-key conventions) stays in that format's own +module; only what's shared across two or more formats -- loading the +lock file, grouping entries by name, judging a specifier -- lives here. """ from __future__ import annotations @@ -27,6 +24,9 @@ from pathlib import Path from typing import Any +from packaging.specifiers import SpecifierSet +from packaging.utils import canonicalize_name + from pitloom.extract._toml_io import TOMLDecodeError, load_toml_file log = logging.getLogger(__name__) @@ -34,10 +34,12 @@ __all__ = [ "POETRY_LOCK_SOURCE_NAME", "find_first_present_key", + "group_versions_by_canonical_name", "index_packages_by_name", "is_usable_version", "load_lock_json", "load_lock_toml", + "single_exact_pin", "warn_non_registry_source", ] @@ -137,29 +139,78 @@ def index_packages_by_name(packages: list[Any]) -> dict[str, list[dict[str, Any] def is_usable_version(version: Any) -> bool: """Return whether *version* is a non-empty string -- the "can this become a real ``name==version`` pin" check every lock/pin extractor - (``poetry.lock``, ``pylock.toml``, ``uv.lock``, ``pdm.lock``) applies - to a ``[[package]]`` entry's ``version`` field before using it. - Factored out once four independent copies of ``not - isinstance(version, str) or not version`` existed, per this repo's - "a pattern hand-copied across 3+ call sites drifts" convention -- - each call site still logs its own ``WARNING:`` when this returns + applies to a ``[[package]]`` entry's ``version`` field before using + it. Each call site still logs its own ``WARNING:`` when this returns ``False``, since the message wording (which field, which format) is genuinely format-specific. """ return isinstance(version, str) and bool(version) +def group_versions_by_canonical_name( + pairs: Iterable[tuple[str, str]], +) -> dict[str, list[tuple[str, str]]]: + """Group ``(name, version)`` pairs by PEP 503-canonicalized *name*, + preserving each pair's original literal name/version and file order + both across and within groups. + + Comparing canonicalized (lowercased, ``-``/``_``/``.``-folded) names + is required, not optional: ``Flask==1.0`` and ``flask==2.0`` name the + same PyPI package under PEP 503, so a caller checking "does this name + resolve to more than one version" must group them together or the + check silently never fires for a mixed-case duplicate. + + A caller decides what a multi-entry group means for its own format: + :mod:`pitloom.extract._pdm_lock` collapses a group to one entry when + every version agrees (its per-extra duplicate records always do) and + skips just that name otherwise; :mod:`pitloom.extract._requirements_txt` + treats any group with more than one distinct version as disqualifying + its whole file, since it has no per-format definition of "expected + duplication" the way an extra-variant lock entry does. + """ + by_canonical: dict[str, list[tuple[str, str]]] = {} + for name, version in pairs: + by_canonical.setdefault(canonicalize_name(name), []).append((name, version)) + return by_canonical + + +def single_exact_pin(specifier_set: SpecifierSet) -> str | None: + """Return the bare version when *specifier_set* contains exactly one + non-wildcard ``==`` specifier (e.g. ``SpecifierSet("==2.31.0")`` -> + ``"2.31.0"``), or ``None`` for anything looser than one exact pin -- + a range, more than one specifier, or a prefix-match wildcard like + ``"==2.31.*"`` (``packaging.specifiers.Specifier`` reports that as + operator ``"=="`` too, but it pins a *range* of versions, not one + exact release). + + Doesn't itself construct *specifier_set* from a raw string -- + :mod:`pitloom.extract._pipfile_lock` and + :mod:`pitloom.extract._requirements_txt` both need a raw-string + parse step first, and each wants different ``WARNING:`` wording for + "unparseable" vs. "parseable but not a single exact pin" -- so + parsing (and catching ``packaging.specifiers.InvalidSpecifier``) + stays the caller's job; this function only judges an already-built + ``SpecifierSet``. + """ + specifiers = list(specifier_set) + if ( + len(specifiers) != 1 + or specifiers[0].operator != "==" + or "*" in specifiers[0].version + ): + return None + return specifiers[0].version + + def warn_non_registry_source(lock_file: str, name: str, source_key: str) -> None: """Log the standard ``WARNING:`` for a non-registry-sourced entry (VCS, local path, archive/URL -- anything a bare ``name==version`` pin can't represent), naming *lock_file* (e.g. ``"uv.lock"``), *name* (the package), and *source_key* (which non-registry marker - was found). - - The exact wording was hand-copied identically into every extractor - (`_poetry_lock.py`, `_pylock.py`, `_uv_lock.py`, `_pdm_lock.py`, - `_pipfile_lock.py`) before being factored out here, per this repo's - "a pattern hand-copied across 3+ call sites drifts" convention. + was found). Shared by every extractor that has a non-registry-source + concept (`_poetry_lock.py`, `_pylock.py`, `_uv_lock.py`, + `_pdm_lock.py`, `_pipfile_lock.py`) so the wording stays identical + across formats. """ log.warning( "Skipping %s entry %r: %s-sourced dependencies cannot be " diff --git a/src/pitloom/extract/_locked_dependencies.py b/src/pitloom/extract/_locked_dependencies.py index b5bd2c41..16d6c3b5 100644 --- a/src/pitloom/extract/_locked_dependencies.py +++ b/src/pitloom/extract/_locked_dependencies.py @@ -40,6 +40,7 @@ from pitloom.extract._pdm_lock import extract_pdm_lock_dependencies from pitloom.extract._pipfile_lock import extract_pipfile_lock_dependencies from pitloom.extract._pylock import extract_pylock_dependencies +from pitloom.extract._requirements_txt import extract_pinned_requirements_dependencies from pitloom.extract._uv_lock import extract_uv_lock_dependencies log = logging.getLogger(__name__) @@ -93,6 +94,11 @@ def _ignore_expected_name(extractor: Callable[[Path], list[str]]) -> _LockExtrac _ignore_expected_name(extract_pipfile_lock_dependencies), "resolved_lockfile", ), + ( + "requirements.txt", + _ignore_expected_name(extract_pinned_requirements_dependencies), + "pinned_requirements", + ), ] diff --git a/src/pitloom/extract/_pdm_lock.py b/src/pitloom/extract/_pdm_lock.py index 5719decf..cd89ffb2 100644 --- a/src/pitloom/extract/_pdm_lock.py +++ b/src/pitloom/extract/_pdm_lock.py @@ -29,11 +29,10 @@ than once, but only to record separate per-extra variants (e.g. a bare ``httpx`` entry alongside an ``httpx`` entry with ``extras = ["socks"]``) that always agree on ``version`` -- collapsed here via -:func:`pitloom.extract._lock_common.index_packages_by_name`, the same -helper ``_uv_lock.py`` uses for its (genuinely ambiguous) case. Only a -name whose entries actually *disagree* on version is treated as -ambiguous and skipped, matching ``uv.lock``'s "don't guess" policy for -that case. +:func:`pitloom.extract._lock_common.group_versions_by_canonical_name`, +also shared with :mod:`pitloom.extract._requirements_txt`. Only a name +whose entries actually *disagree* on version is treated as ambiguous and +skipped, matching ``uv.lock``'s "don't guess" policy for that case. """ from __future__ import annotations @@ -44,7 +43,7 @@ from pitloom.extract._lock_common import ( find_first_present_key, - index_packages_by_name, + group_versions_by_canonical_name, is_usable_version, load_lock_toml, warn_non_registry_source, @@ -137,17 +136,18 @@ def extract_pdm_lock_dependencies(project_dir: Path) -> list[str]: if pkg is not None ] + pairs = [(pkg["name"], pkg["version"]) for pkg in default_group_packages] + dependencies: list[str] = [] - for name, entries in index_packages_by_name(default_group_packages).items(): - versions = {entry["version"] for entry in entries} - if len(versions) > 1: + for group in group_versions_by_canonical_name(pairs).values(): + name, version = group[0] + conflicting_versions = {v for _, v in group} + if len(conflicting_versions) > 1: log.warning( - "Skipping pdm.lock entry %r: %d conflicting resolved " - "versions present (%s)", + "Skipping pdm.lock entry %r: pinned to conflicting versions (%s)", name, - len(versions), - ", ".join(sorted(versions)), + ", ".join(sorted(conflicting_versions)), ) continue - dependencies.append(f"{name}=={entries[0]['version']}") + dependencies.append(f"{name}=={version}") return dependencies diff --git a/src/pitloom/extract/_pipfile_lock.py b/src/pitloom/extract/_pipfile_lock.py index e6b88059..956dd1a7 100644 --- a/src/pitloom/extract/_pipfile_lock.py +++ b/src/pitloom/extract/_pipfile_lock.py @@ -48,6 +48,7 @@ find_first_present_key, is_usable_version, load_lock_json, + single_exact_pin, warn_non_registry_source, ) @@ -139,7 +140,7 @@ def _exact_pinned_version(name: str, version: Any) -> str | None: ) return None try: - specifiers = list(SpecifierSet(version)) + specifier_set = SpecifierSet(version) except InvalidSpecifier: log.warning( "Skipping Pipfile.lock entry %r: %r isn't a valid PEP 440 specifier", @@ -147,11 +148,8 @@ def _exact_pinned_version(name: str, version: Any) -> str | None: version, ) return None - if ( - len(specifiers) != 1 - or specifiers[0].operator != "==" - or "*" in specifiers[0].version - ): + pinned_version = single_exact_pin(specifier_set) + if pinned_version is None: log.warning( "Skipping Pipfile.lock entry %r: 'version' %r isn't a single " "exact '==' pin", @@ -159,4 +157,4 @@ def _exact_pinned_version(name: str, version: Any) -> str | None: version, ) return None - return specifiers[0].version + return pinned_version diff --git a/src/pitloom/extract/_requirements_txt.py b/src/pitloom/extract/_requirements_txt.py new file mode 100644 index 00000000..93c2e877 --- /dev/null +++ b/src/pitloom/extract/_requirements_txt.py @@ -0,0 +1,220 @@ +# SPDX-FileContributor: Arthit Suriyawongkul +# SPDX-FileCopyrightText: 2026-present Arthit Suriyawongkul +# SPDX-FileType: SOURCE +# SPDX-License-Identifier: Apache-2.0 + +"""Extractor for a fully pinned ``requirements.txt``. + +See also: :mod:`pitloom.extract._pipfile_lock` (the sibling extractor +this module mirrors its PEP 440 exact-pin validation from -- +:func:`pitloom.extract._lock_common.single_exact_pin`) and +:mod:`pitloom.extract._locked_dependencies` (the cascade module that +calls this extractor and overlays its output onto +``ProjectMetadata.locked_dependencies``, ranked lowest of every source +there -- see the module-level docstring below for why). + +``requirements.txt`` is source-stage-only, the same class as every +sibling lock format: appropriate for ``loom project``/``loom generate``, +never for ``loom wheel``/``embed-wheel`` (the real wheel's own metadata +is ground truth) or ``loom env`` (live introspection is strictly more +authoritative). + +**Not a real lock file, and ranked accordingly.** Every other source in +the cascade is a resolver-generated artifact carrying real resolution +metadata (often hashes); a plain ``requirements.txt`` is just a list of +lines a human or ``pip freeze`` wrote, with no such guarantee. Pitloom +only trusts it as a resolved-dependency source when it can prove, line +by line, that *every* real dependency line is already an exact ``==`` +pin -- if even one line isn't, the **entire file** is ignored with one +``WARNING:`` naming the first disqualifying line, never partially +included. The same whole-file rejection applies if one name (compared +PEP 503-canonicalized, so ``Flask`` and ``flask`` count as the same +name) repeats with two different pinned versions; a repeat with the +same version is silently collapsed to one entry. Its provenance +``Method`` tag is ``"pinned_requirements"``, distinct from +every other source's ``"resolved_lockfile"``, so a reader of the +generated SBOM can tell the two kinds of evidence apart. + +**A URL-based line (``name @ https://...`` or ``git+https://...``) is a +PEP 508 direct reference, not a PEP 440 version specifier, and always +disqualifies the whole file -- even one that looks like a tagged +release.** Neither spec defines deriving a normalized version from a +URL, and a git tag/filename is an arbitrary string with no guaranteed +relationship to the package's real version. Confirming it would mean +fetching the URL, which conflicts with this repo's "prevent excessive +network access" principle -- every sibling lock format skips its own +VCS/path/URL-sourced entries the same way. +""" + +from __future__ import annotations + +import logging +import re +from pathlib import Path + +from packaging.requirements import InvalidRequirement, Requirement + +from pitloom.extract._lock_common import ( + group_versions_by_canonical_name, + single_exact_pin, +) + +log = logging.getLogger(__name__) + +__all__ = ["extract_pinned_requirements_dependencies"] + +#: A comment starts at a ``#`` preceded by start-of-line or whitespace -- +#: matches ``pip``'s own ``requirements.txt`` comment convention (a +#: literal ``#`` inside a URL's query string is rare, and a line where +#: one occurs is highly likely to be URL-sourced anyway, so still +#: disqualifying downstream regardless of how the comment strip lands). +_COMMENT_RE = re.compile(r"(?:^|\s)#.*$") + +#: A line beginning with any of these is a pip option (``-r``/``-e``/ +#: ``--hash``/``--index-url``/etc.), not a plain dependency line -- an +#: option like ``-e``/``-r`` means this file isn't a simple, fully +#: pinned list, so its presence disqualifies the whole file rather than +#: being silently skipped. +_OPTION_LINE_PREFIX = "-" + + +def extract_pinned_requirements_dependencies(project_dir: Path) -> list[str]: + """Read ``requirements.txt`` next to ``pyproject.toml``/``setup.py`` + and return every dependency as an exact-pin PEP 508 string, but only + when *every* real line in the file is already an exact ``==`` pin. + + Returns an empty list when no ``requirements.txt`` is present, it + can't be read/decoded, or any line disqualifies the whole file (an + option line, a URL-based requirement, an unpinned/ranged specifier, + a malformed line, or one name pinned to two conflicting versions) -- + see the module docstring for why this is all-or-nothing rather than + including only the pinned lines. A leading UTF-8 BOM (common from + Windows editors) and pip's backslash line-continuation syntax are + both handled the same as pip itself handles them, not treated as + malformed. + """ + lock_path = project_dir / "requirements.txt" + if not lock_path.exists(): + return [] + try: + raw_text = lock_path.read_text(encoding="utf-8-sig") + except (OSError, UnicodeDecodeError) as exc: + log.warning("Failed to read %s: %s", lock_path, exc) + return [] + + pins: list[tuple[str, str]] = [] + for lineno, joined_line in _join_continuation_lines(raw_text): + line = _COMMENT_RE.sub("", joined_line).strip() + if not line: + continue + pin = _pinned_name_version_for_line(lock_path, lineno, line) + if pin is None: + return [] + pins.append(pin) + return _collapse_or_none(lock_path, pins) + + +def _join_continuation_lines(raw_text: str) -> list[tuple[int, str]]: + """Join pip's backslash line-continuation syntax (a trailing ``\\`` + at end of physical line) into logical lines, each paired with the + 1-based line number of its *first* physical line -- so a long + requirement or marker expression split across lines parses the same + as if it were written on one line, instead of the trailing ``\\`` + disqualifying the whole file as a malformed line. + + Doesn't make a ``pip-compile --generate-hashes``-style file usable: + joining a continuation still leaves any ``--hash=...`` token on it, + which isn't part of PEP 508 grammar and correctly disqualifies the + whole file the same as any other malformed line -- hash-annotated + files stay unsupported, just for the right documented reason + instead of failing on the raw backslash first. + """ + logical_lines: list[tuple[int, str]] = [] + buffer: list[str] = [] + first_lineno = 1 + for lineno, raw_line in enumerate(raw_text.splitlines(), start=1): + if not buffer: + first_lineno = lineno + trimmed = raw_line.rstrip() + if trimmed.endswith("\\"): + buffer.append(trimmed[:-1]) + continue + buffer.append(raw_line) + logical_lines.append((first_lineno, " ".join(buffer))) + buffer = [] + if buffer: + logical_lines.append((first_lineno, " ".join(buffer))) + return logical_lines + + +def _collapse_or_none(lock_path: Path, pins: list[tuple[str, str]]) -> list[str]: + """Collapse *pins* to one ``name==version`` entry per PEP + 503-canonicalized name, preserving first-seen literal name and file + order -- or ``[]`` (with a ``WARNING:`` naming the name and both + versions) the moment one canonicalized name repeats with two + *different* versions. A plain repeated line (same name, same + version) is silently collapsed to one entry. + """ + result: list[str] = [] + for group in group_versions_by_canonical_name(pins).values(): + name, version = group[0] + conflicting = next((v for _, v in group if v != version), None) + if conflicting is not None: + log.warning( + "%s: %r pinned to conflicting versions (%s, %s) -- " + "ignoring requirements.txt", + lock_path, + name, + version, + conflicting, + ) + return [] + result.append(f"{name}=={version}") + return result + + +def _pinned_name_version_for_line( + lock_path: Path, lineno: int, line: str +) -> tuple[str, str] | None: + """Return ``(name, version)`` for a well-formed, exactly-pinned, + non-URL requirement *line*, or ``None`` (having already logged the + single ``WARNING:`` naming why) when it disqualifies the whole file.""" + if line.startswith(_OPTION_LINE_PREFIX): + log.warning( + "%s:%d: option line %r means this file isn't fully pinned -- " + "ignoring requirements.txt", + lock_path, + lineno, + line, + ) + return None + try: + requirement = Requirement(line) + except InvalidRequirement as exc: + log.warning( + "%s:%d: malformed requirement line: %s -- ignoring requirements.txt", + lock_path, + lineno, + exc, + ) + return None + if requirement.url is not None: + log.warning( + "%s:%d: %r is a direct URL reference, not a version pin -- " + "ignoring requirements.txt", + lock_path, + lineno, + requirement.name, + ) + return None + pinned_version = single_exact_pin(requirement.specifier) + if pinned_version is None: + log.warning( + "%s:%d: %r isn't pinned to a single exact version -- " + "ignoring requirements.txt", + lock_path, + lineno, + requirement.name, + ) + return None + return requirement.name, pinned_version diff --git a/tests/extract/test_lock_common.py b/tests/extract/test_lock_common.py index b02f78dd..88b740f0 100644 --- a/tests/extract/test_lock_common.py +++ b/tests/extract/test_lock_common.py @@ -16,6 +16,7 @@ from pitloom.extract._lock_common import ( find_first_present_key, + group_versions_by_canonical_name, index_packages_by_name, load_lock_toml, ) @@ -85,6 +86,32 @@ def test_index_packages_by_name_empty_list_returns_empty_dict() -> None: assert not index_packages_by_name([]) +def test_group_versions_by_canonical_name_groups_case_and_separator_variants() -> None: + """PEP 503 canonicalization folds case AND ``-``/``_``/``.`` runs -- + both must land in the same group, not just a case-insensitive match.""" + pairs = [ + ("Flask", "2.0"), + ("flask", "2.0"), + ("python_dateutil", "2.9.0"), + ("python-dateutil", "2.9.0"), + ("idna", "3.7"), + ] + + result = group_versions_by_canonical_name(pairs) + + assert list(result.keys()) == ["flask", "python-dateutil", "idna"] + assert result["flask"] == [("Flask", "2.0"), ("flask", "2.0")] + assert result["python-dateutil"] == [ + ("python_dateutil", "2.9.0"), + ("python-dateutil", "2.9.0"), + ] + assert result["idna"] == [("idna", "3.7")] + + +def test_group_versions_by_canonical_name_empty_input_returns_empty_dict() -> None: + assert not group_versions_by_canonical_name([]) + + def test_find_first_present_key_returns_first_match_in_key_order() -> None: """Order is determined by *keys*, not by the mapping's own key order -- callers rely on this to report a stable, predictable diff --git a/tests/extract/test_pdm_lock.py b/tests/extract/test_pdm_lock.py index 5128fa1f..302dd986 100644 --- a/tests/extract/test_pdm_lock.py +++ b/tests/extract/test_pdm_lock.py @@ -193,6 +193,43 @@ def test_same_name_same_version_duplicate_entries_deduped() -> None: assert extract_pdm_lock_dependencies(tmp_path) == ["httpx==0.28.1"] +def test_same_name_different_casing_same_version_deduped() -> None: + """Grouping compares PEP 503-canonicalized names, so a name that + happens to be spelled differently across entries still collapses + when the versions agree.""" + with tempfile.TemporaryDirectory() as tmp: + tmp_path = Path(tmp) + _write_lock( + tmp_path, + '[[package]]\nname = "Httpx"\nversion = "0.28.1"\n' + 'groups = ["default"]\n\n' + '[[package]]\nname = "httpx"\nversion = "0.28.1"\n' + 'groups = ["default"]\n', + ) + + assert extract_pdm_lock_dependencies(tmp_path) == ["Httpx==0.28.1"] + + +def test_same_name_different_casing_conflicting_versions_skipped_and_warns( + caplog: pytest.LogCaptureFixture, +) -> None: + with tempfile.TemporaryDirectory() as tmp: + tmp_path = Path(tmp) + _write_lock( + tmp_path, + '[[package]]\nname = "Httpx"\nversion = "1.0.0"\n' + 'groups = ["default"]\n\n' + '[[package]]\nname = "httpx"\nversion = "2.0.0"\n' + 'groups = ["default"]\n', + ) + + with caplog.at_level(logging.WARNING): + result = extract_pdm_lock_dependencies(tmp_path) + + assert not result + assert "pinned to conflicting versions" in caplog.text + + def test_same_name_conflicting_versions_skipped_and_warns( caplog: pytest.LogCaptureFixture, ) -> None: @@ -212,7 +249,7 @@ def test_same_name_conflicting_versions_skipped_and_warns( result = extract_pdm_lock_dependencies(tmp_path) assert not result - assert "2 conflicting resolved versions" in caplog.text + assert "pinned to conflicting versions" in caplog.text # --- read_project() cascade integration ----------------------------------- diff --git a/tests/extract/test_requirements_txt.py b/tests/extract/test_requirements_txt.py new file mode 100644 index 00000000..f60335a4 --- /dev/null +++ b/tests/extract/test_requirements_txt.py @@ -0,0 +1,432 @@ +# SPDX-FileContributor: Arthit Suriyawongkul +# SPDX-FileCopyrightText: 2026-present Arthit Suriyawongkul +# SPDX-FileType: SOURCE +# SPDX-License-Identifier: Apache-2.0 + +"""Tests for pinned ``requirements.txt`` parsing +(:mod:`pitloom.extract._requirements_txt`) and its overlay onto +``ProjectMetadata.locked_dependencies`` via ``read_project()``'s lock +cascade (:mod:`pitloom.extract._locked_dependencies`). + +See also: test_pipfile_lock.py for the sibling extractor this module's +exact-pin validation is shared with (``single_exact_pin()`` in +``_lock_common.py``); test_locked_dependencies.py for the cascade +mechanism's own tests. +""" + +import logging +import tempfile +from pathlib import Path + +import pytest + +from pitloom.extract._requirements_txt import extract_pinned_requirements_dependencies +from pitloom.extract.project import read_project + +REAL_WORLD_LOCKS = ( + Path(__file__).parent.parent / "fixtures" / "real-world-locks" / "requirements" +) + + +def _write_requirements(tmp_dir: Path, content: str) -> None: + (tmp_dir / "requirements.txt").write_text(content, encoding="utf-8") + + +def test_no_file_returns_empty_list() -> None: + with tempfile.TemporaryDirectory() as tmp: + assert not extract_pinned_requirements_dependencies(Path(tmp)) + + +def test_all_pinned_lines_included() -> None: + with tempfile.TemporaryDirectory() as tmp: + tmp_path = Path(tmp) + _write_requirements( + tmp_path, + "requests==2.31.0\nidna==3.7\n", + ) + + result = extract_pinned_requirements_dependencies(tmp_path) + + assert result == ["requests==2.31.0", "idna==3.7"] + + +def test_blank_lines_and_comments_ignored() -> None: + with tempfile.TemporaryDirectory() as tmp: + tmp_path = Path(tmp) + _write_requirements( + tmp_path, + "# a full-line comment\n\nrequests==2.31.0 # inline comment\n\n", + ) + + result = extract_pinned_requirements_dependencies(tmp_path) + + assert result == ["requests==2.31.0"] + + +def test_marker_present_but_still_a_single_exact_pin_included() -> None: + """A trailing environment marker doesn't affect whether the + specifier itself is a single exact pin -- same "conditional + presence, not a version conflict" simplification as every sibling + format's marker handling.""" + with tempfile.TemporaryDirectory() as tmp: + tmp_path = Path(tmp) + _write_requirements(tmp_path, 'requests==2.31.0; python_version >= "3.8"\n') + + result = extract_pinned_requirements_dependencies(tmp_path) + + assert result == ["requests==2.31.0"] + + +def test_extras_dropped_from_output() -> None: + with tempfile.TemporaryDirectory() as tmp: + tmp_path = Path(tmp) + _write_requirements(tmp_path, "requests[security]==2.31.0\n") + + result = extract_pinned_requirements_dependencies(tmp_path) + + assert result == ["requests==2.31.0"] + + +def test_duplicate_name_same_version_collapsed_to_one_entry() -> None: + with tempfile.TemporaryDirectory() as tmp: + tmp_path = Path(tmp) + _write_requirements(tmp_path, "requests==2.31.0\nrequests==2.31.0\n") + + result = extract_pinned_requirements_dependencies(tmp_path) + + assert result == ["requests==2.31.0"] + + +def test_duplicate_name_conflicting_versions_disqualifies_whole_file( + caplog: pytest.LogCaptureFixture, +) -> None: + with tempfile.TemporaryDirectory() as tmp: + tmp_path = Path(tmp) + _write_requirements(tmp_path, "requests==2.31.0\nidna==3.7\nrequests==2.32.0\n") + + with caplog.at_level(logging.WARNING): + result = extract_pinned_requirements_dependencies(tmp_path) + + assert not result + assert "conflicting versions" in caplog.text + assert "requests" in caplog.text + + +def test_duplicate_name_different_casing_same_version_collapsed() -> None: + with tempfile.TemporaryDirectory() as tmp: + tmp_path = Path(tmp) + _write_requirements(tmp_path, "Flask==2.0\nflask==2.0\n") + + result = extract_pinned_requirements_dependencies(tmp_path) + + assert result == ["Flask==2.0"] + + +def test_duplicate_name_different_casing_conflicting_versions_disqualifies_whole_file( + caplog: pytest.LogCaptureFixture, +) -> None: + """PEP 503 says a package name is compared case-insensitively -- + ``Flask`` and ``flask`` name the same PyPI package, so pinning them + to different versions in the same file is a real conflict, not two + unrelated packages.""" + with tempfile.TemporaryDirectory() as tmp: + tmp_path = Path(tmp) + _write_requirements(tmp_path, "Flask==1.0\nflask==2.0\n") + + with caplog.at_level(logging.WARNING): + result = extract_pinned_requirements_dependencies(tmp_path) + + assert not result + assert "conflicting versions" in caplog.text + + +def test_undecodable_file_returns_empty_list_and_warns( + caplog: pytest.LogCaptureFixture, +) -> None: + with tempfile.TemporaryDirectory() as tmp: + tmp_path = Path(tmp) + (tmp_path / "requirements.txt").write_bytes(b"requests==2.31.0\n\xff\xfe\n") + + with caplog.at_level(logging.WARNING): + result = extract_pinned_requirements_dependencies(tmp_path) + + assert not result + assert "Failed to read" in caplog.text + + +def test_three_way_duplicate_conflicting_versions_disqualifies_whole_file( + caplog: pytest.LogCaptureFixture, +) -> None: + with tempfile.TemporaryDirectory() as tmp: + tmp_path = Path(tmp) + _write_requirements( + tmp_path, "requests==2.31.0\nrequests==2.32.0\nrequests==2.33.0\n" + ) + + with caplog.at_level(logging.WARNING): + result = extract_pinned_requirements_dependencies(tmp_path) + + assert not result + assert "conflicting versions" in caplog.text + + +def test_utf8_bom_does_not_disqualify_the_file() -> None: + with tempfile.TemporaryDirectory() as tmp: + tmp_path = Path(tmp) + (tmp_path / "requirements.txt").write_bytes( + b"\xef\xbb\xbfrequests==2.31.0\nidna==3.7\n" + ) + + result = extract_pinned_requirements_dependencies(tmp_path) + + assert result == ["requests==2.31.0", "idna==3.7"] + + +def test_backslash_continuation_joined_before_parsing() -> None: + with tempfile.TemporaryDirectory() as tmp: + tmp_path = Path(tmp) + _write_requirements( + tmp_path, + 'requests==2.31.0 ; python_version >= "3.10" \\\n' + ' and platform_system == "Linux"\n' + "idna==3.7\n", + ) + + result = extract_pinned_requirements_dependencies(tmp_path) + + assert result == ["requests==2.31.0", "idna==3.7"] + + +def test_hash_annotated_continuation_still_disqualifies_whole_file( + caplog: pytest.LogCaptureFixture, +) -> None: + """Joining a continuation doesn't make ``--hash=...`` tokens valid + PEP 508 syntax -- a pip-compile ``--generate-hashes`` file stays + unsupported, now failing for the right reason instead of on the raw + backslash.""" + with tempfile.TemporaryDirectory() as tmp: + tmp_path = Path(tmp) + _write_requirements( + tmp_path, + "requests==2.31.0 \\\n --hash=sha256:aaaa\n", + ) + + with caplog.at_level(logging.WARNING): + result = extract_pinned_requirements_dependencies(tmp_path) + + assert not result + assert "malformed requirement line" in caplog.text + + +def test_unpinned_bare_name_disqualifies_whole_file( + caplog: pytest.LogCaptureFixture, +) -> None: + with tempfile.TemporaryDirectory() as tmp: + tmp_path = Path(tmp) + _write_requirements(tmp_path, "requests==2.31.0\nidna\n") + + with caplog.at_level(logging.WARNING): + result = extract_pinned_requirements_dependencies(tmp_path) + + assert not result + assert "isn't pinned to a single exact version" in caplog.text + + +@pytest.mark.parametrize( + "version_line", ["requests>=2.0", "requests>=2.0,<3.0", "requests~=2.31"] +) +def test_ranged_specifier_disqualifies_whole_file( + version_line: str, caplog: pytest.LogCaptureFixture +) -> None: + with tempfile.TemporaryDirectory() as tmp: + tmp_path = Path(tmp) + _write_requirements(tmp_path, f"idna==3.7\n{version_line}\n") + + with caplog.at_level(logging.WARNING): + result = extract_pinned_requirements_dependencies(tmp_path) + + assert not result + assert "isn't pinned to a single exact version" in caplog.text + + +def test_prefix_match_specifier_disqualifies_whole_file( + caplog: pytest.LogCaptureFixture, +) -> None: + with tempfile.TemporaryDirectory() as tmp: + tmp_path = Path(tmp) + _write_requirements(tmp_path, "idna==3.7\nrequests==2.31.*\n") + + with caplog.at_level(logging.WARNING): + result = extract_pinned_requirements_dependencies(tmp_path) + + assert not result + assert "isn't pinned to a single exact version" in caplog.text + + +@pytest.mark.parametrize( + "url_line", + [ + "name @ https://github.com/org/repo/archive/refs/tags/v2.31.0.zip", + "name @ https://github.com/org/repo/releases/download/v2.31.0/repo-2.31.0.whl", + ], +) +def test_url_requirement_disqualifies_whole_file_even_when_tag_shaped( + url_line: str, caplog: pytest.LogCaptureFixture +) -> None: + """Regression for the explicit design question: a URL requirement + that merely *looks* like it points at a tagged release must not be + treated as an exact version pin -- PEP 508 direct references carry + no normalized version at all, and nothing guarantees a tag/filename + round-trips to a real PEP 440 version. See the module docstring.""" + with tempfile.TemporaryDirectory() as tmp: + tmp_path = Path(tmp) + _write_requirements(tmp_path, f"idna==3.7\n{url_line}\n") + + with caplog.at_level(logging.WARNING): + result = extract_pinned_requirements_dependencies(tmp_path) + + assert not result + assert "direct URL reference" in caplog.text + + +@pytest.mark.parametrize( + "option_line", + ["-e .", "-r other-requirements.txt", "--hash=sha256:abcd", "-c constraints.txt"], +) +def test_option_line_disqualifies_whole_file( + option_line: str, caplog: pytest.LogCaptureFixture +) -> None: + with tempfile.TemporaryDirectory() as tmp: + tmp_path = Path(tmp) + _write_requirements(tmp_path, f"idna==3.7\n{option_line}\n") + + with caplog.at_level(logging.WARNING): + result = extract_pinned_requirements_dependencies(tmp_path) + + assert not result + assert "isn't fully pinned" in caplog.text + + +def test_bare_url_and_legacy_vcs_syntax_disqualify_as_malformed( + caplog: pytest.LogCaptureFixture, +) -> None: + """A bare URL (no ``name @`` prefix) or legacy ``git+...#egg=name`` + syntax isn't valid PEP 508 -- ``packaging.requirements.Requirement`` + itself rejects both, which this extractor treats the same as any + other malformed line: disqualifying, not silently skipped.""" + with tempfile.TemporaryDirectory() as tmp: + tmp_path = Path(tmp) + _write_requirements( + tmp_path, + "idna==3.7\ngit+https://github.com/org/repo.git@v2.31.0#egg=name\n", + ) + + with caplog.at_level(logging.WARNING): + result = extract_pinned_requirements_dependencies(tmp_path) + + assert not result + assert "malformed requirement line" in caplog.text + + +def test_malformed_requirement_line_disqualifies_whole_file( + caplog: pytest.LogCaptureFixture, +) -> None: + with tempfile.TemporaryDirectory() as tmp: + tmp_path = Path(tmp) + _write_requirements(tmp_path, "idna==3.7\n===not valid===\n") + + with caplog.at_level(logging.WARNING): + result = extract_pinned_requirements_dependencies(tmp_path) + + assert not result + assert "malformed requirement line" in caplog.text + + +def test_first_disqualifying_line_named_in_warning( + caplog: pytest.LogCaptureFixture, +) -> None: + with tempfile.TemporaryDirectory() as tmp: + tmp_path = Path(tmp) + _write_requirements(tmp_path, "idna==3.7\nrequests>=2.0\nurllib3==2.0.0\n") + + with caplog.at_level(logging.WARNING): + extract_pinned_requirements_dependencies(tmp_path) + + assert ":2:" in caplog.text + assert "urllib3" not in caplog.text + + +def test_unreadable_file_returns_empty_list_and_warns( + caplog: pytest.LogCaptureFixture, +) -> None: + with tempfile.TemporaryDirectory() as tmp: + tmp_path = Path(tmp) + req_dir = tmp_path / "requirements.txt" + req_dir.mkdir() # a directory named requirements.txt: read_text() fails + + with caplog.at_level(logging.WARNING): + result = extract_pinned_requirements_dependencies(tmp_path) + + assert not result + assert "Failed to read" in caplog.text + + +# --- read_project() cascade integration ------------------------------- + + +def test_read_project_populates_locked_dependencies_from_setup_py_only() -> None: + """Regression: pinned requirements.txt, like Pipfile.lock, predates + PEP 621 almost entirely -- the cascade must reach it via + read_project()'s setup.py-only dispatch path too.""" + with tempfile.TemporaryDirectory() as tmp: + tmp_path = Path(tmp) + (tmp_path / "setup.py").write_text( + "from setuptools import setup\nsetup(name='demo', version='1.0.0')\n", + encoding="utf-8", + ) + _write_requirements(tmp_path, "requests==2.31.0\n") + + metadata, _config, _path = read_project(tmp_path) + + assert metadata.locked_dependencies == ["requests==2.31.0"] + assert metadata.provenance["locked_dependencies"] == ( + "Source: requirements.txt | Method: pinned_requirements" + ) + + +def test_read_project_pipfile_lock_takes_priority_over_requirements_txt() -> None: + """requirements.txt is the lowest-ranked source -- every real lock + format outranks it, including Pipfile.lock.""" + with tempfile.TemporaryDirectory() as tmp: + tmp_path = Path(tmp) + (tmp_path / "setup.py").write_text( + "from setuptools import setup\nsetup(name='demo', version='1.0.0')\n", + encoding="utf-8", + ) + (tmp_path / "Pipfile.lock").write_text( + '{"default": {"httpx": {"version": "==0.27.0"}}}', + encoding="utf-8", + ) + _write_requirements(tmp_path, "requests==2.31.0\n") + + metadata, _config, _path = read_project(tmp_path) + + assert metadata.locked_dependencies == ["httpx==0.27.0"] + assert metadata.provenance["locked_dependencies"] == ( + "Source: Pipfile.lock | Method: resolved_lockfile" + ) + + +# --- real-world fixtures ------------------------------------------------- + + +def test_real_world_home_assistant_core_rejects_partially_pinned_file() -> None: + """`home-assistant/core`'s real root `requirements.txt` mixes exact + pins with range specifiers -- the whole-file all-or-nothing policy + must reject it entirely, not partially include the pinned lines.""" + metadata, _config, _path = read_project( + REAL_WORLD_LOCKS / "home-assistant-core-2026.9.0" + ) + + assert metadata.locked_dependencies == [] + assert "locked_dependencies" not in metadata.provenance diff --git a/tests/fixtures/real-world-locks/README.md b/tests/fixtures/real-world-locks/README.md index a1026a84..694f2987 100644 --- a/tests/fixtures/real-world-locks/README.md +++ b/tests/fixtures/real-world-locks/README.md @@ -30,7 +30,12 @@ Each `/-/` directory holds: or `requirements.txt`), committed as plain text. - Occasionally a third file the metadata file itself references (e.g. `snowflake-cli`'s `LICENSE`, required because its `pyproject.toml` - declares `license = { file = "LICENSE" }`). + declares `license = { file = "LICENSE" }`; `home-assistant-core`'s + `LICENSE.md` and `homeassistant/backports/LICENSE.Python`, required + because its `pyproject.toml` declares two + `project.license-files` glob patterns that + `StandardMetadata.from_pyproject()` validates actually match a real + file, unrelated to what this fixture is testing). No sdist archive, no `.git` history, no source code -- these fixtures exist only to exercise `pitloom.extract.project.read_project()`'s lock @@ -119,6 +124,20 @@ artifact. lock has several real instances of this (`httpx`, `coverage`, `mkdocstrings`, `hishel`); `unearth`'s doesn't, so together they cover both the dedup path and the plain case. +- **`home-assistant-core`'s `requirements.txt` is the reject case, not + the accept case.** Its real root `requirements.txt` mixes exact pins + (`aiodns==4.0.4`) with range specifiers (`certifi>=2021.5.30`) and a + leading `-c homeassistant/package_constraints.txt` option line -- + either alone disqualifies the whole file under + `extract_pinned_requirements_dependencies()`'s all-or-nothing policy, + so `locked_dependencies` resolves to `[]` for this fixture, on + purpose. No real, cleanly-licensed, fully-`==`-pinned root + `requirements.txt` was found during research (see + `working-docs/implementation/lock-file-cascade.md` for the candidates + checked and ruled out) -- the accept path is instead covered by small, + synthetic, inline content in `tests/extract/test_requirements_txt.py` + itself, per this directory's own "synthetic content isn't vendored + here" convention (see the top of this file). ## Fixtures @@ -137,7 +156,4 @@ artifact. | `pdm.lock` | [frostming/unearth](https://github.com/frostming/unearth) | 0.18.3 | MIT | GitHub tag `0.18.3` | GitHub tag `0.18.3` | | `Pipfile.lock` | [psf/requests-html](https://github.com/psf/requests-html) | 0.10.0 | MIT | GitHub tag `v0.10.0` (`setup.py`) | GitHub tag `v0.10.0` | | `Pipfile.lock` | [kennethreitz/responder](https://github.com/kennethreitz/responder) | 2.0.0 | Apache-2.0 | GitHub tag `v2.0.0` (`setup.py`) | GitHub tag `v2.0.0` | - -Pinned `requirements.txt` fixtures land in their own follow-up change, -alongside its extractor -- see `working-docs/design/roadmap.md`'s -"Remaining lock formats as a resolved-dependency source" item. +| `requirements.txt` (reject case) | [home-assistant/core](https://github.com/home-assistant/core) | 2026.9.0 | Apache-2.0 | GitHub tag `2026.9.0` | GitHub tag `2026.9.0` | diff --git a/tests/fixtures/real-world-locks/requirements/home-assistant-core-2026.9.0/LICENSE.md b/tests/fixtures/real-world-locks/requirements/home-assistant-core-2026.9.0/LICENSE.md new file mode 100644 index 00000000..261eeb9e --- /dev/null +++ b/tests/fixtures/real-world-locks/requirements/home-assistant-core-2026.9.0/LICENSE.md @@ -0,0 +1,201 @@ + Apache License + Version 2.0, January 2004 + http://www.apache.org/licenses/ + + TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION + + 1. Definitions. + + "License" shall mean the terms and conditions for use, reproduction, + and distribution as defined by Sections 1 through 9 of this document. + + "Licensor" shall mean the copyright owner or entity authorized by + the copyright owner that is granting the License. + + "Legal Entity" shall mean the union of the acting entity and all + other entities that control, are controlled by, or are under common + control with that entity. For the purposes of this definition, + "control" means (i) the power, direct or indirect, to cause the + direction or management of such entity, whether by contract or + otherwise, or (ii) ownership of fifty percent (50%) or more of the + outstanding shares, or (iii) beneficial ownership of such entity. + + "You" (or "Your") shall mean an individual or Legal Entity + exercising permissions granted by this License. + + "Source" form shall mean the preferred form for making modifications, + including but not limited to software source code, documentation + source, and configuration files. + + "Object" form shall mean any form resulting from mechanical + transformation or translation of a Source form, including but + not limited to compiled object code, generated documentation, + and conversions to other media types. + + "Work" shall mean the work of authorship, whether in Source or + Object form, made available under the License, as indicated by a + copyright notice that is included in or attached to the work + (an example is provided in the Appendix below). + + "Derivative Works" shall mean any work, whether in Source or Object + form, that is based on (or derived from) the Work and for which the + editorial revisions, annotations, elaborations, or other modifications + represent, as a whole, an original work of authorship. For the purposes + of this License, Derivative Works shall not include works that remain + separable from, or merely link (or bind by name) to the interfaces of, + the Work and Derivative Works thereof. + + "Contribution" shall mean any work of authorship, including + the original version of the Work and any modifications or additions + to that Work or Derivative Works thereof, that is intentionally + submitted to Licensor for inclusion in the Work by the copyright owner + or by an individual or Legal Entity authorized to submit on behalf of + the copyright owner. For the purposes of this definition, "submitted" + means any form of electronic, verbal, or written communication sent + to the Licensor or its representatives, including but not limited to + communication on electronic mailing lists, source code control systems, + and issue tracking systems that are managed by, or on behalf of, the + Licensor for the purpose of discussing and improving the Work, but + excluding communication that is conspicuously marked or otherwise + designated in writing by the copyright owner as "Not a Contribution." + + "Contributor" shall mean Licensor and any individual or Legal Entity + on behalf of whom a Contribution has been received by Licensor and + subsequently incorporated within the Work. + + 2. Grant of Copyright License. Subject to the terms and conditions of + this License, each Contributor hereby grants to You a perpetual, + worldwide, non-exclusive, no-charge, royalty-free, irrevocable + copyright license to reproduce, prepare Derivative Works of, + publicly display, publicly perform, sublicense, and distribute the + Work and such Derivative Works in Source or Object form. + + 3. Grant of Patent License. Subject to the terms and conditions of + this License, each Contributor hereby grants to You a perpetual, + worldwide, non-exclusive, no-charge, royalty-free, irrevocable + (except as stated in this section) patent license to make, have made, + use, offer to sell, sell, import, and otherwise transfer the Work, + where such license applies only to those patent claims licensable + by such Contributor that are necessarily infringed by their + Contribution(s) alone or by combination of their Contribution(s) + with the Work to which such Contribution(s) was submitted. If You + institute patent litigation against any entity (including a + cross-claim or counterclaim in a lawsuit) alleging that the Work + or a Contribution incorporated within the Work constitutes direct + or contributory patent infringement, then any patent licenses + granted to You under this License for that Work shall terminate + as of the date such litigation is filed. + + 4. Redistribution. You may reproduce and distribute copies of the + Work or Derivative Works thereof in any medium, with or without + modifications, and in Source or Object form, provided that You + meet the following conditions: + + (a) You must give any other recipients of the Work or + Derivative Works a copy of this License; and + + (b) You must cause any modified files to carry prominent notices + stating that You changed the files; and + + (c) You must retain, in the Source form of any Derivative Works + that You distribute, all copyright, patent, trademark, and + attribution notices from the Source form of the Work, + excluding those notices that do not pertain to any part of + the Derivative Works; and + + (d) If the Work includes a "NOTICE" text file as part of its + distribution, then any Derivative Works that You distribute must + include a readable copy of the attribution notices contained + within such NOTICE file, excluding those notices that do not + pertain to any part of the Derivative Works, in at least one + of the following places: within a NOTICE text file distributed + as part of the Derivative Works; within the Source form or + documentation, if provided along with the Derivative Works; or, + within a display generated by the Derivative Works, if and + wherever such third-party notices normally appear. The contents + of the NOTICE file are for informational purposes only and + do not modify the License. You may add Your own attribution + notices within Derivative Works that You distribute, alongside + or as an addendum to the NOTICE text from the Work, provided + that such additional attribution notices cannot be construed + as modifying the License. + + You may add Your own copyright statement to Your modifications and + may provide additional or different license terms and conditions + for use, reproduction, or distribution of Your modifications, or + for any such Derivative Works as a whole, provided Your use, + reproduction, and distribution of the Work otherwise complies with + the conditions stated in this License. + + 5. Submission of Contributions. Unless You explicitly state otherwise, + any Contribution intentionally submitted for inclusion in the Work + by You to the Licensor shall be under the terms and conditions of + this License, without any additional terms or conditions. + Notwithstanding the above, nothing herein shall supersede or modify + the terms of any separate license agreement you may have executed + with Licensor regarding such Contributions. + + 6. Trademarks. This License does not grant permission to use the trade + names, trademarks, service marks, or product names of the Licensor, + except as required for reasonable and customary use in describing the + origin of the Work and reproducing the content of the NOTICE file. + + 7. Disclaimer of Warranty. Unless required by applicable law or + agreed to in writing, Licensor provides the Work (and each + Contributor provides its Contributions) on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or + implied, including, without limitation, any warranties or conditions + of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A + PARTICULAR PURPOSE. You are solely responsible for determining the + appropriateness of using or redistributing the Work and assume any + risks associated with Your exercise of permissions under this License. + + 8. Limitation of Liability. In no event and under no legal theory, + whether in tort (including negligence), contract, or otherwise, + unless required by applicable law (such as deliberate and grossly + negligent acts) or agreed to in writing, shall any Contributor be + liable to You for damages, including any direct, indirect, special, + incidental, or consequential damages of any character arising as a + result of this License or out of the use or inability to use the + Work (including but not limited to damages for loss of goodwill, + work stoppage, computer failure or malfunction, or any and all + other commercial damages or losses), even if such Contributor + has been advised of the possibility of such damages. + + 9. Accepting Warranty or Additional Liability. While redistributing + the Work or Derivative Works thereof, You may choose to offer, + and charge a fee for, acceptance of support, warranty, indemnity, + or other liability obligations and/or rights consistent with this + License. However, in accepting such obligations, You may act only + on Your own behalf and on Your sole responsibility, not on behalf + of any other Contributor, and only if You agree to indemnify, + defend, and hold each Contributor harmless for any liability + incurred by, or claims asserted against, such Contributor by reason + of your accepting any such warranty or additional liability. + + END OF TERMS AND CONDITIONS + + APPENDIX: How to apply the Apache License to your work. + + To apply the Apache License to your work, attach the following + boilerplate notice, with the fields enclosed by brackets "[]" + replaced with your own identifying information. (Don't include + the brackets!) The text should be enclosed in the appropriate + comment syntax for the file format. We also recommend that a + file or class name and description of purpose be included on the + same "printed page" as the copyright notice for easier + identification within third-party archives. + + Copyright [yyyy] [name of copyright owner] + + Licensed under the Apache License, Version 2.0 (the "License"); + you may not use this file except in compliance with the License. + You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + + Unless required by applicable law or agreed to in writing, software + distributed under the License is distributed on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + See the License for the specific language governing permissions and + limitations under the License. diff --git a/tests/fixtures/real-world-locks/requirements/home-assistant-core-2026.9.0/homeassistant/backports/LICENSE.Python b/tests/fixtures/real-world-locks/requirements/home-assistant-core-2026.9.0/homeassistant/backports/LICENSE.Python new file mode 100644 index 00000000..f26bcf4d --- /dev/null +++ b/tests/fixtures/real-world-locks/requirements/home-assistant-core-2026.9.0/homeassistant/backports/LICENSE.Python @@ -0,0 +1,279 @@ +A. HISTORY OF THE SOFTWARE +========================== + +Python was created in the early 1990s by Guido van Rossum at Stichting +Mathematisch Centrum (CWI, see https://www.cwi.nl) in the Netherlands +as a successor of a language called ABC. Guido remains Python's +principal author, although it includes many contributions from others. + +In 1995, Guido continued his work on Python at the Corporation for +National Research Initiatives (CNRI, see https://www.cnri.reston.va.us) +in Reston, Virginia where he released several versions of the +software. + +In May 2000, Guido and the Python core development team moved to +BeOpen.com to form the BeOpen PythonLabs team. In October of the same +year, the PythonLabs team moved to Digital Creations, which became +Zope Corporation. In 2001, the Python Software Foundation (PSF, see +https://www.python.org/psf/) was formed, a non-profit organization +created specifically to own Python-related Intellectual Property. +Zope Corporation was a sponsoring member of the PSF. + +All Python releases are Open Source (see https://opensource.org for +the Open Source Definition). Historically, most, but not all, Python +releases have also been GPL-compatible; the table below summarizes +the various releases. + + Release Derived Year Owner GPL- + from compatible? (1) + + 0.9.0 thru 1.2 1991-1995 CWI yes + 1.3 thru 1.5.2 1.2 1995-1999 CNRI yes + 1.6 1.5.2 2000 CNRI no + 2.0 1.6 2000 BeOpen.com no + 1.6.1 1.6 2001 CNRI yes (2) + 2.1 2.0+1.6.1 2001 PSF no + 2.0.1 2.0+1.6.1 2001 PSF yes + 2.1.1 2.1+2.0.1 2001 PSF yes + 2.1.2 2.1.1 2002 PSF yes + 2.1.3 2.1.2 2002 PSF yes + 2.2 and above 2.1.1 2001-now PSF yes + +Footnotes: + +(1) GPL-compatible doesn't mean that we're distributing Python under + the GPL. All Python licenses, unlike the GPL, let you distribute + a modified version without making your changes open source. The + GPL-compatible licenses make it possible to combine Python with + other software that is released under the GPL; the others don't. + +(2) According to Richard Stallman, 1.6.1 is not GPL-compatible, + because its license has a choice of law clause. According to + CNRI, however, Stallman's lawyer has told CNRI's lawyer that 1.6.1 + is "not incompatible" with the GPL. + +Thanks to the many outside volunteers who have worked under Guido's +direction to make these releases possible. + + +B. TERMS AND CONDITIONS FOR ACCESSING OR OTHERWISE USING PYTHON +=============================================================== + +Python software and documentation are licensed under the +Python Software Foundation License Version 2. + +Starting with Python 3.8.6, examples, recipes, and other code in +the documentation are dual licensed under the PSF License Version 2 +and the Zero-Clause BSD license. + +Some software incorporated into Python is under different licenses. +The licenses are listed with code falling under that license. + + +PYTHON SOFTWARE FOUNDATION LICENSE VERSION 2 +-------------------------------------------- + +1. This LICENSE AGREEMENT is between the Python Software Foundation +("PSF"), and the Individual or Organization ("Licensee") accessing and +otherwise using this software ("Python") in source or binary form and +its associated documentation. + +2. Subject to the terms and conditions of this License Agreement, PSF hereby +grants Licensee a nonexclusive, royalty-free, world-wide license to reproduce, +analyze, test, perform and/or display publicly, prepare derivative works, +distribute, and otherwise use Python alone or in any derivative version, +provided, however, that PSF's License Agreement and PSF's notice of copyright, +i.e., "Copyright (c) 2001, 2002, 2003, 2004, 2005, 2006, 2007, 2008, 2009, 2010, +2011, 2012, 2013, 2014, 2015, 2016, 2017, 2018, 2019, 2020, 2021, 2022, 2023 Python Software Foundation; +All Rights Reserved" are retained in Python alone or in any derivative version +prepared by Licensee. + +3. In the event Licensee prepares a derivative work that is based on +or incorporates Python or any part thereof, and wants to make +the derivative work available to others as provided herein, then +Licensee hereby agrees to include in any such work a brief summary of +the changes made to Python. + +4. PSF is making Python available to Licensee on an "AS IS" +basis. PSF MAKES NO REPRESENTATIONS OR WARRANTIES, EXPRESS OR +IMPLIED. BY WAY OF EXAMPLE, BUT NOT LIMITATION, PSF MAKES NO AND +DISCLAIMS ANY REPRESENTATION OR WARRANTY OF MERCHANTABILITY OR FITNESS +FOR ANY PARTICULAR PURPOSE OR THAT THE USE OF PYTHON WILL NOT +INFRINGE ANY THIRD PARTY RIGHTS. + +5. PSF SHALL NOT BE LIABLE TO LICENSEE OR ANY OTHER USERS OF PYTHON +FOR ANY INCIDENTAL, SPECIAL, OR CONSEQUENTIAL DAMAGES OR LOSS AS +A RESULT OF MODIFYING, DISTRIBUTING, OR OTHERWISE USING PYTHON, +OR ANY DERIVATIVE THEREOF, EVEN IF ADVISED OF THE POSSIBILITY THEREOF. + +6. This License Agreement will automatically terminate upon a material +breach of its terms and conditions. + +7. Nothing in this License Agreement shall be deemed to create any +relationship of agency, partnership, or joint venture between PSF and +Licensee. This License Agreement does not grant permission to use PSF +trademarks or trade name in a trademark sense to endorse or promote +products or services of Licensee, or any third party. + +8. By copying, installing or otherwise using Python, Licensee +agrees to be bound by the terms and conditions of this License +Agreement. + + +BEOPEN.COM LICENSE AGREEMENT FOR PYTHON 2.0 +------------------------------------------- + +BEOPEN PYTHON OPEN SOURCE LICENSE AGREEMENT VERSION 1 + +1. This LICENSE AGREEMENT is between BeOpen.com ("BeOpen"), having an +office at 160 Saratoga Avenue, Santa Clara, CA 95051, and the +Individual or Organization ("Licensee") accessing and otherwise using +this software in source or binary form and its associated +documentation ("the Software"). + +2. Subject to the terms and conditions of this BeOpen Python License +Agreement, BeOpen hereby grants Licensee a non-exclusive, +royalty-free, world-wide license to reproduce, analyze, test, perform +and/or display publicly, prepare derivative works, distribute, and +otherwise use the Software alone or in any derivative version, +provided, however, that the BeOpen Python License is retained in the +Software, alone or in any derivative version prepared by Licensee. + +3. BeOpen is making the Software available to Licensee on an "AS IS" +basis. BEOPEN MAKES NO REPRESENTATIONS OR WARRANTIES, EXPRESS OR +IMPLIED. BY WAY OF EXAMPLE, BUT NOT LIMITATION, BEOPEN MAKES NO AND +DISCLAIMS ANY REPRESENTATION OR WARRANTY OF MERCHANTABILITY OR FITNESS +FOR ANY PARTICULAR PURPOSE OR THAT THE USE OF THE SOFTWARE WILL NOT +INFRINGE ANY THIRD PARTY RIGHTS. + +4. BEOPEN SHALL NOT BE LIABLE TO LICENSEE OR ANY OTHER USERS OF THE +SOFTWARE FOR ANY INCIDENTAL, SPECIAL, OR CONSEQUENTIAL DAMAGES OR LOSS +AS A RESULT OF USING, MODIFYING OR DISTRIBUTING THE SOFTWARE, OR ANY +DERIVATIVE THEREOF, EVEN IF ADVISED OF THE POSSIBILITY THEREOF. + +5. This License Agreement will automatically terminate upon a material +breach of its terms and conditions. + +6. This License Agreement shall be governed by and interpreted in all +respects by the law of the State of California, excluding conflict of +law provisions. Nothing in this License Agreement shall be deemed to +create any relationship of agency, partnership, or joint venture +between BeOpen and Licensee. This License Agreement does not grant +permission to use BeOpen trademarks or trade names in a trademark +sense to endorse or promote products or services of Licensee, or any +third party. As an exception, the "BeOpen Python" logos available at +http://www.pythonlabs.com/logos.html may be used according to the +permissions granted on that web page. + +7. By copying, installing or otherwise using the software, Licensee +agrees to be bound by the terms and conditions of this License +Agreement. + + +CNRI LICENSE AGREEMENT FOR PYTHON 1.6.1 +--------------------------------------- + +1. This LICENSE AGREEMENT is between the Corporation for National +Research Initiatives, having an office at 1895 Preston White Drive, +Reston, VA 20191 ("CNRI"), and the Individual or Organization +("Licensee") accessing and otherwise using Python 1.6.1 software in +source or binary form and its associated documentation. + +2. Subject to the terms and conditions of this License Agreement, CNRI +hereby grants Licensee a nonexclusive, royalty-free, world-wide +license to reproduce, analyze, test, perform and/or display publicly, +prepare derivative works, distribute, and otherwise use Python 1.6.1 +alone or in any derivative version, provided, however, that CNRI's +License Agreement and CNRI's notice of copyright, i.e., "Copyright (c) +1995-2001 Corporation for National Research Initiatives; All Rights +Reserved" are retained in Python 1.6.1 alone or in any derivative +version prepared by Licensee. Alternately, in lieu of CNRI's License +Agreement, Licensee may substitute the following text (omitting the +quotes): "Python 1.6.1 is made available subject to the terms and +conditions in CNRI's License Agreement. This Agreement together with +Python 1.6.1 may be located on the internet using the following +unique, persistent identifier (known as a handle): 1895.22/1013. This +Agreement may also be obtained from a proxy server on the internet +using the following URL: http://hdl.handle.net/1895.22/1013". + +3. In the event Licensee prepares a derivative work that is based on +or incorporates Python 1.6.1 or any part thereof, and wants to make +the derivative work available to others as provided herein, then +Licensee hereby agrees to include in any such work a brief summary of +the changes made to Python 1.6.1. + +4. CNRI is making Python 1.6.1 available to Licensee on an "AS IS" +basis. CNRI MAKES NO REPRESENTATIONS OR WARRANTIES, EXPRESS OR +IMPLIED. BY WAY OF EXAMPLE, BUT NOT LIMITATION, CNRI MAKES NO AND +DISCLAIMS ANY REPRESENTATION OR WARRANTY OF MERCHANTABILITY OR FITNESS +FOR ANY PARTICULAR PURPOSE OR THAT THE USE OF PYTHON 1.6.1 WILL NOT +INFRINGE ANY THIRD PARTY RIGHTS. + +5. CNRI SHALL NOT BE LIABLE TO LICENSEE OR ANY OTHER USERS OF PYTHON +1.6.1 FOR ANY INCIDENTAL, SPECIAL, OR CONSEQUENTIAL DAMAGES OR LOSS AS +A RESULT OF MODIFYING, DISTRIBUTING, OR OTHERWISE USING PYTHON 1.6.1, +OR ANY DERIVATIVE THEREOF, EVEN IF ADVISED OF THE POSSIBILITY THEREOF. + +6. This License Agreement will automatically terminate upon a material +breach of its terms and conditions. + +7. This License Agreement shall be governed by the federal +intellectual property law of the United States, including without +limitation the federal copyright law, and, to the extent such +U.S. federal law does not apply, by the law of the Commonwealth of +Virginia, excluding Virginia's conflict of law provisions. +Notwithstanding the foregoing, with regard to derivative works based +on Python 1.6.1 that incorporate non-separable material that was +previously distributed under the GNU General Public License (GPL), the +law of the Commonwealth of Virginia shall govern this License +Agreement only as to issues arising under or with respect to +Paragraphs 4, 5, and 7 of this License Agreement. Nothing in this +License Agreement shall be deemed to create any relationship of +agency, partnership, or joint venture between CNRI and Licensee. This +License Agreement does not grant permission to use CNRI trademarks or +trade name in a trademark sense to endorse or promote products or +services of Licensee, or any third party. + +8. By clicking on the "ACCEPT" button where indicated, or by copying, +installing or otherwise using Python 1.6.1, Licensee agrees to be +bound by the terms and conditions of this License Agreement. + + ACCEPT + + +CWI LICENSE AGREEMENT FOR PYTHON 0.9.0 THROUGH 1.2 +-------------------------------------------------- + +Copyright (c) 1991 - 1995, Stichting Mathematisch Centrum Amsterdam, +The Netherlands. All rights reserved. + +Permission to use, copy, modify, and distribute this software and its +documentation for any purpose and without fee is hereby granted, +provided that the above copyright notice appear in all copies and that +both that copyright notice and this permission notice appear in +supporting documentation, and that the name of Stichting Mathematisch +Centrum or CWI not be used in advertising or publicity pertaining to +distribution of the software without specific, written prior +permission. + +STICHTING MATHEMATISCH CENTRUM DISCLAIMS ALL WARRANTIES WITH REGARD TO +THIS SOFTWARE, INCLUDING ALL IMPLIED WARRANTIES OF MERCHANTABILITY AND +FITNESS, IN NO EVENT SHALL STICHTING MATHEMATISCH CENTRUM BE LIABLE +FOR ANY SPECIAL, INDIRECT OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES +WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN +ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT +OF OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE. + +ZERO-CLAUSE BSD LICENSE FOR CODE IN THE PYTHON DOCUMENTATION +---------------------------------------------------------------------- + +Permission to use, copy, modify, and/or distribute this software for any +purpose with or without fee is hereby granted. + +THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES WITH +REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF MERCHANTABILITY +AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR ANY SPECIAL, DIRECT, +INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES WHATSOEVER RESULTING FROM +LOSS OF USE, DATA OR PROFITS, WHETHER IN AN ACTION OF CONTRACT, NEGLIGENCE OR +OTHER TORTIOUS ACTION, ARISING OUT OF OR IN CONNECTION WITH THE USE OR +PERFORMANCE OF THIS SOFTWARE. diff --git a/tests/fixtures/real-world-locks/requirements/home-assistant-core-2026.9.0/pyproject.toml b/tests/fixtures/real-world-locks/requirements/home-assistant-core-2026.9.0/pyproject.toml new file mode 100644 index 00000000..f3183027 --- /dev/null +++ b/tests/fixtures/real-world-locks/requirements/home-assistant-core-2026.9.0/pyproject.toml @@ -0,0 +1,883 @@ +[build-system] +requires = ["setuptools==78.1.1"] +build-backend = "setuptools.build_meta" + +[project] +name = "homeassistant" +version = "2026.9.0" +license = "Apache-2.0" +license-files = ["LICENSE*", "homeassistant/backports/LICENSE*"] +description = "Open-source home automation platform running on Python 3." +readme = "README.rst" +authors = [ + { name = "The Home Assistant Authors", email = "hello@home-assistant.io" }, +] +keywords = ["home", "automation"] +classifiers = [ + "Development Status :: 5 - Production/Stable", + "Intended Audience :: End Users/Desktop", + "Intended Audience :: Developers", + "Operating System :: OS Independent", + "Programming Language :: Python :: 3.14", + "Topic :: Home Automation", +] +requires-python = ">=3.14.2" +dependencies = [ + "aiodns==4.0.4", + # aiogithubapi is needed by frontend; frontend is unconditionally imported at + # module level in `bootstrap.py` and its requirements thus need to be in + # requirements.txt to ensure they are always installed + "aiogithubapi==26.0.0", + "aiohttp==3.14.3", + "aiohttp_cors==0.8.1", + "aiohttp-fast-zlib==0.3.0", + "aiohttp-asyncmdnsresolver==0.2.0", + "aiozoneinfo==0.2.3", + "annotatedyaml==1.0.2", + "astral==2.2", + "async-interrupt==1.2.2", + "attrs==26.1.0", + "atomicwrites-homeassistant==1.4.1", + "audioop-lts==0.2.2", + "awesomeversion==25.8.0", + "bcrypt==5.0.0", + "certifi>=2021.5.30", + "ciso8601==2.3.3", + "cronsim==2.7", + "fnv-hash-fast==2.0.3", + # hass-nabucasa is imported by helpers which don't depend on the cloud + # integration + "hass-nabucasa==2.7.0", + # When bumping httpx, please check the version pins of + # httpcore, anyio, and h11 in gen_requirements_all + "httpx==0.28.1", + "home-assistant-bluetooth==2.0.0", + "ifaddr==0.2.0", + "Jinja2==3.1.6", + "lru-dict==1.4.1", + "PyJWT==2.13.0", + # PyJWT has loose dependency. We want the latest one. + "cryptography==48.0.1", + "Pillow==12.3.0", + "propcache==0.5.2", + "pyOpenSSL==26.2.0", + "orjson==3.12.0", + "packaging>=23.1", + "psutil-home-assistant==0.0.1", + "python-slugify==8.0.4", + "PyYAML==6.0.3", + "requests==2.34.2", + "securetar==2026.4.1", + "SQLAlchemy==2.0.52", + "standard-aifc==3.13.0", + "standard-telnetlib==3.13.0", + "typing-extensions>=4.16.0,<5.0", + "ulid-transform==2.2.9", + "urllib3>=2.0", + "uv==0.12.5", + "probatio==0.11.4", + "yarl==1.24.5", + "webrtc-models==0.3.0", + "zeroconf==0.151.1", +] + +[project.urls] +"Homepage" = "https://www.home-assistant.io/" +"Source Code" = "https://github.com/home-assistant/core" +"Bug Reports" = "https://github.com/home-assistant/core/issues" +"Docs: Dev" = "https://developers.home-assistant.io/" +"Discord" = "https://www.home-assistant.io/join-chat/" +"Forum" = "https://community.home-assistant.io/" + +[project.scripts] +hass = "homeassistant.__main__:main" + +[tool.setuptools] +include-package-data = true + +[tool.setuptools.packages.find] +include = ["homeassistant*"] + +[tool.pylint.MAIN] +py-version = "3.14" +# Use a conservative default here; 2 should speed up most setups and not hurt +# any too bad. Override on command line as appropriate. +jobs = 2 +init-hook = """\ + from pathlib import Path; \ + import sys; \ + + from pylint.config import find_default_config_files; \ + + sys.path.append( \ + str(Path(next(find_default_config_files())).parent.joinpath('pylint/plugins')) + ) \ + """ +load-plugins = [ + "pylint.extensions.code_style", + "pylint.extensions.typing", + "pylint_home_assistant", + "pylint_per_file_ignores", +] +persistent = false +extension-pkg-allow-list = [ + "av.audio.stream", + "av.logging", + "av.stream", + "ciso8601", + "orjson", + "cv2", +] +fail-on = ["I"] + +[tool.pylint.BASIC] +class-const-naming-style = "any" + +[tool.pylint."MESSAGES CONTROL"] +# Reasons disabled: +# format - handled by ruff +# locally-disabled - it spams too much +# duplicate-code - unavoidable +# cyclic-import - doesn't test if both import on load +# abstract-class-little-used - prevents from setting right foundation +# unused-argument - generic callbacks and setup methods create a lot of warnings +# too-many-* - are not enforced for the sake of readability +# too-few-* - same as too-many-* +# abstract-method - with intro of async there are always methods missing +# inconsistent-return-statements - doesn't handle raise +# too-many-ancestors - it's too strict. +# wrong-import-order - isort guards this +# possibly-used-before-assignment - too many errors / not necessarily issues +# --- +# Pylint CodeStyle plugin +# consider-using-namedtuple-or-dataclass - too opinionated +# consider-using-assignment-expr - decision to use := better left to devs +disable = [ + "format", + "abstract-method", + "cyclic-import", + "duplicate-code", + "inconsistent-return-statements", + "locally-disabled", + "not-context-manager", + "too-few-public-methods", + "too-many-ancestors", + "too-many-arguments", + "too-many-instance-attributes", + "too-many-lines", + "too-many-locals", + "too-many-public-methods", + "too-many-boolean-expressions", + "too-many-positional-arguments", + "wrong-import-order", + "consider-using-namedtuple-or-dataclass", + "consider-using-assignment-expr", + "possibly-used-before-assignment", + + # Disabled while existing violations are being cleaned up + "home-assistant-unused-test-fixture-argument", + + # Handled by ruff + # Ref: + "await-outside-async", # PLE1142 + "bad-str-strip-call", # PLE1310 + "bad-string-format-type", # PLE1307 + "bidirectional-unicode", # PLE2502 + "continue-in-finally", # PLE0116 + "duplicate-bases", # PLE0241 + "misplaced-bare-raise", # PLE0704 + "format-needs-mapping", # F502 + "function-redefined", # F811 + # Needed because ruff does not understand type of __all__ generated by a function + # "invalid-all-format", # PLE0605 + "invalid-all-object", # PLE0604 + "invalid-character-backspace", # PLE2510 + "invalid-character-esc", # PLE2513 + "invalid-character-nul", # PLE2514 + "invalid-character-sub", # PLE2512 + "invalid-character-zero-width-space", # PLE2515 + "logging-too-few-args", # PLE1206 + "logging-too-many-args", # PLE1205 + "missing-format-string-key", # F524 + "mixed-format-string", # F506 + "no-method-argument", # N805 + "no-self-argument", # N805 + "nonexistent-operator", # B002 + "nonlocal-without-binding", # PLE0117 + "not-in-loop", # F701, F702 + "notimplemented-raised", # F901 + "return-in-init", # PLE0101 + "return-outside-function", # F706 + "syntax-error", # E999 + "too-few-format-args", # F524 + "too-many-format-args", # F522 + "too-many-star-expressions", # F622 + "truncated-format-string", # F501 + "undefined-all-variable", # F822 + "undefined-variable", # F821 + "used-prior-global-declaration", # PLE0118 + "yield-inside-async-function", # PLE1700 + "yield-outside-function", # F704 + "anomalous-backslash-in-string", # W605 + "assert-on-string-literal", # PLW0129 + "assert-on-tuple", # F631 + "bad-format-string", # W1302, F + "bad-format-string-key", # W1300, F + "bare-except", # E722 + "binary-op-exception", # PLW0711 + "cell-var-from-loop", # B023 + # "dangerous-default-value", # B006, ruff catches new occurrences, needs more work + "duplicate-except", # B014 + "duplicate-key", # F601 + "duplicate-string-formatting-argument", # F + "duplicate-value", # F + "eval-used", # S307 + "exec-used", # S102 + "expression-not-assigned", # B018 + "f-string-without-interpolation", # F541 + "forgotten-debug-statement", # T100 + "format-string-without-interpolation", # F + # "global-statement", # PLW0603, ruff catches new occurrences, needs more work + "global-variable-not-assigned", # PLW0602 + "implicit-str-concat", # ISC001 + "import-outside-toplevel", # PLC0415 + "import-self", # PLW0406 + "inconsistent-quotes", # Q000 + "invalid-envvar-default", # PLW1508 + "keyword-arg-before-vararg", # B026 + "logging-format-interpolation", # G + "logging-fstring-interpolation", # G + "logging-not-lazy", # G + "misplaced-future", # F404 + "named-expr-without-context", # PLW0131 + "nested-min-max", # PLW3301 + "pointless-statement", # B018 + "raise-missing-from", # B904 + "redefined-builtin", # A001 + "try-except-raise", # TRY302 + "unused-argument", # ARG001, we don't use it + "unused-format-string-argument", #F507 + "unused-format-string-key", # F504 + "unused-import", # F401 + "unused-variable", # F841 + "useless-else-on-loop", # PLW0120 + "wildcard-import", # F403 + "bad-classmethod-argument", # N804 + "consider-iterating-dictionary", # SIM118 + "empty-docstring", # D419 + "invalid-name", # N815 + "line-too-long", # E501, disabled globally + "missing-class-docstring", # D101 + "missing-final-newline", # W292 + "missing-function-docstring", # D103 + "missing-module-docstring", # D100 + "multiple-imports", #E401 + "singleton-comparison", # E711, E712 + "subprocess-run-check", # PLW1510 + "superfluous-parens", # UP034 + "ungrouped-imports", # I001 + "unidiomatic-typecheck", # E721 + "unnecessary-direct-lambda-call", # PLC3002 + "unnecessary-lambda-assignment", # PLC3001 + "unnecessary-pass", # PIE790 + "unneeded-not", # SIM208 + "useless-import-alias", # PLC0414 + "wrong-import-order", # I001 + "wrong-import-position", # E402 + "comparison-of-constants", # PLR0133 + "comparison-with-itself", # PLR0124 + "consider-alternative-union-syntax", # UP007 + "consider-merging-isinstance", # PLR1701 + "consider-using-alias", # UP006 + "consider-using-dict-comprehension", # C402 + "consider-using-generator", # C417 + "consider-using-get", # SIM401 + "consider-using-set-comprehension", # C401 + "consider-using-sys-exit", # PLR1722 + "consider-using-ternary", # SIM108 + "literal-comparison", # F632 + "property-with-parameters", # PLR0206 + "super-with-arguments", # UP008 + "too-many-branches", # PLR0912 + "too-many-return-statements", # PLR0911 + "too-many-statements", # PLR0915 + "trailing-comma-tuple", # COM818 + "unnecessary-comprehension", # C416 + "use-a-generator", # C417 + "use-dict-literal", # C406 + "use-list-literal", # C405 + "useless-object-inheritance", # UP004 + "useless-return", # PLR1711 + "no-else-break", # RET508 + "no-else-continue", # RET507 + "no-else-raise", # RET506 + "no-else-return", # RET505 + "broad-except", # BLE001 + "protected-access", # SLF001 + "broad-exception-raised", # TRY002 + "consider-using-f-string", # PLC0209 + # "no-self-use", # PLR6301 # Optional plugin, not enabled + + # Handled by mypy + # Ref: + "abstract-class-instantiated", + "arguments-differ", + "assigning-non-slot", + "assignment-from-no-return", + "assignment-from-none", + "bad-exception-cause", + "bad-format-character", + "bad-reversed-sequence", + "bad-super-call", + "bad-thread-instantiation", + "catching-non-exception", + "comparison-with-callable", + "deprecated-class", + "dict-iter-missing-items", + "format-combined-specification", + "global-variable-undefined", + "import-error", + "inconsistent-mro", + "inherit-non-class", + "init-is-generator", + "invalid-class-object", + "invalid-enum-extension", + "invalid-envvar-value", + "invalid-format-returned", + "invalid-hash-returned", + "invalid-metaclass", + "invalid-overridden-method", + "invalid-repr-returned", + "invalid-sequence-index", + "invalid-slice-index", + "invalid-slots-object", + "invalid-slots", + "invalid-star-assignment-target", + "invalid-str-returned", + "invalid-unary-operand-type", + "invalid-unicode-codec", + "isinstance-second-argument-not-valid-type", + "method-hidden", + "misplaced-format-function", + "missing-format-argument-key", + "missing-format-attribute", + "missing-kwoa", + "no-member", + "no-value-for-parameter", + "non-iterator-returned", + "non-str-assignment-to-dunder-name", + "nonlocal-and-global", + "not-a-mapping", + "not-an-iterable", + "not-async-context-manager", + "not-callable", + "not-context-manager", + "overridden-final-method", + "raising-bad-type", + "raising-non-exception", + "redundant-keyword-arg", + "relative-beyond-top-level", + "self-cls-assignment", + "signature-differs", + "star-needs-assignment-target", + "subclassed-final-class", + "super-without-brackets", + "too-many-function-args", + "typevar-double-variance", + "typevar-name-mismatch", + "unbalanced-dict-unpacking", + "unbalanced-tuple-unpacking", + "unexpected-keyword-arg", + "unhashable-member", + "unpacking-non-sequence", + "unsubscriptable-object", + "unsupported-assignment-operation", + "unsupported-binary-operation", + "unsupported-delete-operation", + "unsupported-membership-test", + "used-before-assignment", + "using-final-decorator-in-unsupported-version", + "wrong-exception-operation", +] +enable = [ + #"useless-suppression", # temporarily every now and then to clean them up + "use-symbolic-message-instead", +] +per-file-ignores = [ + # redefined-outer-name: Tests reference fixtures in the test function + # use-implicit-booleaness-not-comparison: Tests need to validate that a list + # or a dict is returned + "tests/**:redefined-outer-name,use-implicit-booleaness-not-comparison", +] + +[tool.pylint.REPORTS] +score = false + +[tool.pylint.TYPECHECK] +ignored-classes = [ + "_CountingAttr", # for attrs +] +mixin-class-rgx = ".*[Mm]ix[Ii]n" + +[tool.pylint.FORMAT] +expected-line-ending-format = "LF" + +[tool.pylint.EXCEPTIONS] +overgeneral-exceptions = [ + "builtins.BaseException", + "builtins.Exception", + # "homeassistant.exceptions.HomeAssistantError", # too many issues +] + +[tool.pylint.TYPING] +runtime-typing = false + +[tool.pylint.CODE_STYLE] +max-line-length-suggestions = 72 + +[tool.pytest.ini_options] +pythonpath = ["pylint/plugins"] +testpaths = ["tests"] +norecursedirs = [".git", "testing_config"] +log_format = "%(asctime)s.%(msecs)03d %(levelname)-8s %(threadName)s %(name)s:%(filename)s:%(lineno)s %(message)s" +log_date_format = "%Y-%m-%d %H:%M:%S" +asyncio_debug = true +asyncio_mode = "auto" +asyncio_default_fixture_loop_scope = "function" +filterwarnings = [ + "error::sqlalchemy.exc.SAWarning", + "error:usefixtures\\(\\) in .* without arguments has no effect:UserWarning", # pytest + + # -- HomeAssistant - aiohttp + # Overwrite web.Application to pass a custom default argument to _make_request + "ignore:Inheritance class HomeAssistantApplication from web.Application is discouraged:DeprecationWarning", + # Hass wraps `ClientSession.close` to emit a warning if the session is closed accidentally + "ignore:Setting custom ClientSession.close attribute is discouraged:DeprecationWarning:homeassistant.helpers.aiohttp_client", + # Modify app state for testing + "ignore:Changing state of started or joined application is deprecated:DeprecationWarning:tests.components.http.test_ban", + + # -- HomeAssistant - design choice + # airOS 6 firmware negotiates nothing newer than TLS 1.0 + "ignore:ssl.TLSVersion.TLSv1 is deprecated:DeprecationWarning:homeassistant.components.airos.helpers", + + # -- DeprecationWarning already fixed in our codebase + # https://github.com/kurtmckee/feedparser/ - 6.0.12 + "ignore:.*a temporary mapping .* from `updated_parsed` to `published_parsed` if `updated_parsed` doesn't exist:DeprecationWarning:feedparser.util", + + # -- design choice 3rd party + # https://github.com/gwww/elkm1/blob/2.2.13/elkm1_lib/util.py#L8-L19 + "ignore:ssl.TLSVersion.TLSv1 is deprecated:DeprecationWarning:elkm1_lib.util", + # https://github.com/bachya/regenmaschine/blob/2024.03.0/regenmaschine/client.py#L52 + "ignore:ssl.TLSVersion.SSLv3 is deprecated:DeprecationWarning:regenmaschine.client", + + # -- Setuptools DeprecationWarnings + # https://github.com/Azure/azure-sdk-for-python + "ignore:Deprecated call to `pkg_resources.declare_namespace\\('azure'\\)`:DeprecationWarning:pkg_resources", + + # -- tracked upstream / open PRs + # https://github.com/hacf-fr/meteofrance-api/pull/688 - v1.4.0 - 2025-03-26 + "ignore:datetime.*utcnow\\(\\) is deprecated and scheduled for removal:DeprecationWarning:meteofrance_api.model.forecast", + + # -- fixed, waiting for release / update + # https://github.com/httplib2/httplib2/pull/226 - >=0.21.0 + "ignore:ssl.PROTOCOL_TLS is deprecated:DeprecationWarning:httplib2", + # https://github.com/httplib2/httplib2/pull/253 - >=0.31.1 + "ignore:'(addParseAction|delimitedList|leaveWhitespace|setName|setParseAction)' deprecated:DeprecationWarning:httplib2.auth", + # https://github.com/lawtancool/pyControl4/pull/47 - >=1.6.0 + "ignore:with timeout\\(\\) is deprecated, use async with timeout\\(\\) instead:DeprecationWarning:pyControl4.account", + # https://pypi.org/project/pyqwikswitch/ - >=1.0 + "ignore:client.loop property is deprecated:DeprecationWarning:pyqwikswitch.async_", + "ignore:with timeout\\(\\) is deprecated:DeprecationWarning:pyqwikswitch.async_", + # https://github.com/rytilahti/python-miio/pull/1809 - >=0.6.0.dev0 + "ignore:datetime.*utcnow\\(\\) is deprecated and scheduled for removal:DeprecationWarning:miio.protocol", + "ignore:datetime.*utcnow\\(\\) is deprecated and scheduled for removal:DeprecationWarning:miio.miioprotocol", + # https://github.com/rytilahti/python-miio/pull/1993 - >0.6.0.dev0 + "ignore:functools.partial will be a method descriptor in future Python versions; wrap it in enum.member\\(\\) if you want to preserve the old behavior:FutureWarning:miio.miot_device", + # https://github.com/pyusb/pyusb/pull/545 - >1.3.1 + "ignore:Due to '_pack_', the '.*' Structure will use memory layout compatible with MSVC:DeprecationWarning:usb.backend.libusb0", + # https://github.com/xchwarze/samsung-tv-ws-api/pull/151 - >=3.0.0 - 2024-12-06 # wrong stacklevel in aiohttp + "ignore:verify_ssl is deprecated, use ssl=False instead:DeprecationWarning:aiohttp.client", + + # -- other + # Locale changes might take some time to resolve upstream + # https://github.com/Squachen/micloud/blob/v_0.6/micloud/micloud.py#L35 - v0.6 - 2022-12-08 + "ignore:'locale.getdefaultlocale' is deprecated and slated for removal in Python 3.15:DeprecationWarning:micloud.micloud", + # https://pypi.org/project/agent-py/ - v0.0.24 - 2024-11-07 + "ignore:with timeout\\(\\) is deprecated:DeprecationWarning:agent.a", + # https://github.com/MatsNl/pyatag/issues/11 - v0.3.7.1 - 2023-10-09 + "ignore:datetime.*utcnow\\(\\) is deprecated and scheduled for removal:DeprecationWarning:pyatag.gateway", + # https://github.com/lidatong/dataclasses-json/issues/328 + # https://github.com/lidatong/dataclasses-json/pull/351 + "ignore:The 'default' argument to fields is deprecated. Use 'dump_default' instead:DeprecationWarning:dataclasses_json.mm", + # https://pypi.org/project/emulated-roku/ - v0.3.0 - 2023-12-19 + # https://github.com/martonperei/emulated_roku + "ignore:loop argument is deprecated:DeprecationWarning:emulated_roku", + # https://github.com/EnergieID/energyid-webhooks-py/ - v0.0.14 - 2025-05-06 + "ignore:The V1 WebhookClient is deprecated:DeprecationWarning:energyid_webhooks", + # https://pypi.org/project/foobot_async/ - v1.0.1 - 2024-08-16 + "ignore:with timeout\\(\\) is deprecated:DeprecationWarning:foobot_async", + # https://pypi.org/project/motionblindsble/ - v0.1.3 - 2024-11-12 + # https://github.com/LennP/motionblindsble/blob/0.1.3/motionblindsble/device.py#L390 + "ignore:Passing additional arguments for BLEDevice is deprecated and has no effect:DeprecationWarning:motionblindsble.device", + # https://github.com/thecynic/pylutron - v0.2.18 - 2025-04-15 + "ignore:setDaemon\\(\\) is deprecated, set the daemon attribute instead:DeprecationWarning:pylutron", + # https://pypi.org/project/PyMetEireann/ - v2024.11.0 - 2024-11-23 + "ignore:datetime.*utcnow\\(\\) is deprecated and scheduled for removal:DeprecationWarning:meteireann", + # https://github.com/pschmitt/pynuki/blob/1.6.3/pynuki/utils.py#L21 - v1.6.3 - 2024-02-24 + "ignore:datetime.*utcnow\\(\\) is deprecated and scheduled for removal:DeprecationWarning:pynuki.utils", + # https://github.com/lextudio/pysnmp/blob/v7.1.21/pysnmp/smi/compiler.py#L23-L31 - v7.1.21 - 2025-06-19 + "ignore:smiV1Relaxed is deprecated. Please use smi_v1_relaxed instead:DeprecationWarning:pysnmp.smi.compiler", + "ignore:getReadersFromUrls is deprecated. Please use get_readers_from_urls instead:DeprecationWarning:pysnmp.smi.compiler", + # https://github.com/frenck/python-radios/blob/v0.3.2/src/radios/radio_browser.py#L76 - v0.3.2 - 2024-10-26 + "ignore:query\\(\\) is deprecated, use query_dns\\(\\) instead:DeprecationWarning:radios.radio_browser", + # https://github.com/python-telegram-bot/python-telegram-bot/blob/v22.7/src/telegram/error.py#L243 - 22.7 - 2026-03-16 + "ignore:Deprecated since version v22.2.*attribute `retry_after` will be of type `datetime.timedelta`:DeprecationWarning:telegram.error", + # https://github.com/briis/pyweatherflowudp/blob/v1.4.5/pyweatherflowudp/const.py#L20 - v1.4.5 - 2023-10-10 + "ignore:This function will be removed in future versions of pint:DeprecationWarning:pyweatherflowudp.const", + # - SyntaxWarnings - invalid escape sequence + # https://pypi.org/project/aprslib/ - v0.7.2 - 2022-07-10 + "ignore:.*invalid escape sequence:SyntaxWarning:.*aprslib.parsing.common", + "ignore:datetime.*utcnow\\(\\) is deprecated and scheduled for removal:DeprecationWarning:aprslib.parsing.common", + # https://pypi.org/project/panasonic-viera/ - v0.4.4 - 2025-11-25 + # https://github.com/florianholzapfel/panasonic-viera/blob/0.4.4/panasonic_viera/remote_control.py#L665 + "ignore:.*invalid escape sequence:SyntaxWarning:.*panasonic_viera", + # https://pypi.org/project/pyblackbird/ - v0.6 - 2023-03-15 + # https://github.com/koolsb/pyblackbird/pull/9 -> closed + "ignore:.*invalid escape sequence:SyntaxWarning:.*pyblackbird", + # https://pypi.org/project/pyws66i/ - v1.1 - 2022-04-05 + "ignore:.*invalid escape sequence:SyntaxWarning:.*pyws66i", + # https://pypi.org/project/sanix/ - v1.0.6 - 2024-05-01 + # https://github.com/tomaszsluszniak/sanix_py/blob/v1.0.6/sanix/__init__.py#L42 + "ignore:.*invalid escape sequence:SyntaxWarning:.*sanix", + # https://pypi.org/project/sleekxmppfs/ - v1.4.1 - 2022-08-18 + "ignore:.*invalid escape sequence:SyntaxWarning:.*sleekxmppfs.thirdparty.mini_dateutil", # codespell:ignore thirdparty + # - SyntaxWarning - is with literal + # https://github.com/majuss/lupupy/pull/15 - >0.3.2 + "ignore:\"is.*\" with '.*' literal:SyntaxWarning:.*lupupy.devices.alarm", + # https://pypi.org/project/opuslib/ - v3.0.1 - 2018-01-16 + "ignore:\"is.*\" with '.*' literal:SyntaxWarning:.*opuslib.api.decoder", + # https://pypi.org/project/pyiss/ - v1.0.1 - 2016-12-19 + "ignore:\"is.*\" with '.*' literal:SyntaxWarning:.*pyiss", + # - SyntaxWarning - return in finally + # https://github.com/nextcord/nextcord/pull/1268 - >3.1.1 - 2025-08-16 + "ignore:'return' in a 'finally' block:SyntaxWarning:.*nextcord.(gateway|player)", + # https://pypi.org/project/sleekxmppfs/ - v1.4.1 - 2022-08-18 + "ignore:'return' in a 'finally' block:SyntaxWarning:.*sleekxmppfs.(roster.single|xmlstream.xmlstream)", + + # -- New in Python 3.13 + # https://github.com/youknowone/python-deadlib - Backports for aifc, telnetlib + "ignore:aifc was removed in Python 3.13.*'standard-aifc':DeprecationWarning:speech_recognition", + "ignore:telnetlib was removed in Python 3.13.*'standard-telnetlib':DeprecationWarning:homeassistant.components.hddtemp.sensor", + "ignore:telnetlib was removed in Python 3.13.*'standard-telnetlib':DeprecationWarning:ndms2_client.connection", + "ignore:telnetlib was removed in Python 3.13.*'standard-telnetlib':DeprecationWarning:pyws66i", + + # -- New in Python 3.14 + # https://github.com/litl/backoff/pull/220 - v2.2.1 - 2022-10-05 (archived) + "ignore:'asyncio.iscoroutinefunction' is deprecated and slated for removal in Python 3.16:DeprecationWarning:(backoff._decorator|backoff._async)", + # https://github.com/albertogeniola/elmax-api - v0.0.6.3 - 2024-11-30 + "ignore:'asyncio.iscoroutinefunction' is deprecated and slated for removal in Python 3.16:DeprecationWarning:elmax_api.http", + # https://github.com/nextcord/nextcord/pull/1269 - >3.1.1 - 2025-08-16 + "ignore:'asyncio.iscoroutinefunction' is deprecated and slated for removal in Python 3.16:DeprecationWarning:nextcord.member", + # https://github.com/svinota/pyroute2 + "ignore:Due to '_pack_', the '.*' Structure will use memory layout compatible with MSVC:DeprecationWarning:pyroute2.ethtool.ioctl", + # https://github.com/googleapis/python-genai + "ignore:Inheritance class AiohttpClientSession from ClientSession is discouraged:DeprecationWarning:google.genai._api_client", + "ignore:'_UnionGenericAlias' is deprecated and slated for removal in Python 3.17:DeprecationWarning:google.genai.types", + + # -- Websockets 14.1 + # https://websockets.readthedocs.io/en/stable/howto/upgrade.html + "ignore:websockets.legacy is deprecated:DeprecationWarning:websockets.legacy", + + # -- unmaintained projects, last release about 2+ years + # https://pypi.org/project/colorthief/ - v0.2.1 - 2017-02-09 + "ignore:Image.Image.getdata is deprecated and will be removed in Pillow 14.* Use get_flattened_data instead:DeprecationWarning:colorthief", + # https://pypi.org/project/directv/ - v0.4.0 - 2020-09-12 + "ignore:with timeout\\(\\) is deprecated:DeprecationWarning:directv.directv", + "ignore:datetime.*utcnow\\(\\) is deprecated and scheduled for removal:DeprecationWarning:directv.models", + # https://pypi.org/project/enocean/ - v0.50.1 (installed) -> v0.60.1 - 2021-06-18 + "ignore:It looks like you're using an HTML parser to parse an XML document:UserWarning:enocean.protocol.eep", + # https://pypi.org/project/influxdb/ - v5.3.2 - 2024-04-18 (archived) + "ignore:datetime.*utcfromtimestamp\\(\\) is deprecated and scheduled for removal:DeprecationWarning:influxdb.line_protocol", + # https://pypi.org/project/lark-parser/ - v0.12.0 - 2021-08-30 -> moved to `lark` + # https://pypi.org/project/commentjson/ - v0.9.0 - 2020-10-05 + # https://github.com/vaidik/commentjson/issues/51 + # https://github.com/vaidik/commentjson/pull/52 + # Fixed upstream, commentjson depends on old version and seems to be unmaintained + "ignore:module '(sre_parse|sre_constants)' is deprecate:DeprecationWarning:lark.utils", + # https://pypi.org/project/lomond/ - v0.3.3 - 2018-09-21 + "ignore:ssl.PROTOCOL_TLS is deprecated:DeprecationWarning:lomond.session", + # https://pypi.org/project/oauth2client/ - v4.1.3 - 2018-09-07 (archived) + "ignore:datetime.*utcnow\\(\\) is deprecated and scheduled for removal:DeprecationWarning:oauth2client.client", + # https://pypi.org/project/pilight/ - v0.1.1 - 2016-10-19 + "ignore:pkg_resources is deprecated as an API:UserWarning:pilight", + # https://pypi.org/project/pure-python-adb/ - v0.3.0.dev0 - 2020-08-05 + "ignore:.*invalid escape sequence:SyntaxWarning:.*ppadb", + # https://pypi.org/project/pydub/ - v0.25.1 - 2021-03-10 + "ignore:.*invalid escape sequence:SyntaxWarning:.*pydub.utils", + # https://pypi.org/project/PyPasser/ - v0.0.5 - 2021-10-21 + "ignore:.*invalid escape sequence:SyntaxWarning:.*pypasser.utils", + # https://pypi.org/project/rxv/ - v0.7.0 - 2021-10-10 + "ignore:defusedxml.cElementTree is deprecated, import from defusedxml.ElementTree instead:DeprecationWarning:rxv.ssdp", +] + +[tool.coverage.run] +source = ["homeassistant"] + +[tool.coverage.report] +exclude_lines = [ + # Have to re-enable the standard pragma + "pragma: no cover", + # Don't complain about missing debug-only code: + "def __repr__", + # Don't complain if tests don't hit defensive assertion code: + "raise AssertionError", + "raise NotImplementedError", + # TYPE_CHECKING and @overload blocks are never executed during pytest run + "if TYPE_CHECKING:", + "@overload", +] + +[tool.ruff] +required-version = ">=0.16.3" + +[tool.ruff.lint] +select = [ + "A001", # Variable {name} is shadowing a Python builtin + "ASYNC", # flake8-async + "B", # flake8-bugbear + "BLE", + "C", # complexity + "COM818", # Trailing comma on bare tuple prohibited + "D", # docstrings + "DTZ003", # Use datetime.now(tz=) instead of datetime.utcnow() + "DTZ004", # Use datetime.fromtimestamp(ts, tz=) instead of datetime.utcfromtimestamp(ts) + "DTZ011", # Use datetime.now(tz=).date() instead of date.today() + "E", # pycodestyle + "F", # pyflakes/autoflake + "F541", # f-string without any placeholders + "FLY", # flynt + "FURB", # refurb + "G", # flake8-logging-format + "I", # isort + "INP", # flake8-no-pep420 + "ISC", # flake8-implicit-str-concat + "ICN001", # import concentions; {name} should be imported as {asname} + "LOG", # flake8-logging + "N804", # First argument of a class method should be named cls + "N805", # First argument of a method should be named self + "N806", # Variable {name} in function should be snake_case + "N815", # Variable {name} in class scope should not be mixedCase + "PERF", # Perflint + "PGH", # pygrep-hooks + "PIE", # flake8-pie + "PL", # pylint + "PT", # flake8-pytest-style + "PTH", # flake8-pathlib + "PYI", # flake8-pyi + "RET", # flake8-return + "RSE", # flake8-raise + "RUF", # Ruff-specific rules (see `ignore` for exclusions) + "S107", # Possible hardcoded password assigned to function default + "S102", # Use of exec detected + "S103", # bad-file-permissions + "S108", # hardcoded-temp-file + "S301", # suspicious-pickle-usage + "S306", # suspicious-mktemp-usage + "S307", # suspicious-eval-usage + "S313", # suspicious-xmlc-element-tree-usage + "S314", # suspicious-xml-element-tree-usage + "S315", # suspicious-xml-expat-reader-usage + "S316", # suspicious-xml-expat-builder-usage + "S317", # suspicious-xml-sax-usage + "S318", # suspicious-xml-mini-dom-usage + "S319", # suspicious-xml-pull-dom-usage + "S601", # paramiko-call + "S602", # subprocess-popen-with-shell-equals-true + "S604", # call-with-shell-equals-true + "S608", # hardcoded-sql-expression + "S609", # unix-command-wildcard-injection + "SIM", # flake8-simplify + "SLF", # flake8-self + "SLOT", # flake8-slots + "T100", # Trace found: {name} used + "T20", # flake8-print + "TC", # flake8-type-checking + "TID", # Tidy imports + "TRY", # tryceratops + "UP", # pyupgrade + "UP031", # Use format specifiers instead of percent format + "UP032", # Use f-string instead of `format` call + "W", # pycodestyle +] + +ignore = [ + "ASYNC109", # Async function definition with a `timeout` parameter Use `asyncio.timeout` instead + "ASYNC110", # Use `asyncio.Event` instead of awaiting `asyncio.sleep` in a `while` loop + "ASYNC240", # Use an async function for entering the file system + "B008", # Do not perform function call in argument defaults; commonly used in Home Assistant (e.g. cv.* validators) + "B019", # Use of functools.lru_cache or functools.cache on methods can lead to memory leaks + "D202", # No blank lines allowed after function docstring + "D203", # 1 blank line required before class docstring + "D213", # Multi-line docstring summary should start at the second line + "D406", # Section name should end with a newline + "D407", # Section name underlining + "D417", # Missing argument descriptions in docstring - to allow documenting only non-obvious parameters + "E501", # line too long + + "PLC1901", # {existing} can be simplified to {replacement} as an empty string is falsey; too many false positives + "PLR0911", # Too many return statements ({returns} > {max_returns}) + "PLR0912", # Too many branches ({branches} > {max_branches}) + "PLR0913", # Too many arguments to function call ({c_args} > {max_args}) + "PLR0915", # Too many statements ({statements} > {max_statements}) + "PLR0917", # Too many positional arguments defined for a function ({p_args} > {max_args}) + "PLR2004", # Magic value used in comparison, consider replacing {value} with a constant variable + "PLW0108", # Unnecessary lambda wrapping a function call; can often be replaced by the function itself + "PLW1641", # __eq__ without __hash__ + "PLW2901", # Outer {outer_kind} variable {name} overwritten by inner {inner_kind} target + "PT011", # pytest.raises({exception}) is too broad, set the `match` parameter or use a more specific exception + "PT018", # Assertion should be broken down into multiple parts + "RUF001", # String contains ambiguous unicode character. + "RUF012", # Mutable class attributes should be annotated with typing.ClassVar + "RUF015", # Prefer next(...) over single element slice + "RUF043", # Pattern passed to match= contains metacharacters but is neither escaped nor raw + "SIM102", # Use a single if statement instead of nested if statements + "SIM103", # Return the condition {condition} directly + "SIM108", # Use ternary operator {contents} instead of if-else-block + "SIM115", # Use context handler for opening files + + # Moving imports into type-checking blocks can mess with pytest.patch() + "TC001", # Move application import {} into a type-checking block + "TC002", # Move third-party import {} into a type-checking block + "TC003", # Move standard library import {} into a type-checking block + # Quotes for typing.cast generally not necessary, only for performance critical paths + "TC006", # Add quotes to type expression in typing.cast() + + "TRY003", # Avoid specifying long messages outside the exception class + "TRY400", # Use `logging.exception` instead of `logging.error` + + "UP047", # Non PEP 696 generic function + "UP049", # Avoid private type parameter names + + # May conflict with the formatter, https://docs.astral.sh/ruff/formatter/#conflicting-lint-rules + "W191", + "E111", + "E114", + "E117", + "D206", + "D300", + "Q", + "COM812", + "COM819", + + # Disabled because ruff does not understand type of __all__ generated by a function + "PLE0605", + + "FURB116", + + # Disabled to implement in follow up PRs after ruff 0.16 bump + "ISC004", + "LOG004", +] + +[tool.ruff.lint.flake8-import-conventions.extend-aliases] +"homeassistant.components.air_quality.PLATFORM_SCHEMA" = "AIR_QUALITY_PLATFORM_SCHEMA" +"homeassistant.components.alarm_control_panel.PLATFORM_SCHEMA" = "ALARM_CONTROL_PANEL_PLATFORM_SCHEMA" +"homeassistant.components.binary_sensor.PLATFORM_SCHEMA" = "BINARY_SENSOR_PLATFORM_SCHEMA" +"homeassistant.components.button.PLATFORM_SCHEMA" = "BUTTON_PLATFORM_SCHEMA" +"homeassistant.components.calendar.PLATFORM_SCHEMA" = "CALENDAR_PLATFORM_SCHEMA" +"homeassistant.components.camera.PLATFORM_SCHEMA" = "CAMERA_PLATFORM_SCHEMA" +"homeassistant.components.climate.PLATFORM_SCHEMA" = "CLIMATE_PLATFORM_SCHEMA" +"homeassistant.components.conversation.PLATFORM_SCHEMA" = "CONVERSATION_PLATFORM_SCHEMA" +"homeassistant.components.cover.PLATFORM_SCHEMA" = "COVER_PLATFORM_SCHEMA" +"homeassistant.components.date.PLATFORM_SCHEMA" = "DATE_PLATFORM_SCHEMA" +"homeassistant.components.datetime.PLATFORM_SCHEMA" = "DATETIME_PLATFORM_SCHEMA" +"homeassistant.components.device_tracker.PLATFORM_SCHEMA" = "DEVICE_TRACKER_PLATFORM_SCHEMA" +"homeassistant.components.event.PLATFORM_SCHEMA" = "EVENT_PLATFORM_SCHEMA" +"homeassistant.components.fan.PLATFORM_SCHEMA" = "FAN_PLATFORM_SCHEMA" +"homeassistant.components.geo_location.PLATFORM_SCHEMA" = "GEO_LOCATION_PLATFORM_SCHEMA" +"homeassistant.components.humidifier.PLATFORM_SCHEMA" = "HUMIDIFIER_PLATFORM_SCHEMA" +"homeassistant.components.image.PLATFORM_SCHEMA" = "IMAGE_PLATFORM_SCHEMA" +"homeassistant.components.image_processing.PLATFORM_SCHEMA" = "IMAGE_PROCESSING_PLATFORM_SCHEMA" +"homeassistant.components.lawn_mower.PLATFORM_SCHEMA" = "LAWN_MOWER_PLATFORM_SCHEMA" +"homeassistant.components.light.PLATFORM_SCHEMA" = "LIGHT_PLATFORM_SCHEMA" +"homeassistant.components.lock.PLATFORM_SCHEMA" = "LOCK_PLATFORM_SCHEMA" +"homeassistant.components.media_player.PLATFORM_SCHEMA" = "MEDIA_PLAYER_PLATFORM_SCHEMA" +"homeassistant.components.notify.PLATFORM_SCHEMA" = "NOTIFY_PLATFORM_SCHEMA" +"homeassistant.components.number.PLATFORM_SCHEMA" = "NUMBER_PLATFORM_SCHEMA" +"homeassistant.components.remote.PLATFORM_SCHEMA" = "REMOTE_PLATFORM_SCHEMA" +"homeassistant.components.scene.PLATFORM_SCHEMA" = "SCENE_PLATFORM_SCHEMA" +"homeassistant.components.select.PLATFORM_SCHEMA" = "SELECT_PLATFORM_SCHEMA" +"homeassistant.components.sensor.PLATFORM_SCHEMA" = "SENSOR_PLATFORM_SCHEMA" +"homeassistant.components.siren.PLATFORM_SCHEMA" = "SIREN_PLATFORM_SCHEMA" +"homeassistant.components.stt.PLATFORM_SCHEMA" = "STT_PLATFORM_SCHEMA" +"homeassistant.components.switch.PLATFORM_SCHEMA" = "SWITCH_PLATFORM_SCHEMA" +"homeassistant.components.text.PLATFORM_SCHEMA" = "TEXT_PLATFORM_SCHEMA" +"homeassistant.components.time.PLATFORM_SCHEMA" = "TIME_PLATFORM_SCHEMA" +"homeassistant.components.todo.PLATFORM_SCHEMA" = "TODO_PLATFORM_SCHEMA" +"homeassistant.components.tts.PLATFORM_SCHEMA" = "TTS_PLATFORM_SCHEMA" +"homeassistant.components.vacuum.PLATFORM_SCHEMA" = "VACUUM_PLATFORM_SCHEMA" +"homeassistant.components.valve.PLATFORM_SCHEMA" = "VALVE_PLATFORM_SCHEMA" +"homeassistant.components.update.PLATFORM_SCHEMA" = "UPDATE_PLATFORM_SCHEMA" +"homeassistant.components.wake_word.PLATFORM_SCHEMA" = "WAKE_WORD_PLATFORM_SCHEMA" +"homeassistant.components.water_heater.PLATFORM_SCHEMA" = "WATER_HEATER_PLATFORM_SCHEMA" +"homeassistant.components.weather.PLATFORM_SCHEMA" = "WEATHER_PLATFORM_SCHEMA" +"homeassistant.core.DOMAIN" = "HOMEASSISTANT_DOMAIN" +"homeassistant.helpers.area_registry" = "ar" +"homeassistant.helpers.category_registry" = "cr" +"homeassistant.helpers.config_validation" = "cv" +"homeassistant.helpers.device_registry" = "dr" +"homeassistant.helpers.entity_registry" = "er" +"homeassistant.helpers.floor_registry" = "fr" +"homeassistant.helpers.issue_registry" = "ir" +"homeassistant.helpers.label_registry" = "lr" +"homeassistant.util.color" = "color_util" +"homeassistant.util.dt" = "dt_util" +"homeassistant.util.json" = "json_util" +"homeassistant.util.location" = "location_util" +"homeassistant.util.logging" = "logging_util" +"homeassistant.util.network" = "network_util" +"homeassistant.util.ulid" = "ulid_util" +"homeassistant.util.uuid" = "uuid_util" +"homeassistant.util.yaml" = "yaml_util" + +[tool.ruff.lint.flake8-pytest-style] +fixture-parentheses = false +mark-parentheses = false + +[tool.ruff.lint.flake8-tidy-imports.banned-api] +"async_timeout".msg = "use asyncio.timeout instead" +"pytz".msg = "use zoneinfo instead" +"tests".msg = "You should not import tests" +"__future__.annotations".msg = "It should not be needed because Home Assistant requires Python 3.14+" + +[tool.ruff.lint.isort] +force-sort-within-sections = true +known-first-party = ["homeassistant"] +combine-as-imports = true +split-on-trailing-comma = false + +[tool.ruff.lint.per-file-ignores] + +# Allow for main entry & scripts to write to stdout +"homeassistant/__main__.py" = ["T201"] +"homeassistant/scripts/*" = ["T201"] +"script/*" = ["T20"] + +# Some utils have constants that benefit from being uppercase +"homeassistant/util/*" = ["N806"] + +# Allow relative imports within auth and within components +"homeassistant/auth/*/*" = ["TID252"] +"homeassistant/components/*/*/*" = ["TID252"] +"tests/components/*/*/*" = ["TID252"] + +# Temporary +"homeassistant/**" = ["PTH"] +"tests/**" = ["PTH"] + +[tool.ruff.lint.mccabe] +max-complexity = 25 + +[tool.ruff.lint.pydocstyle] +convention = "google" +property-decorators = ["propcache.api.cached_property"] diff --git a/tests/fixtures/real-world-locks/requirements/home-assistant-core-2026.9.0/requirements.txt b/tests/fixtures/real-world-locks/requirements/home-assistant-core-2026.9.0/requirements.txt new file mode 100644 index 00000000..2ee3711d --- /dev/null +++ b/tests/fixtures/real-world-locks/requirements/home-assistant-core-2026.9.0/requirements.txt @@ -0,0 +1,63 @@ +# Automatically generated by gen_requirements_all.py, do not edit + +-c homeassistant/package_constraints.txt + +# Home Assistant Core +aiodns==4.0.4 +aiogithubapi==26.0.0 +aiohttp-asyncmdnsresolver==0.2.0 +aiohttp-fast-zlib==0.3.0 +aiohttp==3.14.3 +aiohttp_cors==0.8.1 +aiozoneinfo==0.2.3 +annotatedyaml==1.0.2 +astral==2.2 +async-interrupt==1.2.2 +atomicwrites-homeassistant==1.4.1 +attrs==26.1.0 +audioop-lts==0.2.2 +awesomeversion==25.8.0 +bcrypt==5.0.0 +certifi>=2021.5.30 +ciso8601==2.3.3 +cronsim==2.7 +cryptography==48.0.1 +fnv-hash-fast==2.0.3 +gazetteer-matcher==1.1.0 +ha-ffmpeg==3.2.2 +hass-nabucasa==2.7.0 +hassil==3.12.0 +home-assistant-bluetooth==2.0.0 +home-assistant-intents==2026.8.28 +httpx==0.28.1 +ifaddr==0.2.0 +infrared-protocols==9.0.0 +Jinja2==3.1.6 +lru-dict==1.4.1 +mutagen==1.48.1 +orjson==3.12.0 +packaging>=23.1 +Pillow==12.3.0 +probatio==0.11.4 +propcache==0.5.2 +psutil-home-assistant==0.0.1 +PyJWT==2.13.0 +pymicro-vad==1.0.1 +pyOpenSSL==26.2.0 +pyspeex-noise==1.0.2 +python-slugify==8.0.4 +PyTurboJPEG==1.8.3 +PyYAML==6.0.3 +requests==2.34.2 +rf-protocols==4.3.0 +securetar==2026.4.1 +SQLAlchemy==2.0.52 +standard-aifc==3.13.0 +standard-telnetlib==3.13.0 +typing-extensions>=4.16.0,<5.0 +ulid-transform==2.2.9 +urllib3>=2.0 +uv==0.12.5 +webrtc-models==0.3.0 +yarl==1.24.5 +zeroconf==0.151.1 diff --git a/working-docs/design/lock-files.md b/working-docs/design/lock-files.md index 9fa801eb..801356bc 100644 --- a/working-docs/design/lock-files.md +++ b/working-docs/design/lock-files.md @@ -67,7 +67,7 @@ simply by asking users to run `[tool] export --format pylock`. | **1: The Universal Core** | `pylock.toml` (PEP 751) | **Done (2026-09-02)** -- see the "See also" note above. The official Python interoperability standard. Universal fallback. | | | `pyproject.toml` | Standard project metadata (PEP 621) to define the root SBOM component. | | | `uv.lock` | **Done (2026-09-04)** -- see `working-docs/implementation/lock-file-cascade.md`. The dominant lock file for modern, high-performance ML inference stacks (vLLM, FastAPI). | -| | `requirements.txt` | Ubiquitous in ML research Dockerfiles, PyTorch deployments, and Hugging Face spaces. | +| | `requirements.txt` | **Done (2026-09-05)** -- see `working-docs/implementation/lock-file-cascade.md`. Ubiquitous in ML research Dockerfiles, PyTorch deployments, and Hugging Face spaces. | | **2: AI/ML Native Binary** | `pixi.lock` | Essential for AI: natively resolves both Python packages and system-level C/C++ CUDA/Conda binaries. | | | `conda-lock.yml` | Maps Conda data science packages alongside PyPI wheels. | | **3: Corporate Standards** | `poetry.lock` | **Done (2026-08-31)** -- see the "See also" note above. Massive legacy and enterprise footprint in Data Engineering (Airflow, dbt). | diff --git a/working-docs/design/roadmap.md b/working-docs/design/roadmap.md index 933bde48..ec2368d6 100644 --- a/working-docs/design/roadmap.md +++ b/working-docs/design/roadmap.md @@ -178,23 +178,19 @@ table in [non-hatchling-file-discovery.md](non-hatchling-file-discovery.md)); dispatch path, not just the `pyproject.toml` one, since `Pipfile.lock` predates PEP 621 almost entirely in real projects. See [lock-file-cascade.md](../implementation/lock-file-cascade.md). -- [ ] **Remaining lock formats as a resolved-dependency source** - (`pixi.lock`, `conda-lock.yml`, pinned `requirements.txt`) -- `loom - project` still records only the declared version specifier from - `pyproject.toml [project] dependencies` - (`normalize_dependency_specifier`, `src/pitloom/extract/_pyproject.py:220`, - e.g. `requests>=2.0`) for a project with none of the five already-shipped - lock formats present, never a concrete resolved version. Parsing one - when present would let a Source SBOM carry the actual pinned version a - build will use, not just the declared range -- closer to what CISA's - Source SBOM guidance expects. `pylock.toml`/`uv.lock`/`poetry.lock`/ - `pdm.lock`/`Pipfile.lock` establish the pattern (additive - transitive-only edges, `completeness` tagging, source-stage-only - scoping, one shared priority cascade); each further format added - needs its own slot in that same priority order and a provenance - `method` tag. See [lock-files.md](./lock-files.md) for the broader - multi-format extraction-priority roadmap (`pixi.lock`, - `conda-lock.yml`) this item now defers to. +- [x] **pinned `requirements.txt`** -- done (2026-09-05): the lowest- + ranked cascade entry, and the only one that isn't a real lock file -- + used only when *every* real line is already an exact `==` pin (a + URL-based line disqualifies the whole file too, even one that looks + like a tagged release; see [lock-file-cascade.md](../implementation/lock-file-cascade.md) + for the PEP 508/440 reasoning). This closes out + **"Remaining lock formats as a resolved-dependency source"**: + `pylock.toml`/`uv.lock`/`poetry.lock`/`pdm.lock`/`Pipfile.lock`/pinned + `requirements.txt` all now feed `ProjectMetadata.locked_dependencies` + via one shared priority cascade. See + [lock-file-cascade.md](../implementation/lock-file-cascade.md) and + [lock-files.md](./lock-files.md) (`pixi.lock`/`conda-lock.yml` remain + future work there, Phase 2). ### PEP 770 / embed-wheel diff --git a/working-docs/implementation/lock-file-cascade.md b/working-docs/implementation/lock-file-cascade.md index c38002ad..c3e73140 100644 --- a/working-docs/implementation/lock-file-cascade.md +++ b/working-docs/implementation/lock-file-cascade.md @@ -30,10 +30,11 @@ bespoke "extract, check if a result is already set, override with a `_apply_pylock_dependencies()`, since deleted, for the latter). That pattern doesn't scale to `uv.lock`, `pdm.lock`, `Pipfile.lock`, and pinned `requirements.txt` landing on top -- five near-identical -bespoke functions is exactly the "pattern hand-copied across 3+ call -sites drifts" problem this repo's own conventions warn about. This -module (`src/pitloom/extract/_locked_dependencies.py`) replaces every -new format's would-be bespoke function with one shared, ordered cascade. +bespoke functions would have been exactly the "pattern hand-copied +across 3+ call sites drifts" problem this repo's own conventions warn +about. This module (`src/pitloom/extract/_locked_dependencies.py`) +replaces every new format's would-be bespoke function with one shared, +ordered cascade -- all six formats now registered in it. ## The cascade @@ -58,8 +59,11 @@ _LOCK_SOURCES: list[tuple[str, _LockExtractor | None, str | None]] = [ _ignore_expected_name(extract_pipfile_lock_dependencies), "resolved_lockfile", ), - # pinned requirements.txt lands here as its own extractor ships -- - # see roadmap.md. + ( + "requirements.txt", + _ignore_expected_name(extract_pinned_requirements_dependencies), + "pinned_requirements", + ), ] @@ -81,6 +85,14 @@ entry in priority order (highest first) and applies the first non-empty result, in place, onto `metadata.locked_dependencies` and `metadata.provenance["locked_dependencies"]`. +Every entry's `Method` tag is `"resolved_lockfile"` except +`requirements.txt`'s own `"pinned_requirements"` -- it's not a real +lock file (no resolver metadata, no hashes guaranteed), so its +provenance string reads differently on purpose, letting a reader of the +generated SBOM tell "a resolver actually produced this" from "this +merely happened to already be a fully pinned list" -- see +[docs/dependency-sources.md](../../docs/dependency-sources.md). + **`poetry.lock` has no extractor here (`None`, `None`), but it *is* in the table.** It's still applied earlier, gated inside `_try_read_poetry()`'s `include_locked_dependencies` build-stage flag, @@ -106,9 +118,9 @@ beats tool-specific; a real resolver lock beats a merely-pinned file): 3. `poetry.lock` (via `_try_read_poetry()`, not this cascade -- see above) 4. `pdm.lock` 5. `Pipfile.lock` -- JSON, not TOML; see its own notes below. -6. pinned `requirements.txt` -- weakest signal; only usable when every - line is an exact `==` pin (see that format's own implementation - notes once it lands). +6. pinned `requirements.txt` -- weakest signal, lowest rank; not a real + lock file at all, only usable when every line is already an exact + `==` pin. See its own notes below. ## Why `poetry.lock` needs a fixed rank, not just "runs first" @@ -136,11 +148,11 @@ confirms the higher-ranked entries' behaviour didn't change. **Any format ranked below `poetry.lock` needs no extra code for this** -- the same generic rank check covers it once it's added to -`_LOCK_SOURCES` at its documented position; `pdm.lock` and -`Pipfile.lock` both confirmed this when they landed, and pinned -`requirements.txt` (rank 6, lowest) will too. Only a format that would -need to be inserted *around* an existing entry (unlikely, given the -order above is already settled) would need to re-verify this logic. +`_LOCK_SOURCES` at its documented position; `pdm.lock`, `Pipfile.lock`, +and pinned `requirements.txt` (rank 6, lowest) all confirmed this when +they landed. Only a format that would need to be inserted *around* an +existing entry (unlikely, given the order above is already settled) +would need to re-verify this logic. ## Per-format extraction notes @@ -180,8 +192,9 @@ one entry" shape, but for a different, harmless reason: PDM records a separate `[[package]]` entry per requested extra variant of a package (e.g. a bare `httpx` entry alongside one with `extras = ["socks"]`), always agreeing on `version` -- unlike `uv.lock`'s genuinely conflicting -duplicates. It reuses `index_packages_by_name()` (see the next section) -to group entries by name, then only treats a name as ambiguous (skip, +duplicates. It uses `group_versions_by_canonical_name()` (see "Sharing +code across formats" below) to group its `(name, version)` pairs by +PEP 503-canonicalized name, then only treats a name as ambiguous (skip, `WARNING:`) when its entries actually *disagree* on `version`; entries that agree are collapsed to one `name==version`, not two. @@ -200,6 +213,59 @@ an unparseable string) is skipped with a `WARNING:`, the same instead the whole top level splits into `"default"` (included) and `"develop"` (excluded) sections. +`_requirements_txt.py` is the one format that isn't a real lock file at +all -- a `requirements.txt` is just lines a human or `pip freeze` wrote, +with no resolver metadata guaranteed. Its policy is **all-or-nothing**: +every real dependency line must already be a single exact `==` pin, or +the *entire file* is ignored with one `WARNING:` naming the first +disqualifying line (an option line like `-e`/`-r`/`--hash`, an unpinned +or ranged specifier, or a malformed line) -- never partially included, +since a subset of a `requirements.txt` carries no more confidence than +the subset itself would on its own. This is why it's ranked lowest and +tagged `"pinned_requirements"` rather than `"resolved_lockfile"` (see +above). Unlike `pdm.lock`/`uv.lock`, there's no `[[package]]`-style +table to group by name -- so the extractor collects every line's +`(name, version)` pair first, then feeds them to the same +`group_versions_by_canonical_name()` helper `_pdm_lock.py` uses, once +all lines have parsed: a name repeating with agreeing versions +collapses to one entry, a genuine conflict rejects the whole file. The +grouping compares PEP 503-canonicalized names, not the literal spelling +on each line -- a hand-written file mixing `Flask==1.0` and +`flask==2.0` is a real conflict between two spellings of one PyPI +package, not two different packages. + +Two pip file-format quirks are handled before per-line parsing, both +matching pip's own preprocessing: a leading UTF-8 BOM (`encoding= +"utf-8-sig"` instead of `"utf-8"`) and backslash line-continuation +(`_join_continuation_lines()` merges a physical line ending in `\` with +the next before splitting on `#`). Continuation-joining doesn't extend +to `pip-compile --generate-hashes` output specifically -- a joined line +still carries `--hash=...` tokens, which aren't valid PEP 508 syntax +and correctly disqualify the file the same as any other malformed +line, just for that reason instead of failing on the raw backslash. + +**A URL-based requirement line (`name @ https://...`, or the legacy +`git+https://...#egg=name` pip also accepts) always disqualifies, even +one that looks like it points at a tagged release** (e.g. +`.../archive/refs/tags/v2.31.0.zip`). This was an explicit design +question, not an oversight: PEP 508 defines a URL requirement as a +*direct reference*, a wholly separate concept from a PEP 440 version +specifier -- neither spec defines how to derive a normalized version +from a URL. A git tag or filename that merely looks version-shaped is +an arbitrary string the maintainer chose, with no guarantee it +round-trips to a real PEP 440 version (capitalization, a leading `v`, +a non-version tag like `stable`, ...). Confirming the real version +would mean fetching the URL and inspecting the installed package's own +metadata -- against this repo's "prevent excessive network access" +principle -- and every sibling lock format already skips its own +VCS/path/URL-sourced entries the same way, never guessing from context. +So this format doesn't special-case a release-shaped URL either. +`packaging.requirements.Requirement.url` being non-`None` catches the +PEP 508 `name @ url` form directly; the legacy `git+...`/bare-URL forms +pip also accepts aren't valid PEP 508 at all, so `Requirement()` itself +raises `InvalidRequirement` for them -- caught the same way as any other +malformed line, still disqualifying, just via a different message. + ## Sharing code across formats (`_lock_common.py`) Two steps turned out to be identical across every extractor, not just @@ -218,10 +284,13 @@ similar in spirit: `WARNING:`, the one shape TOML's grammar rules out for `load_lock_toml()` but JSON doesn't), different underlying `json`/`tomllib` call. -- **Grouping a flat package list by name.** First written for - `_uv_lock.py`'s ambiguity check, then reused as-is by `_pdm_lock.py`'s - own (milder) version of the same check -- see above. Lives as - `pitloom.extract._lock_common.index_packages_by_name()`. +- **Grouping a flat package list by name.** `_uv_lock.py`'s ambiguity + check groups full `[[package]]` table entries by their raw `name` + field -- `pitloom.extract._lock_common.index_packages_by_name()`. + `_pdm_lock.py` and `_requirements_txt.py` need the narrower "group + just a `(name, version)` pair by *canonicalized* name" shape instead + (their conflict check has to treat `Flask`/`flask` as the same + package) -- `pitloom.extract._lock_common.group_versions_by_canonical_name()`. - **Validating a `version` field is a non-empty string.** `not isinstance(version, str) or not version` existed independently in all five extractors before being factored into @@ -232,11 +301,25 @@ similar in spirit: - **The non-registry-source `WARNING:` message.** `"Skipping entry %r: %s-sourced dependencies cannot be represented as a PEP 508 specifier"` was copy-pasted, wording-identical, into all five - extractors before being factored into + extractors that have this concept (every format except + `requirements.txt`, whose URL check is shaped differently -- see + above) before being factored into `pitloom.extract._lock_common.warn_non_registry_source(lock_file, name, source_key)`. Each extractor still does its own lookup of *which* key triggered it (see below) and only calls this once it has the answer. +- **Judging whether a specifier is a single exact `==` pin.** + `_pipfile_lock.py` and `_requirements_txt.py` both need this -- + Pipfile.lock's `version` field and a `requirements.txt` line's + specifier are both full PEP 440 specifier strings, not bare version + numbers the way every TOML-based format's `version` field is. Lives + as `pitloom.extract._lock_common.single_exact_pin(specifier_set)`, + taking an already-built `SpecifierSet` rather than a raw string -- + each caller parses the raw string itself (`SpecifierSet(...)` for + Pipfile.lock, `Requirement(...).specifier` for `requirements.txt`) + and catches its own parse failure with its own `WARNING:` wording, + since the two call sites want different messages for "unparseable" vs. + "parseable but not a single exact pin." What's deliberately **not** shared: the per-entry lookup for which key marks a non-registry source, and what the `groups`/`dependencies` @@ -263,9 +346,10 @@ project checked while sourcing test fixtures for this cascade (`requests-html`, `responder` pre-`v3.0.0`) is `setup.py`-only, no `pyproject.toml` -- so a cascade wired only inside `read_pyproject()` would never run for the realistic case those two formats actually show -up in. Confirmed once `Pipfile.lock` actually landed: +up in. Confirmed once both landed: `tests/extract/test_pipfile_lock.py::test_read_project_populates_locked_dependencies_from_setup_py_only` -exercises exactly this path against a `setup.py`-only project directory. +and `tests/extract/test_requirements_txt.py::test_read_project_populates_locked_dependencies_from_setup_py_only` +each exercise this path against a `setup.py`-only project directory. `apply_locked_dependencies()` is called once, right before each of `read_project()`'s three directory-based `return` statements (the @@ -299,9 +383,8 @@ uniformly regardless of which metadata source won. `compute_doc_uuid()` (`src/pitloom/core/models.py`) folds `locked_dependencies` (the resolved dependency *content*) into its seed, -but originally not *which source produced it*. With five lock/pin -formats now cascading instead of two (a sixth, pinned -`requirements.txt`, still to come), two different formats resolving +but originally not *which source produced it*. With six lock/pin +formats now cascading instead of two, two different formats resolving to an identical dependency set for a small project became a real, checkable collision risk: two runs -- one with only `poetry.lock` present, one with only `pylock.toml` present -- that happen to resolve @@ -323,9 +406,11 @@ unaffected -- purely additive. malformed or non-registry-sourced. Use `_lock_common.load_lock_toml()` to load the file (or `_lock_common.load_lock_json()` for a JSON-format lock file -- `_pipfile_lock.py` is the precedent), and (if the format - can resolve the same name more than once, the way `uv.lock`/`pdm.lock` - can) `_lock_common.index_packages_by_name()` to group entries before - deciding whether that's ambiguous. Only add a second parameter to the + can resolve the same name more than once, the way `uv.lock`/`pdm.lock`/ + `requirements.txt` can) `_lock_common.index_packages_by_name()` (full + `[[package]]`-style entries) or `_lock_common.group_versions_by_canonical_name()` + (bare `(name, version)` pairs) to group entries before deciding + whether that's ambiguous. Only add a second parameter to the extractor itself if it genuinely needs `expected_name` for disambiguation the way `uv.lock` does (see the cascade code block above) -- otherwise keep the simpler single-`project_dir` signature From 9d346a273e8d91034091812c9fc90982c0574669 Mon Sep 17 00:00:00 2001 From: Arthit Suriyawongkul Date: Sat, 5 Sep 2026 02:59:17 +0700 Subject: [PATCH 08/35] Fix type annotations Signed-off-by: Arthit Suriyawongkul --- CHANGELOG.md | 5 +++-- src/pitloom/extract/_lock_common.py | 16 ++++++++++++---- src/pitloom/extract/_pdm_lock.py | 2 +- src/pitloom/extract/_pipfile_lock.py | 5 ++--- src/pitloom/extract/_poetry_lock.py | 5 ++--- src/pitloom/extract/_pylock.py | 3 +-- src/pitloom/extract/_uv_lock.py | 5 +++-- working-docs/design/roadmap.md | 5 +++-- 8 files changed, 27 insertions(+), 19 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index eaeeea75..d276bada 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,5 +1,5 @@ --- -Last-Modified: 2026-09-04 +Last-Modified: 2026-09-05 SPDX-FileCopyrightText: 2026-present Arthit Suriyawongkul SPDX-FileType: DOCUMENTATION SPDX-License-Identifier: CC0-1.0 @@ -44,7 +44,7 @@ and this project adheres to - Add resolved-dependency parsing for `loom project`/`loom generate` from `pylock.toml` (PEP 751), `uv.lock`, `pdm.lock`, `Pipfile.lock`, and a fully pinned `requirements.txt` -- see [Dependency sources and - precedence](docs/dependency-sources.md) + precedence](docs/dependency-sources.md) ([#208]) ### Fixed @@ -84,6 +84,7 @@ and this project adheres to [#204]: https://github.com/bact/pitloom/pull/204 [#205]: https://github.com/bact/pitloom/pull/205 [#207]: https://github.com/bact/pitloom/pull/207 +[#208]: https://github.com/bact/pitloom/pull/208 ## [0.17.0] - 2026-08-30 diff --git a/src/pitloom/extract/_lock_common.py b/src/pitloom/extract/_lock_common.py index 7753ef82..c6a52bf9 100644 --- a/src/pitloom/extract/_lock_common.py +++ b/src/pitloom/extract/_lock_common.py @@ -22,7 +22,7 @@ import logging from collections.abc import Iterable, Mapping from pathlib import Path -from typing import Any +from typing import Any, TypeGuard from packaging.specifiers import SpecifierSet from packaging.utils import canonicalize_name @@ -107,7 +107,9 @@ def load_lock_json(lock_path: Path) -> dict[str, Any] | None: return data -def index_packages_by_name(packages: list[Any]) -> dict[str, list[dict[str, Any]]]: +def index_packages_by_name( + packages: Iterable[object], +) -> dict[str, list[dict[str, Any]]]: """Group every well-formed entry of *packages* (a lock format's flat ``[[package]]``-style list) by its ``name`` field, preserving file order both across and within names. @@ -136,13 +138,19 @@ def index_packages_by_name(packages: list[Any]) -> dict[str, list[dict[str, Any] return by_name -def is_usable_version(version: Any) -> bool: +def is_usable_version(version: object) -> TypeGuard[str]: """Return whether *version* is a non-empty string -- the "can this become a real ``name==version`` pin" check every lock/pin extractor applies to a ``[[package]]`` entry's ``version`` field before using it. Each call site still logs its own ``WARNING:`` when this returns ``False``, since the message wording (which field, which format) is genuinely format-specific. + + Typed as a :class:`typing.TypeGuard`\\ [``str``] so a caller's usual + ``if not is_usable_version(version): return None`` early-return + narrows *version* to ``str`` for the rest of the function, instead + of needing its own redundant ``isinstance`` check before passing + *version* to something that requires ``str``. """ return isinstance(version, str) and bool(version) @@ -222,7 +230,7 @@ def warn_non_registry_source(lock_file: str, name: str, source_key: str) -> None def find_first_present_key( - mapping: Mapping[str, Any], keys: Iterable[str] + mapping: Mapping[str, object], keys: Iterable[str] ) -> str | None: """Return the first of *keys* (in order) that's a key of *mapping*, or ``None`` if none are. diff --git a/src/pitloom/extract/_pdm_lock.py b/src/pitloom/extract/_pdm_lock.py index cd89ffb2..28d42455 100644 --- a/src/pitloom/extract/_pdm_lock.py +++ b/src/pitloom/extract/_pdm_lock.py @@ -68,7 +68,7 @@ _NON_REGISTRY_KEYS = ("git", "url", "path") -def _default_group_package_or_none(pkg: Any) -> dict[str, Any] | None: +def _default_group_package_or_none(pkg: object) -> dict[str, Any] | None: """Return *pkg* itself when it's a well-formed, default-group, registry-sourced, versioned ``[[package]]`` entry -- ``None`` otherwise (with a ``WARNING:`` for anything malformed or diff --git a/src/pitloom/extract/_pipfile_lock.py b/src/pitloom/extract/_pipfile_lock.py index 956dd1a7..9e8b95d5 100644 --- a/src/pitloom/extract/_pipfile_lock.py +++ b/src/pitloom/extract/_pipfile_lock.py @@ -40,7 +40,6 @@ import logging from pathlib import Path -from typing import Any from packaging.specifiers import InvalidSpecifier, SpecifierSet @@ -96,7 +95,7 @@ def extract_pipfile_lock_dependencies(project_dir: Path) -> list[str]: return dependencies -def _pinned_dep_for_package(name: Any, entry: Any) -> str | None: +def _pinned_dep_for_package(name: object, entry: object) -> str | None: """Return ``name==version`` for one ``"default"``-section entry, or ``None`` when it's malformed, non-registry-sourced, or its ``version`` isn't a single exact ``==`` specifier.""" @@ -124,7 +123,7 @@ def _pinned_dep_for_package(name: Any, entry: Any) -> str | None: return f"{name}=={pinned_version}" -def _exact_pinned_version(name: str, version: Any) -> str | None: +def _exact_pinned_version(name: str, version: object) -> str | None: """Return the bare version string when *version* is a single exact ``==`` PEP 440 specifier with no wildcard (e.g. ``"==2.31.0"`` -> ``"2.31.0"``), or ``None`` (with a ``WARNING:``) when it's missing, diff --git a/src/pitloom/extract/_poetry_lock.py b/src/pitloom/extract/_poetry_lock.py index 2227d7ec..fb3eb45e 100644 --- a/src/pitloom/extract/_poetry_lock.py +++ b/src/pitloom/extract/_poetry_lock.py @@ -25,7 +25,6 @@ import logging from pathlib import Path -from typing import Any from pitloom.extract._lock_common import ( is_usable_version, @@ -78,7 +77,7 @@ def extract_poetry_lock_dependencies(project_dir: Path) -> list[str]: _NON_PEP508_SOURCE_TYPES = frozenset({"directory", "file", "git", "url"}) -def _pinned_dep_for_package(pkg: Any) -> str | None: +def _pinned_dep_for_package(pkg: object) -> str | None: """Return ``name==version`` for one ``[[package]]`` table entry, or ``None`` when it's malformed, not in the ``main`` group, or sourced from a non-PyPI location that ``name==version`` can't represent. @@ -111,7 +110,7 @@ def _pinned_dep_for_package(pkg: Any) -> str | None: return None source = pkg.get("source") source_type = source.get("type") if isinstance(source, dict) else None - if source_type in _NON_PEP508_SOURCE_TYPES: + if isinstance(source_type, str) and source_type in _NON_PEP508_SOURCE_TYPES: warn_non_registry_source("poetry.lock", name, source_type) return None return f"{name}=={version}" diff --git a/src/pitloom/extract/_pylock.py b/src/pitloom/extract/_pylock.py index 91845a8b..ce61825c 100644 --- a/src/pitloom/extract/_pylock.py +++ b/src/pitloom/extract/_pylock.py @@ -29,7 +29,6 @@ import logging from pathlib import Path -from typing import Any from pitloom.extract._lock_common import ( find_first_present_key, @@ -89,7 +88,7 @@ def extract_pylock_dependencies(project_dir: Path) -> list[str]: return dependencies -def _pinned_dep_for_package(pkg: Any) -> str | None: +def _pinned_dep_for_package(pkg: object) -> str | None: """Return ``name==version`` for one ``[[packages]]`` table entry, or ``None`` when it's malformed or sourced from a location that ``name==version`` can't represent. diff --git a/src/pitloom/extract/_uv_lock.py b/src/pitloom/extract/_uv_lock.py index af6b1fee..6a27205e 100644 --- a/src/pitloom/extract/_uv_lock.py +++ b/src/pitloom/extract/_uv_lock.py @@ -44,6 +44,7 @@ from __future__ import annotations import logging +from collections.abc import Iterable from pathlib import Path from typing import Any @@ -76,7 +77,7 @@ def _find_root_package( - packages: list[Any], expected_name: str | None + packages: Iterable[object], expected_name: str | None ) -> dict[str, Any] | None: """Return the ``[[package]]`` entry that is the project's own (identified by an ``editable``/``virtual`` ``source``), or ``None`` @@ -123,7 +124,7 @@ def _find_root_package( def _pinned_dep_for_root_dependency( - dep_ref: Any, by_name: dict[str, list[dict[str, Any]]] + dep_ref: object, by_name: dict[str, list[dict[str, Any]]] ) -> str | None: """Return ``name==version`` for one entry of the root package's own ``dependencies`` list, or ``None`` when it can't be resolved to a diff --git a/working-docs/design/roadmap.md b/working-docs/design/roadmap.md index ec2368d6..28b1ee6d 100644 --- a/working-docs/design/roadmap.md +++ b/working-docs/design/roadmap.md @@ -1,6 +1,6 @@ --- Created: 2026-04-14 -Last-Modified: 2026-09-04 +Last-Modified: 2026-09-05 SPDX-FileCopyrightText: 2026-present Arthit Suriyawongkul SPDX-FileType: DOCUMENTATION SPDX-License-Identifier: CC0-1.0 @@ -184,7 +184,8 @@ table in [non-hatchling-file-discovery.md](non-hatchling-file-discovery.md)); URL-based line disqualifies the whole file too, even one that looks like a tagged release; see [lock-file-cascade.md](../implementation/lock-file-cascade.md) for the PEP 508/440 reasoning). This closes out - **"Remaining lock formats as a resolved-dependency source"**: + **"Remaining lock formats as a resolved-dependency source"** + ([#208](https://github.com/bact/pitloom/pull/208)): `pylock.toml`/`uv.lock`/`poetry.lock`/`pdm.lock`/`Pipfile.lock`/pinned `requirements.txt` all now feed `ProjectMetadata.locked_dependencies` via one shared priority cascade. See From dc256c0afe611703ff6a3965271914a7de5e4b0b Mon Sep 17 00:00:00 2001 From: Arthit Suriyawongkul Date: Sat, 5 Sep 2026 10:16:17 +0700 Subject: [PATCH 09/35] Fix lock file bugs Signed-off-by: Arthit Suriyawongkul --- docs/dependency-sources.md | 8 +- src/pitloom/extract/_lock_common.py | 27 ++++-- src/pitloom/extract/_locked_dependencies.py | 16 ++- src/pitloom/extract/_pdm_lock.py | 13 ++- src/pitloom/extract/_pipfile_lock.py | 23 +++-- src/pitloom/extract/_poetry_lock.py | 13 ++- src/pitloom/extract/_pylock.py | 74 ++++++++++++-- src/pitloom/extract/_pyproject.py | 6 +- src/pitloom/extract/_requirements_txt.py | 31 +++--- src/pitloom/extract/_uv_lock.py | 97 ++++++++++++++----- tests/extract/test_lock_common.py | 19 ++++ tests/extract/test_locked_dependencies.py | 61 ++++++++++++ tests/extract/test_pdm_lock.py | 18 +++- tests/extract/test_pipfile_lock.py | 18 +++- tests/extract/test_poetry_lock.py | 34 +++++-- tests/extract/test_pylock.py | 76 ++++++++++++++- tests/extract/test_requirements_txt.py | 33 ++++++- tests/extract/test_uv_lock.py | 92 +++++++++++++++++- tests/extract/test_uv_lock_integration.py | 41 +++++++- tests/extract/test_uv_lock_root_package.py | 21 +++- .../implementation/lock-file-cascade.md | 82 ++++++++++------ .../implementation/pep751-pylock-support.md | 27 ++++-- 22 files changed, 689 insertions(+), 141 deletions(-) diff --git a/docs/dependency-sources.md b/docs/dependency-sources.md index 0c1d5a9e..c4c4f5b3 100644 --- a/docs/dependency-sources.md +++ b/docs/dependency-sources.md @@ -38,7 +38,7 @@ exactly-pinned entries. | Priority | Format | File | What's included | | :---: | :--- | :--- | :--- | | 1 (highest) | PEP 751 | `pylock.toml` | Every resolved package the file records. | -| 2 | uv | `uv.lock` | Your project's own main/runtime dependencies (not `optional-dependencies` extras or `dev-dependencies` groups). A dependency pinned to more than one version for different Python versions is skipped, not guessed at -- see below. | +| 2 | uv | `uv.lock` | Your project's own main/runtime dependencies, walked transitively (dependencies of dependencies, and so on) -- not `optional-dependencies` extras or `dev-dependencies` groups. A dependency pinned to more than one version for different Python versions is skipped, not guessed at, and nothing depending only on it is walked into either -- see below. | | 3 | Poetry | `poetry.lock` | Packages in the `main` dependency group only (not `[tool.poetry.group.*]` dev/extra groups). | | 4 | PDM | `pdm.lock` | Packages in the `default` dependency group only. | | 5 | Pipenv | `Pipfile.lock` | Packages in the `default` section only (not `develop`). A package whose resolved `version` isn't a single exact `==` pin is skipped, not guessed at. | @@ -62,7 +62,11 @@ whole file the same as an unpinned or ranged one would. than one lock file exists in the same project directory (uncommon, but possible after a build-tool migration), Pitloom picks the one highest in the table above and ignores the rest entirely -- it never merges two -lock files' resolutions together. +lock files' resolutions together. This holds even when the +highest-priority lock resolves to *zero* dependencies: a real, +successfully-parsed lock file that legitimately has nothing to add is +still a definitive answer, and a lower-priority lock present alongside +it is still ignored, not used to fill in what looks like a gap. **A lock entry that can't be resolved to one exact version is left out, not guessed.** `uv.lock` in particular can record the same package diff --git a/src/pitloom/extract/_lock_common.py b/src/pitloom/extract/_lock_common.py index c6a52bf9..51b29ace 100644 --- a/src/pitloom/extract/_lock_common.py +++ b/src/pitloom/extract/_lock_common.py @@ -26,6 +26,7 @@ from packaging.specifiers import SpecifierSet from packaging.utils import canonicalize_name +from packaging.version import InvalidVersion, Version from pitloom.extract._toml_io import TOMLDecodeError, load_toml_file @@ -139,12 +140,18 @@ def index_packages_by_name( def is_usable_version(version: object) -> TypeGuard[str]: - """Return whether *version* is a non-empty string -- the "can this - become a real ``name==version`` pin" check every lock/pin extractor - applies to a ``[[package]]`` entry's ``version`` field before using - it. Each call site still logs its own ``WARNING:`` when this returns - ``False``, since the message wording (which field, which format) is - genuinely format-specific. + """Return whether *version* is a non-empty string that parses as a + valid PEP 440 version -- the "can this become a real + ``name==version`` pin" check every lock/pin extractor applies to a + ``[[package]]`` entry's ``version`` field before using it. Rejects + not just non-strings but a syntactically-string-yet-not-a-version + value too (whitespace, ``"*"``, ``"not a version"``) -- without this, + a malformed lock entry would silently produce an invalid + ``name==`` dependency/PURL instead of being warned and + skipped like every other malformed-field case. Each call site still + logs its own ``WARNING:`` when this returns ``False``, since the + message wording (which field, which format) is genuinely + format-specific. Typed as a :class:`typing.TypeGuard`\\ [``str``] so a caller's usual ``if not is_usable_version(version): return None`` early-return @@ -152,7 +159,13 @@ def is_usable_version(version: object) -> TypeGuard[str]: of needing its own redundant ``isinstance`` check before passing *version* to something that requires ``str``. """ - return isinstance(version, str) and bool(version) + if not isinstance(version, str) or not version: + return False + try: + Version(version) + except InvalidVersion: + return False + return True def group_versions_by_canonical_name( diff --git a/src/pitloom/extract/_locked_dependencies.py b/src/pitloom/extract/_locked_dependencies.py index 16d6c3b5..483e0228 100644 --- a/src/pitloom/extract/_locked_dependencies.py +++ b/src/pitloom/extract/_locked_dependencies.py @@ -47,10 +47,12 @@ __all__ = ["apply_locked_dependencies"] -_LockExtractor = Callable[[Path, str | None], list[str]] +_LockExtractor = Callable[[Path, str | None], list[str] | None] -def _ignore_expected_name(extractor: Callable[[Path], list[str]]) -> _LockExtractor: +def _ignore_expected_name( + extractor: Callable[[Path], list[str] | None], +) -> _LockExtractor: """Adapt a single-argument extractor to :data:`_LockExtractor`'s uniform ``(project_dir, expected_name)`` shape. @@ -133,6 +135,14 @@ def apply_locked_dependencies(metadata: ProjectMetadata, project_dir: Path) -> N is also recorded in the resulting ``provenance["locked_dependencies"]`` string itself (as a trailing ``| Note: supersedes ``), not only logged, so a reader of the generated SBOM can see it too. + + Each extractor returns ``None`` (not applicable here: absent, + unparseable, or otherwise unusable -- try the next source) or a + ``list[str]`` (this source *does* apply, even when that list is + empty: a real lock resolving to zero runtime dependencies is a + genuine, authoritative answer, and must win outright rather than + being conflated with "no lock here" and letting a lower-priority + source add dependencies the winning lock says don't exist). """ previous = metadata.provenance.get("locked_dependencies") previous_source = ( @@ -170,7 +180,7 @@ def apply_locked_dependencies(metadata: ProjectMetadata, project_dir: Path) -> N break dependencies = extractor(project_dir, metadata.name) - if not dependencies: + if dependencies is None: continue provenance = f"Source: {source_name} | Method: {method}" diff --git a/src/pitloom/extract/_pdm_lock.py b/src/pitloom/extract/_pdm_lock.py index 28d42455..38f31b3e 100644 --- a/src/pitloom/extract/_pdm_lock.py +++ b/src/pitloom/extract/_pdm_lock.py @@ -109,17 +109,20 @@ def _default_group_package_or_none(pkg: object) -> dict[str, Any] | None: return pkg -def extract_pdm_lock_dependencies(project_dir: Path) -> list[str]: +def extract_pdm_lock_dependencies(project_dir: Path) -> list[str] | None: """Read ``pdm.lock`` next to ``pyproject.toml`` and return its resolved ``default``-group packages as exact-pin PEP 508 strings. - Returns an empty list when no ``pdm.lock`` is present, or when it - can't be parsed -- this is optional enrichment, never a requirement. + Returns ``None`` when no ``pdm.lock`` is present, or when it can't be + parsed -- this is optional enrichment, never a requirement. ``None`` + (as opposed to a valid-but-empty ``[]``) distinguishes an + absent/unusable lock from a real one that simply resolves to zero + ``default``-group packages. """ lock_path = project_dir / "pdm.lock" data = load_lock_toml(lock_path) if data is None: - return [] + return None packages = data.get("package", []) if not isinstance(packages, list): @@ -128,7 +131,7 @@ def extract_pdm_lock_dependencies(project_dir: Path) -> list[str]: lock_path, type(packages).__name__, ) - return [] + return None default_group_packages = [ pkg diff --git a/src/pitloom/extract/_pipfile_lock.py b/src/pitloom/extract/_pipfile_lock.py index 9e8b95d5..bfb455ee 100644 --- a/src/pitloom/extract/_pipfile_lock.py +++ b/src/pitloom/extract/_pipfile_lock.py @@ -45,7 +45,6 @@ from pitloom.extract._lock_common import ( find_first_present_key, - is_usable_version, load_lock_json, single_exact_pin, warn_non_registry_source, @@ -63,19 +62,21 @@ _NON_REGISTRY_KEYS = ("git", "hg", "bzr", "svn", "path", "file", "editable") -def extract_pipfile_lock_dependencies(project_dir: Path) -> list[str]: +def extract_pipfile_lock_dependencies(project_dir: Path) -> list[str] | None: """Read ``Pipfile.lock`` next to ``Pipfile``/``setup.py`` and return its resolved ``"default"``-section packages as exact-pin PEP 508 strings. - Returns an empty list when no ``Pipfile.lock`` is present, or when - it can't be parsed -- this is optional enrichment, never a - requirement. + Returns ``None`` when no ``Pipfile.lock`` is present, or when it + can't be parsed -- this is optional enrichment, never a requirement. + ``None`` (as opposed to a valid-but-empty ``[]``) distinguishes an + absent/unusable lock from a real one that simply resolves to zero + ``"default"``-section packages. """ lock_path = project_dir / "Pipfile.lock" data = load_lock_json(lock_path) if data is None: - return [] + return None default_section = data.get("default", {}) if not isinstance(default_section, dict): @@ -85,7 +86,7 @@ def extract_pipfile_lock_dependencies(project_dir: Path) -> list[str]: lock_path, type(default_section).__name__, ) - return [] + return None dependencies: list[str] = [] for name, entry in default_section.items(): @@ -132,7 +133,13 @@ def _exact_pinned_version(name: str, version: object) -> str | None: ``packaging.specifiers.Specifier`` also reports as operator ``"=="`` but which pins a *range* of versions, not one exact release. """ - if not is_usable_version(version): + if not isinstance(version, str) or not version: + # Unlike every sibling format, this field is already a PEP 440 + # *specifier* string (e.g. "==2.31.0"), not a plain version -- + # is_usable_version()'s stricter `packaging.version.Version` + # check doesn't apply here (a specifier isn't a bare version and + # would always fail it); SpecifierSet()/single_exact_pin() below + # already validate it's a genuine, single, exact pin. log.warning( "Skipping Pipfile.lock entry %r: missing or non-string 'version'", name, diff --git a/src/pitloom/extract/_poetry_lock.py b/src/pitloom/extract/_poetry_lock.py index fb3eb45e..fa675b5a 100644 --- a/src/pitloom/extract/_poetry_lock.py +++ b/src/pitloom/extract/_poetry_lock.py @@ -37,13 +37,16 @@ __all__ = ["extract_poetry_lock_dependencies"] -def extract_poetry_lock_dependencies(project_dir: Path) -> list[str]: +def extract_poetry_lock_dependencies(project_dir: Path) -> list[str] | None: """Read ``poetry.lock`` next to ``pyproject.toml`` and return its resolved ``main``-group packages as exact-pin PEP 508 strings. - Returns an empty list when no ``poetry.lock`` is present, or when it + Returns ``None`` when no ``poetry.lock`` is present, or when it can't be parsed -- this is optional enrichment on top of - ``[tool.poetry.dependencies]``, never a requirement. + ``[tool.poetry.dependencies]``, never a requirement. ``None`` (as + opposed to a valid-but-empty ``[]``) distinguishes an absent/unusable + lock from a real one that simply resolves to zero ``main``-group + packages. Packages belonging only to a non-``main`` group (``[tool.poetry.group.dev]`` and similar) are excluded, matching the same "not a runtime dependency @@ -54,7 +57,7 @@ def extract_poetry_lock_dependencies(project_dir: Path) -> list[str]: lock_path = project_dir / "poetry.lock" data = load_lock_toml(lock_path) if data is None: - return [] + return None packages = data.get("package", []) if not isinstance(packages, list): @@ -64,7 +67,7 @@ def extract_poetry_lock_dependencies(project_dir: Path) -> list[str]: lock_path, type(packages).__name__, ) - return [] + return None dependencies: list[str] = [] for pkg in packages: diff --git a/src/pitloom/extract/_pylock.py b/src/pitloom/extract/_pylock.py index ce61825c..78aa73ca 100644 --- a/src/pitloom/extract/_pylock.py +++ b/src/pitloom/extract/_pylock.py @@ -43,13 +43,39 @@ _NON_REGISTRY_SOURCE_KEYS = ("vcs", "directory", "archive") +#: The highest ``lock-version`` this extractor understands, as +#: ``(major, minor)``. PEP 751 defines only ``"1.0"`` to date. A +#: consumer must reject a different *major* version outright (a future +#: 2.x could change the schema incompatibly) but may still read a newer +#: *minor* version within the same major (additive, backward-compatible +#: fields only, per PEP 751) -- with a warning that some of its content +#: may go unrecognized. +_SUPPORTED_LOCK_FILE_VERSION = (1, 0) + + +def _parse_lock_version(lock_version: str) -> tuple[int, int] | None: + """Parse a ``lock-version`` string as ``(major, minor)``, or + ``None`` if it isn't a plain ``major.minor`` pair of non-negative + integers -- PEP 751's own grammar for this field, rejecting a value + like ``"2"``, ``"1.0.0"``, or ``"garbage"`` that isn't shaped like a + version at all.""" + parts = lock_version.split(".") + if len(parts) != 2 or not all(part.isdigit() for part in parts): + return None + return int(parts[0]), int(parts[1]) + -def extract_pylock_dependencies(project_dir: Path) -> list[str]: +def extract_pylock_dependencies(project_dir: Path) -> list[str] | None: """Read ``pylock.toml`` next to ``pyproject.toml`` and return its resolved packages as exact-pin PEP 508 strings. - Returns an empty list when no ``pylock.toml`` is present, or when it - can't be parsed -- this is optional enrichment, never a requirement. + Returns ``None`` when no ``pylock.toml`` is present, it can't be + parsed, or its declared ``lock-version`` is unsupported -- this is + optional enrichment, never a requirement, and ``None`` (as opposed + to a valid-but-empty ``[]``) tells :mod:`pitloom.extract._locked_dependencies`'s + cascade this source doesn't apply here, so a lower-priority source + can still be tried, rather than a genuinely dependency-free lock + file being confused with an absent/unusable one. Unlike ``poetry.lock``, PEP 751 has no ``groups``-style per-package membership to filter on: a ``pylock.toml`` is already the flattened, @@ -60,15 +86,45 @@ def extract_pylock_dependencies(project_dir: Path) -> list[str]: lock_path = project_dir / "pylock.toml" data = load_lock_toml(lock_path) if data is None: - return [] + return None - if not isinstance(data.get("lock-version"), str): + raw_lock_version = data.get("lock-version") + parsed_version = ( + _parse_lock_version(raw_lock_version) + if isinstance(raw_lock_version, str) + else None + ) + if parsed_version is None: log.warning( - "%s: missing or non-string top-level 'lock-version' key -- " - "ignoring pylock.toml", + "%s: missing or malformed top-level 'lock-version' key " + "(%r, expected a 'major.minor' string) -- ignoring pylock.toml", lock_path, + raw_lock_version, + ) + return None + major, minor = parsed_version + supported_major, supported_minor = _SUPPORTED_LOCK_FILE_VERSION + if major != supported_major: + log.warning( + "%s: 'lock-version' %r is major version %d, but this Pitloom " + "release only understands major version %d -- ignoring " + "pylock.toml", + lock_path, + raw_lock_version, + major, + supported_major, + ) + return None + if minor > supported_minor: + log.warning( + "%s: 'lock-version' %r is newer than the %d.%d schema this " + "Pitloom release knows -- reading it anyway (PEP 751 minor " + "versions are additive), but newer fields may be ignored", + lock_path, + raw_lock_version, + supported_major, + supported_minor, ) - return [] packages = data.get("packages", []) if not isinstance(packages, list): @@ -78,7 +134,7 @@ def extract_pylock_dependencies(project_dir: Path) -> list[str]: lock_path, type(packages).__name__, ) - return [] + return None dependencies: list[str] = [] for pkg in packages: diff --git a/src/pitloom/extract/_pyproject.py b/src/pitloom/extract/_pyproject.py index e8385c87..e1fb815b 100644 --- a/src/pitloom/extract/_pyproject.py +++ b/src/pitloom/extract/_pyproject.py @@ -461,12 +461,12 @@ def _try_read_poetry( locked_dependencies = ( extract_poetry_lock_dependencies(project_dir) if include_locked_dependencies - else [] + else None ) try: metadata = extract_poetry_metadata(data, project_dir) except (ValueError, KeyError) as exc: - if not locked_dependencies: + if locked_dependencies is None: return None log.warning( "%s: [tool.poetry] metadata could not be parsed (%s) -- " @@ -476,7 +476,7 @@ def _try_read_poetry( exc, ) metadata = ProjectMetadata(name="") - if locked_dependencies: + if locked_dependencies is not None: metadata.locked_dependencies = locked_dependencies metadata.provenance["locked_dependencies"] = ( f"Source: {POETRY_LOCK_SOURCE_NAME} | Method: resolved_lockfile" diff --git a/src/pitloom/extract/_requirements_txt.py b/src/pitloom/extract/_requirements_txt.py index 93c2e877..faaf37d9 100644 --- a/src/pitloom/extract/_requirements_txt.py +++ b/src/pitloom/extract/_requirements_txt.py @@ -78,29 +78,32 @@ _OPTION_LINE_PREFIX = "-" -def extract_pinned_requirements_dependencies(project_dir: Path) -> list[str]: +def extract_pinned_requirements_dependencies(project_dir: Path) -> list[str] | None: """Read ``requirements.txt`` next to ``pyproject.toml``/``setup.py`` and return every dependency as an exact-pin PEP 508 string, but only when *every* real line in the file is already an exact ``==`` pin. - Returns an empty list when no ``requirements.txt`` is present, it - can't be read/decoded, or any line disqualifies the whole file (an - option line, a URL-based requirement, an unpinned/ranged specifier, - a malformed line, or one name pinned to two conflicting versions) -- + Returns ``None`` when no ``requirements.txt`` is present, it can't be + read/decoded, or any line disqualifies the whole file (an option + line, a URL-based requirement, an unpinned/ranged specifier, a + malformed line, or one name pinned to two conflicting versions) -- see the module docstring for why this is all-or-nothing rather than - including only the pinned lines. A leading UTF-8 BOM (common from - Windows editors) and pip's backslash line-continuation syntax are - both handled the same as pip itself handles them, not treated as + including only the pinned lines. ``None`` (as opposed to a + valid-but-empty ``[]``) distinguishes an absent/unusable file from a + real, fully-pinned one that simply lists zero dependencies (e.g. all + comments/blank lines). A leading UTF-8 BOM (common from Windows + editors) and pip's backslash line-continuation syntax are both + handled the same as pip itself handles them, not treated as malformed. """ lock_path = project_dir / "requirements.txt" if not lock_path.exists(): - return [] + return None try: raw_text = lock_path.read_text(encoding="utf-8-sig") except (OSError, UnicodeDecodeError) as exc: log.warning("Failed to read %s: %s", lock_path, exc) - return [] + return None pins: list[tuple[str, str]] = [] for lineno, joined_line in _join_continuation_lines(raw_text): @@ -109,7 +112,7 @@ def extract_pinned_requirements_dependencies(project_dir: Path) -> list[str]: continue pin = _pinned_name_version_for_line(lock_path, lineno, line) if pin is None: - return [] + return None pins.append(pin) return _collapse_or_none(lock_path, pins) @@ -147,10 +150,10 @@ def _join_continuation_lines(raw_text: str) -> list[tuple[int, str]]: return logical_lines -def _collapse_or_none(lock_path: Path, pins: list[tuple[str, str]]) -> list[str]: +def _collapse_or_none(lock_path: Path, pins: list[tuple[str, str]]) -> list[str] | None: """Collapse *pins* to one ``name==version`` entry per PEP 503-canonicalized name, preserving first-seen literal name and file - order -- or ``[]`` (with a ``WARNING:`` naming the name and both + order -- or ``None`` (with a ``WARNING:`` naming the name and both versions) the moment one canonicalized name repeats with two *different* versions. A plain repeated line (same name, same version) is silently collapsed to one entry. @@ -168,7 +171,7 @@ def _collapse_or_none(lock_path: Path, pins: list[tuple[str, str]]) -> list[str] version, conflicting, ) - return [] + return None result.append(f"{name}=={version}") return result diff --git a/src/pitloom/extract/_uv_lock.py b/src/pitloom/extract/_uv_lock.py index 6a27205e..0d03ca4f 100644 --- a/src/pitloom/extract/_uv_lock.py +++ b/src/pitloom/extract/_uv_lock.py @@ -123,12 +123,14 @@ def _find_root_package( return candidates[0] -def _pinned_dep_for_root_dependency( +def _resolved_package_for_dependency( dep_ref: object, by_name: dict[str, list[dict[str, Any]]] -) -> str | None: - """Return ``name==version`` for one entry of the root package's own - ``dependencies`` list, or ``None`` when it can't be resolved to a - single, unambiguous, registry-sourced pin.""" +) -> dict[str, Any] | None: + """Return the single, unambiguous ``[[package]]`` entry that one + ``dependencies``-list reference resolves to -- the root package's + own, or one already-visited package's own nested reference during + the transitive walk in :func:`_collect_transitive_dependencies` -- + or ``None`` when it can't be resolved that way.""" if not isinstance(dep_ref, dict): log.warning( "Skipping malformed uv.lock dependency reference: expected a table, got %s", @@ -150,8 +152,7 @@ def _pinned_dep_for_root_dependency( # environment, which this extractor deliberately doesn't do. log.warning( "Skipping uv.lock dependency %r: marker-conditional version " - "on the root package's own dependency reference (no marker " - "evaluation)", + "on its own dependency reference (no marker evaluation)", name, ) return None @@ -173,7 +174,57 @@ def _pinned_dep_for_root_dependency( ) return None - return _pinned_dep_for_package(candidates[0]) + return candidates[0] + + +def _collect_transitive_dependencies( + root_dependencies: list[object], by_name: dict[str, list[dict[str, Any]]] +) -> list[str]: + """Breadth-first walk of the resolved dependency graph starting from + the project root package's own ``dependencies`` list, returning + every reachable package (not just the root's immediate dependencies) + as exact-pin PEP 508 strings. + + A ``uv.lock``'s flat ``[[package]]`` table records each package's + *own* ``dependencies`` list once, keyed by name -- the actual + installed set is the closure of that graph, not just its first + layer (e.g. a CLI tool's own root dependency on a framework that + itself pulls in several more packages). PEP 503-canonicalized names + guard against revisiting the same package twice (a diamond + dependency shared by two branches) or looping on a cycle; a name + that fails to resolve unambiguously (see + :func:`_resolved_package_for_dependency`) is skipped and not walked + into further, the same "don't guess" policy the root-level case + already applied. + """ + dependencies: dict[str, str] = {} + visited: set[str] = set() + queue: list[object] = list(root_dependencies) + while queue: + dep_ref = queue.pop(0) + pkg = _resolved_package_for_dependency(dep_ref, by_name) + if pkg is None: + continue + canonical_name = canonicalize_name(pkg["name"]) + if canonical_name in visited: + continue + visited.add(canonical_name) + + pin = _pinned_dep_for_package(pkg) + if pin is not None: + dependencies[canonical_name] = pin + + nested = pkg.get("dependencies", []) + if isinstance(nested, list): + queue.extend(nested) + elif nested: + log.warning( + "Skipping uv.lock entry %r nested 'dependencies': " + "expected a list, got %s", + pkg["name"], + type(nested).__name__, + ) + return list(dependencies.values()) def _pinned_dep_for_package(pkg: dict[str, Any]) -> str | None: @@ -214,10 +265,11 @@ def _expected_project_name(project_dir: Path) -> str | None: def extract_uv_lock_dependencies( project_dir: Path, expected_name: str | None = None -) -> list[str]: +) -> list[str] | None: """Read ``uv.lock`` next to ``pyproject.toml`` and return the - project's own main/runtime dependencies as exact-pin PEP 508 - strings. + project's own transitive main/runtime dependencies (the root + package's own ``dependencies``, plus everything *they* in turn + depend on) as exact-pin PEP 508 strings. *expected_name* disambiguates a shared uv workspace lock's multiple local package entries (see :func:`_find_root_package`) -- pass the @@ -227,14 +279,16 @@ def extract_uv_lock_dependencies( caller invoking this extractor directly, outside the cascade), falls back to reading it via :func:`_expected_project_name`. - Returns an empty list when no ``uv.lock`` is present, it can't be - parsed, or the project's own package entry can't be identified -- - this is optional enrichment, never a requirement. + Returns ``None`` when no ``uv.lock`` is present, it can't be parsed, + or the project's own package entry can't be identified -- this is + optional enrichment, never a requirement. ``None`` (as opposed to a + valid-but-empty ``[]``) distinguishes an absent/unusable lock from a + real one whose root package simply has zero runtime dependencies. """ lock_path = project_dir / "uv.lock" data = load_lock_toml(lock_path) if data is None: - return [] + return None packages = data.get("package", []) if not isinstance(packages, list): @@ -243,7 +297,7 @@ def extract_uv_lock_dependencies( lock_path, type(packages).__name__, ) - return [] + return None if expected_name is None: expected_name = _expected_project_name(project_dir) @@ -254,7 +308,7 @@ def extract_uv_lock_dependencies( "source entry) -- ignoring uv.lock", lock_path, ) - return [] + return None root_dependencies = root.get("dependencies", []) if not isinstance(root_dependencies, list): @@ -264,12 +318,7 @@ def extract_uv_lock_dependencies( lock_path, type(root_dependencies).__name__, ) - return [] + return None by_name = index_packages_by_name(packages) - dependencies: list[str] = [] - for dep_ref in root_dependencies: - dep = _pinned_dep_for_root_dependency(dep_ref, by_name) - if dep is not None: - dependencies.append(dep) - return dependencies + return _collect_transitive_dependencies(root_dependencies, by_name) diff --git a/tests/extract/test_lock_common.py b/tests/extract/test_lock_common.py index 88b740f0..3a6f72c7 100644 --- a/tests/extract/test_lock_common.py +++ b/tests/extract/test_lock_common.py @@ -18,6 +18,7 @@ find_first_present_key, group_versions_by_canonical_name, index_packages_by_name, + is_usable_version, load_lock_toml, ) @@ -130,3 +131,21 @@ def test_find_first_present_key_returns_none_when_no_key_present() -> None: def test_find_first_present_key_empty_mapping_returns_none() -> None: assert find_first_present_key({}, ("git", "path")) is None + + +@pytest.mark.parametrize("version", ["2.31.0", "1.0", "0.1.0a1", "2024.1.1", "1!2.0"]) +def test_is_usable_version_accepts_valid_pep440_versions(version: str) -> None: + assert is_usable_version(version) + + +@pytest.mark.parametrize( + "version", + [None, 1, 2.0, [], {}, "", "*", "not a version", " ", "2.31.*", "latest"], +) +def test_is_usable_version_rejects_non_pep440_values(version: object) -> None: + """A value that's the wrong type, empty, or a syntactically-string + but not-a-version value (a wildcard, whitespace, arbitrary text) must + all be rejected -- otherwise a malformed lock entry would silently + produce an invalid ``name==`` dependency/PURL instead of + being warned and skipped.""" + assert not is_usable_version(version) diff --git a/tests/extract/test_locked_dependencies.py b/tests/extract/test_locked_dependencies.py index 3a9808ea..061b4562 100644 --- a/tests/extract/test_locked_dependencies.py +++ b/tests/extract/test_locked_dependencies.py @@ -93,6 +93,67 @@ def test_apply_locked_dependencies_overrides_prior_source_with_note( assert "pylock.toml takes priority" in caplog.text +def test_apply_locked_dependencies_unrecognized_previous_source_warns( + caplog: pytest.LogCaptureFixture, +) -> None: + """A `provenance["locked_dependencies"]` source name that doesn't + match any entry in `_LOCK_SOURCES` (a bug, e.g. after a future rename + drifts the two apart) can't be ranked -- warn, rather than silently + letting every cascade-tried format skip the override check.""" + with tempfile.TemporaryDirectory() as tmp: + tmp_path = Path(tmp) + _write_pylock(tmp_path, "requests", "2.31.0") + metadata = ProjectMetadata( + name="pkg", + locked_dependencies=["mystery==1.0.0"], + provenance={ + "locked_dependencies": ( + "Source: mystery-tool | Method: resolved_lockfile" + ) + }, + ) + + with caplog.at_level(logging.WARNING): + apply_locked_dependencies(metadata, tmp_path) + + assert "doesn't match any known lock source" in caplog.text + assert metadata.locked_dependencies == ["requests==2.31.0"] + + +def test_apply_locked_dependencies_valid_empty_source_wins_over_lower_priority() -> ( + None +): + """Regression: a valid, higher-priority lock that resolves to zero + runtime dependencies must still win outright over a lower-priority + source that *does* have dependencies -- an empty result is a real, + authoritative answer ("this lock says there are none"), not the same + as "this source doesn't apply here". Without distinguishing the two, + the lower-priority ``uv.lock`` here would incorrectly add dependencies + the winning ``pylock.toml`` says don't exist.""" + with tempfile.TemporaryDirectory() as tmp: + tmp_path = Path(tmp) + (tmp_path / "pylock.toml").write_text( + 'lock-version = "1.0"\ncreated-by = "test"\n', encoding="utf-8" + ) + (tmp_path / "uv.lock").write_text( + 'version = 1\nrevision = 1\nrequires-python = ">=3.10"\n' + '[[package]]\nname = "demo"\nversion = "1.0.0"\n' + 'source = { editable = "." }\n' + 'dependencies = [{ name = "requests" }]\n\n' + '[[package]]\nname = "requests"\nversion = "2.31.0"\n' + 'source = { registry = "https://pypi.org/simple" }\n', + encoding="utf-8", + ) + metadata = ProjectMetadata(name="demo") + + apply_locked_dependencies(metadata, tmp_path) + + assert metadata.locked_dependencies == [] + assert metadata.provenance["locked_dependencies"] == ( + "Source: pylock.toml | Method: resolved_lockfile" + ) + + def test_read_project_applies_cascade_for_setup_py_only_project() -> None: """Regression: a project with no `pyproject.toml` at all -- just a bare `setup.py`, the realistic pairing for `Pipfile.lock`/pinned diff --git a/tests/extract/test_pdm_lock.py b/tests/extract/test_pdm_lock.py index 302dd986..49bd6490 100644 --- a/tests/extract/test_pdm_lock.py +++ b/tests/extract/test_pdm_lock.py @@ -33,9 +33,23 @@ def _write_lock(tmp_dir: Path, body: str = "") -> None: (tmp_dir / "pdm.lock").write_text(body, encoding="utf-8") -def test_no_lock_file_returns_empty_list() -> None: +def test_no_lock_file_returns_none() -> None: + """`None` (absent/unusable), not `[]` (valid, zero dependencies) -- + the cascade in `_locked_dependencies.py` relies on this distinction + to let a lower-priority source apply when this one is truly absent.""" with tempfile.TemporaryDirectory() as tmp: - assert not extract_pdm_lock_dependencies(Path(tmp)) + assert extract_pdm_lock_dependencies(Path(tmp)) is None + + +def test_valid_lock_with_no_packages_returns_empty_list_not_none() -> None: + """A `pdm.lock` with zero packages is a real, valid answer -- must be + `[]`, not `None`, so the cascade treats it as a winning (if empty) + result rather than "not present".""" + with tempfile.TemporaryDirectory() as tmp: + tmp_path = Path(tmp) + _write_lock(tmp_path, "") + + assert extract_pdm_lock_dependencies(tmp_path) == [] def test_malformed_toml_returns_empty_list_and_warns( diff --git a/tests/extract/test_pipfile_lock.py b/tests/extract/test_pipfile_lock.py index fd7fb84a..ed0c9deb 100644 --- a/tests/extract/test_pipfile_lock.py +++ b/tests/extract/test_pipfile_lock.py @@ -32,9 +32,12 @@ def _write_lock(tmp_dir: Path, data: dict[str, object]) -> None: (tmp_dir / "Pipfile.lock").write_text(json.dumps(data), encoding="utf-8") -def test_no_lock_file_returns_empty_list() -> None: +def test_no_lock_file_returns_none() -> None: + """`None` (absent/unusable), not `[]` (valid, zero dependencies) -- + the cascade in `_locked_dependencies.py` relies on this distinction + to let a lower-priority source apply when this one is truly absent.""" with tempfile.TemporaryDirectory() as tmp: - assert not extract_pipfile_lock_dependencies(Path(tmp)) + assert extract_pipfile_lock_dependencies(Path(tmp)) is None def test_malformed_json_returns_empty_list_and_warns( @@ -65,14 +68,16 @@ def test_default_section_not_a_dict_returns_empty_list_and_warns( assert "expected a table" in caplog.text -def test_no_default_section_returns_empty_list() -> None: +def test_no_default_section_returns_empty_list_not_none() -> None: """A Pipfile.lock with no `default` key at all (unusual but not - invalid) is treated as zero runtime dependencies, not an error.""" + invalid) is treated as zero runtime dependencies, not an error -- + and must return `[]`, not `None`, so the cascade treats this lock as + a real, winning (if empty) answer rather than "not present".""" with tempfile.TemporaryDirectory() as tmp: tmp_path = Path(tmp) _write_lock(tmp_path, {"develop": {"pytest": {"version": "==8.0.0"}}}) - assert not extract_pipfile_lock_dependencies(tmp_path) + assert extract_pipfile_lock_dependencies(tmp_path) == [] def test_simple_dependency_resolved() -> None: @@ -109,6 +114,7 @@ def test_develop_section_excluded() -> None: result = extract_pipfile_lock_dependencies(tmp_path) + assert result is not None assert result == ["requests==2.31.0"] assert "pytest" not in " ".join(result) @@ -366,6 +372,7 @@ def test_real_world_requests_html() -> None: REAL_WORLD_LOCKS / "requests-html-0.10.0" ) + assert dependencies is not None names = {dep.split("==", maxsplit=1)[0] for dep in dependencies} assert "requests" in names assert "beautifulsoup4" in names @@ -383,6 +390,7 @@ def test_real_world_responder() -> None: REAL_WORLD_LOCKS / "responder-2.0.0" ) + assert dependencies is not None names = {dep.split("==", maxsplit=1)[0] for dep in dependencies} assert "requests" in names assert "responder" not in names # self-referential, editable/path-sourced diff --git a/tests/extract/test_poetry_lock.py b/tests/extract/test_poetry_lock.py index 9eb87671..774ca533 100644 --- a/tests/extract/test_poetry_lock.py +++ b/tests/extract/test_poetry_lock.py @@ -34,9 +34,23 @@ def _write_lock(tmp_dir: Path, content: str) -> None: (tmp_dir / "poetry.lock").write_text(content, encoding="utf-8") -def test_no_lock_file_returns_empty_list() -> None: +def test_no_lock_file_returns_none() -> None: + """`None` (absent/unusable), not `[]` (valid, zero dependencies) -- + the cascade in `_locked_dependencies.py` relies on this distinction + to let a lower-priority source apply when this one is truly absent.""" with tempfile.TemporaryDirectory() as tmp: - assert not extract_poetry_lock_dependencies(Path(tmp)) + assert extract_poetry_lock_dependencies(Path(tmp)) is None + + +def test_valid_lock_with_no_packages_returns_empty_list_not_none() -> None: + """A `poetry.lock` with zero packages is a real, valid answer -- must + be `[]`, not `None`, so the cascade treats it as a winning (if empty) + result rather than "not present".""" + with tempfile.TemporaryDirectory() as tmp: + tmp_path = Path(tmp) + _write_lock(tmp_path, "") + + assert extract_poetry_lock_dependencies(tmp_path) == [] def test_malformed_toml_returns_empty_list_and_warns( @@ -221,6 +235,7 @@ def test_fixture_lock_excludes_dev_group_dependency() -> None: `[tool.poetry.group.dev.dependencies]` and must not appear.""" result = extract_poetry_lock_dependencies(POETRY_FIXTURE) + assert result is not None names = {dep.split("==", maxsplit=1)[0] for dep in result} assert "pytest" not in names assert "numpy" in names @@ -328,14 +343,19 @@ def test_real_world_pastel_tool_poetry_only() -> None: def test_real_world_tomlkit_has_no_main_group_dependencies() -> None: """`tomlkit` is a standalone TOML library with no runtime dependencies -- every entry in its `poetry.lock` belongs to the - `dev`/docs/test groups, none to `main`. A real, valid "empty - resolved set" case: `read_pyproject()` still succeeds, but leaves - `locked_dependencies` empty and sets no provenance for it, same as - the no-lock-file case.""" + `dev`/docs/test groups, none to `main`. A real, valid "empty resolved + set" case: `read_pyproject()` still succeeds, leaves + `locked_dependencies` empty, but *does* record provenance for it -- + a real, parsed ``poetry.lock`` that authoritatively resolves to zero + ``main``-group packages is a genuine answer, not the same as no lock + file being present at all (see ``_locked_dependencies.py``'s + None-vs-``[]`` extractor contract).""" metadata, _config = read_pyproject( REAL_WORLD_LOCKS / "tomlkit-0.15.1" / "pyproject.toml" ) assert metadata.name == "tomlkit" assert metadata.locked_dependencies == [] - assert "locked_dependencies" not in metadata.provenance + assert metadata.provenance["locked_dependencies"] == ( + "Source: poetry.lock | Method: resolved_lockfile" + ) diff --git a/tests/extract/test_pylock.py b/tests/extract/test_pylock.py index 6edb943e..925b2141 100644 --- a/tests/extract/test_pylock.py +++ b/tests/extract/test_pylock.py @@ -34,9 +34,23 @@ def _write_lock(tmp_dir: Path, packages: str = "") -> None: (tmp_dir / "pylock.toml").write_text(_LOCK_VERSION + packages, encoding="utf-8") -def test_no_lock_file_returns_empty_list() -> None: +def test_no_lock_file_returns_none() -> None: + """`None` (absent/unusable), not `[]` (valid, zero dependencies) -- + the cascade in `_locked_dependencies.py` relies on this distinction + to let a lower-priority source apply when this one is truly absent.""" with tempfile.TemporaryDirectory() as tmp: - assert not extract_pylock_dependencies(Path(tmp)) + assert extract_pylock_dependencies(Path(tmp)) is None + + +def test_valid_lock_with_no_packages_returns_empty_list_not_none() -> None: + """A `pylock.toml` with zero resolved packages is a real, valid + answer -- must be `[]`, not `None`, so the cascade treats it as a + winning (if empty) result rather than "not present".""" + with tempfile.TemporaryDirectory() as tmp: + tmp_path = Path(tmp) + _write_lock(tmp_path) + + assert extract_pylock_dependencies(tmp_path) == [] def test_malformed_toml_returns_empty_list_and_warns( @@ -72,6 +86,64 @@ def test_missing_lock_version_returns_empty_list_and_warns( assert "lock-version" in caplog.text +@pytest.mark.parametrize("lock_version", ["garbage", "1", "1.0.0", "not.a.version"]) +def test_malformed_lock_version_returns_none_and_warns( + lock_version: str, caplog: pytest.LogCaptureFixture +) -> None: + with tempfile.TemporaryDirectory() as tmp: + tmp_path = Path(tmp) + (tmp_path / "pylock.toml").write_text( + f'lock-version = "{lock_version}"\n' + '[[packages]]\nname = "requests"\nversion = "2.31.0"\n', + encoding="utf-8", + ) + + with caplog.at_level(logging.WARNING): + result = extract_pylock_dependencies(tmp_path) + + assert result is None + assert "lock-version" in caplog.text + + +def test_unsupported_major_lock_version_returns_none_and_warns( + caplog: pytest.LogCaptureFixture, +) -> None: + with tempfile.TemporaryDirectory() as tmp: + tmp_path = Path(tmp) + (tmp_path / "pylock.toml").write_text( + 'lock-version = "2.0"\n' + '[[packages]]\nname = "requests"\nversion = "2.31.0"\n', + encoding="utf-8", + ) + + with caplog.at_level(logging.WARNING): + result = extract_pylock_dependencies(tmp_path) + + assert result is None + assert "major version" in caplog.text + + +def test_newer_minor_lock_version_still_parsed_with_warning( + caplog: pytest.LogCaptureFixture, +) -> None: + """A newer *minor* version within the same major is forward-compatible + per PEP 751 -- read anyway, just with a warning that some content may + be unrecognized.""" + with tempfile.TemporaryDirectory() as tmp: + tmp_path = Path(tmp) + (tmp_path / "pylock.toml").write_text( + 'lock-version = "1.5"\n' + '[[packages]]\nname = "requests"\nversion = "2.31.0"\n', + encoding="utf-8", + ) + + with caplog.at_level(logging.WARNING): + result = extract_pylock_dependencies(tmp_path) + + assert result == ["requests==2.31.0"] + assert "newer" in caplog.text.lower() + + def test_package_included() -> None: with tempfile.TemporaryDirectory() as tmp: tmp_path = Path(tmp) diff --git a/tests/extract/test_requirements_txt.py b/tests/extract/test_requirements_txt.py index f60335a4..a2c758d7 100644 --- a/tests/extract/test_requirements_txt.py +++ b/tests/extract/test_requirements_txt.py @@ -32,9 +32,24 @@ def _write_requirements(tmp_dir: Path, content: str) -> None: (tmp_dir / "requirements.txt").write_text(content, encoding="utf-8") -def test_no_file_returns_empty_list() -> None: +def test_no_file_returns_none() -> None: + """`None` (absent/unusable), not `[]` (valid, zero dependencies) -- + the cascade in `_locked_dependencies.py` relies on this distinction + to let a lower-priority source apply when this one is truly absent.""" with tempfile.TemporaryDirectory() as tmp: - assert not extract_pinned_requirements_dependencies(Path(tmp)) + assert extract_pinned_requirements_dependencies(Path(tmp)) is None + + +def test_all_comments_and_blank_lines_returns_empty_list_not_none() -> None: + """A `requirements.txt` with no real dependency lines at all is a + real, valid, fully-pinned (vacuously) file -- must be `[]`, not + `None`, so the cascade treats it as a winning (if empty) result + rather than "not present".""" + with tempfile.TemporaryDirectory() as tmp: + tmp_path = Path(tmp) + _write_requirements(tmp_path, "# just a comment\n\n") + + assert extract_pinned_requirements_dependencies(tmp_path) == [] def test_all_pinned_lines_included() -> None: @@ -197,6 +212,20 @@ def test_backslash_continuation_joined_before_parsing() -> None: assert result == ["requests==2.31.0", "idna==3.7"] +def test_dangling_continuation_at_end_of_file_still_joined() -> None: + """A file whose very last physical line ends in a backslash (no + further line follows it at all) must still flush that buffered, + still-unterminated logical line -- not silently drop it -- exercising + the end-of-file flush branch distinct from the normal per-line join.""" + with tempfile.TemporaryDirectory() as tmp: + tmp_path = Path(tmp) + _write_requirements(tmp_path, "requests==2.31.0 \\") + + result = extract_pinned_requirements_dependencies(tmp_path) + + assert result == ["requests==2.31.0"] + + def test_hash_annotated_continuation_still_disqualifies_whole_file( caplog: pytest.LogCaptureFixture, ) -> None: diff --git a/tests/extract/test_uv_lock.py b/tests/extract/test_uv_lock.py index 014e13e3..b7183565 100644 --- a/tests/extract/test_uv_lock.py +++ b/tests/extract/test_uv_lock.py @@ -36,9 +36,12 @@ def _write_lock(tmp_dir: Path, body: str = "") -> None: (tmp_dir / "uv.lock").write_text(_LOCK_HEADER + body, encoding="utf-8") -def test_no_lock_file_returns_empty_list() -> None: +def test_no_lock_file_returns_none() -> None: + """`None` (absent/unusable), not `[]` (valid, zero dependencies) -- + the cascade in `_locked_dependencies.py` relies on this distinction + to let a lower-priority source apply when this one is truly absent.""" with tempfile.TemporaryDirectory() as tmp: - assert not extract_uv_lock_dependencies(Path(tmp)) + assert extract_uv_lock_dependencies(Path(tmp)) is None def test_malformed_toml_returns_empty_list_and_warns( @@ -108,13 +111,15 @@ def test_root_dependencies_not_a_list_returns_empty_list_and_warns( assert "expected a list" in caplog.text -def test_root_with_no_dependencies_key_returns_empty_list() -> None: - """A project with zero runtime dependencies is valid, not an error.""" +def test_root_with_no_dependencies_key_returns_empty_list_not_none() -> None: + """A project with zero runtime dependencies is valid, not an error -- + and must return `[]`, not `None`, so the cascade treats this lock as + a real, winning (if empty) answer rather than "not present".""" with tempfile.TemporaryDirectory() as tmp: tmp_path = Path(tmp) _write_lock(tmp_path, _ROOT_HEADER) - assert not extract_uv_lock_dependencies(tmp_path) + assert extract_uv_lock_dependencies(tmp_path) == [] def test_simple_dependency_resolved() -> None: @@ -290,6 +295,83 @@ def test_dependency_missing_version_skipped_and_warns( assert "missing" in caplog.text.lower() +def test_transitive_dependency_of_a_direct_dependency_is_included() -> None: + """The root's own `dependencies` list is only the first layer -- a + package it depends on can itself have further dependencies, and + those must be walked too, not just the root's immediate list.""" + with tempfile.TemporaryDirectory() as tmp: + tmp_path = Path(tmp) + _write_lock( + tmp_path, + _ROOT_HEADER + 'dependencies = [{ name = "requests" }]\n\n' + '[[package]]\nname = "requests"\nversion = "2.31.0"\n' + 'source = { registry = "https://pypi.org/simple" }\n' + 'dependencies = [{ name = "urllib3" }, { name = "certifi" }]\n\n' + '[[package]]\nname = "urllib3"\nversion = "2.2.0"\n' + 'source = { registry = "https://pypi.org/simple" }\n\n' + '[[package]]\nname = "certifi"\nversion = "2024.2.2"\n' + 'source = { registry = "https://pypi.org/simple" }\n', + ) + + result = extract_uv_lock_dependencies(tmp_path) + + assert result is not None + assert set(result) == { + "requests==2.31.0", + "urllib3==2.2.0", + "certifi==2024.2.2", + } + + +def test_diamond_dependency_visited_only_once() -> None: + """Two of the root's direct dependencies sharing a common transitive + dependency must not cause that shared package to be processed (or + emitted) twice.""" + with tempfile.TemporaryDirectory() as tmp: + tmp_path = Path(tmp) + _write_lock( + tmp_path, + _ROOT_HEADER + 'dependencies = [{ name = "pkg-a" }, { name = "pkg-b" }]\n\n' + '[[package]]\nname = "pkg-a"\nversion = "1.0.0"\n' + 'source = { registry = "https://pypi.org/simple" }\n' + 'dependencies = [{ name = "shared" }]\n\n' + '[[package]]\nname = "pkg-b"\nversion = "1.0.0"\n' + 'source = { registry = "https://pypi.org/simple" }\n' + 'dependencies = [{ name = "shared" }]\n\n' + '[[package]]\nname = "shared"\nversion = "0.1.0"\n' + 'source = { registry = "https://pypi.org/simple" }\n', + ) + + result = extract_uv_lock_dependencies(tmp_path) + + assert result is not None + assert result.count("shared==0.1.0") == 1 + + +def test_nested_dependencies_not_a_list_skipped_and_warns( + caplog: pytest.LogCaptureFixture, +) -> None: + """A resolved package's own `dependencies` key, if present at all, + must be a list -- a malformed non-list value (but still truthy, so + distinct from a missing key) is warned and simply not walked into + further, not treated as a parse error for the whole file.""" + with tempfile.TemporaryDirectory() as tmp: + tmp_path = Path(tmp) + _write_lock( + tmp_path, + _ROOT_HEADER + 'dependencies = [{ name = "requests" }]\n\n' + '[[package]]\nname = "requests"\nversion = "2.31.0"\n' + 'source = { registry = "https://pypi.org/simple" }\n' + 'dependencies = "not-a-list"\n', + ) + + with caplog.at_level(logging.WARNING): + result = extract_uv_lock_dependencies(tmp_path) + + assert result == ["requests==2.31.0"] + assert "nested 'dependencies'" in caplog.text + + def test_dependency_with_no_source_table_still_included() -> None: """A package entry with no `source` key at all (unusual but not invalid) is treated the same as a registry source -- only an diff --git a/tests/extract/test_uv_lock_integration.py b/tests/extract/test_uv_lock_integration.py index 007e49ed..3bb73e47 100644 --- a/tests/extract/test_uv_lock_integration.py +++ b/tests/extract/test_uv_lock_integration.py @@ -147,7 +147,9 @@ def test_real_world_flask() -> None: """`pallets/flask` -- `uv.lock` ships in the PyPI sdist itself (the only fixture where that's true, per real-world-locks/README.md). Has multiple marker-conditional duplicate names (e.g. `click`), - exercising the ambiguity-skip path against real data.""" + exercising the ambiguity-skip path against real data. Transitive + walk adds `zipp` (a dependency of `importlib-metadata`, not a direct + Flask dependency) beyond the root's own immediate dependency list.""" metadata, _config, _path = read_project(REAL_WORLD_LOCKS / "flask-3.1.3") assert metadata.name == "Flask" @@ -159,6 +161,7 @@ def test_real_world_flask() -> None: "jinja2", "markupsafe", "werkzeug", + "zipp", } assert "click" not in names # ambiguous (ships two marker-conditional versions) assert metadata.provenance["locked_dependencies"] == ( @@ -167,25 +170,59 @@ def test_real_world_flask() -> None: def test_real_world_fastapi_cli() -> None: + """Transitive walk pulls in `typer`'s and `uvicorn`'s own + dependencies (`click`, `rich`, `h11`, etc.), not just the root's four + immediate dependencies.""" metadata, _config, _path = read_project(REAL_WORLD_LOCKS / "fastapi-cli-0.0.32") assert metadata.name == "fastapi-cli" names = {dep.split("==", maxsplit=1)[0] for dep in metadata.locked_dependencies} - assert names == {"rich-toolkit", "tomli", "typer", "uvicorn"} + assert names == { + "annotated-doc", + "click", + "colorama", + "h11", + "markdown-it-py", + "mdurl", + "pygments", + "rich", + "rich-toolkit", + "shellingham", + "tomli", + "typer", + "typing-extensions", + "uvicorn", + } def test_real_world_abi3audit() -> None: + """Transitive walk pulls in `requests`'/`requests-cache`'s/`rich`'s + own dependencies (`urllib3`, `certifi`, `cattrs`, etc.), not just the + root's eight immediate dependencies.""" metadata, _config, _path = read_project(REAL_WORLD_LOCKS / "abi3audit-0.0.26") assert metadata.name == "abi3audit" names = {dep.split("==", maxsplit=1)[0] for dep in metadata.locked_dependencies} assert names == { "abi3info", + "attrs", + "cattrs", + "certifi", + "charset-normalizer", + "exceptiongroup", + "idna", "kaitaistruct", + "markdown-it-py", + "mdurl", "packaging", "pefile", + "platformdirs", "pyelftools", + "pygments", "requests", "requests-cache", "rich", + "typing-extensions", + "url-normalize", + "urllib3", } diff --git a/tests/extract/test_uv_lock_root_package.py b/tests/extract/test_uv_lock_root_package.py index cb5fbc24..2182513f 100644 --- a/tests/extract/test_uv_lock_root_package.py +++ b/tests/extract/test_uv_lock_root_package.py @@ -14,16 +14,35 @@ """ import logging +import tempfile +from pathlib import Path import pytest -from pitloom.extract._uv_lock import _find_root_package, _pinned_dep_for_package +from pitloom.extract._uv_lock import ( + _expected_project_name, + _find_root_package, + _pinned_dep_for_package, +) def test_find_root_package_returns_none_for_empty_list() -> None: assert _find_root_package([], None) is None +def test_expected_project_name_returns_none_when_project_table_not_a_dict() -> None: + """A malformed `[project]` value (e.g. a bare string instead of a + table) can't carry a `name` -- treated as "can't determine", not a + parse error.""" + with tempfile.TemporaryDirectory() as tmp: + tmp_path = Path(tmp) + (tmp_path / "pyproject.toml").write_text( + 'project = "not-a-table"\n', encoding="utf-8" + ) + + assert _expected_project_name(tmp_path) is None + + def test_find_root_package_ignores_malformed_entries() -> None: """A malformed top-level `[[package]]` entry (not a table) is silently skipped while searching for the root package -- see diff --git a/working-docs/implementation/lock-file-cascade.md b/working-docs/implementation/lock-file-cascade.md index c3e73140..f1748813 100644 --- a/working-docs/implementation/lock-file-cascade.md +++ b/working-docs/implementation/lock-file-cascade.md @@ -71,19 +71,30 @@ def apply_locked_dependencies(metadata: ProjectMetadata, project_dir: Path) -> N ``` Each entry pairs a source name, an extractor matching the uniform -`_LockExtractor` shape (`(project_dir, expected_name) -> list[str]` of -exact-pin PEP 508 strings, empty when absent/unusable), and a -provenance `Method` tag. Only `uv.lock`'s own extractor uses -*expected_name* (to disambiguate a shared workspace lock's multiple -local package entries without re-reading `pyproject.toml` a second -time); `pylock.toml`'s, `pdm.lock`'s, and `Pipfile.lock`'s extractors -keep their simpler, single-`project_dir` signature and are wrapped with -`_ignore_expected_name()` when registered in `_LOCK_SOURCES` above, -rather than widening every format's own signature for a need only one -of them has. `apply_locked_dependencies()` tries each extractor-bearing -entry in priority order (highest first) and applies the first non-empty -result, in place, onto `metadata.locked_dependencies` and -`metadata.provenance["locked_dependencies"]`. +`_LockExtractor` shape (`(project_dir, expected_name) -> list[str] | None` +of exact-pin PEP 508 strings), and a provenance `Method` tag. Only +`uv.lock`'s own extractor uses *expected_name* (to disambiguate a shared +workspace lock's multiple local package entries without re-reading +`pyproject.toml` a second time); `pylock.toml`'s, `pdm.lock`'s, and +`Pipfile.lock`'s extractors keep their simpler, single-`project_dir` +signature and are wrapped with `_ignore_expected_name()` when registered +in `_LOCK_SOURCES` above, rather than widening every format's own +signature for a need only one of them has. `apply_locked_dependencies()` +tries each extractor-bearing entry in priority order (highest first) and +applies the first result that isn't `None`, in place, onto +`metadata.locked_dependencies` and `metadata.provenance["locked_dependencies"]`. + +**`None` and `[]` mean different things, and the distinction matters.** +`None` means this source doesn't apply here at all (absent, unparseable, +or otherwise unusable) -- try the next source in priority order. `[]` +means this source *does* apply, and is a real, authoritative answer: a +successfully-parsed lock that genuinely resolves to zero runtime +dependencies. The cascade treats `[]` as a win like any other -- it stops +and does *not* fall through to a lower-priority source, since that would +let a lower-priority, less-authoritative source add dependencies the +winning lock says don't exist. Every extractor in `_LOCK_SOURCES` +(and `poetry.lock`'s own `_try_read_poetry()` gate) follows this same +contract. Every entry's `Method` tag is `"resolved_lockfile"` except `requirements.txt`'s own `"pinned_requirements"` -- it's not a real @@ -172,14 +183,23 @@ a real environment, `_uv_lock.py`: `source.virtual` marker -- how uv distinguishes "this is the local project" from a PyPI download) instead of scanning every `[[package]]` entry directly. -2. Reads only that entry's own `dependencies` list (main/runtime -- - `optional-dependencies`/`dev-dependencies` are extras and dev - groups, excluded the same way `poetry.lock`'s non-`main` groups are). +2. Breadth-first walks the dependency graph starting from that entry's + own `dependencies` list (main/runtime -- `optional-dependencies`/ + `dev-dependencies` are extras and dev groups, excluded the same way + `poetry.lock`'s non-`main` groups are), in `_collect_transitive_dependencies()`. + This isn't just the root's *immediate* dependencies: each resolved + package's own `dependencies` list is walked too, since the installed + set is the closure over that graph, not just its first layer (e.g. a + CLI tool's direct dependency on a web framework that itself pulls in + several more packages). PEP 503-canonicalized names guard against + revisiting the same package twice (a diamond dependency shared by two + branches) or looping on a cycle. 3. Resolves each referenced name against the flat table only when exactly one candidate exists for that name; an ambiguous (multiple-version) or marker-conditional (inline `version` on the dependency reference itself) name is skipped with a `WARNING:`, not - guessed. See `tests/fixtures/real-world-locks/README.md`'s `flask` + guessed, and nothing depending only on that skipped name is walked + into either. See `tests/fixtures/real-world-locks/README.md`'s `flask` entry for a real fixture exercising this (its `click` dependency is deliberately absent from `locked_dependencies`). @@ -291,13 +311,19 @@ similar in spirit: just a `(name, version)` pair by *canonicalized* name" shape instead (their conflict check has to treat `Flask`/`flask` as the same package) -- `pitloom.extract._lock_common.group_versions_by_canonical_name()`. -- **Validating a `version` field is a non-empty string.** `not - isinstance(version, str) or not version` existed independently in all - five extractors before being factored into - `pitloom.extract._lock_common.is_usable_version()`. `_pipfile_lock.py` - calls it too, as the first of two checks -- its own `version` - validation is strictly larger (a full PEP 440 exact-`==`-specifier - parse on top), not a replacement for the shared non-empty-string check. +- **Validating a `version` field is a usable PEP 440 version.** + `pitloom.extract._lock_common.is_usable_version()` checks a field is a + non-empty string *and* parses as a valid `packaging.version.Version` + -- rejecting not just non-strings but a syntactically-string-yet-not-a- + version value too (a wildcard like `"*"`, whitespace, arbitrary text), + which a bare non-empty-string check would let through into an invalid + `name==` pin. Shared by `_poetry_lock.py`, `_pylock.py`, + `_uv_lock.py`, and `_pdm_lock.py`, whose `version` fields are plain + version numbers. `_pipfile_lock.py` does **not** use it: its own + `version` field is already a full PEP 440 *specifier* string (e.g. + `"==2.31.0"`), not a bare version, so it does its own + `isinstance`/non-empty check directly before parsing that specifier + with `packaging.specifiers.SpecifierSet` and `single_exact_pin()`. - **The non-registry-source `WARNING:` message.** `"Skipping entry %r: %s-sourced dependencies cannot be represented as a PEP 508 specifier"` was copy-pasted, wording-identical, into all five @@ -399,10 +425,12 @@ unaffected -- purely additive. ## Adding a new format to the cascade -1. Write `extract__dependencies(project_dir: Path) -> list[str]` +1. Write `extract__dependencies(project_dir: Path) -> list[str] | None` in its own `src/pitloom/extract/_.py`, following - `_pylock.py`'s shape: exact-pin PEP 508 strings, empty list when - absent/unusable, `WARNING:` (never a silent drop) for anything + `_pylock.py`'s shape: exact-pin PEP 508 strings, `None` when + absent/unusable (as opposed to a valid-but-empty `[]` -- see the + "`None` and `[]` mean different things" note above), `WARNING:` + (never a silent drop) for anything malformed or non-registry-sourced. Use `_lock_common.load_lock_toml()` to load the file (or `_lock_common.load_lock_json()` for a JSON-format lock file -- `_pipfile_lock.py` is the precedent), and (if the format diff --git a/working-docs/implementation/pep751-pylock-support.md b/working-docs/implementation/pep751-pylock-support.md index a063d0da..42f9ee1a 100644 --- a/working-docs/implementation/pep751-pylock-support.md +++ b/working-docs/implementation/pep751-pylock-support.md @@ -1,6 +1,6 @@ --- Created: 2026-09-02 -Last-Modified: 2026-09-04 +Last-Modified: 2026-09-05 SPDX-FileCopyrightText: 2026-present Arthit Suriyawongkul SPDX-FileType: DOCUMENTATION SPDX-License-Identifier: CC0-1.0 @@ -55,8 +55,11 @@ already covers that layer generically and needed no changes either. Reads `pylock.toml` next to `pyproject.toml` and returns its resolved `[[packages]]` entries as exact-pin `name==version` PEP 508 strings. -Returns an empty list when no `pylock.toml` is present or it can't be -parsed -- optional enrichment, never a requirement. +Returns `None` when no `pylock.toml` is present or it can't be parsed -- +optional enrichment, never a requirement. `None` (as opposed to a +valid-but-empty `[]`) tells the cascade this source doesn't apply here, +so a lower-priority source can still be tried, rather than a genuinely +dependency-free lock file being confused with an absent/unusable one. Unlike `poetry.lock`, PEP 751 has no `groups`-style per-package membership tag to filter on: a `pylock.toml` is already the flattened, @@ -66,11 +69,19 @@ tool that generated it was asked to include (`dependency-groups`/ per-package "which group requested me" marker). So every `[[packages]]` entry is taken as-is, with no group-based filtering. -A malformed lock (missing/non-string top-level `lock-version`, a -`packages` key that isn't a list, or an individual `[[packages]]` entry -missing/non-string `name`/`version`) is skipped with a `WARNING:`, not -silently dropped, per this repo's "no silent deviations" rule -- -mirrors `poetry.lock`'s equivalent malformed-entry handling. +A malformed lock (a `packages` key that isn't a list, or an individual +`[[packages]]` entry missing/non-string `name`/`version`) is skipped +with a `WARNING:`, not silently dropped, per this repo's "no silent +deviations" rule -- mirrors `poetry.lock`'s equivalent malformed-entry +handling. The top-level `lock-version` field is validated more strictly +than a bare presence check: it must parse as a `major.minor` pair +(rejecting e.g. `"garbage"` or `"1.0.0"`), and a *major* version other +than the one this Pitloom release understands (`1`) is rejected +outright -- PEP 751 may define an incompatible schema under a future +major version. A newer *minor* version within the known major (e.g. +`"1.5"` when this release only knows `"1.0"`) is still read, per PEP +751's additive-minor-versions policy, but with a `WARNING:` that some +content may go unrecognized. ## Non-registry sources From 6b6ce1b880db8df55fbb13893e2c522471aec9dc Mon Sep 17 00:00:00 2001 From: Arthit Suriyawongkul Date: Sat, 5 Sep 2026 11:27:06 +0700 Subject: [PATCH 10/35] Fix lock file info leaks and malform handling Signed-off-by: Arthit Suriyawongkul --- docs/dependency-sources.md | 2 +- pyproject.toml | 1 + src/pitloom/extract/_lock_common.py | 63 ++++++ src/pitloom/extract/_pdm_lock.py | 67 ++++--- src/pitloom/extract/_pipfile_lock.py | 23 ++- src/pitloom/extract/_poetry_lock.py | 25 +-- src/pitloom/extract/_pylock.py | 181 +++++++++++++++--- src/pitloom/extract/_uv_lock.py | 86 ++++++--- tests/extract/test_pdm_lock.py | 41 ++++ tests/extract/test_pipfile_lock.py | 18 ++ tests/extract/test_poetry_lock.py | 21 ++ tests/extract/test_pylock.py | 162 +++++++++++++++- tests/extract/test_uv_lock.py | 94 +++++++++ .../implementation/pep751-pylock-support.md | 41 ++-- 14 files changed, 715 insertions(+), 110 deletions(-) diff --git a/docs/dependency-sources.md b/docs/dependency-sources.md index c4c4f5b3..220c7c7a 100644 --- a/docs/dependency-sources.md +++ b/docs/dependency-sources.md @@ -37,7 +37,7 @@ exactly-pinned entries. | Priority | Format | File | What's included | | :---: | :--- | :--- | :--- | -| 1 (highest) | PEP 751 | `pylock.toml` | Every resolved package the file records. | +| 1 (highest) | PEP 751 | `pylock.toml` | Every resolved package the file records for its declared `default-groups` (a package needed only for a non-default dependency-group/extra, per its own `marker` field, is excluded). | | 2 | uv | `uv.lock` | Your project's own main/runtime dependencies, walked transitively (dependencies of dependencies, and so on) -- not `optional-dependencies` extras or `dev-dependencies` groups. A dependency pinned to more than one version for different Python versions is skipped, not guessed at, and nothing depending only on it is walked into either -- see below. | | 3 | Poetry | `poetry.lock` | Packages in the `main` dependency group only (not `[tool.poetry.group.*]` dev/extra groups). | | 4 | PDM | `pdm.lock` | Packages in the `default` dependency group only. | diff --git a/pyproject.toml b/pyproject.toml index 8b44c3ff..51ecd9f8 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -64,6 +64,7 @@ dependencies = [ "flit_core>=3.9", "hatchling>=1.32.0", "licenseid>=0.3.7", + "packaging>=24.0", "pdm-backend>=2.4.1", "pipdeptree>=4.2.3", "poetry-core>=2.4.1", diff --git a/src/pitloom/extract/_lock_common.py b/src/pitloom/extract/_lock_common.py index 51b29ace..065f6b63 100644 --- a/src/pitloom/extract/_lock_common.py +++ b/src/pitloom/extract/_lock_common.py @@ -41,7 +41,11 @@ "load_lock_json", "load_lock_toml", "single_exact_pin", + "warn_malformed_entry_not_table", + "warn_missing_name", + "warn_missing_version", "warn_non_registry_source", + "warn_top_level_key_wrong_type", ] #: The literal ``Source:`` name written into @@ -242,6 +246,65 @@ def warn_non_registry_source(lock_file: str, name: str, source_key: str) -> None ) +def warn_top_level_key_wrong_type( + lock_path: Path, key: str, value: object, expected: str, lock_file: str +) -> None: + """Log the shared ``": top-level '' key is , + expected -- ignoring "`` warning every + extractor's own top-level-shape check produces (a ``packages``/ + ``package`` key that isn't a list, a ``default`` key that isn't a + table) -- four-plus formats retyped this identically modulo the key + name, expected shape, and lock-file name before this was factored + out, the same "pattern hand-copied across 3+ call sites drifts" + concern :func:`warn_non_registry_source` already addresses for the + non-registry-source case. + """ + log.warning( + "%s: top-level '%s' key is %s, expected %s -- ignoring %s", + lock_path, + key, + type(value).__name__, + expected, + lock_file, + ) + + +def warn_missing_version(lock_file: str, name: str) -> None: + """Log the shared ``"Skipping entry '': missing or + non-string 'version'"`` warning -- identical across every format + that validates its ``version`` field via :func:`is_usable_version`.""" + log.warning( + "Skipping %s entry %r: missing or non-string 'version'", + lock_file, + name, + ) + + +def warn_malformed_entry_not_table( + lock_file: str, entry_label: str, value: object +) -> None: + """Log the shared ``"Skipping malformed + entry: expected a table, got "`` warning -- the identical + shape ``poetry.lock``'s, ``pylock.toml``'s, and ``pdm.lock``'s own + ``[[package]]``/``[[packages]]`` malformed-entry checks each retyped + independently before this was factored out.""" + log.warning( + "Skipping malformed %s %s entry: expected a table, got %s", + lock_file, + entry_label, + type(value).__name__, + ) + + +def warn_missing_name(context: str, name: object) -> None: + """Log the shared ``": missing or non-string 'name' + (name=)"`` warning tail -- *context* supplies each call site's + own lead-in (which format, which kind of entry) since that part + genuinely differs per site, while the recurring "missing or + non-string 'name'" wording itself doesn't.""" + log.warning("%s: missing or non-string 'name' (name=%r)", context, name) + + def find_first_present_key( mapping: Mapping[str, object], keys: Iterable[str] ) -> str | None: diff --git a/src/pitloom/extract/_pdm_lock.py b/src/pitloom/extract/_pdm_lock.py index 38f31b3e..913eac17 100644 --- a/src/pitloom/extract/_pdm_lock.py +++ b/src/pitloom/extract/_pdm_lock.py @@ -46,7 +46,11 @@ group_versions_by_canonical_name, is_usable_version, load_lock_toml, + warn_malformed_entry_not_table, + warn_missing_name, + warn_missing_version, warn_non_registry_source, + warn_top_level_key_wrong_type, ) log = logging.getLogger(__name__) @@ -68,6 +72,30 @@ _NON_REGISTRY_KEYS = ("git", "url", "path") +def _shape_validated_package(pkg: object) -> dict[str, Any] | None: + """Return *pkg* itself when it's a well-formed, versioned + ``[[package]]`` table -- ``None`` (with a ``WARNING:``) for a + non-table entry, or one with a missing/non-string ``name`` or + missing/unparseable ``version``. Split out of + :func:`_default_group_package_or_none` purely to keep each + function's own return-statement count under this repo's complexity + ceiling; the two checks it doesn't cover (group membership, + non-registry source) stay there since they need this function's own + early-exit to already have happened first.""" + if not isinstance(pkg, dict): + warn_malformed_entry_not_table("pdm.lock", "[[package]]", pkg) + return None + name = pkg.get("name") + if not isinstance(name, str) or not name: + warn_missing_name("Skipping malformed pdm.lock [[package]] entry", name) + return None + version = pkg.get("version") + if not is_usable_version(version): + warn_missing_version("pdm.lock", name) + return None + return pkg + + def _default_group_package_or_none(pkg: object) -> dict[str, Any] | None: """Return *pkg* itself when it's a well-formed, default-group, registry-sourced, versioned ``[[package]]`` entry -- ``None`` @@ -75,38 +103,27 @@ def _default_group_package_or_none(pkg: object) -> dict[str, Any] | None: non-registry-sourced; silent for a package that's simply not in the default group, the same "expected filtering" as ``poetry.lock``'s non-``main`` group exclusion).""" - if not isinstance(pkg, dict): - log.warning( - "Skipping malformed pdm.lock [[package]] entry: expected a table, got %s", - type(pkg).__name__, - ) + validated = _shape_validated_package(pkg) + if validated is None: return None - name = pkg.get("name") - if not isinstance(name, str) or not name: + name = validated["name"] + + groups = validated.get("groups", [_DEFAULT_GROUP]) + if not isinstance(groups, list): log.warning( - "Skipping malformed pdm.lock [[package]] entry: missing or " - "non-string 'name' (name=%r)", + "Skipping malformed pdm.lock entry %r: 'groups' is %s, expected a list", name, + type(groups).__name__, ) return None - - groups = pkg.get("groups", [_DEFAULT_GROUP]) - if not isinstance(groups, list) or _DEFAULT_GROUP not in groups: + if _DEFAULT_GROUP not in groups: return None - non_registry_key = find_first_present_key(pkg, _NON_REGISTRY_KEYS) + non_registry_key = find_first_present_key(validated, _NON_REGISTRY_KEYS) if non_registry_key is not None: warn_non_registry_source("pdm.lock", name, non_registry_key) return None - - version = pkg.get("version") - if not is_usable_version(version): - log.warning( - "Skipping pdm.lock entry %r: missing or non-string 'version'", - name, - ) - return None - return pkg + return validated def extract_pdm_lock_dependencies(project_dir: Path) -> list[str] | None: @@ -126,10 +143,8 @@ def extract_pdm_lock_dependencies(project_dir: Path) -> list[str] | None: packages = data.get("package", []) if not isinstance(packages, list): - log.warning( - "%s: top-level 'package' key is %s, expected a list -- ignoring pdm.lock", - lock_path, - type(packages).__name__, + warn_top_level_key_wrong_type( + lock_path, "package", packages, "a list", "pdm.lock" ) return None diff --git a/src/pitloom/extract/_pipfile_lock.py b/src/pitloom/extract/_pipfile_lock.py index bfb455ee..a3cc2de7 100644 --- a/src/pitloom/extract/_pipfile_lock.py +++ b/src/pitloom/extract/_pipfile_lock.py @@ -47,7 +47,9 @@ find_first_present_key, load_lock_json, single_exact_pin, + warn_missing_version, warn_non_registry_source, + warn_top_level_key_wrong_type, ) log = logging.getLogger(__name__) @@ -80,11 +82,8 @@ def extract_pipfile_lock_dependencies(project_dir: Path) -> list[str] | None: default_section = data.get("default", {}) if not isinstance(default_section, dict): - log.warning( - "%s: top-level 'default' key is %s, expected a table -- " - "ignoring Pipfile.lock", - lock_path, - type(default_section).__name__, + warn_top_level_key_wrong_type( + lock_path, "default", default_section, "a table", "Pipfile.lock" ) return None @@ -115,7 +114,14 @@ def _pinned_dep_for_package(name: object, entry: object) -> str | None: ) return None non_registry_key = find_first_present_key(entry, _NON_REGISTRY_KEYS) - if non_registry_key is not None: + if non_registry_key is not None and entry[non_registry_key] is not False: + # Every non-registry key except 'editable' is presence-is-enough + # (a git/hg/bzr/svn/path/file URL string). 'editable' is the + # schema's one boolean-valued key -- an explicit + # `"editable": false` (schema-legal, just uncommon) must not be + # mistaken for a real editable/VCS source the same way a + # present-but-falsy key would be for every other format's + # presence-only check. warn_non_registry_source("Pipfile.lock", name, non_registry_key) return None pinned_version = _exact_pinned_version(name, entry.get("version")) @@ -140,10 +146,7 @@ def _exact_pinned_version(name: str, version: object) -> str | None: # check doesn't apply here (a specifier isn't a bare version and # would always fail it); SpecifierSet()/single_exact_pin() below # already validate it's a genuine, single, exact pin. - log.warning( - "Skipping Pipfile.lock entry %r: missing or non-string 'version'", - name, - ) + warn_missing_version("Pipfile.lock", name) return None try: specifier_set = SpecifierSet(version) diff --git a/src/pitloom/extract/_poetry_lock.py b/src/pitloom/extract/_poetry_lock.py index fa675b5a..c2cf6f53 100644 --- a/src/pitloom/extract/_poetry_lock.py +++ b/src/pitloom/extract/_poetry_lock.py @@ -29,7 +29,9 @@ from pitloom.extract._lock_common import ( is_usable_version, load_lock_toml, + warn_malformed_entry_not_table, warn_non_registry_source, + warn_top_level_key_wrong_type, ) log = logging.getLogger(__name__) @@ -61,11 +63,8 @@ def extract_poetry_lock_dependencies(project_dir: Path) -> list[str] | None: packages = data.get("package", []) if not isinstance(packages, list): - log.warning( - "%s: top-level 'package' key is %s, expected a list -- " - "ignoring poetry.lock", - lock_path, - type(packages).__name__, + warn_top_level_key_wrong_type( + lock_path, "package", packages, "a list", "poetry.lock" ) return None @@ -92,11 +91,7 @@ def _pinned_dep_for_package(pkg: object) -> str | None: ordinary published release (wrong PURL, bogus PyPI enrichment lookup). """ if not isinstance(pkg, dict): - log.warning( - "Skipping malformed poetry.lock [[package]] entry: expected a " - "table, got %s", - type(pkg).__name__, - ) + warn_malformed_entry_not_table("poetry.lock", "[[package]]", pkg) return None name = pkg.get("name") version = pkg.get("version") @@ -109,7 +104,15 @@ def _pinned_dep_for_package(pkg: object) -> str | None: ) return None groups = pkg.get("groups", ["main"]) - if not isinstance(groups, list) or "main" not in groups: + if not isinstance(groups, list): + log.warning( + "Skipping malformed poetry.lock [[package]] entry %r: 'groups' " + "is %s, expected a list", + name, + type(groups).__name__, + ) + return None + if "main" not in groups: return None source = pkg.get("source") source_type = source.get("type") if isinstance(source, dict) else None diff --git a/src/pitloom/extract/_pylock.py b/src/pitloom/extract/_pylock.py index 78aa73ca..be804e9c 100644 --- a/src/pitloom/extract/_pylock.py +++ b/src/pitloom/extract/_pylock.py @@ -29,12 +29,19 @@ import logging from pathlib import Path +from typing import Any + +from packaging.markers import InvalidMarker, Marker from pitloom.extract._lock_common import ( find_first_present_key, is_usable_version, load_lock_toml, + warn_malformed_entry_not_table, + warn_missing_name, + warn_missing_version, warn_non_registry_source, + warn_top_level_key_wrong_type, ) log = logging.getLogger(__name__) @@ -43,6 +50,18 @@ _NON_REGISTRY_SOURCE_KEYS = ("vcs", "directory", "archive") +#: PEP 751 pseudo-environment marker variables naming which +#: extras/dependency-groups are active for a given consumption -- the +#: only two this extractor's marker handling understands (see +#: :func:`_group_marker_excludes`). Every other PEP 508 marker variable +#: (``python_version``, ``sys_platform``, etc.) is deliberately left +#: unevaluated, the same "no marker evaluation" limitation this format +#: shares with every sibling lock format -- evaluating those against +#: Pitloom's own running interpreter/platform would make the SBOM's +#: contents depend on which machine generated it, violating this repo's +#: determinism requirement. +_GROUP_MARKER_VARIABLES = frozenset({"extras", "dependency_groups"}) + #: The highest ``lock-version`` this extractor understands, as #: ``(major, minor)``. PEP 751 defines only ``"1.0"`` to date. A #: consumer must reject a different *major* version outright (a future @@ -78,10 +97,14 @@ def extract_pylock_dependencies(project_dir: Path) -> list[str] | None: file being confused with an absent/unusable one. Unlike ``poetry.lock``, PEP 751 has no ``groups``-style per-package - membership to filter on: a ``pylock.toml`` is already the flattened, - fully resolved package set for whichever extras/dependency-groups the - tool that generated it was asked to include, so every ``[[packages]]`` - entry is taken as-is. + membership *field*: a ``pylock.toml`` can bundle more than one + dependency-group's packages in a single flattened ``[[packages]]`` + list, distinguished only by an optional per-package ``marker`` string + referencing the pseudo-environment variables ``extras``/ + ``dependency_groups`` (e.g. ``"'dev' in dependency_groups"``). This + extractor filters to the file's own declared ``default-groups`` (no + extras) the same way ``poetry.lock``/``pdm.lock`` filter to their + ``main``/``default`` group -- see :func:`_group_marker_excludes`. """ lock_path = project_dir / "pylock.toml" data = load_lock_toml(lock_path) @@ -128,23 +151,139 @@ def extract_pylock_dependencies(project_dir: Path) -> list[str] | None: packages = data.get("packages", []) if not isinstance(packages, list): - log.warning( - "%s: top-level 'packages' key is %s, expected a list -- " - "ignoring pylock.toml", - lock_path, - type(packages).__name__, + warn_top_level_key_wrong_type( + lock_path, "packages", packages, "a list", "pylock.toml" ) return None + environment = _default_group_environment(lock_path, data) dependencies: list[str] = [] for pkg in packages: - dep = _pinned_dep_for_package(pkg) + dep = _pinned_dep_for_package(pkg, environment) if dep is not None: dependencies.append(dep) return dependencies -def _pinned_dep_for_package(pkg: object) -> str | None: +def _default_group_environment( + lock_path: Path, data: dict[str, Any] +) -> dict[str, frozenset[str]]: + """Build the ``dependency_groups``/``extras`` pseudo-environment + representing "no extras, only the file's own declared + ``default-groups``" -- the same runtime-only scope + ``poetry.lock``/``pdm.lock`` restrict to via their ``main``/ + ``default`` group filters. A missing or malformed ``default-groups`` + key is treated as ``[]`` (no default group at all) with a + ``WARNING:``, rather than silently keeping every group active.""" + default_groups = data.get("default-groups", []) + if not isinstance(default_groups, list) or not all( + isinstance(g, str) for g in default_groups + ): + log.warning( + "%s: top-level 'default-groups' key is %r, expected a list of " + "strings -- treating as empty (no default dependency-group)", + lock_path, + default_groups, + ) + default_groups = [] + return {"dependency_groups": frozenset(default_groups), "extras": frozenset()} + + +def _evaluate_group_leaf( + node: tuple[Any, Any, Any], environment: dict[str, frozenset[str]] +) -> bool | None: + """Evaluate one marker leaf ``(lhs, op, rhs)`` against *environment*, + or ``None`` ("unknown") when it isn't an ``in``/``not in`` clause + naming a ``extras``/``dependency_groups`` variable -- see + :func:`_group_marker_excludes` for why every other PEP 508 marker + variable is treated as unknown rather than really evaluated.""" + lhs, raw_op, rhs = node + op = str(raw_op) + if op not in ("in", "not in"): + return None + lhs_str, rhs_str = str(lhs), str(rhs) + if rhs_str in _GROUP_MARKER_VARIABLES: + variable, literal = rhs_str, lhs_str + elif lhs_str in _GROUP_MARKER_VARIABLES: + variable, literal = lhs_str, rhs_str + else: + return None + member = literal in environment[variable] + return member if op == "in" else not member + + +def _combine_group_results( + operator: str, left: bool | None, right: bool | None +) -> bool | None: + """3-valued ``and``/``or`` combination of two + :func:`_evaluate_group_leaf`-shaped results (``None`` meaning + "unknown", not a real true/false).""" + if operator == "and": + if left is False or right is False: + return False + return None if left is None or right is None else True + if left is True or right is True: + return True + return None if left is None or right is None else False + + +def _evaluate_group_node( + node: Any, environment: dict[str, frozenset[str]] +) -> bool | None: + """Recursively evaluate one node of a parsed + ``packaging.markers.Marker``'s tree (a leaf tuple, or a list of + nodes interleaved with ``"and"``/``"or"`` operator strings) using + 3-valued group/extras-only logic -- see :func:`_group_marker_excludes`.""" + if isinstance(node, tuple): + return _evaluate_group_leaf(node, environment) + result = _evaluate_group_node(node[0], environment) + for index in range(1, len(node), 2): + other = _evaluate_group_node(node[index + 1], environment) + result = _combine_group_results(node[index], result, other) + return result + + +def _group_marker_excludes( + marker_str: str, environment: dict[str, frozenset[str]], name: str +) -> bool: + """Return whether a package's PEP 751 ``marker`` string proves it's + *not* part of the active ``dependency_groups``/``extras`` scope in + *environment* -- ``True`` only when that's certain from the group/ + extras clauses alone. + + Uses 3-valued logic over the marker's parsed tree: a clause testing + ``extras``/``dependency_groups`` membership evaluates to a concrete + ``True``/``False`` against *environment*; every other PEP 508 marker + variable (``python_version``, ``sys_platform``, etc.) evaluates to + ``None`` ("unknown") rather than a real environment reading -- + evaluating those against Pitloom's own running interpreter/platform + would make the result depend on which machine ran Pitloom, which + this repo's "no marker evaluation" policy (shared by every sibling + lock format) and its determinism requirement both rule out. A + package is only excluded when the tree provably evaluates to + ``False`` from the known group/extras clauses regardless of any + unknown clause's real value; ``True``/``None`` both mean "include", + the same marker-blind default every other format already applies to + non-group markers. + """ + try: + # pylint: disable=protected-access + tree = Marker(marker_str)._markers # noqa: SLF001 + except InvalidMarker as exc: + log.warning( + "Skipping pylock.toml entry %r's 'marker' %r: %s -- treating " + "as an unconstrained (included) marker", + name, + marker_str, + exc, + ) + return False + return _evaluate_group_node(tree, environment) is False + + +def _pinned_dep_for_package( + pkg: object, environment: dict[str, frozenset[str]] +) -> str | None: """Return ``name==version`` for one ``[[packages]]`` table entry, or ``None`` when it's malformed or sourced from a location that ``name==version`` can't represent. @@ -159,19 +298,14 @@ def _pinned_dep_for_package(pkg: object) -> str | None: no source table at all) is always included when it has a version. """ if not isinstance(pkg, dict): - log.warning( - "Skipping malformed pylock.toml [[packages]] entry: expected a " - "table, got %s", - type(pkg).__name__, - ) + warn_malformed_entry_not_table("pylock.toml", "[[packages]]", pkg) return None name = pkg.get("name") if not isinstance(name, str) or not name: - log.warning( - "Skipping malformed pylock.toml [[packages]] entry: missing or " - "non-string 'name' (name=%r)", - name, - ) + warn_missing_name("Skipping malformed pylock.toml [[packages]] entry", name) + return None + marker = pkg.get("marker") + if isinstance(marker, str) and _group_marker_excludes(marker, environment, name): return None non_registry_source = find_first_present_key(pkg, _NON_REGISTRY_SOURCE_KEYS) if non_registry_source is not None: @@ -179,9 +313,6 @@ def _pinned_dep_for_package(pkg: object) -> str | None: return None version = pkg.get("version") if not is_usable_version(version): - log.warning( - "Skipping pylock.toml entry %r: missing or non-string 'version'", - name, - ) + warn_missing_version("pylock.toml", name) return None return f"{name}=={version}" diff --git a/src/pitloom/extract/_uv_lock.py b/src/pitloom/extract/_uv_lock.py index 0d03ca4f..1aba61ac 100644 --- a/src/pitloom/extract/_uv_lock.py +++ b/src/pitloom/extract/_uv_lock.py @@ -44,6 +44,7 @@ from __future__ import annotations import logging +from collections import deque from collections.abc import Iterable from pathlib import Path from typing import Any @@ -55,7 +56,11 @@ index_packages_by_name, is_usable_version, load_lock_toml, + warn_malformed_entry_not_table, + warn_missing_name, + warn_missing_version, warn_non_registry_source, + warn_top_level_key_wrong_type, ) log = logging.getLogger(__name__) @@ -76,6 +81,25 @@ _ROOT_SOURCE_KEYS = ("editable", "virtual") +def _warn_malformed_packages(lock_path: Path, packages: Iterable[object]) -> None: + """Log a ``WARNING:`` for each top-level ``[[package]]`` entry that + :func:`pitloom.extract._lock_common.index_packages_by_name` and + :func:`_find_root_package` silently exclude (a non-table entry, or a + table with a missing/non-string/empty ``name``) -- every sibling + lock format's own package-list loop warns on this same shape of + malformed entry, so a corrupted ``uv.lock`` package doesn't + disappear from extraction with no diagnostic at all.""" + for pkg in packages: + if not isinstance(pkg, dict): + warn_malformed_entry_not_table("uv.lock", "[[package]]", pkg) + continue + name = pkg.get("name") + if not isinstance(name, str) or not name: + warn_missing_name( + f"{lock_path}: skipping malformed [[package]] entry", name + ) + + def _find_root_package( packages: Iterable[object], expected_name: str | None ) -> dict[str, Any] | None: @@ -132,18 +156,11 @@ def _resolved_package_for_dependency( the transitive walk in :func:`_collect_transitive_dependencies` -- or ``None`` when it can't be resolved that way.""" if not isinstance(dep_ref, dict): - log.warning( - "Skipping malformed uv.lock dependency reference: expected a table, got %s", - type(dep_ref).__name__, - ) + warn_malformed_entry_not_table("uv.lock", "dependency reference", dep_ref) return None name = dep_ref.get("name") if not isinstance(name, str) or not name: - log.warning( - "Skipping malformed uv.lock dependency reference: missing or " - "non-string 'name' (name=%r)", - name, - ) + warn_missing_name("Skipping malformed uv.lock dependency reference", name) return None if "version" in dep_ref: # An inline version on the reference itself means this @@ -157,7 +174,7 @@ def _resolved_package_for_dependency( ) return None - candidates = by_name.get(name, []) + candidates = by_name.get(canonicalize_name(name), []) if not candidates: log.warning( "Skipping uv.lock dependency %r: referenced but not found in " @@ -199,9 +216,9 @@ def _collect_transitive_dependencies( """ dependencies: dict[str, str] = {} visited: set[str] = set() - queue: list[object] = list(root_dependencies) + queue: deque[object] = deque(root_dependencies) while queue: - dep_ref = queue.pop(0) + dep_ref = queue.popleft() pkg = _resolved_package_for_dependency(dep_ref, by_name) if pkg is None: continue @@ -239,10 +256,7 @@ def _pinned_dep_for_package(pkg: dict[str, Any]) -> str | None: return None version = pkg.get("version") if not is_usable_version(version): - log.warning( - "Skipping uv.lock entry %r: missing or non-string 'version'", - name, - ) + warn_missing_version("uv.lock", name) return None return f"{name}=={version}" @@ -292,14 +306,21 @@ def extract_uv_lock_dependencies( packages = data.get("package", []) if not isinstance(packages, list): - log.warning( - "%s: top-level 'package' key is %s, expected a list -- ignoring uv.lock", - lock_path, - type(packages).__name__, + warn_top_level_key_wrong_type( + lock_path, "package", packages, "a list", "uv.lock" ) return None - - if expected_name is None: + _warn_malformed_packages(lock_path, packages) + + if not expected_name: + # `ProjectMetadata.name` is typed `str`, never `None` -- a + # cascade caller whose own name resolution failed (e.g. a + # `setup.py`-only project with a dynamic, AST-unresolvable + # `name=`) passes `""`, not `None`. Falling back here on any + # falsy value (not just `None`) keeps that case from silently + # skipping the same re-read `_expected_project_name()` would + # have done for an explicit `None` -- an empty name could never + # usefully match a real workspace member's name anyway. expected_name = _expected_project_name(project_dir) root = _find_root_package(packages, expected_name) if root is None: @@ -320,5 +341,24 @@ def extract_uv_lock_dependencies( ) return None - by_name = index_packages_by_name(packages) + by_name = _index_by_canonical_name(packages) return _collect_transitive_dependencies(root_dependencies, by_name) + + +def _index_by_canonical_name( + packages: Iterable[object], +) -> dict[str, list[dict[str, Any]]]: + """PEP 503-canonicalized variant of + :func:`pitloom.extract._lock_common.index_packages_by_name`: uv + itself normalizes every ``name`` field it writes, but a dependency + *reference* and the package's own top-level entry are two separately + literal strings in the file -- grouping by canonical name (as + :func:`_collect_transitive_dependencies`'s ``visited`` set already + does) keeps lookup consistent with a name that differs only in + case/``-``/``_``/``.`` folding, instead of a literal-string mismatch + silently causing a resolvable dependency to be reported as + "not found".""" + by_name: dict[str, list[dict[str, Any]]] = {} + for name, entries in index_packages_by_name(packages).items(): + by_name.setdefault(canonicalize_name(name), []).extend(entries) + return by_name diff --git a/tests/extract/test_pdm_lock.py b/tests/extract/test_pdm_lock.py index 49bd6490..0b095bd1 100644 --- a/tests/extract/test_pdm_lock.py +++ b/tests/extract/test_pdm_lock.py @@ -125,6 +125,47 @@ def test_missing_groups_key_defaults_to_default_group() -> None: assert extract_pdm_lock_dependencies(tmp_path) == ["legacy-pkg==1.0.0"] +def test_malformed_groups_field_skipped_and_warns( + caplog: pytest.LogCaptureFixture, +) -> None: + """A `groups` field present but not a list is a malformed entry, not + ordinary "not in default group" filtering -- must warn like every + other malformed-field case (parity with poetry.lock's equivalent + check).""" + with tempfile.TemporaryDirectory() as tmp: + tmp_path = Path(tmp) + _write_lock( + tmp_path, + '[[package]]\nname = "odd-pkg"\nversion = "1.0.0"\ngroups = "default"\n', + ) + + with caplog.at_level(logging.WARNING): + result = extract_pdm_lock_dependencies(tmp_path) + + assert not result + assert "'groups'" in caplog.text + + +def test_version_validated_even_when_not_in_default_group( + caplog: pytest.LogCaptureFixture, +) -> None: + """A malformed `version` on a non-default-group package still warns + -- version validation must not be short-circuited by the group + filter, matching poetry.lock's unconditional name/version check.""" + with tempfile.TemporaryDirectory() as tmp: + tmp_path = Path(tmp) + _write_lock( + tmp_path, + '[[package]]\nname = "dev-only"\ngroups = ["dev"]\n', + ) + + with caplog.at_level(logging.WARNING): + result = extract_pdm_lock_dependencies(tmp_path) + + assert not result + assert "missing or non-string 'version'" in caplog.text + + def test_malformed_package_entry_skipped_and_warns( caplog: pytest.LogCaptureFixture, ) -> None: diff --git a/tests/extract/test_pipfile_lock.py b/tests/extract/test_pipfile_lock.py index ed0c9deb..f92c839b 100644 --- a/tests/extract/test_pipfile_lock.py +++ b/tests/extract/test_pipfile_lock.py @@ -188,6 +188,24 @@ def test_non_registry_sourced_dependency_excluded( assert "local-dep" in caplog.text +def test_editable_false_not_treated_as_non_registry_source() -> None: + """`editable` is Pipfile.lock's one boolean-valued non-registry key + (every other one -- `git`/`hg`/`bzr`/`svn`/`path`/`file` -- is a + string, so mere presence means non-registry). An explicit + `"editable": false` (schema-legal, just uncommon) is a normal, + ordinary registry-resolved pin, not a local/editable source -- unlike + a *truthy* `editable` value, which the parametrized test above + already covers as correctly excluded.""" + with tempfile.TemporaryDirectory() as tmp: + tmp_path = Path(tmp) + _write_lock( + tmp_path, + {"default": {"requests": {"version": "==2.31.0", "editable": False}}}, + ) + + assert extract_pipfile_lock_dependencies(tmp_path) == ["requests==2.31.0"] + + def test_invalid_specifier_skipped_and_warns( caplog: pytest.LogCaptureFixture, ) -> None: diff --git a/tests/extract/test_poetry_lock.py b/tests/extract/test_poetry_lock.py index 774ca533..53101a14 100644 --- a/tests/extract/test_poetry_lock.py +++ b/tests/extract/test_poetry_lock.py @@ -115,6 +115,27 @@ def test_missing_groups_key_defaults_to_main() -> None: assert extract_poetry_lock_dependencies(tmp_path) == ["legacy-pkg==1.0.0"] +def test_malformed_groups_field_skipped_and_warns( + caplog: pytest.LogCaptureFixture, +) -> None: + """A `groups` field present but not a list (a bare string, say) is a + malformed entry, not ordinary "not in main group" filtering -- must + warn like every other malformed-field case, not silently disappear + the same way a routine non-main-group package does.""" + with tempfile.TemporaryDirectory() as tmp: + tmp_path = Path(tmp) + _write_lock( + tmp_path, + '[[package]]\nname = "odd-pkg"\nversion = "1.0.0"\ngroups = "main"\n', + ) + + with caplog.at_level(logging.WARNING): + result = extract_poetry_lock_dependencies(tmp_path) + + assert not result + assert "'groups'" in caplog.text + + def test_package_table_not_a_list_returns_empty_list() -> None: """A ``poetry.lock`` where top-level ``package`` isn't an array of tables (malformed/unexpected shape) must degrade to an empty list, diff --git a/tests/extract/test_pylock.py b/tests/extract/test_pylock.py index 925b2141..4f408ee7 100644 --- a/tests/extract/test_pylock.py +++ b/tests/extract/test_pylock.py @@ -24,6 +24,15 @@ from pitloom.extract.project import read_project _LOCK_VERSION = 'lock-version = "1.0"\ncreated-by = "test"\n' +#: The "no extras, no default-groups active" environment -- +#: `_pinned_dep_for_package()`'s second argument, built by +#: `extract_pylock_dependencies()` itself in normal use via +#: `_default_group_environment()`; unit tests calling the helper +#: directly supply it explicitly instead. +_NO_GROUPS_ENV: dict[str, frozenset[str]] = { + "dependency_groups": frozenset(), + "extras": frozenset(), +} REAL_WORLD_LOCKS = ( Path(__file__).parent.parent / "fixtures" / "real-world-locks" / "pylock" @@ -167,8 +176,10 @@ def test_packages_key_not_a_list_returns_empty_list_and_warns( def test_pinned_dep_for_package_non_dict_entry_returns_none() -> None: - assert _pinned_dep_for_package("not-a-dict") is None - assert _pinned_dep_for_package(["still", "not", "a", "dict"]) is None + assert _pinned_dep_for_package("not-a-dict", _NO_GROUPS_ENV) is None + assert ( + _pinned_dep_for_package(["still", "not", "a", "dict"], _NO_GROUPS_ENV) is None + ) def test_malformed_package_entry_skipped_and_warns( @@ -235,6 +246,153 @@ def test_sdist_sourced_package_included() -> None: assert extract_pylock_dependencies(tmp_path) == ["requests==2.31.0"] +def test_non_default_group_package_excluded() -> None: + """Regression: a package needed only for a non-default + dependency-group (e.g. `dev`), tagged via PEP 751's `marker` field + referencing the `dependency_groups` pseudo-environment variable, must + not leak into `locked_dependencies` as an ordinary runtime pin -- + the same "main"/"default"-group-only policy `poetry.lock`/`pdm.lock` + already apply, here expressed as a marker instead of a per-package + field.""" + with tempfile.TemporaryDirectory() as tmp: + tmp_path = Path(tmp) + _write_lock( + tmp_path, + 'default-groups = ["default"]\n' + '[[packages]]\nname = "pytz"\nversion = "2026.1"\n\n' + '[[packages]]\nname = "pytest"\nversion = "8.0.0"\n' + "marker = \"'dev' in dependency_groups\"\n", + ) + + assert extract_pylock_dependencies(tmp_path) == ["pytz==2026.1"] + + +def test_default_group_package_included_alongside_excluded_dev_group() -> None: + """A package whose marker combines a non-default group check with an + ordinary (unevaluated) environment condition is still excluded on the + group check alone -- the 3-valued evaluator doesn't need to know the + real Python version/platform to prove the group clause is false.""" + with tempfile.TemporaryDirectory() as tmp: + tmp_path = Path(tmp) + _write_lock( + tmp_path, + 'default-groups = ["default"]\n' + '[[packages]]\nname = "black"\nversion = "26.1.0"\n' + "marker = \"('dev' in dependency_groups) and " + "(python_version >= '3.10')\"\n", + ) + + assert extract_pylock_dependencies(tmp_path) == [] + + +def test_package_with_no_marker_included_regardless_of_default_groups() -> None: + """A package with no `marker` field at all is an ordinary, + always-active runtime dependency -- unaffected by `default-groups` + filtering.""" + with tempfile.TemporaryDirectory() as tmp: + tmp_path = Path(tmp) + _write_lock( + tmp_path, + 'default-groups = []\n[[packages]]\nname = "pytz"\nversion = "2026.1"\n', + ) + + assert extract_pylock_dependencies(tmp_path) == ["pytz==2026.1"] + + +def test_default_groups_not_a_list_warns_and_treated_as_empty( + caplog: pytest.LogCaptureFixture, +) -> None: + with tempfile.TemporaryDirectory() as tmp: + tmp_path = Path(tmp) + _write_lock( + tmp_path, + 'default-groups = "default"\n' + '[[packages]]\nname = "pytest"\nversion = "8.0.0"\n' + "marker = \"'default' in dependency_groups\"\n", + ) + + with caplog.at_level(logging.WARNING): + result = extract_pylock_dependencies(tmp_path) + + assert result == [] + assert "'default-groups'" in caplog.text + + +def test_or_combined_group_clauses_evaluated() -> None: + """The `or` branch of the 3-valued combiner + (`_combine_group_results`) is exercised alongside the `and` branch + tested above -- a package needed for *either* of two non-default + groups is still excluded when neither is active.""" + with tempfile.TemporaryDirectory() as tmp: + tmp_path = Path(tmp) + _write_lock( + tmp_path, + 'default-groups = ["default"]\n' + '[[packages]]\nname = "black"\nversion = "26.1.0"\n' + "marker = \"'dev' in dependency_groups or 'test' in dependency_groups\"\n", + ) + + assert extract_pylock_dependencies(tmp_path) == [] + + +def test_or_combined_group_clauses_true_when_one_group_active() -> None: + """The `or` combiner's `True` result (at least one side proven + true) alongside the `False` case tested above -- a package needed + for either of two groups is included once one of them is active.""" + with tempfile.TemporaryDirectory() as tmp: + tmp_path = Path(tmp) + _write_lock( + tmp_path, + 'default-groups = ["default", "test"]\n' + '[[packages]]\nname = "black"\nversion = "26.1.0"\n' + "marker = \"'dev' in dependency_groups or 'test' in dependency_groups\"\n", + ) + + assert extract_pylock_dependencies(tmp_path) == ["black==26.1.0"] + + +def test_reversed_operand_group_clause_evaluated() -> None: + """PEP 751 always writes the group/extras variable on the *right* of + `in` (e.g. `"'dev' in dependency_groups"`) in real output, but PEP + 508 grammar allows either operand order -- `_evaluate_group_leaf`'s + `elif lhs_str in _GROUP_MARKER_VARIABLES` branch (variable on the + left) must still be reachable and correct, not just the more common + literal-on-left form tested elsewhere.""" + with tempfile.TemporaryDirectory() as tmp: + tmp_path = Path(tmp) + _write_lock( + tmp_path, + 'default-groups = ["dev"]\n' + '[[packages]]\nname = "black"\nversion = "26.1.0"\n' + "marker = \"dependency_groups in 'dev'\"\n", + ) + + assert extract_pylock_dependencies(tmp_path) == ["black==26.1.0"] + + +def test_malformed_marker_string_included_and_warns( + caplog: pytest.LogCaptureFixture, +) -> None: + """An unparseable `marker` string can't prove group membership either + way -- treated as the same marker-blind "include" default every + other non-group marker gets, but with a `WARNING:` (not a crash, not + a silent unconditional include) rather than raising `InvalidMarker` + out of the extractor.""" + with tempfile.TemporaryDirectory() as tmp: + tmp_path = Path(tmp) + _write_lock( + tmp_path, + '[[packages]]\nname = "broken"\nversion = "1.0.0"\n' + 'marker = "not a valid marker (("\n', + ) + + with caplog.at_level(logging.WARNING): + result = extract_pylock_dependencies(tmp_path) + + assert result == ["broken==1.0.0"] + assert "'marker'" in caplog.text + + def test_read_project_populates_locked_dependencies() -> None: """Integration: `read_project()`'s lock cascade overlays `pylock.toml` parsing onto `ProjectMetadata.locked_dependencies` with its own diff --git a/tests/extract/test_uv_lock.py b/tests/extract/test_uv_lock.py index b7183565..493f1ad7 100644 --- a/tests/extract/test_uv_lock.py +++ b/tests/extract/test_uv_lock.py @@ -93,6 +93,33 @@ def test_no_root_package_returns_empty_list_and_warns( assert "no project package found" in caplog.text +def test_empty_string_expected_name_falls_back_to_pyproject_toml() -> None: + """`ProjectMetadata.name` is typed `str`, never `None` -- a caller + whose own name resolution failed passes `""`, not `None`. This must + still trigger the same `_expected_project_name()` re-read fallback + an explicit `None` gets, not be treated as a real (if unmatched) + workspace-member name.""" + with tempfile.TemporaryDirectory() as tmp: + tmp_path = Path(tmp) + (tmp_path / "pyproject.toml").write_text( + '[project]\nname = "demo"\nversion = "1.0.0"\n', encoding="utf-8" + ) + _write_lock( + tmp_path, + '[[package]]\nname = "demo"\nversion = "1.0.0"\n' + 'source = { editable = "." }\n' + 'dependencies = [{ name = "requests" }]\n\n' + '[[package]]\nname = "other-member"\nversion = "1.0.0"\n' + 'source = { editable = "./other" }\n\n' + '[[package]]\nname = "requests"\nversion = "2.31.0"\n' + 'source = { registry = "https://pypi.org/simple" }\n', + ) + + assert extract_uv_lock_dependencies(tmp_path, expected_name="") == [ + "requests==2.31.0" + ] + + def test_root_dependencies_not_a_list_returns_empty_list_and_warns( caplog: pytest.LogCaptureFixture, ) -> None: @@ -152,6 +179,73 @@ def test_dependency_with_marker_but_no_inline_version_still_resolved() -> None: assert extract_uv_lock_dependencies(tmp_path) == ["requests==2.31.0"] +def test_malformed_top_level_package_entry_warns( + caplog: pytest.LogCaptureFixture, +) -> None: + """A corrupted top-level `[[package]]` entry (not a table, or + missing/non-string `name`) must warn like every sibling lock + format's own malformed-entry check -- even when nothing in the + resolved dependency graph ever references it by name, so it can't + silently vanish with zero diagnostic.""" + with tempfile.TemporaryDirectory() as tmp: + tmp_path = Path(tmp) + _write_lock( + tmp_path, + _ROOT_HEADER + "dependencies = []\n\n" + '[[package]]\nversion = "9.9.9"\n' + 'source = { registry = "https://pypi.org/simple" }\n', + ) + + with caplog.at_level(logging.WARNING): + result = extract_uv_lock_dependencies(tmp_path) + + assert result == [] + assert "malformed" in caplog.text.lower() + + +def test_non_table_top_level_package_entry_warns( + caplog: pytest.LogCaptureFixture, +) -> None: + """The other half of `_warn_malformed_packages()`'s check: a + top-level `package` array entry that isn't a table at all (not just + one missing `name`), e.g. a bare string slipped in alongside genuine + `[[package]]` tables -- must also warn, matching every sibling + format's own "expected a table, got %s" malformed-entry check.""" + with tempfile.TemporaryDirectory() as tmp: + tmp_path = Path(tmp) + (tmp_path / "uv.lock").write_text( + _LOCK_HEADER + 'package = ["not-a-table", ' + '{ name = "demo", version = "1.0.0", ' + 'source = { editable = "." }, dependencies = [] }]\n', + encoding="utf-8", + ) + + with caplog.at_level(logging.WARNING): + result = extract_uv_lock_dependencies(tmp_path) + + assert result == [] + assert "malformed" in caplog.text.lower() + assert "expected a table" in caplog.text.lower() + + +def test_dependency_resolved_across_name_case_difference() -> None: + """A dependency reference and the package's own top-level entry are + two separately-literal strings in the file -- resolution must + compare them PEP 503-canonicalized, the same as the `visited`-set + dedup guard already does, so a differently-cased/`-`-vs-`_` name + still resolves instead of spuriously reporting "not found".""" + with tempfile.TemporaryDirectory() as tmp: + tmp_path = Path(tmp) + _write_lock( + tmp_path, + _ROOT_HEADER + 'dependencies = [{ name = "My_Package" }]\n\n' + '[[package]]\nname = "my-package"\nversion = "1.2.3"\n' + 'source = { registry = "https://pypi.org/simple" }\n', + ) + + assert extract_uv_lock_dependencies(tmp_path) == ["my-package==1.2.3"] + + def test_malformed_dependency_reference_skipped_and_warns( caplog: pytest.LogCaptureFixture, ) -> None: diff --git a/working-docs/implementation/pep751-pylock-support.md b/working-docs/implementation/pep751-pylock-support.md index 42f9ee1a..3275aa57 100644 --- a/working-docs/implementation/pep751-pylock-support.md +++ b/working-docs/implementation/pep751-pylock-support.md @@ -62,12 +62,23 @@ so a lower-priority source can still be tried, rather than a genuinely dependency-free lock file being confused with an absent/unusable one. Unlike `poetry.lock`, PEP 751 has no `groups`-style per-package -membership tag to filter on: a `pylock.toml` is already the flattened, -fully resolved package set for whichever extras/dependency-groups the -tool that generated it was asked to include (`dependency-groups`/ -`default-groups`/`extras` are file-level generation inputs, not a -per-package "which group requested me" marker). So every `[[packages]]` -entry is taken as-is, with no group-based filtering. +membership *field*: a `pylock.toml` can bundle more than one +dependency-group's packages in a single flattened `[[packages]]` list, +distinguished only by an optional per-package `marker` string +referencing the pseudo-environment variables `extras`/ +`dependency_groups` (e.g. `"'dev' in dependency_groups"`). This +extractor filters to the file's own declared `default-groups` (no +extras) the same way `poetry.lock`/`pdm.lock` filter to their +`main`/`default` group: `_default_group_environment()` builds a +`{"dependency_groups": frozenset(default-groups), "extras": +frozenset()}` environment, and `_group_marker_excludes()` evaluates +each package's `marker` against it with 3-valued logic -- a clause +testing `extras`/`dependency_groups` membership gets a real +`True`/`False`, every other PEP 508 marker variable (`python_version`, +`sys_platform`, etc.) evaluates to "unknown" rather than a real +environment reading (see "Known limitations" below), and a package is +excluded only when the tree provably evaluates `False` from the known +group/extras clauses alone. A malformed lock (a `packages` key that isn't a list, or an individual `[[packages]]` entry missing/non-string `name`/`version`) is skipped @@ -125,12 +136,18 @@ cascade is called from `read_project()` rather than `dependsOn` edge straight from the main package (`_locked_transitive_only_dependencies()` in `document.py`), the same as `poetry.lock`. -- **No marker evaluation.** A `pylock.toml` entry may carry a `marker` - (environment marker) restricting when it applies (e.g. a - platform-specific package). This extractor doesn't evaluate markers - against any particular environment -- every `[[packages]]` entry is - included regardless, the same simplification `poetry.lock` parsing - already makes for direct dependency constraints. +- **No non-group marker evaluation.** A `pylock.toml` entry's `marker` + can also carry ordinary PEP 508 conditions (`python_version`, + `sys_platform`, etc.) alongside or instead of a group/extras clause. + Only the `extras`/`dependency_groups` portion is evaluated (see + above, for group filtering); every other variable is treated as + unknown and never evaluated against a real environment -- a + platform-specific package's marker still lets it through regardless + of platform, the same simplification `poetry.lock` parsing already + makes for direct dependency constraints. Evaluating those against + Pitloom's own running interpreter/platform would make the SBOM's + contents depend on which machine generated it, which this repo's + determinism requirement rules out. - **`pylock..toml` named locks are not discovered.** PEP 751 allows a `pylock..toml` naming convention (e.g. `pylock.dev.toml`) for multiple named locks in one project; only the From 4d07e44d46234a3ae40c2faa1e2788f6d0f530f2 Mon Sep 17 00:00:00 2001 From: Arthit Suriyawongkul Date: Sat, 5 Sep 2026 12:38:50 +0700 Subject: [PATCH 11/35] Fix lock files bugs Signed-off-by: Arthit Suriyawongkul --- pyproject.toml | 6 ++++- src/pitloom/extract/_lock_common.py | 20 +++++++---------- src/pitloom/extract/_pipfile_lock.py | 18 +++++++++------ src/pitloom/extract/_pylock.py | 8 +++---- src/pitloom/extract/_uv_lock.py | 33 +++++++++++++++------------- tests/extract/test_pipfile_lock.py | 16 ++++++++++++++ tests/extract/test_pylock.py | 23 +++++++++++++++++++ 7 files changed, 85 insertions(+), 39 deletions(-) diff --git a/pyproject.toml b/pyproject.toml index 51ecd9f8..ef19d696 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -64,7 +64,11 @@ dependencies = [ "flit_core>=3.9", "hatchling>=1.32.0", "licenseid>=0.3.7", - "packaging>=24.0", + # 25.0 floor: `packaging.markers.Marker` only accepts the PEP 751 + # `extras`/`dependency_groups` pseudo-environment variable names + # starting at this release (24.x raises InvalidMarker for them) -- + # required by pitloom.extract._pylock's default-groups filtering. + "packaging>=25.0", "pdm-backend>=2.4.1", "pipdeptree>=4.2.3", "poetry-core>=2.4.1", diff --git a/src/pitloom/extract/_lock_common.py b/src/pitloom/extract/_lock_common.py index 065f6b63..fcd5a54a 100644 --- a/src/pitloom/extract/_lock_common.py +++ b/src/pitloom/extract/_lock_common.py @@ -250,14 +250,11 @@ def warn_top_level_key_wrong_type( lock_path: Path, key: str, value: object, expected: str, lock_file: str ) -> None: """Log the shared ``": top-level '' key is , - expected -- ignoring "`` warning every - extractor's own top-level-shape check produces (a ``packages``/ - ``package`` key that isn't a list, a ``default`` key that isn't a - table) -- four-plus formats retyped this identically modulo the key - name, expected shape, and lock-file name before this was factored - out, the same "pattern hand-copied across 3+ call sites drifts" - concern :func:`warn_non_registry_source` already addresses for the - non-registry-source case. + expected -- ignoring "`` warning for a top-level + lock-file key of the wrong shape (a ``packages``/``package`` key + that isn't a list, a ``default`` key that isn't a table) -- shared + across formats the same way :func:`warn_non_registry_source` is + shared for the non-registry-source case. """ log.warning( "%s: top-level '%s' key is %s, expected %s -- ignoring %s", @@ -284,10 +281,9 @@ def warn_malformed_entry_not_table( lock_file: str, entry_label: str, value: object ) -> None: """Log the shared ``"Skipping malformed - entry: expected a table, got "`` warning -- the identical - shape ``poetry.lock``'s, ``pylock.toml``'s, and ``pdm.lock``'s own - ``[[package]]``/``[[packages]]`` malformed-entry checks each retyped - independently before this was factored out.""" + entry: expected a table, got "`` warning for a top-level + ``[[package]]``/``[[packages]]``-style entry that isn't a table -- + shared across every format with this malformed-entry shape.""" log.warning( "Skipping malformed %s %s entry: expected a table, got %s", lock_file, diff --git a/src/pitloom/extract/_pipfile_lock.py b/src/pitloom/extract/_pipfile_lock.py index a3cc2de7..d81fca2b 100644 --- a/src/pitloom/extract/_pipfile_lock.py +++ b/src/pitloom/extract/_pipfile_lock.py @@ -114,14 +114,18 @@ def _pinned_dep_for_package(name: object, entry: object) -> str | None: ) return None non_registry_key = find_first_present_key(entry, _NON_REGISTRY_KEYS) - if non_registry_key is not None and entry[non_registry_key] is not False: + if non_registry_key is not None and ( + non_registry_key != "editable" or entry[non_registry_key] is not False + ): # Every non-registry key except 'editable' is presence-is-enough - # (a git/hg/bzr/svn/path/file URL string). 'editable' is the - # schema's one boolean-valued key -- an explicit - # `"editable": false` (schema-legal, just uncommon) must not be - # mistaken for a real editable/VCS source the same way a - # present-but-falsy key would be for every other format's - # presence-only check. + # (a git/hg/bzr/svn/path/file URL string) -- an explicit falsy + # value there (e.g. a malformed `"git": false`) is not a real + # exemption and still disqualifies the entry. 'editable' is the + # schema's one genuinely boolean-valued key -- only there does an + # explicit `"editable": false` (schema-legal, just uncommon) need + # to be read as "not editable" rather than mistaken for a real + # editable/VCS source the way a present-but-falsy key would be + # for every other format's presence-only check. warn_non_registry_source("Pipfile.lock", name, non_registry_key) return None pinned_version = _exact_pinned_version(name, entry.get("version")) diff --git a/src/pitloom/extract/_pylock.py b/src/pitloom/extract/_pylock.py index be804e9c..5488a3be 100644 --- a/src/pitloom/extract/_pylock.py +++ b/src/pitloom/extract/_pylock.py @@ -304,6 +304,10 @@ def _pinned_dep_for_package( if not isinstance(name, str) or not name: warn_missing_name("Skipping malformed pylock.toml [[packages]] entry", name) return None + version = pkg.get("version") + if not is_usable_version(version): + warn_missing_version("pylock.toml", name) + return None marker = pkg.get("marker") if isinstance(marker, str) and _group_marker_excludes(marker, environment, name): return None @@ -311,8 +315,4 @@ def _pinned_dep_for_package( if non_registry_source is not None: warn_non_registry_source("pylock.toml", name, non_registry_source) return None - version = pkg.get("version") - if not is_usable_version(version): - warn_missing_version("pylock.toml", name) - return None return f"{name}=={version}" diff --git a/src/pitloom/extract/_uv_lock.py b/src/pitloom/extract/_uv_lock.py index 1aba61ac..95ac395f 100644 --- a/src/pitloom/extract/_uv_lock.py +++ b/src/pitloom/extract/_uv_lock.py @@ -53,7 +53,6 @@ from pitloom.extract._lock_common import ( find_first_present_key, - index_packages_by_name, is_usable_version, load_lock_toml, warn_malformed_entry_not_table, @@ -81,13 +80,13 @@ _ROOT_SOURCE_KEYS = ("editable", "virtual") -def _warn_malformed_packages(lock_path: Path, packages: Iterable[object]) -> None: +def _warn_malformed_packages(packages: Iterable[object]) -> None: """Log a ``WARNING:`` for each top-level ``[[package]]`` entry that - :func:`pitloom.extract._lock_common.index_packages_by_name` and - :func:`_find_root_package` silently exclude (a non-table entry, or a - table with a missing/non-string/empty ``name``) -- every sibling - lock format's own package-list loop warns on this same shape of - malformed entry, so a corrupted ``uv.lock`` package doesn't + :func:`_index_by_canonical_name` and :func:`_find_root_package` + silently exclude (a non-table entry, or a table with a + missing/non-string/empty ``name``) -- every sibling lock format's + own package-list loop warns on this same shape of malformed entry, + so a corrupted ``uv.lock`` package doesn't disappear from extraction with no diagnostic at all.""" for pkg in packages: if not isinstance(pkg, dict): @@ -95,9 +94,7 @@ def _warn_malformed_packages(lock_path: Path, packages: Iterable[object]) -> Non continue name = pkg.get("name") if not isinstance(name, str) or not name: - warn_missing_name( - f"{lock_path}: skipping malformed [[package]] entry", name - ) + warn_missing_name("Skipping malformed uv.lock [[package]] entry", name) def _find_root_package( @@ -310,7 +307,7 @@ def extract_uv_lock_dependencies( lock_path, "package", packages, "a list", "uv.lock" ) return None - _warn_malformed_packages(lock_path, packages) + _warn_malformed_packages(packages) if not expected_name: # `ProjectMetadata.name` is typed `str`, never `None` -- a @@ -348,8 +345,10 @@ def extract_uv_lock_dependencies( def _index_by_canonical_name( packages: Iterable[object], ) -> dict[str, list[dict[str, Any]]]: - """PEP 503-canonicalized variant of - :func:`pitloom.extract._lock_common.index_packages_by_name`: uv + """Group every well-formed ``[[package]]`` entry by its PEP + 503-canonicalized ``name`` (a non-table entry, or one with a + missing/non-string/empty ``name``, is excluded here -- see + :func:`_warn_malformed_packages` for the diagnostic on those). uv itself normalizes every ``name`` field it writes, but a dependency *reference* and the package's own top-level entry are two separately literal strings in the file -- grouping by canonical name (as @@ -359,6 +358,10 @@ def _index_by_canonical_name( silently causing a resolvable dependency to be reported as "not found".""" by_name: dict[str, list[dict[str, Any]]] = {} - for name, entries in index_packages_by_name(packages).items(): - by_name.setdefault(canonicalize_name(name), []).extend(entries) + for pkg in packages: + if not isinstance(pkg, dict): + continue + name = pkg.get("name") + if isinstance(name, str) and name: + by_name.setdefault(canonicalize_name(name), []).append(pkg) return by_name diff --git a/tests/extract/test_pipfile_lock.py b/tests/extract/test_pipfile_lock.py index f92c839b..0cf45c0b 100644 --- a/tests/extract/test_pipfile_lock.py +++ b/tests/extract/test_pipfile_lock.py @@ -206,6 +206,22 @@ def test_editable_false_not_treated_as_non_registry_source() -> None: assert extract_pipfile_lock_dependencies(tmp_path) == ["requests==2.31.0"] +def test_git_false_still_treated_as_non_registry_source() -> None: + """Regression: the falsy-value exemption above is `editable`-specific, + not blanket. Every other non-registry key (`git`/`hg`/`bzr`/`svn`/ + `path`/`file`) is a string when it means anything at all -- a + malformed `"git": false` must still disqualify the entry, not be + read as "no git source" the way `"editable": false` correctly is.""" + with tempfile.TemporaryDirectory() as tmp: + tmp_path = Path(tmp) + _write_lock( + tmp_path, + {"default": {"sneaky": {"version": "==1.0.0", "git": False}}}, + ) + + assert extract_pipfile_lock_dependencies(tmp_path) == [] + + def test_invalid_specifier_skipped_and_warns( caplog: pytest.LogCaptureFixture, ) -> None: diff --git a/tests/extract/test_pylock.py b/tests/extract/test_pylock.py index 4f408ee7..c2b3bec6 100644 --- a/tests/extract/test_pylock.py +++ b/tests/extract/test_pylock.py @@ -212,6 +212,29 @@ def test_missing_version_skipped_and_warns(caplog: pytest.LogCaptureFixture) -> assert "missing" in caplog.text.lower() +def test_version_validated_even_when_group_marker_excludes_package( + caplog: pytest.LogCaptureFixture, +) -> None: + """A malformed `version` on a non-default-group (marker-excluded) + package still warns -- version validation must not be short-circuited + by the group/marker filter, matching poetry.lock's/pdm.lock's own + unconditional name/version check ordering.""" + with tempfile.TemporaryDirectory() as tmp: + tmp_path = Path(tmp) + _write_lock( + tmp_path, + 'default-groups = ["default"]\n' + '[[packages]]\nname = "dev-only"\n' + "marker = \"'dev' in dependency_groups\"\n", + ) + + with caplog.at_level(logging.WARNING): + result = extract_pylock_dependencies(tmp_path) + + assert not result + assert "missing or non-string 'version'" in caplog.text + + @pytest.mark.parametrize("source_key", ["vcs", "directory", "archive"]) def test_non_registry_sourced_package_excluded( source_key: str, caplog: pytest.LogCaptureFixture From eea16082bf2585ad070938b3257229b76e2a2018 Mon Sep 17 00:00:00 2001 From: Arthit Suriyawongkul Date: Sat, 5 Sep 2026 13:40:56 +0700 Subject: [PATCH 12/35] Fix lock file bug per reviews Signed-off-by: Arthit Suriyawongkul --- docs/dependency-sources.md | 15 ++++-- src/pitloom/core/models.py | 37 +++++++++---- src/pitloom/extract/_lock_common.py | 12 ++++- src/pitloom/extract/_pipfile_lock.py | 7 +-- src/pitloom/extract/_poetry_lock.py | 47 +++++++++++----- src/pitloom/extract/_pylock.py | 52 +++++++++++------- .../assemble/test_deps_locked_dependencies.py | 39 ++++++++++++-- tests/extract/test_lock_common.py | 37 +++++++++++++ tests/extract/test_poetry_lock.py | 24 ++++++++- tests/extract/test_pylock.py | 45 ++++++++++++++++ working-docs/design/roadmap.md | 54 +++++-------------- .../implementation/lock-file-cascade.md | 14 +++-- 12 files changed, 279 insertions(+), 104 deletions(-) diff --git a/docs/dependency-sources.md b/docs/dependency-sources.md index 220c7c7a..59856b27 100644 --- a/docs/dependency-sources.md +++ b/docs/dependency-sources.md @@ -108,10 +108,17 @@ either. Every SBOM element built from a lock-resolved dependency carries a provenance annotation naming the file and method Pitloom used, e.g. -`Source: pylock.toml | Method: resolved_lockfile`. If a lower-priority -lock file was present but ignored in favor of a higher-priority one, -the annotation also says so, e.g. `Source: pylock.toml | Method: -resolved_lockfile | Note: supersedes poetry.lock`. See [Metadata +`Source: pylock.toml | Method: resolved_lockfile`. The cascade stops at +the first usable source it tries, so it doesn't itself check whether a +still-lower-priority lock file is *also* present on disk -- the one +case it does detect and annotate is `poetry.lock`, since that one is +resolved earlier, before the cascade runs, and the cascade can see its +already-set result: if a higher-priority format then wins over it, the +annotation adds a note, e.g. `Source: pylock.toml | Method: +resolved_lockfile | Note: supersedes poetry.lock`. Two lock files that +are both tried by the cascade itself (e.g. `pdm.lock` and `Pipfile.lock` +both present) never produce this note -- only the single winning +source's own annotation appears. See [Metadata provenance](metadata-provenance.md) for how to read these annotations in the generated SBOM. diff --git a/src/pitloom/core/models.py b/src/pitloom/core/models.py index bc5cba85..480c2a67 100644 --- a/src/pitloom/core/models.py +++ b/src/pitloom/core/models.py @@ -113,12 +113,13 @@ def compute_doc_uuid( """Compute a deterministic UUIDv5 for the SPDX document. *locked_dependencies* (e.g. ``poetry.lock``-resolved transitive - dependencies), when given and non-empty, is folded into the seed too -- - otherwise two documents with identical direct dependencies but - different lock-resolved graphs would collide on the same UUID despite - describing different dependency content. Omitted or empty leaves the - seed byte-identical to a document with no locked dependencies at all, - so every non-Poetry (and lock-less Poetry) document is unaffected. + dependencies) is folded into the seed too, whenever either it or + *locked_dependencies_provenance* is given -- otherwise two documents + with identical direct dependencies but different lock-resolved + graphs would collide on the same UUID despite describing different + dependency content. Both omitted leaves the seed byte-identical to a + document with no locked dependencies at all, so every non-Poetry + (and lock-less Poetry) document is unaffected. *locked_dependencies_provenance* (the resolved ``ProjectMetadata.provenance["locked_dependencies"]`` string, e.g. @@ -132,13 +133,29 @@ def compute_doc_uuid( dependency content alone would collide those two documents' UUIDs despite their generated ``provenance["locked_dependencies"]`` fields (and any override note) differing -- a real content difference the - seed is supposed to guard against. Omitted or empty leaves the seed - unaffected, same as *locked_dependencies*. + seed is supposed to guard against. + + Deliberately keyed on *either* argument being given, not just + *locked_dependencies* being non-empty: a real, successfully-resolved + lock file that legitimately has zero runtime dependencies still + carries its own distinct provenance string, which must still + distinguish that document from one with no lock present at all, or + from a different lock source that also happened to resolve to zero + dependencies -- gating on non-empty *locked_dependencies* alone would + silently collide all three onto the same UUID. """ normalized_deps = sorted(_normalize_dep(dep) for dep in dependencies) seed = "\x00".join([name, version, "\x00".join(normalized_deps)]) - if locked_dependencies: - normalized_locked = sorted(_normalize_dep(dep) for dep in locked_dependencies) + if locked_dependencies or locked_dependencies_provenance: + # A real, valid lock resolving to zero dependencies still has a + # *provenance* string that differs from "no lock at all" (and + # from a different lock source that also resolved to empty) -- + # gating this whole block on `locked_dependencies` being + # non-empty would fold in neither, silently colliding an + # empty-but-real lock's UUID with a lockless document's. + normalized_locked = sorted( + _normalize_dep(dep) for dep in (locked_dependencies or []) + ) seed += "\x00" + "\x00".join(normalized_locked) if locked_dependencies_provenance: seed += "\x00" + locked_dependencies_provenance diff --git a/src/pitloom/extract/_lock_common.py b/src/pitloom/extract/_lock_common.py index fcd5a54a..184bd825 100644 --- a/src/pitloom/extract/_lock_common.py +++ b/src/pitloom/extract/_lock_common.py @@ -74,7 +74,11 @@ def load_lock_toml(lock_path: Path) -> dict[str, Any] | None: return load_toml_file(lock_path) except FileNotFoundError: return None - except (OSError, TOMLDecodeError) as exc: + except (OSError, TOMLDecodeError, UnicodeDecodeError) as exc: + # tomllib/tomli's underlying decode step raises a bare + # UnicodeDecodeError (not its own TOMLDecodeError) for invalid + # UTF-8 bytes -- still just a malformed/unparseable file, not a + # reason to abort the whole cascade. log.warning("Failed to parse %s: %s", lock_path, exc) return None @@ -99,7 +103,11 @@ def load_lock_json(lock_path: Path) -> dict[str, Any] | None: data = json.load(f) except FileNotFoundError: return None - except (OSError, json.JSONDecodeError) as exc: + except (OSError, json.JSONDecodeError, UnicodeDecodeError) as exc: + # Invalid UTF-8 bytes raise a bare UnicodeDecodeError from the + # text-mode read itself, not json.JSONDecodeError -- still just + # a malformed/unparseable file, not a reason to abort the whole + # cascade. log.warning("Failed to parse %s: %s", lock_path, exc) return None if not isinstance(data, dict): diff --git a/src/pitloom/extract/_pipfile_lock.py b/src/pitloom/extract/_pipfile_lock.py index d81fca2b..9b152f8b 100644 --- a/src/pitloom/extract/_pipfile_lock.py +++ b/src/pitloom/extract/_pipfile_lock.py @@ -47,6 +47,7 @@ find_first_present_key, load_lock_json, single_exact_pin, + warn_missing_name, warn_missing_version, warn_non_registry_source, warn_top_level_key_wrong_type, @@ -100,11 +101,7 @@ def _pinned_dep_for_package(name: object, entry: object) -> str | None: ``None`` when it's malformed, non-registry-sourced, or its ``version`` isn't a single exact ``==`` specifier.""" if not isinstance(name, str) or not name: - log.warning( - "Skipping malformed Pipfile.lock entry: non-string or empty " - "package name (name=%r)", - name, - ) + warn_missing_name("Skipping malformed Pipfile.lock entry", name) return None if not isinstance(entry, dict): log.warning( diff --git a/src/pitloom/extract/_poetry_lock.py b/src/pitloom/extract/_poetry_lock.py index c2cf6f53..99041e74 100644 --- a/src/pitloom/extract/_poetry_lock.py +++ b/src/pitloom/extract/_poetry_lock.py @@ -25,11 +25,14 @@ import logging from pathlib import Path +from typing import Any from pitloom.extract._lock_common import ( is_usable_version, load_lock_toml, warn_malformed_entry_not_table, + warn_missing_name, + warn_missing_version, warn_non_registry_source, warn_top_level_key_wrong_type, ) @@ -79,6 +82,29 @@ def extract_poetry_lock_dependencies(project_dir: Path) -> list[str] | None: _NON_PEP508_SOURCE_TYPES = frozenset({"directory", "file", "git", "url"}) +def _shape_validated_package(pkg: object) -> dict[str, Any] | None: + """Return *pkg* itself when it's a well-formed, versioned + ``[[package]]`` table -- ``None`` (with a ``WARNING:``) for a + non-table entry, or one with a missing/non-string ``name`` or + missing/unparseable ``version``. Split out of + :func:`_pinned_dep_for_package` purely to keep each function's own + return-statement count under this repo's complexity ceiling, the + same split :func:`pitloom.extract._pdm_lock._shape_validated_package` + already uses for the analogous check.""" + if not isinstance(pkg, dict): + warn_malformed_entry_not_table("poetry.lock", "[[package]]", pkg) + return None + name = pkg.get("name") + if not isinstance(name, str) or not name: + warn_missing_name("Skipping malformed poetry.lock [[package]] entry", name) + return None + version = pkg.get("version") + if not is_usable_version(version): + warn_missing_version("poetry.lock", name) + return None + return pkg + + def _pinned_dep_for_package(pkg: object) -> str | None: """Return ``name==version`` for one ``[[package]]`` table entry, or ``None`` when it's malformed, not in the ``main`` group, or sourced @@ -90,20 +116,13 @@ def _pinned_dep_for_package(pkg: object) -> str | None: version pin, so including it here would misrepresent it as an ordinary published release (wrong PURL, bogus PyPI enrichment lookup). """ - if not isinstance(pkg, dict): - warn_malformed_entry_not_table("poetry.lock", "[[package]]", pkg) + validated = _shape_validated_package(pkg) + if validated is None: return None - name = pkg.get("name") - version = pkg.get("version") - if not isinstance(name, str) or not name or not is_usable_version(version): - log.warning( - "Skipping malformed poetry.lock [[package]] entry: missing or " - "non-string 'name'/'version' (name=%r, version=%r)", - name, - version, - ) - return None - groups = pkg.get("groups", ["main"]) + name = validated["name"] + version = validated["version"] + + groups = validated.get("groups", ["main"]) if not isinstance(groups, list): log.warning( "Skipping malformed poetry.lock [[package]] entry %r: 'groups' " @@ -114,7 +133,7 @@ def _pinned_dep_for_package(pkg: object) -> str | None: return None if "main" not in groups: return None - source = pkg.get("source") + source = validated.get("source") source_type = source.get("type") if isinstance(source, dict) else None if isinstance(source_type, str) and source_type in _NON_PEP508_SOURCE_TYPES: warn_non_registry_source("poetry.lock", name, source_type) diff --git a/src/pitloom/extract/_pylock.py b/src/pitloom/extract/_pylock.py index 5488a3be..5552268c 100644 --- a/src/pitloom/extract/_pylock.py +++ b/src/pitloom/extract/_pylock.py @@ -212,19 +212,20 @@ def _evaluate_group_leaf( return member if op == "in" else not member -def _combine_group_results( - operator: str, left: bool | None, right: bool | None -) -> bool | None: - """3-valued ``and``/``or`` combination of two - :func:`_evaluate_group_leaf`-shaped results (``None`` meaning - "unknown", not a real true/false).""" - if operator == "and": - if left is False or right is False: - return False - return None if left is None or right is None else True - if left is True or right is True: +def _all3(values: list[bool | None]) -> bool | None: + """3-valued ``all()``: ``False`` if any value is ``False``, else + ``None`` if any value is ``None``, else ``True``.""" + if any(v is False for v in values): + return False + return None if any(v is None for v in values) else True + + +def _any3(values: list[bool | None]) -> bool | None: + """3-valued ``any()``: ``True`` if any value is ``True``, else + ``None`` if any value is ``None``, else ``False``.""" + if any(v is True for v in values): return True - return None if left is None or right is None else False + return None if any(v is None for v in values) else False def _evaluate_group_node( @@ -233,14 +234,29 @@ def _evaluate_group_node( """Recursively evaluate one node of a parsed ``packaging.markers.Marker``'s tree (a leaf tuple, or a list of nodes interleaved with ``"and"``/``"or"`` operator strings) using - 3-valued group/extras-only logic -- see :func:`_group_marker_excludes`.""" + 3-valued group/extras-only logic -- see :func:`_group_marker_excludes`. + + PEP 508 gives ``and`` higher precedence than ``or``, but + ``Marker()._markers`` doesn't nest same-precedence-level terms to + reflect that -- an unparenthesized ``A or B and C`` is one flat list + ``[A, "or", B, "and", C]``, not ``[A, "or", [B, "and", C]]``. A plain + left-to-right fold over that list would compute ``(A or B) and C`` + instead of the correct ``A or (B and C)``. Grouping every term at + each ``"or"`` boundary into its own list -- mirroring + ``packaging.markers._evaluate_markers()``'s own ``groups`` + algorithm, just with 3-valued ``all``/``any`` instead of Python's + real ones -- restores that precedence regardless of how flat or + nested the parsed tree is. + """ if isinstance(node, tuple): return _evaluate_group_leaf(node, environment) - result = _evaluate_group_node(node[0], environment) - for index in range(1, len(node), 2): - other = _evaluate_group_node(node[index + 1], environment) - result = _combine_group_results(node[index], result, other) - return result + groups: list[list[bool | None]] = [[]] + for item in node: + if item == "or": + groups.append([]) + elif item != "and": + groups[-1].append(_evaluate_group_node(item, environment)) + return _any3([_all3(group) for group in groups]) def _group_marker_excludes( diff --git a/tests/assemble/test_deps_locked_dependencies.py b/tests/assemble/test_deps_locked_dependencies.py index 6d9a404a..eb6b623b 100644 --- a/tests/assemble/test_deps_locked_dependencies.py +++ b/tests/assemble/test_deps_locked_dependencies.py @@ -245,10 +245,43 @@ def test_locked_dependencies_provenance_omitted_matches_empty_string() -> None: def test_locked_dependencies_omitted_matches_empty_list() -> None: """Omitting ``locked_dependencies`` entirely (every pre-existing call - site) must produce the same UUID as passing an empty list -- the new - parameter is purely additive, never a behavior change for callers that - don't know about it.""" + site) must produce the same UUID as passing an empty list *with no + provenance* -- the new parameter is purely additive, never a + behavior change for callers that don't know about it.""" omitted = compute_doc_uuid("pkg", "1.0.0", ["requests>=2.0"]) empty = compute_doc_uuid("pkg", "1.0.0", ["requests>=2.0"], locked_dependencies=[]) assert omitted == empty + + +def test_valid_empty_lock_does_not_collide_with_no_lock_or_a_different_empty_lock() -> ( + None +): + """Regression: a real, successfully-resolved lock file that legitimately + has zero runtime dependencies still carries its own distinct + ``provenance["locked_dependencies"]`` string -- gating the UUID seed's + locked-dependencies contribution on `locked_dependencies` being + *non-empty* (rather than on either it or the provenance string being + given) would silently collide such a document with both (a) a document + with no lock present at all, and (b) a different lock source that also + happened to resolve to zero dependencies -- three genuinely different + provenance outcomes must not share one UUID.""" + no_lock_at_all = compute_doc_uuid("pkg", "1.0.0", ["requests>=2.0"]) + empty_from_pylock = compute_doc_uuid( + "pkg", + "1.0.0", + ["requests>=2.0"], + locked_dependencies=[], + locked_dependencies_provenance=( + "Source: pylock.toml | Method: resolved_lockfile" + ), + ) + empty_from_uv = compute_doc_uuid( + "pkg", + "1.0.0", + ["requests>=2.0"], + locked_dependencies=[], + locked_dependencies_provenance=("Source: uv.lock | Method: resolved_lockfile"), + ) + + assert len({no_lock_at_all, empty_from_pylock, empty_from_uv}) == 3 diff --git a/tests/extract/test_lock_common.py b/tests/extract/test_lock_common.py index 3a6f72c7..62b4054d 100644 --- a/tests/extract/test_lock_common.py +++ b/tests/extract/test_lock_common.py @@ -19,6 +19,7 @@ group_versions_by_canonical_name, index_packages_by_name, is_usable_version, + load_lock_json, load_lock_toml, ) @@ -50,6 +51,42 @@ def test_load_lock_toml_valid_file_returns_data() -> None: assert load_lock_toml(lock_path) == {"key": "value"} +def test_load_lock_toml_invalid_utf8_returns_none_and_warns( + caplog: pytest.LogCaptureFixture, +) -> None: + """Regression: tomllib/tomli's decode step raises a bare + ``UnicodeDecodeError`` (not its own ``TOMLDecodeError``) for invalid + UTF-8 bytes -- must still degrade to ``None`` with a ``WARNING:``, + not propagate out and abort the whole cascade.""" + with tempfile.TemporaryDirectory() as tmp: + lock_path = Path(tmp) / "some.lock" + lock_path.write_bytes(b'name = "\xff\xfebad"\n') + + with caplog.at_level(logging.WARNING): + result = load_lock_toml(lock_path) + + assert result is None + assert "Failed to parse" in caplog.text + + +def test_load_lock_json_invalid_utf8_returns_none_and_warns( + caplog: pytest.LogCaptureFixture, +) -> None: + """Regression: reading invalid UTF-8 bytes in text mode raises a + bare ``UnicodeDecodeError`` (not ``json.JSONDecodeError``) -- must + still degrade to ``None`` with a ``WARNING:``, not propagate out and + abort the whole cascade.""" + with tempfile.TemporaryDirectory() as tmp: + lock_path = Path(tmp) / "some.json" + lock_path.write_bytes(b'{"name": "\xff\xfebad"}') + + with caplog.at_level(logging.WARNING): + result = load_lock_json(lock_path) + + assert result is None + assert "Failed to parse" in caplog.text + + def test_index_packages_by_name_groups_by_name_preserving_order() -> None: packages = [ {"name": "a", "version": "1.0.0"}, diff --git a/tests/extract/test_poetry_lock.py b/tests/extract/test_poetry_lock.py index 53101a14..930688db 100644 --- a/tests/extract/test_poetry_lock.py +++ b/tests/extract/test_poetry_lock.py @@ -171,7 +171,10 @@ def test_malformed_package_entry_skipped() -> None: def test_malformed_package_entry_warns(caplog: pytest.LogCaptureFixture) -> None: """Regression: a malformed ``[[package]]`` entry (missing ``version``) used to be dropped with zero logging, violating "no silent - deviations" -- it must now emit a ``WARNING:``.""" + deviations" -- it must now emit a ``WARNING:``, matching the same + ``missing or non-string 'version'`` wording every sibling format's + own version check uses + (:func:`pitloom.extract._lock_common.warn_missing_version`).""" with tempfile.TemporaryDirectory() as tmp: tmp_path = Path(tmp) _write_lock(tmp_path, '[[package]]\nname = "incomplete"\n') @@ -180,7 +183,24 @@ def test_malformed_package_entry_warns(caplog: pytest.LogCaptureFixture) -> None result = extract_poetry_lock_dependencies(tmp_path) assert not result - assert "malformed" in caplog.text.lower() + assert "missing or non-string 'version'" in caplog.text + + +def test_missing_name_skipped_and_warns(caplog: pytest.LogCaptureFixture) -> None: + """A ``[[package]]`` entry missing ``name`` is validated and warned + about separately from a missing ``version`` -- matching every + sibling format's own split name-then-version check ordering + (:func:`pitloom.extract._lock_common.warn_missing_name`, tried + before ``version`` is ever read).""" + with tempfile.TemporaryDirectory() as tmp: + tmp_path = Path(tmp) + _write_lock(tmp_path, '[[package]]\nversion = "1.0.0"\n') + + with caplog.at_level(logging.WARNING): + result = extract_poetry_lock_dependencies(tmp_path) + + assert not result + assert "missing or non-string 'name'" in caplog.text def test_malformed_package_entry_empty_version_skipped() -> None: diff --git a/tests/extract/test_pylock.py b/tests/extract/test_pylock.py index c2b3bec6..061cf108 100644 --- a/tests/extract/test_pylock.py +++ b/tests/extract/test_pylock.py @@ -308,6 +308,51 @@ def test_default_group_package_included_alongside_excluded_dev_group() -> None: assert extract_pylock_dependencies(tmp_path) == [] +def test_marker_operator_precedence_and_binds_tighter_than_or() -> None: + """Regression: PEP 508 gives `and` higher precedence than `or`, but + `Marker()._markers` doesn't nest same-precedence terms to reflect + that -- an unparenthesized `A or B and C` is one flat list, not + `[A, "or", [B, "and", C]]`. A naive left-to-right fold over that flat + list would compute `(A or B) and C` instead of the correct + `A or (B and C)`. Here `A` is an unevaluated (unknown) environment + condition, `B` is a *true* group-membership clause (the group IS + active), and `C` is a *false* ordinary condition -- correct PEP 508 + semantics (`A or (B and C)`) is `unknown or (True and False)` = + `unknown or False` = unknown, so the package must still be included + (unknown means "can't prove excluded"). The buggy left-fold instead + computes `(unknown or True) and False` = `True and False` = False, + wrongly excluding it.""" + with tempfile.TemporaryDirectory() as tmp: + tmp_path = Path(tmp) + _write_lock( + tmp_path, + 'default-groups = ["default", "dev"]\n' + '[[packages]]\nname = "precedence-test"\nversion = "1.0.0"\n' + "marker = \"python_version >= '3.99' or " + "'dev' in dependency_groups and python_version < '2.0'\"\n", + ) + + assert extract_pylock_dependencies(tmp_path) == ["precedence-test==1.0.0"] + + +def test_marker_operator_precedence_still_excludes_when_no_or_clause_is_true() -> None: + """The precedence fix must not become "always include": when every + `or`-separated group provably evaluates `False` from the known + group/extras clauses alone (no unknown clause anywhere), the package + is still excluded.""" + with tempfile.TemporaryDirectory() as tmp: + tmp_path = Path(tmp) + _write_lock( + tmp_path, + 'default-groups = ["default"]\n' + '[[packages]]\nname = "still-excluded"\nversion = "1.0.0"\n' + "marker = \"'dev' in dependency_groups or " + "'test' in dependency_groups\"\n", + ) + + assert extract_pylock_dependencies(tmp_path) == [] + + def test_package_with_no_marker_included_regardless_of_default_groups() -> None: """A package with no `marker` field at all is an ordinary, always-active runtime dependency -- unaffected by `default-groups` diff --git a/working-docs/design/roadmap.md b/working-docs/design/roadmap.md index 28b1ee6d..ecb731e0 100644 --- a/working-docs/design/roadmap.md +++ b/working-docs/design/roadmap.md @@ -150,48 +150,20 @@ table in [non-hatchling-file-discovery.md](non-hatchling-file-discovery.md)); an existing installed package as a high-fidelity source when present (editable installs, virtual environments). See [metadata-sources.md](./metadata-sources.md). -- [x] **`poetry.lock`** -- done (2026-08-31): `loom project`/`loom generate` - against a Poetry project reads a sibling `poetry.lock` for - `main`-group resolved transitive dependencies, additive to the - direct constraints, source-stage-only. See - [poetry-support.md](../implementation/poetry-support.md). -- [x] **`pylock.toml` (PEP 751)** -- done (2026-09-04): `loom project`/ - `loom generate` reads a sibling `pylock.toml`, when present, for its - resolved `[[packages]]` set, reusing `ProjectMetadata.locked_dependencies` - and the same additive `dependsOn`/`RelationshipCompleteness.complete` - wiring as `poetry.lock`. Build-backend-agnostic, so it's checked - unconditionally rather than gated behind `[tool.poetry]` detection. - See [pep751-pylock-support.md](../implementation/pep751-pylock-support.md) - and [lock-file-cascade.md](../implementation/lock-file-cascade.md) for - the shared priority mechanism across all lock formats. -- [x] **`uv.lock`** -- done (2026-09-04): reads a sibling `uv.lock`'s - resolved main/runtime dependencies, ranked below `pylock.toml` and - above `poetry.lock` in the shared priority cascade. See - [lock-file-cascade.md](../implementation/lock-file-cascade.md). -- [x] **`pdm.lock`** -- done (2026-09-04): reads a sibling `pdm.lock`'s - resolved `default`-group dependencies, ranked below `poetry.lock`. - See [lock-file-cascade.md](../implementation/lock-file-cascade.md). -- [x] **`Pipfile.lock`** -- done (2026-09-05): reads a sibling - `Pipfile.lock`'s resolved `default`-section dependencies (JSON, not - TOML -- the one format that isn't), ranked below `pdm.lock`, lowest - cascade priority. Reached via `read_project()`'s `setup.py`-only - dispatch path, not just the `pyproject.toml` one, since `Pipfile.lock` - predates PEP 621 almost entirely in real projects. See - [lock-file-cascade.md](../implementation/lock-file-cascade.md). -- [x] **pinned `requirements.txt`** -- done (2026-09-05): the lowest- - ranked cascade entry, and the only one that isn't a real lock file -- - used only when *every* real line is already an exact `==` pin (a - URL-based line disqualifies the whole file too, even one that looks - like a tagged release; see [lock-file-cascade.md](../implementation/lock-file-cascade.md) - for the PEP 508/440 reasoning). This closes out +- [x] **Lock/pin formats as a resolved-dependency source** -- done + (2026-08-31 through 2026-09-05): `poetry.lock`, `pylock.toml` (PEP + 751), `uv.lock`, `pdm.lock`, `Pipfile.lock`, and pinned + `requirements.txt` all feed `ProjectMetadata.locked_dependencies` + via one shared priority cascade, closing **"Remaining lock formats as a resolved-dependency source"** - ([#208](https://github.com/bact/pitloom/pull/208)): - `pylock.toml`/`uv.lock`/`poetry.lock`/`pdm.lock`/`Pipfile.lock`/pinned - `requirements.txt` all now feed `ProjectMetadata.locked_dependencies` - via one shared priority cascade. See - [lock-file-cascade.md](../implementation/lock-file-cascade.md) and - [lock-files.md](./lock-files.md) (`pixi.lock`/`conda-lock.yml` remain - future work there, Phase 2). + ([#208](https://github.com/bact/pitloom/pull/208)). See + [lock-file-cascade.md](../implementation/lock-file-cascade.md) (the + cascade, priority order, and per-format details) and + [poetry-support.md](../implementation/poetry-support.md)/ + [pep751-pylock-support.md](../implementation/pep751-pylock-support.md) + for the two formats with their own dedicated doc. + [lock-files.md](./lock-files.md) (`pixi.lock`/`conda-lock.yml` + remain future work there, Phase 2). ### PEP 770 / embed-wheel diff --git a/working-docs/implementation/lock-file-cascade.md b/working-docs/implementation/lock-file-cascade.md index f1748813..1b257d5e 100644 --- a/working-docs/implementation/lock-file-cascade.md +++ b/working-docs/implementation/lock-file-cascade.md @@ -377,11 +377,15 @@ up in. Confirmed once both landed: and `tests/extract/test_requirements_txt.py::test_read_project_populates_locked_dependencies_from_setup_py_only` each exercise this path against a `setup.py`-only project directory. -`apply_locked_dependencies()` is called once, right before each of -`read_project()`'s three directory-based `return` statements (the -sdist-archive branch is skipped -- there's no sibling directory to -check for a lock file against a single archive file), so it runs -uniformly regardless of which metadata source won. +`apply_locked_dependencies()` is called once, at the single point where +`read_project()`'s three directory-based branches (`pyproject.toml`, +the `setup.cfg`/`setup.py` fallback, and `setup.cfg`/`setup.py` alone) +have already converged onto one shared `metadata`/`pitloom_config` +before its one `return` statement -- the sdist-archive branch returns +separately, earlier, and is skipped entirely (there's no sibling +directory to check for a lock file against a single archive file). One +call site covers every directory-based metadata source uniformly, +rather than one call per branch. ## Provenance recording From c71ad0103a40485fa24b18d6ec88426ef37f6db5 Mon Sep 17 00:00:00 2001 From: Arthit Suriyawongkul Date: Sat, 5 Sep 2026 18:22:38 +0700 Subject: [PATCH 13/35] Fix Poetry parity Signed-off-by: Arthit Suriyawongkul --- AGENTS.md | 62 ++++++++++++++ docs/dependency-sources.md | 23 ++++-- src/pitloom/assemble/spdx3/document.py | 35 +++++++- src/pitloom/extract/_poetry_lock.py | 27 ++++++ .../assemble/test_deps_locked_dependencies.py | 82 ++++++++++++++++++- tests/extract/test_hatch_hook_metadata.py | 3 +- tests/extract/test_pdm_lock.py | 3 +- tests/extract/test_poetry_lock.py | 65 ++++++++++++++- tests/extract/test_project.py | 6 +- tests/extract/test_pylock.py | 31 ++++++- tests/extract/test_uv_lock.py | 25 ++++++ tests/extract/test_uv_lock_integration.py | 3 +- .../implementation/pep751-pylock-support.md | 38 +++++++++ 13 files changed, 382 insertions(+), 21 deletions(-) diff --git a/AGENTS.md b/AGENTS.md index 4a767dbd..b7444bdf 100644 --- a/AGENTS.md +++ b/AGENTS.md @@ -54,6 +54,68 @@ Pitloom is invoked from several usage surfaces (CLI, the Hatchling build hook, t - **Consolidate Patterns**: Extract duplicated logic into shared utilities, constants files, or decorators immediately. Don't copy-paste code. - **Enforce File Size Limits**: Strictly obey the ~400-500 lines soft limit. Split files *before* they become a problem. +## Recurring bug patterns + +General-purpose failure modes that have recurred across more than one +subsystem -- worth checking for by name in any code that resembles the +shape described, not just the module where each was first found. + +- **`None` vs `[]`/`{}` (empty-but-present) is a distinct signal, not two + spellings of the same thing.** In a cascade/fallback/gap-fill chain, + `None` (or "absent key") means "this source doesn't apply here, try + the next one"; an empty-but-real container means "this source is + valid and authoritative, with zero results -- stop looking." Confusing + them has recurred in unrelated places: a truthiness check + (`if some_list:`) used where "was a result produced at all" was + needed, silently colliding two cases that should stay distinct; a + `dict.get(key, [])`-style default that treated "key legitimately + absent" the same as "key present with an empty value"; a source- + priority cascade that couldn't tell "this source is real but empty" + from "this source doesn't apply." Before writing `x or default`, + `dict.get(key, [])`, or `if some_container:`, ask whether the empty + case and the absent case are supposed to behave the same -- they + usually aren't. +- **Compare domain identifiers the way the ecosystem/spec does, not as + raw strings.** A raw `==`/dict-key comparison silently fails to match + values that a spec treats as equivalent (e.g. PEP 503 package-name + canonicalization: case-fold, `-`/`_`/`.` treated as interchangeable). + Whenever two identifiers of the same kind are compared or one is used + as a dict key, canonicalize both sides first per the format/spec that + defines them, rather than assuming byte-for-byte equality is enough. +- **A private third-party API (`obj._attr`) does not owe you any + structural guarantee beyond what it happens to return today.** E.g. + `packaging.markers.Marker()._markers` does not pre-group same- + precedence-level boolean terms -- an unparenthesized `A or B and C` + parses to the flat list `[A, 'or', B, 'and', C]`, and the spec's real + precedence has to be reconstructed by the caller, not assumed from the + shape of the list. When consuming a private/internal structure, + verify its actual shape interactively before writing logic that folds + over it, and prefer mirroring that same library's own *public* + algorithm for the equivalent operation over inventing a new one. +- **Warning/error/log wording drifts across sibling modules that perform + the same kind of check.** When several modules of the same family + (e.g. one per supported file format) each need to warn about the same + handful of malformed-input shapes, factor the shared message into one + helper/constant and have every sibling call it, instead of hand- + rolling a similarly-worded message per module. When adding a new check + to one sibling, check whether the others need the identical check + too. +- **A decode/parse helper that only catches the format-specific + exception can still crash on a lower-level encoding failure.** + `tomllib`/`tomli`, `json`, and text-mode `open(..., encoding="utf-8")` + all raise a bare `UnicodeDecodeError` for invalid bytes -- separate + from `TOMLDecodeError`/`json.JSONDecodeError`. A "load and gracefully + degrade on bad input" helper needs to catch the encoding-level + exception alongside the format-level one, or a bad-encoding file + crashes instead of degrading like every other malformed-input case. +- **A doc/docstring claim about "how this mechanism decides" needs to be + re-verified against the actual code before being trusted or restated** + (see the `physical_path`/`distribution_path` and "how surface X does + Y" rules above -- the same failure mode recurs in any doc describing + a cascade, fallback, or precedence order: re-read the current + implementation before repeating or extending a prior description of + its behavior, rather than assuming an existing doc still matches it). + ## CLI output Unix philosophy. Consistent, predictable, parseable. diff --git a/docs/dependency-sources.md b/docs/dependency-sources.md index 59856b27..20236741 100644 --- a/docs/dependency-sources.md +++ b/docs/dependency-sources.md @@ -58,15 +58,20 @@ relationship to the package's real, normalized version -- Pitloom doesn't fetch the URL to check, so a line like that disqualifies the whole file the same as an unpinned or ranged one would. -**Only the single highest-priority lock file present is used.** If more -than one lock file exists in the same project directory (uncommon, but -possible after a build-tool migration), Pitloom picks the one highest in -the table above and ignores the rest entirely -- it never merges two -lock files' resolutions together. This holds even when the -highest-priority lock resolves to *zero* dependencies: a real, -successfully-parsed lock file that legitimately has nothing to add is -still a definitive answer, and a lower-priority lock present alongside -it is still ignored, not used to fill in what looks like a gap. +**Only the single highest-priority *usable* lock file present is used.** +If more than one lock file exists in the same project directory +(uncommon, but possible after a build-tool migration), Pitloom picks the +highest-ranked one in the table above that it can actually read and +parse, and ignores every other one entirely -- it never merges two lock +files' resolutions together. "Usable" matters: a higher-priority lock +file that's absent, unparseable, or otherwise not a genuine file of its +claimed format doesn't win by merely being *present* -- Pitloom moves on +to the next-highest-priority source instead, the same as if that file +weren't there at all. This holds even when the winning lock resolves to +*zero* dependencies: a real, successfully-parsed lock file that +legitimately has nothing to add is still a definitive answer, and a +lower-priority lock present alongside it is still ignored, not used to +fill in what looks like a gap. **A lock entry that can't be resolved to one exact version is left out, not guessed.** `uv.lock` in particular can record the same package diff --git a/src/pitloom/assemble/spdx3/document.py b/src/pitloom/assemble/spdx3/document.py index 02387720..df791f9d 100644 --- a/src/pitloom/assemble/spdx3/document.py +++ b/src/pitloom/assemble/spdx3/document.py @@ -35,6 +35,7 @@ build_enrichment_fragment, build_model, ) +from pitloom.assemble.spdx3._provenance_encoders import parse_provenance_value from pitloom.assemble.spdx3.ai import add_ai_models from pitloom.assemble.spdx3.creation_info import build_creation_info from pitloom.assemble.spdx3.deps import ( @@ -173,6 +174,38 @@ def _locked_transitive_only_dependencies(metadata: ProjectMetadata) -> list[str] ] +#: `locked_dependencies` provenance `Method` tags that represent a real +#: resolver's output -- a full, hash-verifiable transitive closure, not +#: just a list of exact pins someone happened to write down. Every +#: format in `pitloom.extract._locked_dependencies`'s cascade uses this +#: tag except pinned `requirements.txt`, whose own `"pinned_requirements"` +#: tag is deliberately excluded below. +_RESOLVED_LOCKFILE_METHOD = "resolved_lockfile" + + +def _locked_dependencies_completeness(metadata: ProjectMetadata) -> str | None: + """Return the `RelationshipCompleteness` value for the locked-only + `dependsOn` edges :func:`_locked_transitive_only_dependencies` + produces, or `None` to leave it unset. + + A real resolver lock (`poetry.lock`, `pylock.toml`, `uv.lock`, + `pdm.lock`, `Pipfile.lock` -- every cascade entry tagged + `Method: resolved_lockfile`) genuinely proves the full transitive + dependency closure, so its edges are marked `complete`. Pinned + `requirements.txt` (tagged `Method: pinned_requirements`) is + different: it's just a list of exact-pin lines a human or `pip + freeze` wrote, with no resolver guarantee that every real transitive + dependency is actually present -- marking those edges `complete` + would overstate what the file actually proves, so this returns + `None` (unset) for that one source instead. + """ + provenance = metadata.provenance.get("locked_dependencies") + method = parse_provenance_value(provenance).get("method") if provenance else None + if method is not None and method != _RESOLVED_LOCKFILE_METHOD: + return None + return spdx3.RelationshipCompleteness.complete + + def _prefetch_combined_release_info( dependencies: list[str], transitive_only: list[str] ) -> dict[tuple[str, str | None], dict[str, Any] | None]: @@ -353,7 +386,7 @@ def build( encoder=encoder, content_type_method=content_type_method, release_info_cache=release_info_cache, - completeness=spdx3.RelationshipCompleteness.complete, + completeness=_locked_dependencies_completeness(metadata), ) # --- Files --- diff --git a/src/pitloom/extract/_poetry_lock.py b/src/pitloom/extract/_poetry_lock.py index 99041e74..ca3d880d 100644 --- a/src/pitloom/extract/_poetry_lock.py +++ b/src/pitloom/extract/_poetry_lock.py @@ -42,6 +42,26 @@ __all__ = ["extract_poetry_lock_dependencies"] +def _has_poetry_metadata(data: dict[str, Any]) -> bool: + """Return whether *data* has poetry.lock's own identifying + structure: a top-level ``[metadata]`` table with a string + ``lock-version`` key. + + A ``package`` key absent entirely is ambiguous on its own -- it's + the same shape whether the lock genuinely resolves to zero packages + (rare, but poetry itself still always writes ``[metadata]`` for + that case) or the file is some unrelated, syntactically-valid TOML + document that merely happens to be named/found as ``poetry.lock`` + (e.g. truncated, hand-edited, or from an unrelated tool). Every real + ``poetry lock``-generated file, empty or not, always carries this + ``[metadata]`` table -- checking for it distinguishes "genuinely + poetry.lock, zero dependencies" from "not actually a poetry.lock", + so the latter can't silently win the cascade over a genuinely usable + lower-priority lock format via a spurious authoritative-empty result.""" + metadata = data.get("metadata") + return isinstance(metadata, dict) and isinstance(metadata.get("lock-version"), str) + + def extract_poetry_lock_dependencies(project_dir: Path) -> list[str] | None: """Read ``poetry.lock`` next to ``pyproject.toml`` and return its resolved ``main``-group packages as exact-pin PEP 508 strings. @@ -63,6 +83,13 @@ def extract_poetry_lock_dependencies(project_dir: Path) -> list[str] | None: data = load_lock_toml(lock_path) if data is None: return None + if not _has_poetry_metadata(data): + log.warning( + "%s: no top-level 'metadata' table with a 'lock-version' key -- " + "doesn't look like a genuine poetry.lock, ignoring", + lock_path, + ) + return None packages = data.get("package", []) if not isinstance(packages, list): diff --git a/tests/assemble/test_deps_locked_dependencies.py b/tests/assemble/test_deps_locked_dependencies.py index eb6b623b..b2d9aaa3 100644 --- a/tests/assemble/test_deps_locked_dependencies.py +++ b/tests/assemble/test_deps_locked_dependencies.py @@ -21,7 +21,7 @@ from spdx_python_model.bindings import v3_0_1 as spdx3 from pitloom.assemble.spdx3.deps import add_dependencies -from pitloom.assemble.spdx3.document import build +from pitloom.assemble.spdx3.document import _locked_dependencies_completeness, build from pitloom.core.creation import CreationMetadata from pitloom.core.document import DocumentModel from pitloom.core.models import _clear_doc_counters, compute_doc_uuid @@ -125,6 +125,86 @@ def test_locked_dependencies_add_transitive_only_edges() -> None: assert depends_on[packages["idna"]["spdxId"]]["completeness"] == "complete" +def test_pinned_requirements_transitive_edges_leave_completeness_unset() -> None: + """Regression: pinned `requirements.txt` is just a list of exact-pin + lines a human or `pip freeze` wrote, with no resolver guarantee that + every real transitive dependency is present -- unlike a real + resolver lock (`poetry.lock`, `pylock.toml`, `uv.lock`, `pdm.lock`, + `Pipfile.lock`), its locked-only `dependsOn` edges must NOT be + tagged `complete`, which would overstate what the file actually + proves.""" + project = ProjectMetadata( + name="main-project", + version="1.0.0", + dependencies=["requests>=2.0"], + locked_dependencies=["requests==2.31.0", "urllib3==2.2.0"], + provenance={ + "locked_dependencies": ( + "Source: requirements.txt | Method: pinned_requirements" + ) + }, + ) + doc = DocumentModel(project=project, creation_metadata=CreationMetadata()) + + exporter = build(doc, offline=True) + graph = json.loads(exporter.to_json())["@graph"] + + packages = {e["name"]: e for e in graph if e.get("type") == "software_Package"} + relationships = [ + e + for e in graph + if e.get("type") == "Relationship" and e["relationshipType"] == "dependsOn" + ] + main_id = packages["main-project"]["spdxId"] + depends_on = {r["to"][0]: r for r in relationships if r["from"] == main_id} + + assert "completeness" not in depends_on[packages["urllib3"]["spdxId"]] + + +def test_locked_dependencies_completeness_by_method() -> None: + """Unit-level coverage of `_locked_dependencies_completeness()`'s + three branches: `resolved_lockfile` (a real resolver lock) is + `complete`; `pinned_requirements` is unset (`None`); an unrecognized + future `Method` tag defaults to unset too, the same conservative + "don't claim completeness we can't back up" choice as the pinned- + requirements case, rather than assuming it's resolver-grade.""" + resolved = ProjectMetadata( + name="pkg", + locked_dependencies=["idna==3.7"], + provenance={ + "locked_dependencies": "Source: poetry.lock | Method: resolved_lockfile" + }, + ) + pinned = ProjectMetadata( + name="pkg", + locked_dependencies=["idna==3.7"], + provenance={ + "locked_dependencies": ( + "Source: requirements.txt | Method: pinned_requirements" + ) + }, + ) + unrecognized = ProjectMetadata( + name="pkg", + locked_dependencies=["idna==3.7"], + provenance={ + "locked_dependencies": "Source: mystery.lock | Method: future_method" + }, + ) + no_provenance = ProjectMetadata(name="pkg", locked_dependencies=["idna==3.7"]) + + assert ( + _locked_dependencies_completeness(resolved) + == spdx3.RelationshipCompleteness.complete + ) + assert _locked_dependencies_completeness(pinned) is None + assert _locked_dependencies_completeness(unrecognized) is None + assert ( + _locked_dependencies_completeness(no_provenance) + == spdx3.RelationshipCompleteness.complete + ) + + def test_locked_dependencies_dedup_is_case_and_separator_insensitive() -> None: """A direct dependency declared with the author's own casing (e.g. ``Django``) must still dedup against a lock-resolved entry that PEP diff --git a/tests/extract/test_hatch_hook_metadata.py b/tests/extract/test_hatch_hook_metadata.py index ba795ada..57dcff74 100644 --- a/tests/extract/test_hatch_hook_metadata.py +++ b/tests/extract/test_hatch_hook_metadata.py @@ -447,7 +447,8 @@ def test_metadata_from_hatchling_does_not_leak_poetry_lock_dependencies() -> Non tmp_path = Path(tmp) write_pyproject(tmp_path, POETRY_GAP_FILL_PYPROJECT) (tmp_path / "poetry.lock").write_text( - '[[package]]\nname = "requests"\nversion = "2.31.0"\ngroups = ["main"]\n', + '[[package]]\nname = "requests"\nversion = "2.31.0"\ngroups = ["main"]\n' + '[metadata]\nlock-version = "2.1"\n', encoding="utf-8", ) diff --git a/tests/extract/test_pdm_lock.py b/tests/extract/test_pdm_lock.py index 0b095bd1..1008a764 100644 --- a/tests/extract/test_pdm_lock.py +++ b/tests/extract/test_pdm_lock.py @@ -345,7 +345,8 @@ class of bug a naive "first entry with data wins" cascade would '[tool.poetry]\nname = "demo"\nversion = "1.0.0"\n', encoding="utf-8" ) (tmp_path / "poetry.lock").write_text( - '[[package]]\nname = "requests"\nversion = "2.31.0"\ngroups = ["main"]\n', + '[[package]]\nname = "requests"\nversion = "2.31.0"\ngroups = ["main"]\n' + '[metadata]\nlock-version = "2.1"\n', encoding="utf-8", ) _write_lock( diff --git a/tests/extract/test_poetry_lock.py b/tests/extract/test_poetry_lock.py index 930688db..f7ee169a 100644 --- a/tests/extract/test_poetry_lock.py +++ b/tests/extract/test_poetry_lock.py @@ -30,8 +30,22 @@ ) +#: Every real ``poetry lock``-generated file carries this table -- +#: `extract_poetry_lock_dependencies()` uses its presence to distinguish +#: a genuine (if empty) poetry.lock from an unrelated/truncated TOML +#: document that merely happens to be named ``poetry.lock``. Prepended +#: by `_write_lock()` below so every other test in this file, which +#: exercises `package`-list handling rather than this check itself, does +#: not need to repeat it. +_METADATA = '[metadata]\nlock-version = "2.1"\n' + + def _write_lock(tmp_dir: Path, content: str) -> None: - (tmp_dir / "poetry.lock").write_text(content, encoding="utf-8") + # Appended, not prepended: a bare top-level `key = value` line in + # *content* (e.g. a malformed `package = "not-a-list"` test fixture) + # would otherwise land inside the `[metadata]` table itself if + # `_METADATA`'s `[metadata]` header came first in the file. + (tmp_dir / "poetry.lock").write_text(content + _METADATA, encoding="utf-8") def test_no_lock_file_returns_none() -> None: @@ -43,9 +57,11 @@ def test_no_lock_file_returns_none() -> None: def test_valid_lock_with_no_packages_returns_empty_list_not_none() -> None: - """A `poetry.lock` with zero packages is a real, valid answer -- must - be `[]`, not `None`, so the cascade treats it as a winning (if empty) - result rather than "not present".""" + """A `poetry.lock` with zero packages is a real, valid answer (its + `[metadata]` table -- always present in a genuine poetry.lock, even + an empty one -- proves it's not just some unrelated/truncated TOML + file) -- must be `[]`, not `None`, so the cascade treats it as a + winning (if empty) result rather than "not present".""" with tempfile.TemporaryDirectory() as tmp: tmp_path = Path(tmp) _write_lock(tmp_path, "") @@ -53,6 +69,47 @@ def test_valid_lock_with_no_packages_returns_empty_list_not_none() -> None: assert extract_poetry_lock_dependencies(tmp_path) == [] +def test_missing_metadata_table_returns_none_and_warns( + caplog: pytest.LogCaptureFixture, +) -> None: + """Regression: a syntactically valid but empty/truncated file with no + `[metadata]` table at all is ambiguous -- it could be a genuine + zero-dependency poetry.lock, or it could be some unrelated TOML + document (hand-edited, from a different tool, truncated by a bad + write) that merely happens to be found as `poetry.lock`. Without + this check, the latter would be silently treated as an authoritative + empty lock and block a genuinely usable lower-priority source (e.g. + `pdm.lock`) in the cascade -- must return `None` instead, so a lower- + priority source can still be tried.""" + with tempfile.TemporaryDirectory() as tmp: + tmp_path = Path(tmp) + (tmp_path / "poetry.lock").write_text("", encoding="utf-8") + + with caplog.at_level(logging.WARNING): + result = extract_poetry_lock_dependencies(tmp_path) + + assert result is None + assert "doesn't look like a genuine poetry.lock" in caplog.text + + +def test_metadata_table_missing_lock_version_returns_none_and_warns( + caplog: pytest.LogCaptureFixture, +) -> None: + """A `[metadata]` table present but missing/non-string `lock-version` + is just as ambiguous as no `[metadata]` table at all.""" + with tempfile.TemporaryDirectory() as tmp: + tmp_path = Path(tmp) + (tmp_path / "poetry.lock").write_text( + '[metadata]\ncontent-hash = "abc123"\n', encoding="utf-8" + ) + + with caplog.at_level(logging.WARNING): + result = extract_poetry_lock_dependencies(tmp_path) + + assert result is None + assert "doesn't look like a genuine poetry.lock" in caplog.text + + def test_malformed_toml_returns_empty_list_and_warns( caplog: pytest.LogCaptureFixture, ) -> None: diff --git a/tests/extract/test_project.py b/tests/extract/test_project.py index 71234dd6..825f0075 100644 --- a/tests/extract/test_project.py +++ b/tests/extract/test_project.py @@ -145,7 +145,8 @@ def test_read_project_fallback_preserves_already_resolved_poetry_lock( '[tool.poetry]\nversion = "1.0.0"\n', encoding="utf-8" ) (tmp_path / "poetry.lock").write_text( - '[[package]]\nname = "requests"\nversion = "2.31.0"\ngroups = ["main"]\n', + '[[package]]\nname = "requests"\nversion = "2.31.0"\ngroups = ["main"]\n' + '[metadata]\nlock-version = "2.1"\n', encoding="utf-8", ) (tmp_path / "setup.cfg").write_text( @@ -274,7 +275,8 @@ def test_read_project_include_locked_dependencies_false_also_skips_poetry_lock( '[tool.poetry]\nname = "pkg"\nversion = "1.0.0"\n', encoding="utf-8" ) (tmp_path / "poetry.lock").write_text( - '[[package]]\nname = "requests"\nversion = "2.31.0"\ngroups = ["main"]\n', + '[[package]]\nname = "requests"\nversion = "2.31.0"\ngroups = ["main"]\n' + '[metadata]\nlock-version = "2.1"\n', encoding="utf-8", ) diff --git a/tests/extract/test_pylock.py b/tests/extract/test_pylock.py index 061cf108..438aa5d0 100644 --- a/tests/extract/test_pylock.py +++ b/tests/extract/test_pylock.py @@ -353,6 +353,34 @@ def test_marker_operator_precedence_still_excludes_when_no_or_clause_is_true() - assert extract_pylock_dependencies(tmp_path) == [] +def test_extras_gated_package_excluded_by_default() -> None: + """PEP 751 supports both single-use lockfiles (one fixed purpose, no + group/extras complexity -- every package is simply included, as the + other tests in this file already cover via `default-groups`) and + multi-use lockfiles, which bundle multiple installable + configurations into one file via per-package `marker` clauses on + *both* pseudo-environment variables PEP 751 defines for this: + `dependency_groups` (covered above) and `extras`. A package gated on + `'' in extras` must be excluded from the default resolved set + the same way one gated on `dependency_groups` is -- Pitloom's SBOM + represents the base/default install with no extras requested, + consistent with every sibling format excluding optional-dependencies/ + extras from its own default resolved set (`poetry.lock`'s `main`-only + group, `uv.lock`'s runtime-only `dependencies`, `pdm.lock`'s + `default`-only group).""" + with tempfile.TemporaryDirectory() as tmp: + tmp_path = Path(tmp) + _write_lock( + tmp_path, + 'extras = ["security"]\n' + '[[packages]]\nname = "requests"\nversion = "2.31.0"\n\n' + '[[packages]]\nname = "pyopenssl"\nversion = "24.0.0"\n' + "marker = \"'security' in extras\"\n", + ) + + assert extract_pylock_dependencies(tmp_path) == ["requests==2.31.0"] + + def test_package_with_no_marker_included_regardless_of_default_groups() -> None: """A package with no `marker` field at all is an ordinary, always-active runtime dependency -- unaffected by `default-groups` @@ -506,7 +534,8 @@ def test_read_project_pylock_takes_priority_over_poetry_lock( '[tool.poetry]\nname = "pkg"\nversion = "1.0.0"\n', encoding="utf-8" ) (tmp_path / "poetry.lock").write_text( - '[[package]]\nname = "requests"\nversion = "2.31.0"\ngroups = ["main"]\n', + '[[package]]\nname = "requests"\nversion = "2.31.0"\ngroups = ["main"]\n' + '[metadata]\nlock-version = "2.1"\n', encoding="utf-8", ) _write_lock(tmp_path, '[[packages]]\nname = "httpx"\nversion = "0.27.0"\n') diff --git a/tests/extract/test_uv_lock.py b/tests/extract/test_uv_lock.py index 493f1ad7..b8a2187e 100644 --- a/tests/extract/test_uv_lock.py +++ b/tests/extract/test_uv_lock.py @@ -466,6 +466,31 @@ def test_nested_dependencies_not_a_list_skipped_and_warns( assert "nested 'dependencies'" in caplog.text +def test_nested_dependencies_falsy_non_list_silently_skipped( + caplog: pytest.LogCaptureFixture, +) -> None: + """A falsy non-list `dependencies` value (e.g. `false` -- TOML has + no `null`, so this is the practical malformed-but-empty shape) + behaves like a missing/empty key, not like the truthy-malformed case + above -- no `WARNING:`, and nothing to walk into, but the package's + own pin is still resolved.""" + with tempfile.TemporaryDirectory() as tmp: + tmp_path = Path(tmp) + _write_lock( + tmp_path, + _ROOT_HEADER + 'dependencies = [{ name = "requests" }]\n\n' + '[[package]]\nname = "requests"\nversion = "2.31.0"\n' + 'source = { registry = "https://pypi.org/simple" }\n' + "dependencies = false\n", + ) + + with caplog.at_level(logging.WARNING): + result = extract_uv_lock_dependencies(tmp_path) + + assert result == ["requests==2.31.0"] + assert "nested 'dependencies'" not in caplog.text + + def test_dependency_with_no_source_table_still_included() -> None: """A package entry with no `source` key at all (unusual but not invalid) is treated the same as a registry source -- only an diff --git a/tests/extract/test_uv_lock_integration.py b/tests/extract/test_uv_lock_integration.py index 3bb73e47..c8434f9e 100644 --- a/tests/extract/test_uv_lock_integration.py +++ b/tests/extract/test_uv_lock_integration.py @@ -67,7 +67,8 @@ def test_read_project_uv_lock_takes_priority_over_poetry_lock( '[tool.poetry]\nname = "demo"\nversion = "1.0.0"\n', encoding="utf-8" ) (tmp_path / "poetry.lock").write_text( - '[[package]]\nname = "requests"\nversion = "2.31.0"\ngroups = ["main"]\n', + '[[package]]\nname = "requests"\nversion = "2.31.0"\ngroups = ["main"]\n' + '[metadata]\nlock-version = "2.1"\n', encoding="utf-8", ) _write_lock( diff --git a/working-docs/implementation/pep751-pylock-support.md b/working-docs/implementation/pep751-pylock-support.md index 3275aa57..03652397 100644 --- a/working-docs/implementation/pep751-pylock-support.md +++ b/working-docs/implementation/pep751-pylock-support.md @@ -94,6 +94,44 @@ major version. A newer *minor* version within the known major (e.g. 751's additive-minor-versions policy, but with a `WARNING:` that some content may go unrecognized. +## Single-use vs multi-use lockfiles + +PEP 751 explicitly supports two shapes of `pylock.toml`, and this +extractor handles both without needing to detect which one it's +looking at: + +- **Single-use** -- like `requirements.txt`, one file serves one fixed + purpose (e.g. a production-only or dev-only export). No package + carries a group/extras-referencing `marker`, since there's no second + configuration to distinguish from; every `[[packages]]` entry is + simply included, `default-groups` filtering is a no-op (nothing to + filter), and `_group_marker_excludes()` never has anything to + evaluate. `tests/fixtures/real-world-locks/pylock/snowflake-cli-3.26.0/` + is this shape in practice: its packages' own `marker` fields are + ordinary `python_version`/`sys_platform` conditions only, never + `dependency_groups`/`extras`. +- **Multi-use** -- one file bundles more than one installable + configuration (e.g. base + a `dev` dependency-group) to avoid + duplicating packages shared between them, distinguishing membership + per package via a `marker` referencing the `dependency_groups`/ + `extras` pseudo-environment variables (e.g. `"'dev' in + dependency_groups"`, `"'security' in extras"`). This extractor + resolves *both* variables against a fixed "no extras, only the file's + own `default-groups`" environment (see above) -- Pitloom's SBOM + always represents the base/default install, the same policy already + applied to `poetry.lock`'s `main`-only group, `pdm.lock`'s + `default`-only group, and `uv.lock`'s runtime-only (non-optional) + dependencies. `tests/fixtures/real-world-locks/pylock/pipenv-2026.8.0/` + is this shape: `dependency-groups = ["dev"]`, `default-groups = + ["default"]`, and its `dev`-only packages (`alabaster`, `arpeggio`, + etc.) are correctly excluded from `locked_dependencies`. + +Both shapes are covered by dedicated tests in `tests/extract/test_pylock.py` +(`test_non_default_group_package_excluded`, +`test_extras_gated_package_excluded_by_default`, +`test_package_with_no_marker_included_regardless_of_default_groups`), +not just incidentally by the two real-world fixtures. + ## Non-registry sources A package pinned via PEP 751's `vcs`, `directory`, or `archive` source From 2ec613289e9c70cfe2a0c1be86628c4436d6d4c7 Mon Sep 17 00:00:00 2001 From: Arthit Suriyawongkul Date: Sat, 5 Sep 2026 19:19:15 +0700 Subject: [PATCH 14/35] Split tests Signed-off-by: Arthit Suriyawongkul --- src/pitloom/extract/_lock_common.py | 105 +++++++++- src/pitloom/extract/_pdm_lock.py | 50 ++--- src/pitloom/extract/_pipfile_lock.py | 8 + src/pitloom/extract/_poetry_lock.py | 66 +----- src/pitloom/extract/_uv_lock.py | 38 ++-- tests/extract/test_pdm_lock.py | 56 ++++- tests/extract/test_pipfile_lock.py | 59 +++++- tests/extract/test_poetry_lock.py | 15 +- tests/extract/test_project.py | 3 +- tests/extract/test_pylock.py | 227 +-------------------- tests/extract/test_pylock_markers.py | 249 +++++++++++++++++++++++ tests/extract/test_requirements_txt.py | 3 +- tests/extract/test_uv_lock.py | 128 +----------- tests/extract/test_uv_lock_transitive.py | 152 ++++++++++++++ 14 files changed, 679 insertions(+), 480 deletions(-) create mode 100644 tests/extract/test_pylock_markers.py create mode 100644 tests/extract/test_uv_lock_transitive.py diff --git a/src/pitloom/extract/_lock_common.py b/src/pitloom/extract/_lock_common.py index 184bd825..62a42de2 100644 --- a/src/pitloom/extract/_lock_common.py +++ b/src/pitloom/extract/_lock_common.py @@ -20,7 +20,7 @@ import json import logging -from collections.abc import Iterable, Mapping +from collections.abc import Callable, Iterable, Mapping from pathlib import Path from typing import Any, TypeGuard @@ -34,12 +34,15 @@ __all__ = [ "POETRY_LOCK_SOURCE_NAME", + "default_group_included", "find_first_present_key", "group_versions_by_canonical_name", + "has_required_top_level_table", "index_packages_by_name", "is_usable_version", "load_lock_json", "load_lock_toml", + "shape_validated_package", "single_exact_pin", "warn_malformed_entry_not_table", "warn_missing_name", @@ -120,12 +123,51 @@ def load_lock_json(lock_path: Path) -> dict[str, Any] | None: return data +def has_required_top_level_table( + data: dict[str, Any], + table_key: str, + required_key: str, + value_type: type | tuple[type, ...] = object, +) -> bool: + """Return whether *data* has a top-level table *table_key* containing + key *required_key* with a value that's an instance of *value_type* -- + the "does this look like a genuine file of this format" shape check + every TOML/JSON-based lock extractor needs before treating an empty + ``[[package]]``-style list as an authoritative, zero-dependency + result. + + A format-defining key absent entirely is ambiguous on its own -- the + shape is identical whether the lock genuinely resolves to zero + packages (rare, but every genuine lock-writer tool still emits its + own identifying top-level structure for that case) or the file is + some unrelated, syntactically-valid document that merely happens to + be named/found as this format's lock file (e.g. truncated, + hand-edited, or from an unrelated tool). Checking for the format's + own marker distinguishes "genuinely this format, zero dependencies" + from "not actually this format", so the latter can't silently win + the cascade over a genuinely usable lower-priority lock format via a + spurious authoritative-empty result -- e.g. ``poetry.lock``'s + string-valued ``metadata.lock-version``, ``pdm.lock``'s + string-valued ``metadata.lock_version``, ``Pipfile.lock``'s + int-valued ``_meta.pipfile-spec`` (each caller passes its own + format's real value type as *value_type*; a key present with a value + of the wrong shape is exactly as ambiguous as the key being absent + entirely, so it isn't treated as a looser pass than outright + absence). + """ + table = data.get(table_key) + return isinstance(table, dict) and isinstance(table.get(required_key), value_type) + + def index_packages_by_name( packages: Iterable[object], + key: Callable[[str], str] = str, ) -> dict[str, list[dict[str, Any]]]: """Group every well-formed entry of *packages* (a lock format's flat - ``[[package]]``-style list) by its ``name`` field, preserving file - order both across and within names. + ``[[package]]``-style list) by its ``name`` field (passed through + *key*, e.g. :func:`packaging.utils.canonicalize_name` when a caller + needs PEP 503-canonicalized grouping instead of the literal name), + preserving file order both across and within names. A non-table entry, or a table with a missing/non-string/empty ``name``, is silently excluded -- it can never be the target of a @@ -147,7 +189,7 @@ def index_packages_by_name( continue name = pkg.get("name") if isinstance(name, str) and name: - by_name.setdefault(name, []).append(pkg) + by_name.setdefault(key(name), []).append(pkg) return by_name @@ -329,3 +371,58 @@ def find_first_present_key( use this helper. """ return next((key for key in keys if key in mapping), None) + + +def shape_validated_package( + pkg: object, lock_file: str, entry_label: str = "[[package]]" +) -> dict[str, Any] | None: + """Return *pkg* itself when it's a well-formed, versioned + list-of-tables entry -- ``None`` (with a ``WARNING:``) for a + non-table entry, or one with a missing/non-string ``name`` or + missing/unparseable ``version``. + + Shared by every lock format whose per-package entries are a flat + table with a plain ``name``/``version`` pair (``poetry.lock``, + ``pdm.lock``) -- factored out of two independently-drifting, + near-identical per-format copies so a wording/behavior change to + this check lands once instead of needing to be repeated at each + format's own call site. + """ + if not isinstance(pkg, dict): + warn_malformed_entry_not_table(lock_file, entry_label, pkg) + return None + name = pkg.get("name") + if not isinstance(name, str) or not name: + warn_missing_name(f"Skipping malformed {lock_file} {entry_label} entry", name) + return None + version = pkg.get("version") + if not is_usable_version(version): + warn_missing_version(lock_file, name) + return None + return pkg + + +def default_group_included( + validated: Mapping[str, object], lock_file: str, default_group: str, name: str +) -> bool | None: + """Return whether *validated* (an already shape-validated package + entry) belongs to *default_group* per its ``groups`` list -- ``None`` + (with a ``WARNING:``) when ``groups`` is present but not a list. + + Shared by every lock format whose per-package group membership is a + flat ``groups`` list defaulting to a single-element list naming the + format's own default group (``poetry.lock``'s ``"main"``, + ``pdm.lock``'s ``"default"``) -- factored out of two + independently-drifting, near-identical per-format copies the same + way :func:`shape_validated_package` was. + """ + groups = validated.get("groups", [default_group]) + if not isinstance(groups, list): + log.warning( + "Skipping malformed %s entry %r: 'groups' is %s, expected a list", + lock_file, + name, + type(groups).__name__, + ) + return None + return default_group in groups diff --git a/src/pitloom/extract/_pdm_lock.py b/src/pitloom/extract/_pdm_lock.py index 913eac17..5e4155fb 100644 --- a/src/pitloom/extract/_pdm_lock.py +++ b/src/pitloom/extract/_pdm_lock.py @@ -42,13 +42,12 @@ from typing import Any from pitloom.extract._lock_common import ( + default_group_included, find_first_present_key, group_versions_by_canonical_name, - is_usable_version, + has_required_top_level_table, load_lock_toml, - warn_malformed_entry_not_table, - warn_missing_name, - warn_missing_version, + shape_validated_package, warn_non_registry_source, warn_top_level_key_wrong_type, ) @@ -72,30 +71,6 @@ _NON_REGISTRY_KEYS = ("git", "url", "path") -def _shape_validated_package(pkg: object) -> dict[str, Any] | None: - """Return *pkg* itself when it's a well-formed, versioned - ``[[package]]`` table -- ``None`` (with a ``WARNING:``) for a - non-table entry, or one with a missing/non-string ``name`` or - missing/unparseable ``version``. Split out of - :func:`_default_group_package_or_none` purely to keep each - function's own return-statement count under this repo's complexity - ceiling; the two checks it doesn't cover (group membership, - non-registry source) stay there since they need this function's own - early-exit to already have happened first.""" - if not isinstance(pkg, dict): - warn_malformed_entry_not_table("pdm.lock", "[[package]]", pkg) - return None - name = pkg.get("name") - if not isinstance(name, str) or not name: - warn_missing_name("Skipping malformed pdm.lock [[package]] entry", name) - return None - version = pkg.get("version") - if not is_usable_version(version): - warn_missing_version("pdm.lock", name) - return None - return pkg - - def _default_group_package_or_none(pkg: object) -> dict[str, Any] | None: """Return *pkg* itself when it's a well-formed, default-group, registry-sourced, versioned ``[[package]]`` entry -- ``None`` @@ -103,20 +78,12 @@ def _default_group_package_or_none(pkg: object) -> dict[str, Any] | None: non-registry-sourced; silent for a package that's simply not in the default group, the same "expected filtering" as ``poetry.lock``'s non-``main`` group exclusion).""" - validated = _shape_validated_package(pkg) + validated = shape_validated_package(pkg, "pdm.lock") if validated is None: return None name = validated["name"] - groups = validated.get("groups", [_DEFAULT_GROUP]) - if not isinstance(groups, list): - log.warning( - "Skipping malformed pdm.lock entry %r: 'groups' is %s, expected a list", - name, - type(groups).__name__, - ) - return None - if _DEFAULT_GROUP not in groups: + if not default_group_included(validated, "pdm.lock", _DEFAULT_GROUP, name): return None non_registry_key = find_first_present_key(validated, _NON_REGISTRY_KEYS) @@ -140,6 +107,13 @@ def extract_pdm_lock_dependencies(project_dir: Path) -> list[str] | None: data = load_lock_toml(lock_path) if data is None: return None + if not has_required_top_level_table(data, "metadata", "lock_version", str): + log.warning( + "%s: no top-level 'metadata' table with a 'lock_version' key -- " + "doesn't look like a genuine pdm.lock, ignoring", + lock_path, + ) + return None packages = data.get("package", []) if not isinstance(packages, list): diff --git a/src/pitloom/extract/_pipfile_lock.py b/src/pitloom/extract/_pipfile_lock.py index 9b152f8b..2417c908 100644 --- a/src/pitloom/extract/_pipfile_lock.py +++ b/src/pitloom/extract/_pipfile_lock.py @@ -45,6 +45,7 @@ from pitloom.extract._lock_common import ( find_first_present_key, + has_required_top_level_table, load_lock_json, single_exact_pin, warn_missing_name, @@ -80,6 +81,13 @@ def extract_pipfile_lock_dependencies(project_dir: Path) -> list[str] | None: data = load_lock_json(lock_path) if data is None: return None + if not has_required_top_level_table(data, "_meta", "pipfile-spec", int): + log.warning( + "%s: no top-level '_meta' object with a 'pipfile-spec' key -- " + "doesn't look like a genuine Pipfile.lock, ignoring", + lock_path, + ) + return None default_section = data.get("default", {}) if not isinstance(default_section, dict): diff --git a/src/pitloom/extract/_poetry_lock.py b/src/pitloom/extract/_poetry_lock.py index ca3d880d..75c0a5ab 100644 --- a/src/pitloom/extract/_poetry_lock.py +++ b/src/pitloom/extract/_poetry_lock.py @@ -25,14 +25,12 @@ import logging from pathlib import Path -from typing import Any from pitloom.extract._lock_common import ( - is_usable_version, + default_group_included, + has_required_top_level_table, load_lock_toml, - warn_malformed_entry_not_table, - warn_missing_name, - warn_missing_version, + shape_validated_package, warn_non_registry_source, warn_top_level_key_wrong_type, ) @@ -41,25 +39,7 @@ __all__ = ["extract_poetry_lock_dependencies"] - -def _has_poetry_metadata(data: dict[str, Any]) -> bool: - """Return whether *data* has poetry.lock's own identifying - structure: a top-level ``[metadata]`` table with a string - ``lock-version`` key. - - A ``package`` key absent entirely is ambiguous on its own -- it's - the same shape whether the lock genuinely resolves to zero packages - (rare, but poetry itself still always writes ``[metadata]`` for - that case) or the file is some unrelated, syntactically-valid TOML - document that merely happens to be named/found as ``poetry.lock`` - (e.g. truncated, hand-edited, or from an unrelated tool). Every real - ``poetry lock``-generated file, empty or not, always carries this - ``[metadata]`` table -- checking for it distinguishes "genuinely - poetry.lock, zero dependencies" from "not actually a poetry.lock", - so the latter can't silently win the cascade over a genuinely usable - lower-priority lock format via a spurious authoritative-empty result.""" - metadata = data.get("metadata") - return isinstance(metadata, dict) and isinstance(metadata.get("lock-version"), str) +_DEFAULT_GROUP = "main" def extract_poetry_lock_dependencies(project_dir: Path) -> list[str] | None: @@ -83,7 +63,7 @@ def extract_poetry_lock_dependencies(project_dir: Path) -> list[str] | None: data = load_lock_toml(lock_path) if data is None: return None - if not _has_poetry_metadata(data): + if not has_required_top_level_table(data, "metadata", "lock-version", str): log.warning( "%s: no top-level 'metadata' table with a 'lock-version' key -- " "doesn't look like a genuine poetry.lock, ignoring", @@ -109,29 +89,6 @@ def extract_poetry_lock_dependencies(project_dir: Path) -> list[str] | None: _NON_PEP508_SOURCE_TYPES = frozenset({"directory", "file", "git", "url"}) -def _shape_validated_package(pkg: object) -> dict[str, Any] | None: - """Return *pkg* itself when it's a well-formed, versioned - ``[[package]]`` table -- ``None`` (with a ``WARNING:``) for a - non-table entry, or one with a missing/non-string ``name`` or - missing/unparseable ``version``. Split out of - :func:`_pinned_dep_for_package` purely to keep each function's own - return-statement count under this repo's complexity ceiling, the - same split :func:`pitloom.extract._pdm_lock._shape_validated_package` - already uses for the analogous check.""" - if not isinstance(pkg, dict): - warn_malformed_entry_not_table("poetry.lock", "[[package]]", pkg) - return None - name = pkg.get("name") - if not isinstance(name, str) or not name: - warn_missing_name("Skipping malformed poetry.lock [[package]] entry", name) - return None - version = pkg.get("version") - if not is_usable_version(version): - warn_missing_version("poetry.lock", name) - return None - return pkg - - def _pinned_dep_for_package(pkg: object) -> str | None: """Return ``name==version`` for one ``[[package]]`` table entry, or ``None`` when it's malformed, not in the ``main`` group, or sourced @@ -143,22 +100,13 @@ def _pinned_dep_for_package(pkg: object) -> str | None: version pin, so including it here would misrepresent it as an ordinary published release (wrong PURL, bogus PyPI enrichment lookup). """ - validated = _shape_validated_package(pkg) + validated = shape_validated_package(pkg, "poetry.lock") if validated is None: return None name = validated["name"] version = validated["version"] - groups = validated.get("groups", ["main"]) - if not isinstance(groups, list): - log.warning( - "Skipping malformed poetry.lock [[package]] entry %r: 'groups' " - "is %s, expected a list", - name, - type(groups).__name__, - ) - return None - if "main" not in groups: + if not default_group_included(validated, "poetry.lock", _DEFAULT_GROUP, name): return None source = validated.get("source") source_type = source.get("type") if isinstance(source, dict) else None diff --git a/src/pitloom/extract/_uv_lock.py b/src/pitloom/extract/_uv_lock.py index 95ac395f..752e2419 100644 --- a/src/pitloom/extract/_uv_lock.py +++ b/src/pitloom/extract/_uv_lock.py @@ -53,6 +53,7 @@ from pitloom.extract._lock_common import ( find_first_present_key, + index_packages_by_name, is_usable_version, load_lock_toml, warn_malformed_entry_not_table, @@ -338,30 +339,15 @@ def extract_uv_lock_dependencies( ) return None - by_name = _index_by_canonical_name(packages) + # uv itself normalizes every ``name`` field it writes, but a + # dependency *reference* and the package's own top-level entry are + # two separately literal strings in the file -- grouping by + # canonical name (as ``_collect_transitive_dependencies``'s + # ``visited`` set already does) keeps lookup consistent with a name + # that differs only in case/``-``/``_``/``.`` folding, instead of a + # literal-string mismatch silently causing a resolvable dependency + # to be reported as "not found". A non-table entry, or one with a + # missing/non-string/empty ``name``, is excluded here -- see + # ``_warn_malformed_packages`` for the diagnostic on those. + by_name = index_packages_by_name(packages, key=canonicalize_name) return _collect_transitive_dependencies(root_dependencies, by_name) - - -def _index_by_canonical_name( - packages: Iterable[object], -) -> dict[str, list[dict[str, Any]]]: - """Group every well-formed ``[[package]]`` entry by its PEP - 503-canonicalized ``name`` (a non-table entry, or one with a - missing/non-string/empty ``name``, is excluded here -- see - :func:`_warn_malformed_packages` for the diagnostic on those). uv - itself normalizes every ``name`` field it writes, but a dependency - *reference* and the package's own top-level entry are two separately - literal strings in the file -- grouping by canonical name (as - :func:`_collect_transitive_dependencies`'s ``visited`` set already - does) keeps lookup consistent with a name that differs only in - case/``-``/``_``/``.`` folding, instead of a literal-string mismatch - silently causing a resolvable dependency to be reported as - "not found".""" - by_name: dict[str, list[dict[str, Any]]] = {} - for pkg in packages: - if not isinstance(pkg, dict): - continue - name = pkg.get("name") - if isinstance(name, str) and name: - by_name.setdefault(canonicalize_name(name), []).append(pkg) - return by_name diff --git a/tests/extract/test_pdm_lock.py b/tests/extract/test_pdm_lock.py index 1008a764..e834b723 100644 --- a/tests/extract/test_pdm_lock.py +++ b/tests/extract/test_pdm_lock.py @@ -28,9 +28,15 @@ Path(__file__).parent.parent / "fixtures" / "real-world-locks" / "pdm" ) +#: Every genuine `pdm.lock` carries this table -- appended to *body* by +#: default so tests that aren't specifically about the genuineness check +#: itself don't need to repeat it. +_METADATA = '[metadata]\nlock_version = "4.5.1"\n' -def _write_lock(tmp_dir: Path, body: str = "") -> None: - (tmp_dir / "pdm.lock").write_text(body, encoding="utf-8") + +def _write_lock(tmp_dir: Path, body: str = "", include_metadata: bool = True) -> None: + content = body + _METADATA if include_metadata else body + (tmp_dir / "pdm.lock").write_text(content, encoding="utf-8") def test_no_lock_file_returns_none() -> None: @@ -52,6 +58,52 @@ def test_valid_lock_with_no_packages_returns_empty_list_not_none() -> None: assert extract_pdm_lock_dependencies(tmp_path) == [] +def test_missing_metadata_table_returns_none_and_warns( + caplog: pytest.LogCaptureFixture, +) -> None: + """An empty/unrelated-but-parseable TOML file with no `[metadata]` + table (this repo's `None`-vs-`[]` recurring bug pattern) must not be + treated as "a real, empty pdm.lock" -- it could otherwise silently + win the cascade over a genuinely usable lower-priority lock.""" + with tempfile.TemporaryDirectory() as tmp: + tmp_path = Path(tmp) + _write_lock(tmp_path, "", include_metadata=False) + + with caplog.at_level(logging.WARNING): + result = extract_pdm_lock_dependencies(tmp_path) + + assert result is None + assert "doesn't look like a genuine pdm.lock" in caplog.text + + +@pytest.mark.parametrize( + "metadata_body", + [ + pytest.param('strategy = ["inherit_metadata"]\n', id="missing"), + pytest.param("lock_version = 4\n", id="non-string"), + ], +) +def test_metadata_table_missing_lock_version_returns_none_and_warns( + metadata_body: str, caplog: pytest.LogCaptureFixture +) -> None: + """A present but wrong-shaped `lock_version` (e.g. an int instead of + a string) must not pass more easily than the key being absent + entirely.""" + with tempfile.TemporaryDirectory() as tmp: + tmp_path = Path(tmp) + _write_lock( + tmp_path, + f"[metadata]\n{metadata_body}", + include_metadata=False, + ) + + with caplog.at_level(logging.WARNING): + result = extract_pdm_lock_dependencies(tmp_path) + + assert result is None + assert "doesn't look like a genuine pdm.lock" in caplog.text + + def test_malformed_toml_returns_empty_list_and_warns( caplog: pytest.LogCaptureFixture, ) -> None: diff --git a/tests/extract/test_pipfile_lock.py b/tests/extract/test_pipfile_lock.py index 0cf45c0b..aca73b6d 100644 --- a/tests/extract/test_pipfile_lock.py +++ b/tests/extract/test_pipfile_lock.py @@ -28,8 +28,17 @@ ) -def _write_lock(tmp_dir: Path, data: dict[str, object]) -> None: - (tmp_dir / "Pipfile.lock").write_text(json.dumps(data), encoding="utf-8") +#: Every genuine `Pipfile.lock` carries this key -- merged into *data* by +#: default so tests that aren't specifically about the genuineness check +#: itself don't need to repeat it. +_META = {"pipfile-spec": 6} + + +def _write_lock( + tmp_dir: Path, data: dict[str, object], include_meta: bool = True +) -> None: + full_data = {"_meta": _META, **data} if include_meta else data + (tmp_dir / "Pipfile.lock").write_text(json.dumps(full_data), encoding="utf-8") def test_no_lock_file_returns_none() -> None: @@ -54,6 +63,49 @@ def test_malformed_json_returns_empty_list_and_warns( assert "Failed to parse" in caplog.text +def test_missing_meta_key_returns_none_and_warns( + caplog: pytest.LogCaptureFixture, +) -> None: + """An empty/unrelated-but-parseable JSON object with no top-level + `_meta` key (this repo's `None`-vs-`[]` recurring bug pattern) must + not be treated as "a real, empty Pipfile.lock" -- it could otherwise + silently win the cascade over a genuinely usable lower-priority + lock.""" + with tempfile.TemporaryDirectory() as tmp: + tmp_path = Path(tmp) + _write_lock(tmp_path, {}, include_meta=False) + + with caplog.at_level(logging.WARNING): + result = extract_pipfile_lock_dependencies(tmp_path) + + assert result is None + assert "doesn't look like a genuine Pipfile.lock" in caplog.text + + +@pytest.mark.parametrize( + "meta", + [ + pytest.param({"requires": {}}, id="missing"), + pytest.param({"pipfile-spec": "6"}, id="non-int"), + ], +) +def test_meta_missing_pipfile_spec_returns_none_and_warns( + meta: dict[str, object], caplog: pytest.LogCaptureFixture +) -> None: + """A present but wrong-shaped `pipfile-spec` (e.g. a string instead + of an int) must not pass more easily than the key being absent + entirely.""" + with tempfile.TemporaryDirectory() as tmp: + tmp_path = Path(tmp) + _write_lock(tmp_path, {"_meta": meta}, include_meta=False) + + with caplog.at_level(logging.WARNING): + result = extract_pipfile_lock_dependencies(tmp_path) + + assert result is None + assert "doesn't look like a genuine Pipfile.lock" in caplog.text + + def test_default_section_not_a_dict_returns_empty_list_and_warns( caplog: pytest.LogCaptureFixture, ) -> None: @@ -372,7 +424,8 @@ def test_read_project_pdm_lock_takes_priority_over_pipfile_lock() -> None: '[project]\nname = "demo"\nversion = "1.0.0"\n', encoding="utf-8" ) (tmp_path / "pdm.lock").write_text( - '[[package]]\nname = "httpx"\nversion = "0.27.0"\ngroups = ["default"]\n', + '[[package]]\nname = "httpx"\nversion = "0.27.0"\ngroups = ["default"]\n' + '[metadata]\nlock_version = "4.5.1"\n', encoding="utf-8", ) _write_lock(tmp_path, {"default": {"requests": {"version": "==2.31.0"}}}) diff --git a/tests/extract/test_poetry_lock.py b/tests/extract/test_poetry_lock.py index f7ee169a..57d7f309 100644 --- a/tests/extract/test_poetry_lock.py +++ b/tests/extract/test_poetry_lock.py @@ -92,15 +92,24 @@ def test_missing_metadata_table_returns_none_and_warns( assert "doesn't look like a genuine poetry.lock" in caplog.text +@pytest.mark.parametrize( + "metadata_body", + [ + pytest.param('content-hash = "abc123"\n', id="missing"), + pytest.param("lock-version = 2\n", id="non-string"), + ], +) def test_metadata_table_missing_lock_version_returns_none_and_warns( - caplog: pytest.LogCaptureFixture, + metadata_body: str, caplog: pytest.LogCaptureFixture ) -> None: """A `[metadata]` table present but missing/non-string `lock-version` - is just as ambiguous as no `[metadata]` table at all.""" + is just as ambiguous as no `[metadata]` table at all -- a present but + wrong-shaped value (e.g. an int instead of a string) must not pass + more easily than the key being absent entirely.""" with tempfile.TemporaryDirectory() as tmp: tmp_path = Path(tmp) (tmp_path / "poetry.lock").write_text( - '[metadata]\ncontent-hash = "abc123"\n', encoding="utf-8" + f"[metadata]\n{metadata_body}", encoding="utf-8" ) with caplog.at_level(logging.WARNING): diff --git a/tests/extract/test_project.py b/tests/extract/test_project.py index 825f0075..51a8fd17 100644 --- a/tests/extract/test_project.py +++ b/tests/extract/test_project.py @@ -153,7 +153,8 @@ def test_read_project_fallback_preserves_already_resolved_poetry_lock( "[metadata]\nname = real-pkg\nversion = 1.2.3\n", encoding="utf-8" ) (tmp_path / "pdm.lock").write_text( - '[[package]]\nname = "httpx"\nversion = "0.28.1"\ngroups = ["default"]\n', + '[[package]]\nname = "httpx"\nversion = "0.28.1"\ngroups = ["default"]\n' + '[metadata]\nlock_version = "4.5.1"\n', encoding="utf-8", ) diff --git a/tests/extract/test_pylock.py b/tests/extract/test_pylock.py index 438aa5d0..5d412d52 100644 --- a/tests/extract/test_pylock.py +++ b/tests/extract/test_pylock.py @@ -8,8 +8,11 @@ ``ProjectMetadata.locked_dependencies`` via ``read_project()``'s lock cascade (:mod:`pitloom.extract._locked_dependencies`). -See also: test_poetry_lock.py for the sibling ``poetry.lock`` extractor -this module's tests mirror in shape; test_locked_dependencies.py for the +See also: test_pylock_markers.py for this same module's +``dependency_groups``/``extras`` marker-evaluation tests, split out once +that cluster alone grew past this repo's per-file line-count guidance; +test_poetry_lock.py for the sibling ``poetry.lock`` extractor this +module's tests mirror in shape; test_locked_dependencies.py for the cascade mechanism's own tests (priority ordering, the ``setup.py``-only wiring, the override provenance note). """ @@ -269,226 +272,6 @@ def test_sdist_sourced_package_included() -> None: assert extract_pylock_dependencies(tmp_path) == ["requests==2.31.0"] -def test_non_default_group_package_excluded() -> None: - """Regression: a package needed only for a non-default - dependency-group (e.g. `dev`), tagged via PEP 751's `marker` field - referencing the `dependency_groups` pseudo-environment variable, must - not leak into `locked_dependencies` as an ordinary runtime pin -- - the same "main"/"default"-group-only policy `poetry.lock`/`pdm.lock` - already apply, here expressed as a marker instead of a per-package - field.""" - with tempfile.TemporaryDirectory() as tmp: - tmp_path = Path(tmp) - _write_lock( - tmp_path, - 'default-groups = ["default"]\n' - '[[packages]]\nname = "pytz"\nversion = "2026.1"\n\n' - '[[packages]]\nname = "pytest"\nversion = "8.0.0"\n' - "marker = \"'dev' in dependency_groups\"\n", - ) - - assert extract_pylock_dependencies(tmp_path) == ["pytz==2026.1"] - - -def test_default_group_package_included_alongside_excluded_dev_group() -> None: - """A package whose marker combines a non-default group check with an - ordinary (unevaluated) environment condition is still excluded on the - group check alone -- the 3-valued evaluator doesn't need to know the - real Python version/platform to prove the group clause is false.""" - with tempfile.TemporaryDirectory() as tmp: - tmp_path = Path(tmp) - _write_lock( - tmp_path, - 'default-groups = ["default"]\n' - '[[packages]]\nname = "black"\nversion = "26.1.0"\n' - "marker = \"('dev' in dependency_groups) and " - "(python_version >= '3.10')\"\n", - ) - - assert extract_pylock_dependencies(tmp_path) == [] - - -def test_marker_operator_precedence_and_binds_tighter_than_or() -> None: - """Regression: PEP 508 gives `and` higher precedence than `or`, but - `Marker()._markers` doesn't nest same-precedence terms to reflect - that -- an unparenthesized `A or B and C` is one flat list, not - `[A, "or", [B, "and", C]]`. A naive left-to-right fold over that flat - list would compute `(A or B) and C` instead of the correct - `A or (B and C)`. Here `A` is an unevaluated (unknown) environment - condition, `B` is a *true* group-membership clause (the group IS - active), and `C` is a *false* ordinary condition -- correct PEP 508 - semantics (`A or (B and C)`) is `unknown or (True and False)` = - `unknown or False` = unknown, so the package must still be included - (unknown means "can't prove excluded"). The buggy left-fold instead - computes `(unknown or True) and False` = `True and False` = False, - wrongly excluding it.""" - with tempfile.TemporaryDirectory() as tmp: - tmp_path = Path(tmp) - _write_lock( - tmp_path, - 'default-groups = ["default", "dev"]\n' - '[[packages]]\nname = "precedence-test"\nversion = "1.0.0"\n' - "marker = \"python_version >= '3.99' or " - "'dev' in dependency_groups and python_version < '2.0'\"\n", - ) - - assert extract_pylock_dependencies(tmp_path) == ["precedence-test==1.0.0"] - - -def test_marker_operator_precedence_still_excludes_when_no_or_clause_is_true() -> None: - """The precedence fix must not become "always include": when every - `or`-separated group provably evaluates `False` from the known - group/extras clauses alone (no unknown clause anywhere), the package - is still excluded.""" - with tempfile.TemporaryDirectory() as tmp: - tmp_path = Path(tmp) - _write_lock( - tmp_path, - 'default-groups = ["default"]\n' - '[[packages]]\nname = "still-excluded"\nversion = "1.0.0"\n' - "marker = \"'dev' in dependency_groups or " - "'test' in dependency_groups\"\n", - ) - - assert extract_pylock_dependencies(tmp_path) == [] - - -def test_extras_gated_package_excluded_by_default() -> None: - """PEP 751 supports both single-use lockfiles (one fixed purpose, no - group/extras complexity -- every package is simply included, as the - other tests in this file already cover via `default-groups`) and - multi-use lockfiles, which bundle multiple installable - configurations into one file via per-package `marker` clauses on - *both* pseudo-environment variables PEP 751 defines for this: - `dependency_groups` (covered above) and `extras`. A package gated on - `'' in extras` must be excluded from the default resolved set - the same way one gated on `dependency_groups` is -- Pitloom's SBOM - represents the base/default install with no extras requested, - consistent with every sibling format excluding optional-dependencies/ - extras from its own default resolved set (`poetry.lock`'s `main`-only - group, `uv.lock`'s runtime-only `dependencies`, `pdm.lock`'s - `default`-only group).""" - with tempfile.TemporaryDirectory() as tmp: - tmp_path = Path(tmp) - _write_lock( - tmp_path, - 'extras = ["security"]\n' - '[[packages]]\nname = "requests"\nversion = "2.31.0"\n\n' - '[[packages]]\nname = "pyopenssl"\nversion = "24.0.0"\n' - "marker = \"'security' in extras\"\n", - ) - - assert extract_pylock_dependencies(tmp_path) == ["requests==2.31.0"] - - -def test_package_with_no_marker_included_regardless_of_default_groups() -> None: - """A package with no `marker` field at all is an ordinary, - always-active runtime dependency -- unaffected by `default-groups` - filtering.""" - with tempfile.TemporaryDirectory() as tmp: - tmp_path = Path(tmp) - _write_lock( - tmp_path, - 'default-groups = []\n[[packages]]\nname = "pytz"\nversion = "2026.1"\n', - ) - - assert extract_pylock_dependencies(tmp_path) == ["pytz==2026.1"] - - -def test_default_groups_not_a_list_warns_and_treated_as_empty( - caplog: pytest.LogCaptureFixture, -) -> None: - with tempfile.TemporaryDirectory() as tmp: - tmp_path = Path(tmp) - _write_lock( - tmp_path, - 'default-groups = "default"\n' - '[[packages]]\nname = "pytest"\nversion = "8.0.0"\n' - "marker = \"'default' in dependency_groups\"\n", - ) - - with caplog.at_level(logging.WARNING): - result = extract_pylock_dependencies(tmp_path) - - assert result == [] - assert "'default-groups'" in caplog.text - - -def test_or_combined_group_clauses_evaluated() -> None: - """The `or` branch of the 3-valued combiner - (`_combine_group_results`) is exercised alongside the `and` branch - tested above -- a package needed for *either* of two non-default - groups is still excluded when neither is active.""" - with tempfile.TemporaryDirectory() as tmp: - tmp_path = Path(tmp) - _write_lock( - tmp_path, - 'default-groups = ["default"]\n' - '[[packages]]\nname = "black"\nversion = "26.1.0"\n' - "marker = \"'dev' in dependency_groups or 'test' in dependency_groups\"\n", - ) - - assert extract_pylock_dependencies(tmp_path) == [] - - -def test_or_combined_group_clauses_true_when_one_group_active() -> None: - """The `or` combiner's `True` result (at least one side proven - true) alongside the `False` case tested above -- a package needed - for either of two groups is included once one of them is active.""" - with tempfile.TemporaryDirectory() as tmp: - tmp_path = Path(tmp) - _write_lock( - tmp_path, - 'default-groups = ["default", "test"]\n' - '[[packages]]\nname = "black"\nversion = "26.1.0"\n' - "marker = \"'dev' in dependency_groups or 'test' in dependency_groups\"\n", - ) - - assert extract_pylock_dependencies(tmp_path) == ["black==26.1.0"] - - -def test_reversed_operand_group_clause_evaluated() -> None: - """PEP 751 always writes the group/extras variable on the *right* of - `in` (e.g. `"'dev' in dependency_groups"`) in real output, but PEP - 508 grammar allows either operand order -- `_evaluate_group_leaf`'s - `elif lhs_str in _GROUP_MARKER_VARIABLES` branch (variable on the - left) must still be reachable and correct, not just the more common - literal-on-left form tested elsewhere.""" - with tempfile.TemporaryDirectory() as tmp: - tmp_path = Path(tmp) - _write_lock( - tmp_path, - 'default-groups = ["dev"]\n' - '[[packages]]\nname = "black"\nversion = "26.1.0"\n' - "marker = \"dependency_groups in 'dev'\"\n", - ) - - assert extract_pylock_dependencies(tmp_path) == ["black==26.1.0"] - - -def test_malformed_marker_string_included_and_warns( - caplog: pytest.LogCaptureFixture, -) -> None: - """An unparseable `marker` string can't prove group membership either - way -- treated as the same marker-blind "include" default every - other non-group marker gets, but with a `WARNING:` (not a crash, not - a silent unconditional include) rather than raising `InvalidMarker` - out of the extractor.""" - with tempfile.TemporaryDirectory() as tmp: - tmp_path = Path(tmp) - _write_lock( - tmp_path, - '[[packages]]\nname = "broken"\nversion = "1.0.0"\n' - 'marker = "not a valid marker (("\n', - ) - - with caplog.at_level(logging.WARNING): - result = extract_pylock_dependencies(tmp_path) - - assert result == ["broken==1.0.0"] - assert "'marker'" in caplog.text - - def test_read_project_populates_locked_dependencies() -> None: """Integration: `read_project()`'s lock cascade overlays `pylock.toml` parsing onto `ProjectMetadata.locked_dependencies` with its own diff --git a/tests/extract/test_pylock_markers.py b/tests/extract/test_pylock_markers.py new file mode 100644 index 00000000..adfc1a21 --- /dev/null +++ b/tests/extract/test_pylock_markers.py @@ -0,0 +1,249 @@ +# SPDX-FileContributor: Arthit Suriyawongkul +# SPDX-FileCopyrightText: 2026-present Arthit Suriyawongkul +# SPDX-FileType: SOURCE +# SPDX-License-Identifier: Apache-2.0 + +"""Tests for PEP 751 ``pylock.toml``'s ``dependency_groups``/``extras`` +marker evaluation (:mod:`pitloom.extract._pylock`'s group/marker +filtering, including the 3-valued PEP 508 precedence evaluator). + +Split out of test_pylock.py (which covers this same module's basic +per-package parsing/validation and its ``read_project()`` cascade +integration) once the marker-evaluation cluster alone grew past this +repo's own per-file line-count guidance -- see test_pylock.py's own +module docstring for the sibling-file map. +""" + +import logging +import tempfile +from pathlib import Path + +import pytest + +from pitloom.extract._pylock import extract_pylock_dependencies + +_LOCK_VERSION = 'lock-version = "1.0"\ncreated-by = "test"\n' + + +def _write_lock(tmp_dir: Path, packages: str = "") -> None: + (tmp_dir / "pylock.toml").write_text(_LOCK_VERSION + packages, encoding="utf-8") + + +def test_non_default_group_package_excluded() -> None: + """Regression: a package needed only for a non-default + dependency-group (e.g. `dev`), tagged via PEP 751's `marker` field + referencing the `dependency_groups` pseudo-environment variable, must + not leak into `locked_dependencies` as an ordinary runtime pin -- + the same "main"/"default"-group-only policy `poetry.lock`/`pdm.lock` + already apply, here expressed as a marker instead of a per-package + field.""" + with tempfile.TemporaryDirectory() as tmp: + tmp_path = Path(tmp) + _write_lock( + tmp_path, + 'default-groups = ["default"]\n' + '[[packages]]\nname = "pytz"\nversion = "2026.1"\n\n' + '[[packages]]\nname = "pytest"\nversion = "8.0.0"\n' + "marker = \"'dev' in dependency_groups\"\n", + ) + + assert extract_pylock_dependencies(tmp_path) == ["pytz==2026.1"] + + +def test_default_group_package_included_alongside_excluded_dev_group() -> None: + """A package whose marker combines a non-default group check with an + ordinary (unevaluated) environment condition is still excluded on the + group check alone -- the 3-valued evaluator doesn't need to know the + real Python version/platform to prove the group clause is false.""" + with tempfile.TemporaryDirectory() as tmp: + tmp_path = Path(tmp) + _write_lock( + tmp_path, + 'default-groups = ["default"]\n' + '[[packages]]\nname = "black"\nversion = "26.1.0"\n' + "marker = \"('dev' in dependency_groups) and " + "(python_version >= '3.10')\"\n", + ) + + assert extract_pylock_dependencies(tmp_path) == [] + + +def test_marker_operator_precedence_and_binds_tighter_than_or() -> None: + """Regression: PEP 508 gives `and` higher precedence than `or`, but + `Marker()._markers` doesn't nest same-precedence terms to reflect + that -- an unparenthesized `A or B and C` is one flat list, not + `[A, "or", [B, "and", C]]`. A naive left-to-right fold over that flat + list would compute `(A or B) and C` instead of the correct + `A or (B and C)`. Here `A` is an unevaluated (unknown) environment + condition, `B` is a *true* group-membership clause (the group IS + active), and `C` is a *false* ordinary condition -- correct PEP 508 + semantics (`A or (B and C)`) is `unknown or (True and False)` = + `unknown or False` = unknown, so the package must still be included + (unknown means "can't prove excluded"). The buggy left-fold instead + computes `(unknown or True) and False` = `True and False` = False, + wrongly excluding it.""" + with tempfile.TemporaryDirectory() as tmp: + tmp_path = Path(tmp) + _write_lock( + tmp_path, + 'default-groups = ["default", "dev"]\n' + '[[packages]]\nname = "precedence-test"\nversion = "1.0.0"\n' + "marker = \"python_version >= '3.99' or " + "'dev' in dependency_groups and python_version < '2.0'\"\n", + ) + + assert extract_pylock_dependencies(tmp_path) == ["precedence-test==1.0.0"] + + +def test_marker_operator_precedence_still_excludes_when_no_or_clause_is_true() -> None: + """The precedence fix must not become "always include": when every + `or`-separated group provably evaluates `False` from the known + group/extras clauses alone (no unknown clause anywhere), the package + is still excluded.""" + with tempfile.TemporaryDirectory() as tmp: + tmp_path = Path(tmp) + _write_lock( + tmp_path, + 'default-groups = ["default"]\n' + '[[packages]]\nname = "still-excluded"\nversion = "1.0.0"\n' + "marker = \"'dev' in dependency_groups or " + "'test' in dependency_groups\"\n", + ) + + assert extract_pylock_dependencies(tmp_path) == [] + + +def test_extras_gated_package_excluded_by_default() -> None: + """PEP 751 supports both single-use lockfiles (one fixed purpose, no + group/extras complexity -- every package is simply included, as the + other tests in this file already cover via `default-groups`) and + multi-use lockfiles, which bundle multiple installable + configurations into one file via per-package `marker` clauses on + *both* pseudo-environment variables PEP 751 defines for this: + `dependency_groups` (covered above) and `extras`. A package gated on + `'' in extras` must be excluded from the default resolved set + the same way one gated on `dependency_groups` is -- Pitloom's SBOM + represents the base/default install with no extras requested, + consistent with every sibling format excluding optional-dependencies/ + extras from its own default resolved set (`poetry.lock`'s `main`-only + group, `uv.lock`'s runtime-only `dependencies`, `pdm.lock`'s + `default`-only group).""" + with tempfile.TemporaryDirectory() as tmp: + tmp_path = Path(tmp) + _write_lock( + tmp_path, + 'extras = ["security"]\n' + '[[packages]]\nname = "requests"\nversion = "2.31.0"\n\n' + '[[packages]]\nname = "pyopenssl"\nversion = "24.0.0"\n' + "marker = \"'security' in extras\"\n", + ) + + assert extract_pylock_dependencies(tmp_path) == ["requests==2.31.0"] + + +def test_package_with_no_marker_included_regardless_of_default_groups() -> None: + """A package with no `marker` field at all is an ordinary, + always-active runtime dependency -- unaffected by `default-groups` + filtering.""" + with tempfile.TemporaryDirectory() as tmp: + tmp_path = Path(tmp) + _write_lock( + tmp_path, + 'default-groups = []\n[[packages]]\nname = "pytz"\nversion = "2026.1"\n', + ) + + assert extract_pylock_dependencies(tmp_path) == ["pytz==2026.1"] + + +def test_default_groups_not_a_list_warns_and_treated_as_empty( + caplog: pytest.LogCaptureFixture, +) -> None: + with tempfile.TemporaryDirectory() as tmp: + tmp_path = Path(tmp) + _write_lock( + tmp_path, + 'default-groups = "default"\n' + '[[packages]]\nname = "pytest"\nversion = "8.0.0"\n' + "marker = \"'default' in dependency_groups\"\n", + ) + + with caplog.at_level(logging.WARNING): + result = extract_pylock_dependencies(tmp_path) + + assert result == [] + assert "'default-groups'" in caplog.text + + +def test_or_combined_group_clauses_evaluated() -> None: + """The `or` branch of the 3-valued combiner + (`_combine_group_results`) is exercised alongside the `and` branch + tested above -- a package needed for *either* of two non-default + groups is still excluded when neither is active.""" + with tempfile.TemporaryDirectory() as tmp: + tmp_path = Path(tmp) + _write_lock( + tmp_path, + 'default-groups = ["default"]\n' + '[[packages]]\nname = "black"\nversion = "26.1.0"\n' + "marker = \"'dev' in dependency_groups or 'test' in dependency_groups\"\n", + ) + + assert extract_pylock_dependencies(tmp_path) == [] + + +def test_or_combined_group_clauses_true_when_one_group_active() -> None: + """The `or` combiner's `True` result (at least one side proven + true) alongside the `False` case tested above -- a package needed + for either of two groups is included once one of them is active.""" + with tempfile.TemporaryDirectory() as tmp: + tmp_path = Path(tmp) + _write_lock( + tmp_path, + 'default-groups = ["default", "test"]\n' + '[[packages]]\nname = "black"\nversion = "26.1.0"\n' + "marker = \"'dev' in dependency_groups or 'test' in dependency_groups\"\n", + ) + + assert extract_pylock_dependencies(tmp_path) == ["black==26.1.0"] + + +def test_reversed_operand_group_clause_evaluated() -> None: + """PEP 751 always writes the group/extras variable on the *right* of + `in` (e.g. `"'dev' in dependency_groups"`) in real output, but PEP + 508 grammar allows either operand order -- `_evaluate_group_leaf`'s + `elif lhs_str in _GROUP_MARKER_VARIABLES` branch (variable on the + left) must still be reachable and correct, not just the more common + literal-on-left form tested elsewhere.""" + with tempfile.TemporaryDirectory() as tmp: + tmp_path = Path(tmp) + _write_lock( + tmp_path, + 'default-groups = ["dev"]\n' + '[[packages]]\nname = "black"\nversion = "26.1.0"\n' + "marker = \"dependency_groups in 'dev'\"\n", + ) + + assert extract_pylock_dependencies(tmp_path) == ["black==26.1.0"] + + +def test_malformed_marker_string_included_and_warns( + caplog: pytest.LogCaptureFixture, +) -> None: + """An unparseable `marker` string can't prove group membership either + way -- treated as the same marker-blind "include" default every + other non-group marker gets, but with a `WARNING:` (not a crash, not + a silent unconditional include) rather than raising `InvalidMarker` + out of the extractor.""" + with tempfile.TemporaryDirectory() as tmp: + tmp_path = Path(tmp) + _write_lock( + tmp_path, + '[[packages]]\nname = "broken"\nversion = "1.0.0"\n' + 'marker = "not a valid marker (("\n', + ) + + with caplog.at_level(logging.WARNING): + result = extract_pylock_dependencies(tmp_path) + + assert result == ["broken==1.0.0"] + assert "'marker'" in caplog.text diff --git a/tests/extract/test_requirements_txt.py b/tests/extract/test_requirements_txt.py index a2c758d7..f4550595 100644 --- a/tests/extract/test_requirements_txt.py +++ b/tests/extract/test_requirements_txt.py @@ -433,7 +433,8 @@ def test_read_project_pipfile_lock_takes_priority_over_requirements_txt() -> Non encoding="utf-8", ) (tmp_path / "Pipfile.lock").write_text( - '{"default": {"httpx": {"version": "==0.27.0"}}}', + '{"_meta": {"pipfile-spec": 6}, ' + '"default": {"httpx": {"version": "==0.27.0"}}}', encoding="utf-8", ) _write_requirements(tmp_path, "requests==2.31.0\n") diff --git a/tests/extract/test_uv_lock.py b/tests/extract/test_uv_lock.py index b8a2187e..f2134340 100644 --- a/tests/extract/test_uv_lock.py +++ b/tests/extract/test_uv_lock.py @@ -8,10 +8,13 @@ malformed/missing input handling, dependency-reference resolution, and the marker-ambiguity skip policy. -See also: test_uv_lock_root_package.py (root/workspace-member package -selection), test_uv_lock_integration.py (``read_project()`` cascade -wiring and real-world fixtures), test_pylock.py/test_poetry_lock.py for -the sibling lock extractors this module's tests mirror in shape, and +See also: test_uv_lock_transitive.py (the transitive-dependency walk +over a resolved package's own nested ``dependencies``, split out once +that cluster alone grew past this repo's per-file line-count guidance), +test_uv_lock_root_package.py (root/workspace-member package selection), +test_uv_lock_integration.py (``read_project()`` cascade wiring and +real-world fixtures), test_pylock.py/test_poetry_lock.py for the +sibling lock extractors this module's tests mirror in shape, and test_locked_dependencies.py for the cascade mechanism's own tests. """ @@ -387,120 +390,3 @@ def test_dependency_missing_version_skipped_and_warns( assert not result assert "missing" in caplog.text.lower() - - -def test_transitive_dependency_of_a_direct_dependency_is_included() -> None: - """The root's own `dependencies` list is only the first layer -- a - package it depends on can itself have further dependencies, and - those must be walked too, not just the root's immediate list.""" - with tempfile.TemporaryDirectory() as tmp: - tmp_path = Path(tmp) - _write_lock( - tmp_path, - _ROOT_HEADER + 'dependencies = [{ name = "requests" }]\n\n' - '[[package]]\nname = "requests"\nversion = "2.31.0"\n' - 'source = { registry = "https://pypi.org/simple" }\n' - 'dependencies = [{ name = "urllib3" }, { name = "certifi" }]\n\n' - '[[package]]\nname = "urllib3"\nversion = "2.2.0"\n' - 'source = { registry = "https://pypi.org/simple" }\n\n' - '[[package]]\nname = "certifi"\nversion = "2024.2.2"\n' - 'source = { registry = "https://pypi.org/simple" }\n', - ) - - result = extract_uv_lock_dependencies(tmp_path) - - assert result is not None - assert set(result) == { - "requests==2.31.0", - "urllib3==2.2.0", - "certifi==2024.2.2", - } - - -def test_diamond_dependency_visited_only_once() -> None: - """Two of the root's direct dependencies sharing a common transitive - dependency must not cause that shared package to be processed (or - emitted) twice.""" - with tempfile.TemporaryDirectory() as tmp: - tmp_path = Path(tmp) - _write_lock( - tmp_path, - _ROOT_HEADER + 'dependencies = [{ name = "pkg-a" }, { name = "pkg-b" }]\n\n' - '[[package]]\nname = "pkg-a"\nversion = "1.0.0"\n' - 'source = { registry = "https://pypi.org/simple" }\n' - 'dependencies = [{ name = "shared" }]\n\n' - '[[package]]\nname = "pkg-b"\nversion = "1.0.0"\n' - 'source = { registry = "https://pypi.org/simple" }\n' - 'dependencies = [{ name = "shared" }]\n\n' - '[[package]]\nname = "shared"\nversion = "0.1.0"\n' - 'source = { registry = "https://pypi.org/simple" }\n', - ) - - result = extract_uv_lock_dependencies(tmp_path) - - assert result is not None - assert result.count("shared==0.1.0") == 1 - - -def test_nested_dependencies_not_a_list_skipped_and_warns( - caplog: pytest.LogCaptureFixture, -) -> None: - """A resolved package's own `dependencies` key, if present at all, - must be a list -- a malformed non-list value (but still truthy, so - distinct from a missing key) is warned and simply not walked into - further, not treated as a parse error for the whole file.""" - with tempfile.TemporaryDirectory() as tmp: - tmp_path = Path(tmp) - _write_lock( - tmp_path, - _ROOT_HEADER + 'dependencies = [{ name = "requests" }]\n\n' - '[[package]]\nname = "requests"\nversion = "2.31.0"\n' - 'source = { registry = "https://pypi.org/simple" }\n' - 'dependencies = "not-a-list"\n', - ) - - with caplog.at_level(logging.WARNING): - result = extract_uv_lock_dependencies(tmp_path) - - assert result == ["requests==2.31.0"] - assert "nested 'dependencies'" in caplog.text - - -def test_nested_dependencies_falsy_non_list_silently_skipped( - caplog: pytest.LogCaptureFixture, -) -> None: - """A falsy non-list `dependencies` value (e.g. `false` -- TOML has - no `null`, so this is the practical malformed-but-empty shape) - behaves like a missing/empty key, not like the truthy-malformed case - above -- no `WARNING:`, and nothing to walk into, but the package's - own pin is still resolved.""" - with tempfile.TemporaryDirectory() as tmp: - tmp_path = Path(tmp) - _write_lock( - tmp_path, - _ROOT_HEADER + 'dependencies = [{ name = "requests" }]\n\n' - '[[package]]\nname = "requests"\nversion = "2.31.0"\n' - 'source = { registry = "https://pypi.org/simple" }\n' - "dependencies = false\n", - ) - - with caplog.at_level(logging.WARNING): - result = extract_uv_lock_dependencies(tmp_path) - - assert result == ["requests==2.31.0"] - assert "nested 'dependencies'" not in caplog.text - - -def test_dependency_with_no_source_table_still_included() -> None: - """A package entry with no `source` key at all (unusual but not - invalid) is treated the same as a registry source -- only an - explicit non-registry key excludes it.""" - with tempfile.TemporaryDirectory() as tmp: - tmp_path = Path(tmp) - _write_lock( - tmp_path, - _ROOT_HEADER + 'dependencies = [{ name = "no-source" }]\n\n' - '[[package]]\nname = "no-source"\nversion = "1.2.3"\n', - ) - - assert extract_uv_lock_dependencies(tmp_path) == ["no-source==1.2.3"] diff --git a/tests/extract/test_uv_lock_transitive.py b/tests/extract/test_uv_lock_transitive.py new file mode 100644 index 00000000..93e2b26f --- /dev/null +++ b/tests/extract/test_uv_lock_transitive.py @@ -0,0 +1,152 @@ +# SPDX-FileContributor: Arthit Suriyawongkul +# SPDX-FileCopyrightText: 2026-present Arthit Suriyawongkul +# SPDX-FileType: SOURCE +# SPDX-License-Identifier: Apache-2.0 + +"""Tests for ``uv.lock``'s transitive-dependency walk +(:mod:`pitloom.extract._uv_lock`)'s BFS/DFS over a resolved package's own +nested ``dependencies`` list, starting from the root package. + +Split out of test_uv_lock.py (which covers this same module's basic +malformed-input handling and direct dependency-reference resolution) +once the transitive-walk cluster alone grew past this repo's own +per-file line-count guidance -- see test_uv_lock.py's own module +docstring for the sibling-file map. +""" + +import logging +import tempfile +from pathlib import Path + +import pytest + +from pitloom.extract._uv_lock import extract_uv_lock_dependencies + +_LOCK_HEADER = 'version = 1\nrevision = 1\nrequires-python = ">=3.10"\n' + +#: A minimal root/project package entry -- every test that needs one +#: root dependency composes this with its own `dependencies` block. +_ROOT_HEADER = ( + '[[package]]\nname = "demo"\nversion = "1.0.0"\nsource = { editable = "." }\n' +) + + +def _write_lock(tmp_dir: Path, body: str = "") -> None: + (tmp_dir / "uv.lock").write_text(_LOCK_HEADER + body, encoding="utf-8") + + +def test_transitive_dependency_of_a_direct_dependency_is_included() -> None: + """The root's own `dependencies` list is only the first layer -- a + package it depends on can itself have further dependencies, and + those must be walked too, not just the root's immediate list.""" + with tempfile.TemporaryDirectory() as tmp: + tmp_path = Path(tmp) + _write_lock( + tmp_path, + _ROOT_HEADER + 'dependencies = [{ name = "requests" }]\n\n' + '[[package]]\nname = "requests"\nversion = "2.31.0"\n' + 'source = { registry = "https://pypi.org/simple" }\n' + 'dependencies = [{ name = "urllib3" }, { name = "certifi" }]\n\n' + '[[package]]\nname = "urllib3"\nversion = "2.2.0"\n' + 'source = { registry = "https://pypi.org/simple" }\n\n' + '[[package]]\nname = "certifi"\nversion = "2024.2.2"\n' + 'source = { registry = "https://pypi.org/simple" }\n', + ) + + result = extract_uv_lock_dependencies(tmp_path) + + assert result is not None + assert set(result) == { + "requests==2.31.0", + "urllib3==2.2.0", + "certifi==2024.2.2", + } + + +def test_diamond_dependency_visited_only_once() -> None: + """Two of the root's direct dependencies sharing a common transitive + dependency must not cause that shared package to be processed (or + emitted) twice.""" + with tempfile.TemporaryDirectory() as tmp: + tmp_path = Path(tmp) + _write_lock( + tmp_path, + _ROOT_HEADER + 'dependencies = [{ name = "pkg-a" }, { name = "pkg-b" }]\n\n' + '[[package]]\nname = "pkg-a"\nversion = "1.0.0"\n' + 'source = { registry = "https://pypi.org/simple" }\n' + 'dependencies = [{ name = "shared" }]\n\n' + '[[package]]\nname = "pkg-b"\nversion = "1.0.0"\n' + 'source = { registry = "https://pypi.org/simple" }\n' + 'dependencies = [{ name = "shared" }]\n\n' + '[[package]]\nname = "shared"\nversion = "0.1.0"\n' + 'source = { registry = "https://pypi.org/simple" }\n', + ) + + result = extract_uv_lock_dependencies(tmp_path) + + assert result is not None + assert result.count("shared==0.1.0") == 1 + + +def test_nested_dependencies_not_a_list_skipped_and_warns( + caplog: pytest.LogCaptureFixture, +) -> None: + """A resolved package's own `dependencies` key, if present at all, + must be a list -- a malformed non-list value (but still truthy, so + distinct from a missing key) is warned and simply not walked into + further, not treated as a parse error for the whole file.""" + with tempfile.TemporaryDirectory() as tmp: + tmp_path = Path(tmp) + _write_lock( + tmp_path, + _ROOT_HEADER + 'dependencies = [{ name = "requests" }]\n\n' + '[[package]]\nname = "requests"\nversion = "2.31.0"\n' + 'source = { registry = "https://pypi.org/simple" }\n' + 'dependencies = "not-a-list"\n', + ) + + with caplog.at_level(logging.WARNING): + result = extract_uv_lock_dependencies(tmp_path) + + assert result == ["requests==2.31.0"] + assert "nested 'dependencies'" in caplog.text + + +def test_nested_dependencies_falsy_non_list_silently_skipped( + caplog: pytest.LogCaptureFixture, +) -> None: + """A falsy non-list `dependencies` value (e.g. `false` -- TOML has + no `null`, so this is the practical malformed-but-empty shape) + behaves like a missing/empty key, not like the truthy-malformed case + above -- no `WARNING:`, and nothing to walk into, but the package's + own pin is still resolved.""" + with tempfile.TemporaryDirectory() as tmp: + tmp_path = Path(tmp) + _write_lock( + tmp_path, + _ROOT_HEADER + 'dependencies = [{ name = "requests" }]\n\n' + '[[package]]\nname = "requests"\nversion = "2.31.0"\n' + 'source = { registry = "https://pypi.org/simple" }\n' + "dependencies = false\n", + ) + + with caplog.at_level(logging.WARNING): + result = extract_uv_lock_dependencies(tmp_path) + + assert result == ["requests==2.31.0"] + assert "nested 'dependencies'" not in caplog.text + + +def test_dependency_with_no_source_table_still_included() -> None: + """A package entry with no `source` key at all (unusual but not + invalid) is treated the same as a registry source -- only an + explicit non-registry key excludes it.""" + with tempfile.TemporaryDirectory() as tmp: + tmp_path = Path(tmp) + _write_lock( + tmp_path, + _ROOT_HEADER + 'dependencies = [{ name = "no-source" }]\n\n' + '[[package]]\nname = "no-source"\nversion = "1.2.3"\n', + ) + + assert extract_uv_lock_dependencies(tmp_path) == ["no-source==1.2.3"] From 086ac8eab7324686adabc25fb10c4369d6409f0f Mon Sep 17 00:00:00 2001 From: Arthit Suriyawongkul Date: Sat, 5 Sep 2026 23:47:33 +0700 Subject: [PATCH 15/35] Fix grammar Signed-off-by: Arthit Suriyawongkul --- src/pitloom/core/models.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/pitloom/core/models.py b/src/pitloom/core/models.py index 480c2a67..03a2f4e6 100644 --- a/src/pitloom/core/models.py +++ b/src/pitloom/core/models.py @@ -117,7 +117,7 @@ def compute_doc_uuid( *locked_dependencies_provenance* is given -- otherwise two documents with identical direct dependencies but different lock-resolved graphs would collide on the same UUID despite describing different - dependency content. Both omitted leaves the seed byte-identical to a + dependency content. Both omitted leave the seed byte-identical to a document with no locked dependencies at all, so every non-Poetry (and lock-less Poetry) document is unaffected. From 6e4d3ee6941b599c6a12ae30cc12a1723bd9eab1 Mon Sep 17 00:00:00 2001 From: Arthit Suriyawongkul Date: Sun, 6 Sep 2026 00:19:20 +0700 Subject: [PATCH 16/35] Dedup Signed-off-by: Arthit Suriyawongkul --- src/pitloom/assemble/spdx3/document.py | 21 ++- src/pitloom/extract/_lock_common.py | 22 ++- src/pitloom/extract/_locked_dependencies.py | 6 +- src/pitloom/extract/_pipfile_lock.py | 44 ++++- src/pitloom/extract/_pylock.py | 63 ++++--- src/pitloom/extract/_uv_lock.py | 74 ++++---- .../assemble/test_deps_locked_dependencies.py | 21 ++- tests/extract/test_lock_common.py | 174 ++++++++++++++++++ tests/extract/test_pipfile_lock.py | 48 +++++ tests/extract/test_pylock.py | 55 +++++- tests/extract/test_uv_lock.py | 2 +- tests/extract/test_uv_lock_root_package.py | 31 ++-- 12 files changed, 452 insertions(+), 109 deletions(-) diff --git a/src/pitloom/assemble/spdx3/document.py b/src/pitloom/assemble/spdx3/document.py index df791f9d..df44125e 100644 --- a/src/pitloom/assemble/spdx3/document.py +++ b/src/pitloom/assemble/spdx3/document.py @@ -191,19 +191,22 @@ def _locked_dependencies_completeness(metadata: ProjectMetadata) -> str | None: A real resolver lock (`poetry.lock`, `pylock.toml`, `uv.lock`, `pdm.lock`, `Pipfile.lock` -- every cascade entry tagged `Method: resolved_lockfile`) genuinely proves the full transitive - dependency closure, so its edges are marked `complete`. Pinned - `requirements.txt` (tagged `Method: pinned_requirements`) is - different: it's just a list of exact-pin lines a human or `pip + dependency closure, so its edges are marked `complete`. Every other + case -- pinned `requirements.txt` (tagged `Method: + pinned_requirements`, just a list of exact-pin lines a human or `pip freeze` wrote, with no resolver guarantee that every real transitive - dependency is actually present -- marking those edges `complete` - would overstate what the file actually proves, so this returns - `None` (unset) for that one source instead. + dependency is actually present), an unrecognized future `Method` tag, + or no provenance recorded at all -- returns `None` (unset) instead: + an inclusion check (only the one tag known to prove completeness + claims it) rather than an exclusion check, so a future lock source + that forgets to record its own `Method` tag fails safe to "unset" + rather than silently defaulting to overstating completeness. """ provenance = metadata.provenance.get("locked_dependencies") method = parse_provenance_value(provenance).get("method") if provenance else None - if method is not None and method != _RESOLVED_LOCKFILE_METHOD: - return None - return spdx3.RelationshipCompleteness.complete + if method == _RESOLVED_LOCKFILE_METHOD: + return spdx3.RelationshipCompleteness.complete + return None def _prefetch_combined_release_info( diff --git a/src/pitloom/extract/_lock_common.py b/src/pitloom/extract/_lock_common.py index 62a42de2..11ba0cd7 100644 --- a/src/pitloom/extract/_lock_common.py +++ b/src/pitloom/extract/_lock_common.py @@ -249,14 +249,22 @@ def group_versions_by_canonical_name( return by_canonical +#: PEP 440 operators that pin to exactly one release: ``==`` (the +#: ordinary case) and ``===`` (arbitrary-equality, for a legacy/ +#: non-normalizable version string a resolver would otherwise reject -- +#: rare in practice, but just as exact a pin as ``==`` once present). +_EXACT_PIN_OPERATORS = frozenset({"==", "==="}) + + def single_exact_pin(specifier_set: SpecifierSet) -> str | None: """Return the bare version when *specifier_set* contains exactly one - non-wildcard ``==`` specifier (e.g. ``SpecifierSet("==2.31.0")`` -> - ``"2.31.0"``), or ``None`` for anything looser than one exact pin -- - a range, more than one specifier, or a prefix-match wildcard like - ``"==2.31.*"`` (``packaging.specifiers.Specifier`` reports that as - operator ``"=="`` too, but it pins a *range* of versions, not one - exact release). + non-wildcard exact-pin specifier (``==`` or PEP 440's arbitrary- + equality ``===``, e.g. ``SpecifierSet("==2.31.0")`` -> ``"2.31.0"``), + or ``None`` for anything looser than one exact pin -- a range, more + than one specifier, or a prefix-match wildcard like ``"==2.31.*"`` + (``packaging.specifiers.Specifier`` reports that as operator ``"=="`` + too, but it pins a *range* of versions, not one exact release -- + ``===`` has no wildcard form, so this check only matters for ``==``). Doesn't itself construct *specifier_set* from a raw string -- :mod:`pitloom.extract._pipfile_lock` and @@ -270,7 +278,7 @@ def single_exact_pin(specifier_set: SpecifierSet) -> str | None: specifiers = list(specifier_set) if ( len(specifiers) != 1 - or specifiers[0].operator != "==" + or specifiers[0].operator not in _EXACT_PIN_OPERATORS or "*" in specifiers[0].version ): return None diff --git a/src/pitloom/extract/_locked_dependencies.py b/src/pitloom/extract/_locked_dependencies.py index 483e0228..27a9644c 100644 --- a/src/pitloom/extract/_locked_dependencies.py +++ b/src/pitloom/extract/_locked_dependencies.py @@ -109,8 +109,10 @@ def apply_locked_dependencies(metadata: ProjectMetadata, project_dir: Path) -> N dependencies onto *metadata*, in place. Tries each extractor-bearing entry of :data:`_LOCK_SOURCES` in - priority order; the first one that yields a non-empty result wins. - Crucially, this respects *every* source's rank, not just the ones + priority order; the first one that yields a result at all (a + ``list[str]``, even an empty one -- see the "no silent deviations" + paragraph below) wins. Crucially, this respects *every* source's + rank, not just the ones this cascade itself tries: once the already-applied source (e.g. ``poetry.lock``, applied earlier by ``_try_read_poetry()``) outranks every remaining untried entry, the loop stops -- a lower-priority diff --git a/src/pitloom/extract/_pipfile_lock.py b/src/pitloom/extract/_pipfile_lock.py index 2417c908..920b8f1b 100644 --- a/src/pitloom/extract/_pipfile_lock.py +++ b/src/pitloom/extract/_pipfile_lock.py @@ -45,6 +45,7 @@ from pitloom.extract._lock_common import ( find_first_present_key, + group_versions_by_canonical_name, has_required_top_level_table, load_lock_json, single_exact_pin, @@ -96,18 +97,45 @@ def extract_pipfile_lock_dependencies(project_dir: Path) -> list[str] | None: ) return None + pairs = [ + pair + for pair in ( + _pinned_pair_for_package(name, entry) + for name, entry in default_section.items() + ) + if pair is not None + ] + dependencies: list[str] = [] - for name, entry in default_section.items(): - dep = _pinned_dep_for_package(name, entry) - if dep is not None: - dependencies.append(dep) + for group in group_versions_by_canonical_name(pairs).values(): + name, version = group[0] + conflicting_versions = {v for _, v in group} + if len(conflicting_versions) > 1: + log.warning( + "Skipping Pipfile.lock entry %r: pinned to conflicting versions (%s)", + name, + ", ".join(sorted(conflicting_versions)), + ) + continue + dependencies.append(f"{name}=={version}") return dependencies -def _pinned_dep_for_package(name: object, entry: object) -> str | None: - """Return ``name==version`` for one ``"default"``-section entry, or +def _pinned_pair_for_package(name: object, entry: object) -> tuple[str, str] | None: + """Return ``(name, version)`` for one ``"default"``-section entry, or ``None`` when it's malformed, non-registry-sourced, or its - ``version`` isn't a single exact ``==`` specifier.""" + ``version`` isn't a single exact ``==`` specifier. + + Returning the raw pair (not the formatted ``name==version`` string) + lets the caller group same-canonical-name entries via + :func:`pitloom.extract._lock_common.group_versions_by_canonical_name` + and skip a name that resolves to more than one distinct version -- + unlike every sibling format, this extractor's input is a JSON object + keyed directly by literal (not canonicalized) name, so a hand-edited + or foreign-tool-produced ``Pipfile.lock`` could otherwise legally + carry both a ``"Flask"`` and a ``"flask"`` key and silently emit two + conflicting dependency lines for the same real package. + """ if not isinstance(name, str) or not name: warn_missing_name("Skipping malformed Pipfile.lock entry", name) return None @@ -136,7 +164,7 @@ def _pinned_dep_for_package(name: object, entry: object) -> str | None: pinned_version = _exact_pinned_version(name, entry.get("version")) if pinned_version is None: return None - return f"{name}=={pinned_version}" + return name, pinned_version def _exact_pinned_version(name: str, version: object) -> str | None: diff --git a/src/pitloom/extract/_pylock.py b/src/pitloom/extract/_pylock.py index 5552268c..4d085ef0 100644 --- a/src/pitloom/extract/_pylock.py +++ b/src/pitloom/extract/_pylock.py @@ -35,11 +35,9 @@ from pitloom.extract._lock_common import ( find_first_present_key, - is_usable_version, + group_versions_by_canonical_name, load_lock_toml, - warn_malformed_entry_not_table, - warn_missing_name, - warn_missing_version, + shape_validated_package, warn_non_registry_source, warn_top_level_key_wrong_type, ) @@ -157,11 +155,24 @@ def extract_pylock_dependencies(project_dir: Path) -> list[str] | None: return None environment = _default_group_environment(lock_path, data) + pairs = [ + pair + for pair in (_pinned_pair_for_package(pkg, environment) for pkg in packages) + if pair is not None + ] + dependencies: list[str] = [] - for pkg in packages: - dep = _pinned_dep_for_package(pkg, environment) - if dep is not None: - dependencies.append(dep) + for group in group_versions_by_canonical_name(pairs).values(): + name, version = group[0] + conflicting_versions = {v for _, v in group} + if len(conflicting_versions) > 1: + log.warning( + "Skipping pylock.toml entry %r: pinned to conflicting versions (%s)", + name, + ", ".join(sorted(conflicting_versions)), + ) + continue + dependencies.append(f"{name}=={version}") return dependencies @@ -297,10 +308,10 @@ def _group_marker_excludes( return _evaluate_group_node(tree, environment) is False -def _pinned_dep_for_package( +def _pinned_pair_for_package( pkg: object, environment: dict[str, frozenset[str]] -) -> str | None: - """Return ``name==version`` for one ``[[packages]]`` table entry, or +) -> tuple[str, str] | None: + """Return ``(name, version)`` for one ``[[packages]]`` table entry, or ``None`` when it's malformed or sourced from a location that ``name==version`` can't represent. @@ -312,23 +323,27 @@ def _pinned_dep_for_package( skip in :func:`pitloom.extract._poetry_lock._pinned_dep_for_package`. A registry-resolved package sourced via ``sdist``/``wheels`` (or with no source table at all) is always included when it has a version. + + Returning the raw pair (not the formatted ``name==version`` string) + lets the caller group same-name entries via + :func:`pitloom.extract._lock_common.group_versions_by_canonical_name` + and skip a name that resolves to more than one distinct version -- + reachable here specifically because this extractor's marker handling + only evaluates ``extras``/``dependency_groups`` clauses (see + :func:`_group_marker_excludes`), so two entries for the same package + gated on different, unevaluated ``python_version``/``sys_platform`` + markers can both survive to this point. """ - if not isinstance(pkg, dict): - warn_malformed_entry_not_table("pylock.toml", "[[packages]]", pkg) - return None - name = pkg.get("name") - if not isinstance(name, str) or not name: - warn_missing_name("Skipping malformed pylock.toml [[packages]] entry", name) - return None - version = pkg.get("version") - if not is_usable_version(version): - warn_missing_version("pylock.toml", name) + validated = shape_validated_package(pkg, "pylock.toml", "[[packages]]") + if validated is None: return None - marker = pkg.get("marker") + name = validated["name"] + version = validated["version"] + marker = validated.get("marker") if isinstance(marker, str) and _group_marker_excludes(marker, environment, name): return None - non_registry_source = find_first_present_key(pkg, _NON_REGISTRY_SOURCE_KEYS) + non_registry_source = find_first_present_key(validated, _NON_REGISTRY_SOURCE_KEYS) if non_registry_source is not None: warn_non_registry_source("pylock.toml", name, non_registry_source) return None - return f"{name}=={version}" + return name, version diff --git a/src/pitloom/extract/_uv_lock.py b/src/pitloom/extract/_uv_lock.py index 752e2419..4ed59bcf 100644 --- a/src/pitloom/extract/_uv_lock.py +++ b/src/pitloom/extract/_uv_lock.py @@ -53,7 +53,6 @@ from pitloom.extract._lock_common import ( find_first_present_key, - index_packages_by_name, is_usable_version, load_lock_toml, warn_malformed_entry_not_table, @@ -81,14 +80,24 @@ _ROOT_SOURCE_KEYS = ("editable", "virtual") -def _warn_malformed_packages(packages: Iterable[object]) -> None: - """Log a ``WARNING:`` for each top-level ``[[package]]`` entry that - :func:`_index_by_canonical_name` and :func:`_find_root_package` - silently exclude (a non-table entry, or a table with a - missing/non-string/empty ``name``) -- every sibling lock format's - own package-list loop warns on this same shape of malformed entry, - so a corrupted ``uv.lock`` package doesn't - disappear from extraction with no diagnostic at all.""" +def _scan_packages( + packages: Iterable[object], +) -> tuple[dict[str, list[dict[str, Any]]], list[dict[str, Any]]]: + """Single pass over the top-level ``[[package]]`` list, returning + the canonical-name index :func:`_collect_transitive_dependencies` + needs and the local/workspace-root candidates :func:`_find_root_package` + needs -- folded into one scan instead of three independent ones, + since building the index and finding root candidates only need to + look at each entry once. + + Warns (the same way every sibling lock format's own package-list + loop does) on a non-table entry or a table with a + missing/non-string/empty ``name``, then excludes it from both + results -- it can never be the target of a real dependency reference + by name, nor a real root-package candidate. + """ + by_name: dict[str, list[dict[str, Any]]] = {} + root_candidates: list[dict[str, Any]] = [] for pkg in packages: if not isinstance(pkg, dict): warn_malformed_entry_not_table("uv.lock", "[[package]]", pkg) @@ -96,14 +105,31 @@ def _warn_malformed_packages(packages: Iterable[object]) -> None: name = pkg.get("name") if not isinstance(name, str) or not name: warn_missing_name("Skipping malformed uv.lock [[package]] entry", name) + continue + # uv itself normalizes every ``name`` field it writes, but a + # dependency *reference* and the package's own top-level entry + # are two separately literal strings in the file -- grouping by + # canonical name (as ``_collect_transitive_dependencies``'s + # ``visited`` set already does) keeps lookup consistent with a + # name that differs only in case/``-``/``_``/``.`` folding, + # instead of a literal-string mismatch silently causing a + # resolvable dependency to be reported as "not found". + by_name.setdefault(canonicalize_name(name), []).append(pkg) + source = pkg.get("source") + if ( + isinstance(source, dict) + and find_first_present_key(source, _ROOT_SOURCE_KEYS) is not None + ): + root_candidates.append(pkg) + return by_name, root_candidates def _find_root_package( - packages: Iterable[object], expected_name: str | None + candidates: list[dict[str, Any]], expected_name: str | None ) -> dict[str, Any] | None: - """Return the ``[[package]]`` entry that is the project's own - (identified by an ``editable``/``virtual`` ``source``), or ``None`` - if none is found. + """Return the entry in *candidates* (every ``editable``/``virtual``- + sourced ``[[package]]`` entry, from :func:`_scan_packages`) that is + the project's own, or ``None`` if none is found. A shared ``uv.lock`` (a uv workspace) can list more than one such entry -- one per local workspace member. When *expected_name* (the @@ -116,13 +142,6 @@ def _find_root_package( only in normalization); with more than one candidate and no match, returns ``None`` rather than guess. """ - candidates = [ - pkg - for pkg in packages - if isinstance(pkg, dict) - and isinstance(pkg.get("source"), dict) - and find_first_present_key(pkg["source"], _ROOT_SOURCE_KEYS) is not None - ] if not candidates: return None @@ -308,7 +327,7 @@ def extract_uv_lock_dependencies( lock_path, "package", packages, "a list", "uv.lock" ) return None - _warn_malformed_packages(packages) + by_name, root_candidates = _scan_packages(packages) if not expected_name: # `ProjectMetadata.name` is typed `str`, never `None` -- a @@ -320,7 +339,7 @@ def extract_uv_lock_dependencies( # have done for an explicit `None` -- an empty name could never # usefully match a real workspace member's name anyway. expected_name = _expected_project_name(project_dir) - root = _find_root_package(packages, expected_name) + root = _find_root_package(root_candidates, expected_name) if root is None: log.warning( "%s: no project package found (no 'editable'/'virtual' " @@ -339,15 +358,4 @@ def extract_uv_lock_dependencies( ) return None - # uv itself normalizes every ``name`` field it writes, but a - # dependency *reference* and the package's own top-level entry are - # two separately literal strings in the file -- grouping by - # canonical name (as ``_collect_transitive_dependencies``'s - # ``visited`` set already does) keeps lookup consistent with a name - # that differs only in case/``-``/``_``/``.`` folding, instead of a - # literal-string mismatch silently causing a resolvable dependency - # to be reported as "not found". A non-table entry, or one with a - # missing/non-string/empty ``name``, is excluded here -- see - # ``_warn_malformed_packages`` for the diagnostic on those. - by_name = index_packages_by_name(packages, key=canonicalize_name) return _collect_transitive_dependencies(root_dependencies, by_name) diff --git a/tests/assemble/test_deps_locked_dependencies.py b/tests/assemble/test_deps_locked_dependencies.py index b2d9aaa3..5473d622 100644 --- a/tests/assemble/test_deps_locked_dependencies.py +++ b/tests/assemble/test_deps_locked_dependencies.py @@ -102,6 +102,9 @@ def test_locked_dependencies_add_transitive_only_edges() -> None: version="1.0.0", dependencies=["requests>=2.0"], locked_dependencies=["requests==2.31.0", "urllib3==2.2.0", "idna==3.7"], + provenance={ + "locked_dependencies": "Source: poetry.lock | Method: resolved_lockfile" + }, ) doc = DocumentModel(project=project, creation_metadata=CreationMetadata()) @@ -163,11 +166,14 @@ def test_pinned_requirements_transitive_edges_leave_completeness_unset() -> None def test_locked_dependencies_completeness_by_method() -> None: """Unit-level coverage of `_locked_dependencies_completeness()`'s - three branches: `resolved_lockfile` (a real resolver lock) is - `complete`; `pinned_requirements` is unset (`None`); an unrecognized - future `Method` tag defaults to unset too, the same conservative - "don't claim completeness we can't back up" choice as the pinned- - requirements case, rather than assuming it's resolver-grade.""" + branches: `resolved_lockfile` (a real resolver lock) is `complete`; + everything else -- `pinned_requirements`, an unrecognized future + `Method` tag, and no provenance recorded at all -- is unset (`None`), + the same conservative "don't claim completeness we can't back up" + choice, via a positive inclusion check rather than an exclusion + check (so a future lock source that forgets to set its own `Method` + tag fails safe to unset instead of silently defaulting to + `complete`).""" resolved = ProjectMetadata( name="pkg", locked_dependencies=["idna==3.7"], @@ -199,10 +205,7 @@ def test_locked_dependencies_completeness_by_method() -> None: ) assert _locked_dependencies_completeness(pinned) is None assert _locked_dependencies_completeness(unrecognized) is None - assert ( - _locked_dependencies_completeness(no_provenance) - == spdx3.RelationshipCompleteness.complete - ) + assert _locked_dependencies_completeness(no_provenance) is None def test_locked_dependencies_dedup_is_case_and_separator_insensitive() -> None: diff --git a/tests/extract/test_lock_common.py b/tests/extract/test_lock_common.py index 62b4054d..0ea7e6c3 100644 --- a/tests/extract/test_lock_common.py +++ b/tests/extract/test_lock_common.py @@ -13,14 +13,24 @@ from pathlib import Path import pytest +from packaging.specifiers import SpecifierSet from pitloom.extract._lock_common import ( + default_group_included, find_first_present_key, group_versions_by_canonical_name, + has_required_top_level_table, index_packages_by_name, is_usable_version, load_lock_json, load_lock_toml, + shape_validated_package, + single_exact_pin, + warn_malformed_entry_not_table, + warn_missing_name, + warn_missing_version, + warn_non_registry_source, + warn_top_level_key_wrong_type, ) @@ -186,3 +196,167 @@ def test_is_usable_version_rejects_non_pep440_values(version: object) -> None: produce an invalid ``name==`` dependency/PURL instead of being warned and skipped.""" assert not is_usable_version(version) + + +def test_has_required_top_level_table_valid_type_returns_true() -> None: + assert has_required_top_level_table( + {"metadata": {"lock-version": "2.0"}}, "metadata", "lock-version", str + ) + + +def test_has_required_top_level_table_missing_table_returns_false() -> None: + assert not has_required_top_level_table({}, "metadata", "lock-version", str) + + +def test_has_required_top_level_table_table_not_a_dict_returns_false() -> None: + assert not has_required_top_level_table( + {"metadata": "not-a-table"}, "metadata", "lock-version", str + ) + + +def test_has_required_top_level_table_missing_key_returns_false() -> None: + assert not has_required_top_level_table( + {"metadata": {}}, "metadata", "lock-version", str + ) + + +def test_has_required_top_level_table_wrong_value_type_returns_false() -> None: + """Regression: a present key with the wrong-shaped value (e.g. an + int where the format's own identifying field is always a string) + must be rejected exactly like the key being absent -- not treated + as a looser pass than outright absence.""" + assert not has_required_top_level_table( + {"metadata": {"lock-version": 2}}, "metadata", "lock-version", str + ) + + +def test_shape_validated_package_valid_entry_returned_unchanged() -> None: + pkg = {"name": "requests", "version": "2.31.0"} + assert shape_validated_package(pkg, "poetry.lock") is pkg + + +def test_shape_validated_package_not_a_dict_returns_none_and_warns( + caplog: pytest.LogCaptureFixture, +) -> None: + with caplog.at_level(logging.WARNING): + result = shape_validated_package("not-a-dict", "poetry.lock") + + assert result is None + assert "expected a table" in caplog.text + + +def test_shape_validated_package_missing_name_returns_none_and_warns( + caplog: pytest.LogCaptureFixture, +) -> None: + with caplog.at_level(logging.WARNING): + result = shape_validated_package({"version": "1.0.0"}, "poetry.lock") + + assert result is None + assert "missing or non-string 'name'" in caplog.text + + +def test_shape_validated_package_missing_version_returns_none_and_warns( + caplog: pytest.LogCaptureFixture, +) -> None: + with caplog.at_level(logging.WARNING): + result = shape_validated_package({"name": "requests"}, "poetry.lock") + + assert result is None + assert "missing or non-string 'version'" in caplog.text + + +def test_default_group_included_defaults_when_groups_absent() -> None: + assert default_group_included( + {"name": "requests"}, "poetry.lock", "main", "requests" + ) + + +def test_default_group_included_true_when_present_in_list() -> None: + assert default_group_included( + {"groups": ["main", "dev"]}, "poetry.lock", "main", "requests" + ) + + +def test_default_group_included_false_when_absent_from_list() -> None: + assert not default_group_included( + {"groups": ["dev"]}, "poetry.lock", "main", "requests" + ) + + +def test_default_group_included_not_a_list_returns_none_and_warns( + caplog: pytest.LogCaptureFixture, +) -> None: + with caplog.at_level(logging.WARNING): + result = default_group_included( + {"groups": "main"}, "poetry.lock", "main", "requests" + ) + + assert result is None + assert "'groups' is str, expected a list" in caplog.text + + +@pytest.mark.parametrize("operator", ["==", "==="]) +def test_single_exact_pin_accepts_exact_operators(operator: str) -> None: + assert single_exact_pin(SpecifierSet(f"{operator}2.31.0")) == "2.31.0" + + +def test_single_exact_pin_rejects_wildcard() -> None: + assert single_exact_pin(SpecifierSet("==2.31.*")) is None + + +def test_single_exact_pin_rejects_range() -> None: + assert single_exact_pin(SpecifierSet(">=2.31.0")) is None + + +def test_single_exact_pin_rejects_multiple_specifiers() -> None: + assert single_exact_pin(SpecifierSet(">=2.31.0,<3.0.0")) is None + + +def test_warn_non_registry_source_logs_expected_message( + caplog: pytest.LogCaptureFixture, +) -> None: + with caplog.at_level(logging.WARNING): + warn_non_registry_source("uv.lock", "requests", "git") + + assert "uv.lock" in caplog.text + assert "'requests'" in caplog.text + assert "git-sourced" in caplog.text + + +def test_warn_top_level_key_wrong_type_logs_expected_message( + caplog: pytest.LogCaptureFixture, +) -> None: + with caplog.at_level(logging.WARNING): + warn_top_level_key_wrong_type( + Path("Pipfile.lock"), "default", "not-a-table", "a table", "Pipfile.lock" + ) + + assert "top-level 'default' key is str, expected a table" in caplog.text + + +def test_warn_missing_version_logs_expected_message( + caplog: pytest.LogCaptureFixture, +) -> None: + with caplog.at_level(logging.WARNING): + warn_missing_version("poetry.lock", "requests") + + assert "missing or non-string 'version'" in caplog.text + assert "'requests'" in caplog.text + + +def test_warn_malformed_entry_not_table_logs_expected_message( + caplog: pytest.LogCaptureFixture, +) -> None: + with caplog.at_level(logging.WARNING): + warn_malformed_entry_not_table("uv.lock", "[[package]]", "not-a-table") + + assert "expected a table, got str" in caplog.text + + +def test_warn_missing_name_logs_expected_message( + caplog: pytest.LogCaptureFixture, +) -> None: + with caplog.at_level(logging.WARNING): + warn_missing_name("Skipping malformed poetry.lock entry", None) + + assert "missing or non-string 'name'" in caplog.text diff --git a/tests/extract/test_pipfile_lock.py b/tests/extract/test_pipfile_lock.py index aca73b6d..27b6a3d4 100644 --- a/tests/extract/test_pipfile_lock.py +++ b/tests/extract/test_pipfile_lock.py @@ -171,6 +171,54 @@ def test_develop_section_excluded() -> None: assert "pytest" not in " ".join(result) +def test_same_name_different_casing_same_version_deduped() -> None: + """Grouping compares PEP 503-canonicalized names, so a name that + happens to be spelled differently across entries still collapses + when the versions agree.""" + with tempfile.TemporaryDirectory() as tmp: + tmp_path = Path(tmp) + _write_lock( + tmp_path, + { + "default": { + "Flask": {"version": "==3.0.0"}, + "flask": {"version": "==3.0.0"}, + } + }, + ) + + assert extract_pipfile_lock_dependencies(tmp_path) == ["Flask==3.0.0"] + + +def test_same_name_different_casing_conflicting_versions_skipped_and_warns( + caplog: pytest.LogCaptureFixture, +) -> None: + """Regression: `Pipfile.lock`'s `"default"` section is a JSON object + keyed directly by literal name -- unlike every sibling format, this + extractor previously had no PEP 503 canonicalization step, so a + `Pipfile.lock` with both a `"Flask"` and a `"flask"` key (schema-legal + JSON) would silently emit two conflicting dependency lines instead of + being flagged like every other lock format's own duplicate-name + case.""" + with tempfile.TemporaryDirectory() as tmp: + tmp_path = Path(tmp) + _write_lock( + tmp_path, + { + "default": { + "Flask": {"version": "==3.0.0"}, + "flask": {"version": "==2.0.0"}, + } + }, + ) + + with caplog.at_level(logging.WARNING): + result = extract_pipfile_lock_dependencies(tmp_path) + + assert not result + assert "pinned to conflicting versions" in caplog.text + + def test_malformed_entry_not_a_dict_skipped_and_warns( caplog: pytest.LogCaptureFixture, ) -> None: diff --git a/tests/extract/test_pylock.py b/tests/extract/test_pylock.py index 5d412d52..82befb89 100644 --- a/tests/extract/test_pylock.py +++ b/tests/extract/test_pylock.py @@ -23,12 +23,15 @@ import pytest -from pitloom.extract._pylock import _pinned_dep_for_package, extract_pylock_dependencies +from pitloom.extract._pylock import ( + _pinned_pair_for_package, + extract_pylock_dependencies, +) from pitloom.extract.project import read_project _LOCK_VERSION = 'lock-version = "1.0"\ncreated-by = "test"\n' #: The "no extras, no default-groups active" environment -- -#: `_pinned_dep_for_package()`'s second argument, built by +#: `_pinned_pair_for_package()`'s second argument, built by #: `extract_pylock_dependencies()` itself in normal use via #: `_default_group_environment()`; unit tests calling the helper #: directly supply it explicitly instead. @@ -164,6 +167,48 @@ def test_package_included() -> None: assert extract_pylock_dependencies(tmp_path) == ["requests==2.31.0"] +def test_same_name_same_version_duplicate_entries_deduped() -> None: + with tempfile.TemporaryDirectory() as tmp: + tmp_path = Path(tmp) + _write_lock( + tmp_path, + '[[packages]]\nname = "httpx"\nversion = "0.28.1"\n\n' + '[[packages]]\nname = "httpx"\nversion = "0.28.1"\n', + ) + + assert extract_pylock_dependencies(tmp_path) == ["httpx==0.28.1"] + + +def test_same_name_conflicting_versions_skipped_and_warns( + caplog: pytest.LogCaptureFixture, +) -> None: + """Regression: this extractor only evaluates `extras`/ + `dependency_groups` markers -- every other PEP 508 marker variable + (`python_version`, `sys_platform`, ...) is deliberately left + unevaluated, so a `pylock.toml` built from a multi-platform/ + multi-Python-version resolve can legitimately contain the same + package name twice at different versions, distinguished only by an + unevaluated marker. Both entries must not silently pass through as + two conflicting `name==version` lines -- skip and warn instead, the + same "don't guess" policy `pdm.lock`/`requirements.txt` already + apply to their own duplicate-name case.""" + with tempfile.TemporaryDirectory() as tmp: + tmp_path = Path(tmp) + _write_lock( + tmp_path, + '[[packages]]\nname = "conflicted"\nversion = "1.0.0"\n' + "marker = \"python_version < '3.10'\"\n\n" + '[[packages]]\nname = "conflicted"\nversion = "2.0.0"\n' + "marker = \"python_version >= '3.10'\"\n", + ) + + with caplog.at_level(logging.WARNING): + result = extract_pylock_dependencies(tmp_path) + + assert not result + assert "pinned to conflicting versions" in caplog.text + + def test_packages_key_not_a_list_returns_empty_list_and_warns( caplog: pytest.LogCaptureFixture, ) -> None: @@ -178,10 +223,10 @@ def test_packages_key_not_a_list_returns_empty_list_and_warns( assert "expected a list" in caplog.text -def test_pinned_dep_for_package_non_dict_entry_returns_none() -> None: - assert _pinned_dep_for_package("not-a-dict", _NO_GROUPS_ENV) is None +def test_pinned_pair_for_package_non_dict_entry_returns_none() -> None: + assert _pinned_pair_for_package("not-a-dict", _NO_GROUPS_ENV) is None assert ( - _pinned_dep_for_package(["still", "not", "a", "dict"], _NO_GROUPS_ENV) is None + _pinned_pair_for_package(["still", "not", "a", "dict"], _NO_GROUPS_ENV) is None ) diff --git a/tests/extract/test_uv_lock.py b/tests/extract/test_uv_lock.py index f2134340..b82dbe37 100644 --- a/tests/extract/test_uv_lock.py +++ b/tests/extract/test_uv_lock.py @@ -209,7 +209,7 @@ def test_malformed_top_level_package_entry_warns( def test_non_table_top_level_package_entry_warns( caplog: pytest.LogCaptureFixture, ) -> None: - """The other half of `_warn_malformed_packages()`'s check: a + """The other half of `_scan_packages()`'s malformed-entry check: a top-level `package` array entry that isn't a table at all (not just one missing `name`), e.g. a bare string slipped in alongside genuine `[[package]]` tables -- must also warn, matching every sibling diff --git a/tests/extract/test_uv_lock_root_package.py b/tests/extract/test_uv_lock_root_package.py index 2182513f..b0db9e6a 100644 --- a/tests/extract/test_uv_lock_root_package.py +++ b/tests/extract/test_uv_lock_root_package.py @@ -4,8 +4,10 @@ # SPDX-License-Identifier: Apache-2.0 """Tests for ``uv.lock``'s root/workspace-member package selection -(:func:`pitloom.extract._uv_lock._find_root_package`) and single-entry -pin resolution (:func:`pitloom.extract._uv_lock._pinned_dep_for_package`). +(:func:`pitloom.extract._uv_lock._scan_packages`'s root-candidate +collection and :func:`pitloom.extract._uv_lock._find_root_package`'s +selection among them) and single-entry pin resolution +(:func:`pitloom.extract._uv_lock._pinned_dep_for_package`). See also: test_uv_lock.py (extraction correctness this module's tests were split from -- see that module's own docstring for the split @@ -16,6 +18,7 @@ import logging import tempfile from pathlib import Path +from typing import Any import pytest @@ -23,6 +26,7 @@ _expected_project_name, _find_root_package, _pinned_dep_for_package, + _scan_packages, ) @@ -43,18 +47,23 @@ def test_expected_project_name_returns_none_when_project_table_not_a_dict() -> N assert _expected_project_name(tmp_path) is None -def test_find_root_package_ignores_malformed_entries() -> None: - """A malformed top-level `[[package]]` entry (not a table) is - silently skipped while searching for the root package -- see - test_lock_common.py for the equivalent `index_packages_by_name()` - coverage this and `_uv_lock.py`'s own extraction share.""" +def test_scan_packages_ignores_malformed_entries_when_collecting_root_candidates() -> ( + None +): + """A malformed top-level `[[package]]` entry (not a table, or + missing/non-string `name`) is silently excluded from + `_scan_packages()`'s root-candidate list (the same list + `_find_root_package()` then searches) -- see test_lock_common.py for + the equivalent `index_packages_by_name()` coverage this shares.""" packages: list[object] = [ "not-a-dict", {"version": "1.0.0"}, # missing name, still not editable/virtual {"name": "requests", "version": "2.31.0"}, ] - assert _find_root_package(packages, None) is None + _by_name, root_candidates = _scan_packages(packages) + + assert _find_root_package(root_candidates, None) is None def test_find_root_package_single_candidate_used_even_without_name_match() -> None: @@ -63,7 +72,7 @@ def test_find_root_package_single_candidate_used_even_without_name_match() -> No -- there's no ambiguity about *which* entry, only whether the name happens to match, so guessing wrong here isn't the workspace-mixup risk multiple candidates pose.""" - packages: list[object] = [ + packages: list[dict[str, Any]] = [ {"name": "actual-name", "source": {"editable": "."}}, ] @@ -72,7 +81,7 @@ def test_find_root_package_single_candidate_used_even_without_name_match() -> No def test_find_root_package_prefers_name_match_among_multiple_candidates() -> None: - packages: list[object] = [ + packages: list[dict[str, Any]] = [ {"name": "pkg-a", "source": {"editable": "."}}, {"name": "pkg-b", "source": {"editable": "."}}, ] @@ -89,7 +98,7 @@ def test_find_root_package_multiple_candidates_no_name_match_returns_none_and_wa silently attribute the wrong member's dependencies -- this is the regression case: picking `packages[0]` unconditionally here would misattribute `pkg-a`'s (or `pkg-b`'s) dependencies to `pkg-c`.""" - packages: list[object] = [ + packages: list[dict[str, Any]] = [ {"name": "pkg-a", "source": {"editable": "."}}, {"name": "pkg-b", "source": {"editable": "."}}, ] From 5c787e309055eba8240e31b4053d32496c8690b6 Mon Sep 17 00:00:00 2001 From: Arthit Suriyawongkul Date: Sun, 6 Sep 2026 01:30:32 +0700 Subject: [PATCH 17/35] Fix bugs Signed-off-by: Arthit Suriyawongkul --- skills/sbom-generate/SKILL.md | 26 +++- skills/sbom-generate/references/examples.md | 10 +- src/pitloom/assemble/spdx3/deps.py | 17 ++- src/pitloom/assemble/spdx3/deps_installed.py | 23 ++-- src/pitloom/assemble/spdx3/document.py | 39 +++++- src/pitloom/core/project.py | 38 +++++- src/pitloom/extract/_pyproject.py | 1 + .../test_deps_enrichment_names_versions.py | 21 +++ .../assemble/test_deps_locked_dependencies.py | 35 +++++ tests/core/test_project_metadata.py | 65 ++++++++++ tests/extract/test_pipfile_lock.py | 105 +-------------- .../extract/test_pipfile_lock_integration.py | 121 ++++++++++++++++++ tests/extract/test_poetry_pyproject.py | 24 ++++ 13 files changed, 403 insertions(+), 122 deletions(-) create mode 100644 tests/extract/test_pipfile_lock_integration.py diff --git a/skills/sbom-generate/SKILL.md b/skills/sbom-generate/SKILL.md index 53ff06c5..cfd562a7 100644 --- a/skills/sbom-generate/SKILL.md +++ b/skills/sbom-generate/SKILL.md @@ -1,6 +1,6 @@ --- # Created: 2026-07-05 -# Last-Modified: 2026-08-31 +# Last-Modified: 2026-09-06 # SPDX-FileCopyrightText: 2026-present Arthit Suriyawongkul # SPDX-FileType: SOURCE # SPDX-License-Identifier: Apache-2.0 @@ -114,6 +114,30 @@ loom env -o env.spdx3.json loom merge .spdx3-fragments/ -o combined.spdx3.json ``` +### Automatic lock file discovery & resolved dependencies + +When generating an SBOM for a project directory (`loom project .` or +`loom generate .`), Pitloom automatically inspects the project root for lock +files to discover exact, pinned dependency versions and transitive +dependencies. + +Supported lock formats in priority order: +1. `pylock.toml` (PEP 751 standard lock file) +2. `uv.lock` (uv workspace/resolver) +3. `poetry.lock` (Poetry resolver) +4. `pdm.lock` (PDM resolver) +5. `Pipfile.lock` (Pipenv resolver) +6. `requirements.txt` (Strictly fully-pinned requirement file) + +When a lock file is present: +- Direct dependencies declared with version ranges (e.g. `requests>=2.0`) + automatically resolve to their exact locked version rather than falling + back to host environment introspection. +- Transitive dependencies from the lock file are emitted as SPDX 3 + `software_Package` elements connected via `dependsOn` relationships. +- For resolver lock files (formats 1–5), relationships are marked with + `completeness: complete`. + ## Embed an SBOM into a wheel (PEP 770) For a request to *embed* an SBOM into a built wheel rather than write it diff --git a/skills/sbom-generate/references/examples.md b/skills/sbom-generate/references/examples.md index f18c362c..2f0cd676 100644 --- a/skills/sbom-generate/references/examples.md +++ b/skills/sbom-generate/references/examples.md @@ -1,6 +1,6 @@ --- Created: 2026-07-05 -Last-Modified: 2026-08-14 +Last-Modified: 2026-09-06 SPDX-FileCopyrightText: 2026-present Arthit Suriyawongkul SPDX-FileType: DOCUMENTATION SPDX-License-Identifier: CC0-1.0 @@ -19,6 +19,14 @@ uvx pitloom project . -o sbom.spdx3.json --pretty uvx pitloom project dist/mypackage-1.0.0.tar.gz -o sbom.spdx3.json ``` +## Project SBOM with lock file (resolved transitive dependencies) + +```bash +# Automatically discovers pylock.toml, uv.lock, poetry.lock, pdm.lock, +# Pipfile.lock, or pinned requirements.txt in the project directory: +loom project . -o sbom.spdx3.json --pretty +``` + ## Project SBOM, already-installed Pitloom ```bash diff --git a/src/pitloom/assemble/spdx3/deps.py b/src/pitloom/assemble/spdx3/deps.py index e93099b7..ca18c1d1 100644 --- a/src/pitloom/assemble/spdx3/deps.py +++ b/src/pitloom/assemble/spdx3/deps.py @@ -15,6 +15,7 @@ from typing import Any +from packaging.utils import canonicalize_name from spdx_python_model.bindings import v3_0_1 as spdx3 from pitloom.assemble.spdx3.deps_installed import ( @@ -226,6 +227,7 @@ def add_dependencies( completeness: str | None = None, release_info_cache: dict[tuple[str, str | None], dict[str, Any] | None] | None = None, + locked_versions: dict[str, str] | None = None, ) -> None: """Build SPDX ``software_Package`` and ``Relationship`` elements for dependencies. @@ -249,11 +251,24 @@ def add_dependencies( can prefetch a single combined batch up front and share it across every call, instead of paying for one PyPI network round-trip per call. + + *locked_versions*, when given, maps PEP 503-canonicalized package names + to exact version strings resolved from a project lock file. A direct + dependency declared with a version range (e.g. ``requests>=2.0``) + resolves to this locked version rather than falling back to host + environment introspection. """ resolved = [] for dep in dependencies: dep_name = _parse_dep_name(dep) - dep_version, version_note = _resolve_version(dep_name, dep) + locked_ver = ( + locked_versions.get(canonicalize_name(dep_name)) + if locked_versions is not None + else None + ) + dep_version, version_note = _resolve_version( + dep_name, dep, locked_version=locked_ver + ) resolved.append((dep, dep_name, dep_version, version_note)) if release_info_cache is None and not offline: release_info_cache = _prefetch_pypi_release_infos( diff --git a/src/pitloom/assemble/spdx3/deps_installed.py b/src/pitloom/assemble/spdx3/deps_installed.py index bbcdc6d6..f403cd33 100644 --- a/src/pitloom/assemble/spdx3/deps_installed.py +++ b/src/pitloom/assemble/spdx3/deps_installed.py @@ -48,18 +48,22 @@ def _parse_dep_name(dep: str) -> str: return dep.strip() -def _resolve_version(dep_name: str, dep: str) -> tuple[str, str | None]: +def _resolve_version( + dep_name: str, dep: str, locked_version: str | None = None +) -> tuple[str, str | None]: """Return ``(version_string, resolved_from)`` for a dependency. An exact ``==``/``===`` pin already present in *dep* -- e.g. a resolved ``poetry.lock`` entry, or any dependency the project itself pins - exactly -- is authoritative and checked first: it reflects a decision - already resolved by the dependency's own source and must never be - silently overridden by whatever happens to be installed in Pitloom's - own execution environment, which has no relationship to the target - project's environment. The installed-environment lookup is a fallback - for the common case where the constraint doesn't pin an exact version - (e.g. ``requests>=2.0``). + exactly -- is authoritative and checked first. Likewise, a *locked_version* + provided by a project lock file (PEP 751 ``pylock.toml``, ``uv.lock``, + ``poetry.lock``, etc.) for a direct dependency declared as a range (e.g. + ``requests>=2.0``) is authoritative over the host environment. Both reflect + a decision resolved by the dependency's own data sources and must never be + silently overridden by whatever happens to be installed in Pitloom's own + execution environment, which has no relationship to the target project's + environment. The installed-environment lookup is a fallback for the case + where neither pins an exact version. """ try: pinned = [ @@ -75,6 +79,9 @@ def _resolve_version(dep_name: str, dep: str) -> tuple[str, str | None]: if pinned: return pinned[0], None + if locked_version is not None: + return locked_version, None + try: return get_package_version(dep_name), ( "Version resolved: Build-time environment (importlib.metadata)" diff --git a/src/pitloom/assemble/spdx3/document.py b/src/pitloom/assemble/spdx3/document.py index df44125e..bed95246 100644 --- a/src/pitloom/assemble/spdx3/document.py +++ b/src/pitloom/assemble/spdx3/document.py @@ -209,15 +209,44 @@ def _locked_dependencies_completeness(metadata: ProjectMetadata) -> str | None: return None +def _extract_locked_version_map(locked_dependencies: list[str]) -> dict[str, str]: + """Map canonical package names to their exact locked version string. + + Enables direct dependencies declared as ranges (e.g. ``requests>=2.0``) + to resolve to their authoritative locked version rather than falling back + to introspecting Pitloom's host environment. + """ + result: dict[str, str] = {} + for dep in locked_dependencies: + dep_name = _parse_dep_name(dep) + version, _ = _resolve_version(dep_name, dep) + if version != "unknown": + result[canonicalize_name(dep_name)] = version + return result + + def _prefetch_combined_release_info( - dependencies: list[str], transitive_only: list[str] + dependencies: list[str], + transitive_only: list[str], + locked_versions: dict[str, str] | None = None, ) -> dict[tuple[str, str | None], dict[str, Any] | None]: """Prefetch PyPI release info once for every dependency a document will emit -- direct and lock-resolved-transitive alike -- so the result can be shared across both :func:`add_dependencies` calls in :func:`build` instead of each call paying for its own network round-trip.""" name_version_pairs = [] - for dep in (*dependencies, *transitive_only): + for dep in dependencies: + dep_name = _parse_dep_name(dep) + locked_ver = ( + locked_versions.get(canonicalize_name(dep_name)) + if locked_versions is not None + else None + ) + dep_version, _version_note = _resolve_version( + dep_name, dep, locked_version=locked_ver + ) + name_version_pairs.append((dep_name, dep_version)) + for dep in transitive_only: dep_name = _parse_dep_name(dep) dep_version, _version_note = _resolve_version(dep_name, dep) name_version_pairs.append((dep_name, dep_version)) @@ -351,10 +380,13 @@ def build( # --- Locked (e.g. poetry.lock-resolved) transitive-only dependencies --- transitive_only = _locked_transitive_only_dependencies(metadata) + locked_versions = _extract_locked_version_map(metadata.locked_dependencies) release_info_cache = ( None if offline - else _prefetch_combined_release_info(metadata.dependencies, transitive_only) + else _prefetch_combined_release_info( + metadata.dependencies, transitive_only, locked_versions=locked_versions + ) ) # --- Dependencies --- @@ -371,6 +403,7 @@ def build( encoder=encoder, content_type_method=content_type_method, release_info_cache=release_info_cache, + locked_versions=locked_versions, ) if transitive_only: diff --git a/src/pitloom/core/project.py b/src/pitloom/core/project.py index e56092cc..6cc320c6 100644 --- a/src/pitloom/core/project.py +++ b/src/pitloom/core/project.py @@ -121,11 +121,22 @@ class ProjectMetadata: files: list[ProjectFile] = field(default_factory=list) +#: Maps a :class:`ProjectMetadata` field name to the literal provenance key +#: its extractors actually record it under, for the one known case where +#: they differ -- every ``license_name`` producer (``_pyproject.py``, +#: ``_setuptools_py.py``, ``_setuptools_cfg.py``) writes +#: ``provenance["license"]``, never ``provenance["license_name"]``. Consulted +#: by :func:`merge_project_metadata`'s "explicitly declared" check so it +#: looks up the key extractors actually use instead of a field name that's +#: never present in *provenance*. +_PROVENANCE_KEY_ALIASES: dict[str, str] = {"license_name": "license"} + + def merge_project_metadata( primary: ProjectMetadata, secondary: ProjectMetadata ) -> ProjectMetadata: """Merge two :class:`ProjectMetadata` instances, *primary* winning - field-by-field; *secondary* fills gaps where *primary*'s value is falsy. + field-by-field; *secondary* fills gaps where *primary*'s value is absent. Iterates :func:`dataclasses.fields` instead of hand-listing every field, so a newly added :class:`ProjectMetadata` field (like ``license_concluded``, @@ -137,7 +148,7 @@ def merge_project_metadata( in sync by hand; ``license_concluded`` was missing from one of them until this fix, precisely because that discipline had already lapsed once. - Two fields are special-cased rather than "primary if truthy else + Two fields are special-cased rather than "primary when present else secondary": - ``name`` -- always *primary*'s, even if empty (a project's own name is @@ -145,15 +156,25 @@ def merge_project_metadata( - ``provenance`` -- dict-merged, *primary*'s entries winning on key conflict, rather than replaced wholesale. - Every other field: *primary*'s value when truthy, else *secondary*'s -- - including every list-valued field (``dependencies``, ``locked_dependencies``, - ``keywords``, ``authors``, ``files``): a non-empty *primary* list replaces - *secondary*'s wholesale, it is never unioned with it. If a future + Every other field: *primary*'s value when present, else *secondary*'s. + An empty container (``dependencies``, ``keywords``, ``urls``, etc.) + with provenance confirming it was explicitly declared in *primary* is + authoritative and preserved. Default-constructed empty containers (absent + from *primary*'s provenance) or ``None`` values are treated as absent and + filled from *secondary*. A non-empty *primary* list replaces *secondary*'s + wholesale, it is never unioned with it. If a future ``locked_dependencies`` source needs union-not-replace semantics (e.g. combining two lock-derived dependency sets), that is a deliberate deviation from every sibling list field here and belongs in a dedicated merge step at the call site, not a silent special case in this otherwise-uniform field-by-field loop. + + The "explicitly declared" check looks up *provenance* by the field's own + name (e.g. ``provenance["keywords"]``) -- except ``license_name``, whose + extractors have historically recorded its provenance under the literal + key ``"license"`` (see ``_pyproject.py``/``_setuptools_py.py``/ + ``_setuptools_cfg.py``), not ``"license_name"``; :data:`_PROVENANCE_KEY_ALIASES` + maps that one known mismatch so the same presence check still finds it. """ merged = dataclasses.replace(primary) merged.provenance = {**secondary.provenance, **primary.provenance} @@ -161,6 +182,9 @@ def merge_project_metadata( if f.name in ("name", "provenance"): continue primary_value = getattr(primary, f.name) - if not primary_value: + provenance_key = _PROVENANCE_KEY_ALIASES.get(f.name, f.name) + if primary_value is None or ( + not primary_value and provenance_key not in primary.provenance + ): setattr(merged, f.name, getattr(secondary, f.name)) return merged diff --git a/src/pitloom/extract/_pyproject.py b/src/pitloom/extract/_pyproject.py index e1fb815b..fbaaf459 100644 --- a/src/pitloom/extract/_pyproject.py +++ b/src/pitloom/extract/_pyproject.py @@ -276,6 +276,7 @@ def read_pyproject( "dependencies": "Source: pyproject.toml | Field: project.dependencies", "authors": "Source: pyproject.toml | Field: project.authors", "license": "Source: pyproject.toml | Field: project.license", + "keywords": "Source: pyproject.toml | Field: project.keywords", } diff --git a/tests/assemble/test_deps_enrichment_names_versions.py b/tests/assemble/test_deps_enrichment_names_versions.py index 5a1c9737..cee32400 100644 --- a/tests/assemble/test_deps_enrichment_names_versions.py +++ b/tests/assemble/test_deps_enrichment_names_versions.py @@ -163,6 +163,27 @@ def test_resolve_version_falls_back_to_installed_when_unpinned( assert note == "Version resolved: Build-time environment (importlib.metadata)" +def test_resolve_version_uses_locked_version_over_installed_when_unpinned( + monkeypatch: pytest.MonkeyPatch, +) -> None: + """Per GEMINI.md ("Explicit pin beats local environment"): when a lock + file pins an exact version for a dependency declared with a range, + the lock pin is authoritative and Pitloom's host environment is never + consulted.""" + monkeypatch.setattr( + deps_mod, + "get_package_version", + lambda _name: pytest.fail("host environment should not be consulted"), + ) + + version, note = _resolve_version( + "requests", "requests>=2.0", locked_version="2.31.0" + ) + + assert version == "2.31.0" + assert note is None + + # --------------------------------------------------------------------------- # build_pypi_purl / add_dependencies -- PURL even without a resolved version # --------------------------------------------------------------------------- diff --git a/tests/assemble/test_deps_locked_dependencies.py b/tests/assemble/test_deps_locked_dependencies.py index 5473d622..dad29e87 100644 --- a/tests/assemble/test_deps_locked_dependencies.py +++ b/tests/assemble/test_deps_locked_dependencies.py @@ -18,8 +18,10 @@ import json +import pytest from spdx_python_model.bindings import v3_0_1 as spdx3 +from pitloom.assemble.spdx3 import deps_installed from pitloom.assemble.spdx3.deps import add_dependencies from pitloom.assemble.spdx3.document import _locked_dependencies_completeness, build from pitloom.core.creation import CreationMetadata @@ -368,3 +370,36 @@ def test_valid_empty_lock_does_not_collide_with_no_lock_or_a_different_empty_loc ) assert len({no_lock_at_all, empty_from_pylock, empty_from_uv}) == 3 + + +def test_direct_dependency_range_resolves_to_locked_version_over_host_environment( + monkeypatch: pytest.MonkeyPatch, +) -> None: + """Per GEMINI.md ("Explicit pin beats local environment"): when + metadata.dependencies declares a range (requests>=2.0) and + metadata.locked_dependencies has an exact pin (requests==2.31.0), + the assembled Package node must use the locked version (2.31.0), + never introspecting Pitloom's host environment.""" + monkeypatch.setattr( + deps_installed, + "get_package_version", + lambda _name: pytest.fail("host environment should not be consulted"), + ) + + project = ProjectMetadata( + name="main-project", + version="1.0.0", + dependencies=["requests>=2.0"], + locked_dependencies=["requests==2.31.0", "urllib3==2.2.0"], + provenance={ + "locked_dependencies": "Source: pylock.toml | Method: resolved_lockfile" + }, + ) + doc = DocumentModel(project=project, creation_metadata=CreationMetadata()) + + exporter = build(doc, offline=True) + graph = json.loads(exporter.to_json())["@graph"] + packages = {e["name"]: e for e in graph if e.get("type") == "software_Package"} + + assert packages["requests"]["software_packageVersion"] == "2.31.0" + assert packages["urllib3"]["software_packageVersion"] == "2.2.0" diff --git a/tests/core/test_project_metadata.py b/tests/core/test_project_metadata.py index c8c78e24..8afa251b 100644 --- a/tests/core/test_project_metadata.py +++ b/tests/core/test_project_metadata.py @@ -78,6 +78,71 @@ def test_merge_project_metadata_empty_lists_filled_from_secondary() -> None: assert merged.dependencies == ["requests>=2.0"] +def test_merge_project_metadata_explicit_empty_container_preserved() -> None: + """Per GEMINI.md, ``None`` vs ``[]``/``{}`` (empty-but-present) is a distinct + signal: an empty container with provenance confirming it was explicitly + declared in *primary* is authoritative (zero dependencies/keywords) and + must NOT be overwritten by secondary.""" + primary = ProjectMetadata( + name="pkg", + dependencies=[], + keywords=[], + provenance={ + "dependencies": "Source: pyproject.toml | Field: project.dependencies", + "keywords": "Source: pyproject.toml | Field: project.keywords", + }, + ) + secondary = ProjectMetadata( + name="pkg", + dependencies=["requests>=2.0"], + keywords=["tool", "utility"], + urls={"Homepage": "https://example.com"}, + provenance={ + "dependencies": "Source: tool.poetry | Field: tool.poetry.dependencies", + "keywords": "Source: tool.poetry | Field: tool.poetry.keywords", + "urls": "Source: tool.poetry | Field: tool.poetry.urls", + }, + ) + merged = merge_project_metadata(primary, secondary) + assert merged.dependencies == [] + assert merged.keywords == [] + assert merged.urls == {"Homepage": "https://example.com"} + assert merged.provenance["dependencies"] == ( + "Source: pyproject.toml | Field: project.dependencies" + ) + assert merged.provenance["urls"] == ( + "Source: tool.poetry | Field: tool.poetry.urls" + ) + + +def test_merge_project_metadata_explicit_empty_license_name_preserved() -> None: + """Regression: every ``license_name`` producer + (``_pyproject.py``/``_setuptools_py.py``/``_setuptools_cfg.py``) records + its provenance under the literal key ``"license"``, not + ``"license_name"`` -- the field/provenance-key name mismatch the + ``_PROVENANCE_KEY_ALIASES`` map exists to bridge. An explicitly + declared-but-empty ``license_name`` in *primary* (e.g. `license = ""`) + with that provenance recorded must not be overwritten by *secondary*'s + ``license_name``.""" + primary = ProjectMetadata( + name="pkg", + license_name="", + provenance={"license": "Source: pyproject.toml | Field: project.license"}, + ) + secondary = ProjectMetadata( + name="pkg", + license_name="MIT", + provenance={"license": "Source: tool.poetry | Field: tool.poetry.license"}, + ) + + merged = merge_project_metadata(primary, secondary) + + assert merged.license_name == "" + assert merged.provenance["license"] == ( + "Source: pyproject.toml | Field: project.license" + ) + + def test_merge_project_metadata_license_concluded_preserved() -> None: """Regression for the specific bug this function was introduced to fix: a newly added field (license_concluded, for G2) must merge with the diff --git a/tests/extract/test_pipfile_lock.py b/tests/extract/test_pipfile_lock.py index 27b6a3d4..519d3686 100644 --- a/tests/extract/test_pipfile_lock.py +++ b/tests/extract/test_pipfile_lock.py @@ -4,12 +4,11 @@ # SPDX-License-Identifier: Apache-2.0 """Tests for ``Pipfile.lock`` resolved-dependency parsing -(:mod:`pitloom.extract._pipfile_lock`) and its overlay onto -``ProjectMetadata.locked_dependencies`` via ``read_project()``'s lock -cascade (:mod:`pitloom.extract._locked_dependencies`). +(:mod:`pitloom.extract._pipfile_lock`). -See also: test_poetry_lock.py/test_pylock.py/test_uv_lock.py for the -sibling lock extractors this module's tests mirror in shape; +See also: test_pipfile_lock_integration.py for ``read_project()`` lock cascade +integration and real-world fixture coverage; +test_poetry_lock.py/test_pylock.py/test_uv_lock.py for sibling lock extractors; test_locked_dependencies.py for the cascade mechanism's own tests. """ @@ -21,12 +20,6 @@ import pytest from pitloom.extract._pipfile_lock import extract_pipfile_lock_dependencies -from pitloom.extract.project import read_project - -REAL_WORLD_LOCKS = ( - Path(__file__).parent.parent / "fixtures" / "real-world-locks" / "pipfile" -) - #: Every genuine `Pipfile.lock` carries this key -- merged into *data* by #: default so tests that aren't specifically about the genuineness check @@ -439,93 +432,3 @@ def test_missing_or_empty_name_skipped_and_warns( assert result == ["requests==2.31.0"] assert "malformed" in caplog.text.lower() - - -# --- read_project() cascade integration ------------------------------- - - -def test_read_project_populates_locked_dependencies_from_setup_py_only() -> None: - """Regression: Pipfile.lock predates PEP 621 almost entirely -- - every real project pairs it with a bare setup.py, never - pyproject.toml. The cascade must reach it via read_project()'s - setup.py-only dispatch path, not only the pyproject.toml one.""" - with tempfile.TemporaryDirectory() as tmp: - tmp_path = Path(tmp) - (tmp_path / "setup.py").write_text( - "from setuptools import setup\nsetup(name='demo', version='1.0.0')\n", - encoding="utf-8", - ) - _write_lock(tmp_path, {"default": {"requests": {"version": "==2.31.0"}}}) - - metadata, _config, _path = read_project(tmp_path) - - assert metadata.locked_dependencies == ["requests==2.31.0"] - assert metadata.provenance["locked_dependencies"] == ( - "Source: Pipfile.lock | Method: resolved_lockfile" - ) - - -def test_read_project_pdm_lock_takes_priority_over_pipfile_lock() -> None: - with tempfile.TemporaryDirectory() as tmp: - tmp_path = Path(tmp) - (tmp_path / "pyproject.toml").write_text( - '[project]\nname = "demo"\nversion = "1.0.0"\n', encoding="utf-8" - ) - (tmp_path / "pdm.lock").write_text( - '[[package]]\nname = "httpx"\nversion = "0.27.0"\ngroups = ["default"]\n' - '[metadata]\nlock_version = "4.5.1"\n', - encoding="utf-8", - ) - _write_lock(tmp_path, {"default": {"requests": {"version": "==2.31.0"}}}) - - metadata, _config, _path = read_project(tmp_path) - - assert metadata.locked_dependencies == ["httpx==0.27.0"] - assert metadata.provenance["locked_dependencies"] == ( - "Source: pdm.lock | Method: resolved_lockfile" - ) - - -# --- real-world fixtures ------------------------------------------------- - - -def test_real_world_requests_html() -> None: - """`psf/requests-html` -- real, unmodified `Pipfile.lock` from the - matching GitHub tag, read directly via the extractor rather than - `read_project()`: `requests-html`'s `setup.py` declares `name`/ - `version` via module-level constants (`NAME = 'requests-html'`, - `setup(name=NAME, ...)`), which `_setuptools_py.py`'s literal-only - AST resolution can't follow -- a known, separate, pre-existing gap - (see the `pyyaml` entry in `real-world-projects/README.md`) that - makes `read_setup_py()` raise `ValueError` and, with no `setup.cfg` - fallback either, `read_project()` raise `FileNotFoundError` entirely - for this fixture. That's this fixture's own known limitation, not - something for the lock-cascade extractor to work around -- so this - test exercises `extract_pipfile_lock_dependencies()` directly - against the real fixture data instead.""" - dependencies = extract_pipfile_lock_dependencies( - REAL_WORLD_LOCKS / "requests-html-0.10.0" - ) - - assert dependencies is not None - names = {dep.split("==", maxsplit=1)[0] for dep in dependencies} - assert "requests" in names - assert "beautifulsoup4" in names - - -def test_real_world_responder() -> None: - """`kennethreitz/responder` -- also has a self-referential editable - `path`-sourced entry (`responder` itself) in its own `default` - section, exercising the non-registry-source skip against real data. - Same `setup.py`-constant limitation as `requests-html` above applies - here too, so this also calls the extractor directly.""" - from pitloom.extract._pipfile_lock import extract_pipfile_lock_dependencies - - dependencies = extract_pipfile_lock_dependencies( - REAL_WORLD_LOCKS / "responder-2.0.0" - ) - - assert dependencies is not None - names = {dep.split("==", maxsplit=1)[0] for dep in dependencies} - assert "requests" in names - assert "responder" not in names # self-referential, editable/path-sourced diff --git a/tests/extract/test_pipfile_lock_integration.py b/tests/extract/test_pipfile_lock_integration.py new file mode 100644 index 00000000..2c7fdead --- /dev/null +++ b/tests/extract/test_pipfile_lock_integration.py @@ -0,0 +1,121 @@ +# SPDX-FileContributor: Arthit Suriyawongkul +# SPDX-FileCopyrightText: 2026-present Arthit Suriyawongkul +# SPDX-FileType: SOURCE +# SPDX-License-Identifier: Apache-2.0 + +"""Tests for ``Pipfile.lock``'s overlay onto +``ProjectMetadata.locked_dependencies`` via ``read_project()``'s lock +cascade (:mod:`pitloom.extract._locked_dependencies`), and real-world +fixture coverage. + +See also: test_pipfile_lock.py (core extraction and validation unit +tests this module's integration tests were split from). +""" + +import json +import tempfile +from pathlib import Path + +from pitloom.extract._pipfile_lock import extract_pipfile_lock_dependencies +from pitloom.extract.project import read_project + +REAL_WORLD_LOCKS = ( + Path(__file__).parent.parent / "fixtures" / "real-world-locks" / "pipfile" +) + +_META = {"pipfile-spec": 6} + + +def _write_lock( + tmp_dir: Path, data: dict[str, object], include_meta: bool = True +) -> None: + full_data = {"_meta": _META, **data} if include_meta else data + (tmp_dir / "Pipfile.lock").write_text(json.dumps(full_data), encoding="utf-8") + + +# --- read_project() cascade integration ------------------------------- + + +def test_read_project_populates_locked_dependencies_from_setup_py_only() -> None: + """Regression: Pipfile.lock predates PEP 621 almost entirely -- + every real project pairs it with a bare setup.py, never + pyproject.toml. The cascade must reach it via read_project()'s + setup.py-only dispatch path, not only the pyproject.toml one.""" + with tempfile.TemporaryDirectory() as tmp: + tmp_path = Path(tmp) + (tmp_path / "setup.py").write_text( + "from setuptools import setup\nsetup(name='demo', version='1.0.0')\n", + encoding="utf-8", + ) + _write_lock(tmp_path, {"default": {"requests": {"version": "==2.31.0"}}}) + + metadata, _config, _path = read_project(tmp_path) + + assert metadata.locked_dependencies == ["requests==2.31.0"] + assert metadata.provenance["locked_dependencies"] == ( + "Source: Pipfile.lock | Method: resolved_lockfile" + ) + + +def test_read_project_pdm_lock_takes_priority_over_pipfile_lock() -> None: + with tempfile.TemporaryDirectory() as tmp: + tmp_path = Path(tmp) + (tmp_path / "pyproject.toml").write_text( + '[project]\nname = "demo"\nversion = "1.0.0"\n', encoding="utf-8" + ) + (tmp_path / "pdm.lock").write_text( + '[[package]]\nname = "httpx"\nversion = "0.27.0"\ngroups = ["default"]\n' + '[metadata]\nlock_version = "4.5.1"\n', + encoding="utf-8", + ) + _write_lock(tmp_path, {"default": {"requests": {"version": "==2.31.0"}}}) + + metadata, _config, _path = read_project(tmp_path) + + assert metadata.locked_dependencies == ["httpx==0.27.0"] + assert metadata.provenance["locked_dependencies"] == ( + "Source: pdm.lock | Method: resolved_lockfile" + ) + + +# --- real-world fixtures ------------------------------------------------- + + +def test_real_world_requests_html() -> None: + """`psf/requests-html` -- real, unmodified `Pipfile.lock` from the + matching GitHub tag, read directly via the extractor rather than + `read_project()`: `requests-html`'s `setup.py` declares `name`/ + `version` via module-level constants (`NAME = 'requests-html'`, + `setup(name=NAME, ...)`), which `_setuptools_py.py`'s literal-only + AST resolution can't follow -- a known, separate, pre-existing gap + (see the `pyyaml` entry in `real-world-projects/README.md`) that + makes `read_setup_py()` raise `ValueError` and, with no `setup.cfg` + fallback either, `read_project()` raise `FileNotFoundError` entirely + for this fixture. That's this fixture's own known limitation, not + something for the lock-cascade extractor to work around -- so this + test exercises `extract_pipfile_lock_dependencies()` directly + against the real fixture data instead.""" + dependencies = extract_pipfile_lock_dependencies( + REAL_WORLD_LOCKS / "requests-html-0.10.0" + ) + + assert dependencies is not None + names = {dep.split("==", maxsplit=1)[0] for dep in dependencies} + assert "requests" in names + assert "beautifulsoup4" in names + + +def test_real_world_responder() -> None: + """`kennethreitz/responder` -- also has a self-referential editable + `path`-sourced entry (`responder` itself) in its own `default` + section, exercising the non-registry-source skip against real data. + Same `setup.py`-constant limitation as `requests-html` above applies + here too, so this also calls the extractor directly.""" + dependencies = extract_pipfile_lock_dependencies( + REAL_WORLD_LOCKS / "responder-2.0.0" + ) + + assert dependencies is not None + names = {dep.split("==", maxsplit=1)[0] for dep in dependencies} + assert "requests" in names + assert "responder" not in names # self-referential, editable/path-sourced diff --git a/tests/extract/test_poetry_pyproject.py b/tests/extract/test_poetry_pyproject.py index c622eebf..49e00651 100644 --- a/tests/extract/test_poetry_pyproject.py +++ b/tests/extract/test_poetry_pyproject.py @@ -95,6 +95,30 @@ def test_read_pyproject_project_overrides_poetry() -> None: assert metadata.keywords == ["from-poetry"] +def test_read_pyproject_explicit_empty_keywords_not_filled_from_poetry() -> None: + """Regression: `[project] keywords = []` is an explicit declaration of + zero keywords, not an omission -- `[tool.poetry] keywords` must not + silently fill it back in via `merge_project_metadata()`'s + empty-container gap-fill (see `_FIELD_PROVENANCE`'s `"keywords"` entry, + which records this explicit-declaration provenance).""" + content = """ +[project] +name = "project-name" +version = "1.0.0" +keywords = [] + +[tool.poetry] +name = "project-name" +version = "1.0.0" +keywords = ["from-poetry"] +""" + with tempfile.TemporaryDirectory() as d: + (Path(d) / "pyproject.toml").write_text(content) + metadata, _ = read_pyproject(Path(d) / "pyproject.toml") + + assert metadata.keywords == [] + + def test_read_pyproject_poetry_fills_missing_project_fields() -> None: content = """ [project] From fd0e4271ac4141b314e08bf8aa1d5735e47c6bfa Mon Sep 17 00:00:00 2001 From: Arthit Suriyawongkul Date: Sun, 6 Sep 2026 14:23:20 +0700 Subject: [PATCH 18/35] Fix canonicalize names Signed-off-by: Arthit Suriyawongkul --- src/pitloom/assemble/spdx3/deps.py | 18 ++-- src/pitloom/assemble/spdx3/deps_installed.py | 64 ++++++++++---- src/pitloom/assemble/spdx3/deps_pypi.py | 32 ++++--- src/pitloom/extract/_lock_common.py | 35 ++++++++ src/pitloom/extract/_locked_dependencies.py | 27 +++--- src/pitloom/extract/_pdm_lock.py | 14 +--- src/pitloom/extract/_pipfile_lock.py | 14 ++-- src/pitloom/extract/_poetry_lock.py | 48 ++++++----- src/pitloom/extract/_pylock.py | 11 +-- src/pitloom/extract/_requirements_txt.py | 7 +- src/pitloom/extract/_uv_lock.py | 70 ++++++++++++---- .../test_deps_enrichment_names_versions.py | 83 +++++++++++++++++++ tests/extract/test_hatch_hook_metadata.py | 69 +++++++++++---- tests/extract/test_lock_common.py | 5 ++ tests/extract/test_pdm_lock.py | 8 +- tests/extract/test_pipfile_lock.py | 8 +- tests/extract/test_poetry_lock.py | 47 ++++++++--- tests/extract/test_project.py | 61 ++++++++++++++ tests/extract/test_pylock.py | 35 ++++++-- tests/extract/test_requirements_txt.py | 56 +++++++++---- tests/extract/test_uv_lock.py | 38 +++++++-- tests/extract/test_uv_lock_integration.py | 9 ++ working-docs/design/roadmap.md | 12 ++- 23 files changed, 588 insertions(+), 183 deletions(-) diff --git a/src/pitloom/assemble/spdx3/deps.py b/src/pitloom/assemble/spdx3/deps.py index ca18c1d1..59049de2 100644 --- a/src/pitloom/assemble/spdx3/deps.py +++ b/src/pitloom/assemble/spdx3/deps.py @@ -80,7 +80,7 @@ def _enrich_from_pypi( """Best-effort PyPI JSON API fallback for originator, license, and hash.""" version = dep_version if dep_version != "unknown" else None release_info = ( - release_info_cache.get((dep_name, version)) + release_info_cache.get((canonicalize_name(dep_name), version)) if release_info_cache is not None else _fetch_pypi_release_info(dep_name, version) ) @@ -275,13 +275,15 @@ def add_dependencies( (dep_name, dep_version) for _dep, dep_name, dep_version, _note in resolved ) - grouped: dict[tuple[str, str], list[tuple[str, str | None]]] = {} + grouped: dict[tuple[str, str], list[tuple[str, str, str | None]]] = {} for dep, dep_name, dep_version, version_note in resolved: - grouped.setdefault((dep_name, dep_version), []).append((dep, version_note)) + canon_key = (canonicalize_name(dep_name), dep_version) + grouped.setdefault(canon_key, []).append((dep, dep_name, version_note)) - for (dep_name, dep_version), declared in grouped.items(): - declared_constraints = [dep for dep, _note in declared] - version_note = next((note for _dep, note in declared if note), None) + for (_canon_name, dep_version), declared in grouped.items(): + display_dep_name = declared[0][1] + declared_constraints = [dep for dep, _raw_name, _note in declared] + version_note = next((note for _dep, _raw_name, note in declared if note), None) dep_provenance_fields: dict[str, str] = { "dependencies": dep_provenance, "declared_constraint": " | ".join(declared_constraints), @@ -291,14 +293,14 @@ def add_dependencies( dep_package = spdx3.software_Package( spdxId=generate_spdx_id("Package", doc_name=doc_name, doc_uuid=doc_uuid), - name=dep_name, + name=display_dep_name, creationInfo=creation_info, ) dep_package.software_packageVersion = dep_version dep_package.software_primaryPurpose = spdx3.software_SoftwarePurpose.library _finish_dependency_enrichment( - dep_name, + display_dep_name, dep_version, dep_package, creation_info, diff --git a/src/pitloom/assemble/spdx3/deps_installed.py b/src/pitloom/assemble/spdx3/deps_installed.py index f403cd33..5f3b8c5f 100644 --- a/src/pitloom/assemble/spdx3/deps_installed.py +++ b/src/pitloom/assemble/spdx3/deps_installed.py @@ -10,6 +10,7 @@ from __future__ import annotations +import logging from importlib.metadata import PackageMetadata, PackageNotFoundError from importlib.metadata import metadata as get_pkg_metadata from importlib.metadata import version as get_package_version @@ -35,6 +36,8 @@ _HOMEPAGE_LABELS = ("homepage", "home page", "home") _DOWNLOAD_LABELS = ("download",) +log = logging.getLogger(__name__) + def _parse_dep_name(dep: str) -> str: """Return the bare package name from a PEP 508 dependency specifier.""" @@ -55,32 +58,57 @@ def _resolve_version( An exact ``==``/``===`` pin already present in *dep* -- e.g. a resolved ``poetry.lock`` entry, or any dependency the project itself pins - exactly -- is authoritative and checked first. Likewise, a *locked_version* - provided by a project lock file (PEP 751 ``pylock.toml``, ``uv.lock``, - ``poetry.lock``, etc.) for a direct dependency declared as a range (e.g. - ``requests>=2.0``) is authoritative over the host environment. Both reflect - a decision resolved by the dependency's own data sources and must never be - silently overridden by whatever happens to be installed in Pitloom's own - execution environment, which has no relationship to the target project's - environment. The installed-environment lookup is a fallback for the case - where neither pins an exact version. + exactly -- is authoritative and checked first: it reflects a decision + already resolved by the dependency's own source and must never be + silently overridden by whatever happens to be installed in Pitloom's + own execution environment or a conflicting lock file entry. + + Likewise, a *locked_version* provided by a project lock file (PEP 751 + ``pylock.toml``, ``uv.lock``, ``poetry.lock``, etc.) for a direct dependency + declared as a range or unpinned (e.g. ``requests>=2.0``) is authoritative + over the host environment. When it does not satisfy the declared constraint + in *dep*, a warning is emitted. + + The installed-environment lookup is a fallback for the case where neither + pins an exact version. """ + req: Requirement | None = None try: - pinned = [ - spec.version - for spec in Requirement(dep).specifier - if spec.operator in ("==", "===") - ] + req = Requirement(dep) except InvalidRequirement: - pinned = [] unparseable = True else: unparseable = False - if pinned: - return pinned[0], None + + if req is not None: + pinned = [ + spec.version for spec in req.specifier if spec.operator in ("==", "===") + ] + if pinned: + if locked_version is not None and locked_version != pinned[0]: + log.warning( + "Locked version %r for dependency %r conflicts with declared" + " exact pin %r -- using declared pin", + locked_version, + dep_name, + pinned[0], + ) + return pinned[0], None if locked_version is not None: - return locked_version, None + if ( + req is not None + and req.specifier + and not req.specifier.contains(locked_version, prereleases=True) + ): + log.warning( + "Locked version %r for dependency %r does not satisfy declared" + " constraint %r -- using locked version", + locked_version, + dep_name, + dep, + ) + return locked_version, "Version resolved: Project lock file" try: return get_package_version(dep_name), ( diff --git a/src/pitloom/assemble/spdx3/deps_pypi.py b/src/pitloom/assemble/spdx3/deps_pypi.py index b53736da..b0bbbe61 100644 --- a/src/pitloom/assemble/spdx3/deps_pypi.py +++ b/src/pitloom/assemble/spdx3/deps_pypi.py @@ -16,6 +16,8 @@ from typing import Any from urllib.parse import quote as url_quote +from packaging.utils import canonicalize_name + from pitloom.assemble.spdx3.deps_originator import _extract_name_email_pairs from pitloom.extract._extract_utils import fetch_json @@ -121,8 +123,8 @@ def _prefetch_pypi_release_infos( name_versions: Iterable[tuple[str, str]], ) -> dict[tuple[str, str | None], dict[str, Any] | None]: """Concurrently fetch PyPI JSON API release info for each distinct - ``(name, version)`` pair, so N dependencies cost roughly one network - round-trip's worth of wall time instead of N sequential ones (each + ``(canonicalize_name(name), version)`` pair, so N dependencies cost roughly + one network round-trip's worth of wall time instead of N sequential ones (each with its own TCP+TLS handshake and up to a :data:`_PYPI_TIMEOUT_SECONDS` timeout on failure). @@ -131,20 +133,26 @@ def _prefetch_pypi_release_infos( semantics -- so two dependencies that both have an unresolved version share a single fetch instead of one per occurrence. """ - keys = { - (name, version if version != "unknown" else None) - for name, version in name_versions - } - if not keys: + canon_to_name: dict[tuple[str, str | None], tuple[str, str | None]] = {} + for name, version in name_versions: + norm_version = version if version != "unknown" else None + canon_key: tuple[str, str | None] = ( + str(canonicalize_name(name)), + norm_version, + ) + if canon_key not in canon_to_name: + canon_to_name[canon_key] = (name, norm_version) + + if not canon_to_name: return {} results: dict[tuple[str, str | None], dict[str, Any] | None] = {} with ThreadPoolExecutor( - max_workers=min(_PYPI_MAX_CONCURRENT_FETCHES, len(keys)) + max_workers=min(_PYPI_MAX_CONCURRENT_FETCHES, len(canon_to_name)) ) as pool: futures = { - pool.submit(_fetch_pypi_release_info, name, version): (name, version) - for name, version in keys + pool.submit(_fetch_pypi_release_info, orig_name, norm_ver): k + for k, (orig_name, norm_ver) in canon_to_name.items() } - for future, key in futures.items(): - results[key] = future.result() + for future, k in futures.items(): + results[k] = future.result() return results diff --git a/src/pitloom/extract/_lock_common.py b/src/pitloom/extract/_lock_common.py index 11ba0cd7..0f9cf714 100644 --- a/src/pitloom/extract/_lock_common.py +++ b/src/pitloom/extract/_lock_common.py @@ -44,10 +44,12 @@ "load_lock_toml", "shape_validated_package", "single_exact_pin", + "warn_conflicting_versions", "warn_malformed_entry_not_table", "warn_missing_name", "warn_missing_version", "warn_non_registry_source", + "warn_not_genuine_lock_file", "warn_top_level_key_wrong_type", ] @@ -285,6 +287,39 @@ def single_exact_pin(specifier_set: SpecifierSet) -> str | None: return specifiers[0].version +def warn_conflicting_versions( + lock_file: str, name: str, conflicting_versions: Iterable[str] +) -> None: + """Log the standard ``WARNING:`` when multiple variants of a package disagree + on version in a lock file.""" + log.warning( + "Skipping %s entry %r: pinned to conflicting versions (%s)", + lock_file, + name, + ", ".join(sorted(conflicting_versions)), + ) + + +def warn_not_genuine_lock_file( + lock_path: Path, + table_key: str, + required_key: str, + lock_file: str, + container_type: str = "table", +) -> None: + """Log the standard ``WARNING:`` when a lock file lacks required top-level + metadata.""" + log.warning( + "%s: no top-level %r %s with a %r key -- " + "doesn't look like a genuine %s, ignoring", + lock_path, + table_key, + container_type, + required_key, + lock_file, + ) + + def warn_non_registry_source(lock_file: str, name: str, source_key: str) -> None: """Log the standard ``WARNING:`` for a non-registry-sourced entry (VCS, local path, archive/URL -- anything a bare ``name==version`` diff --git a/src/pitloom/extract/_locked_dependencies.py b/src/pitloom/extract/_locked_dependencies.py index 27a9644c..318baf8f 100644 --- a/src/pitloom/extract/_locked_dependencies.py +++ b/src/pitloom/extract/_locked_dependencies.py @@ -16,16 +16,11 @@ with a bare ``setup.py`` in real projects, never a ``pyproject.toml``, so a cascade wired only inside ``read_pyproject()`` would never see them. -``poetry.lock`` has no extractor entry in :data:`_LOCK_SOURCES` -- it -stays gated inside -:func:`pitloom.extract._pyproject._try_read_poetry`'s -``include_locked_dependencies`` build-stage flag, since it only ever -makes sense alongside a ``[tool.poetry]`` table, which requires -``pyproject.toml`` to exist regardless, so it's applied earlier, before -this cascade runs. It *is* still listed in :data:`_LOCK_SOURCES`, as a -placeholder entry with no extractor, purely to fix its rank in the one -priority order every source (cascade-tried or not) is compared against --- see :func:`apply_locked_dependencies`. +``poetry.lock`` is registered in :data:`_LOCK_SOURCES` with an extractor so +that Poetry 2.0+ PEP 621 projects (which lack a ``[tool.poetry]`` table) and +``setup.py`` projects discover ``poetry.lock`` through this cascade. When +``_try_read_poetry()`` already applied ``poetry.lock`` during pyproject reading, +this cascade preserves that result without redundant re-extraction. """ from __future__ import annotations @@ -39,6 +34,7 @@ from pitloom.extract._lock_common import POETRY_LOCK_SOURCE_NAME from pitloom.extract._pdm_lock import extract_pdm_lock_dependencies from pitloom.extract._pipfile_lock import extract_pipfile_lock_dependencies +from pitloom.extract._poetry_lock import extract_poetry_lock_dependencies from pitloom.extract._pylock import extract_pylock_dependencies from pitloom.extract._requirements_txt import extract_pinned_requirements_dependencies from pitloom.extract._uv_lock import extract_uv_lock_dependencies @@ -85,7 +81,11 @@ def _ignore_expected_name( "resolved_lockfile", ), ("uv.lock", extract_uv_lock_dependencies, "resolved_lockfile"), - (POETRY_LOCK_SOURCE_NAME, None, None), + ( + POETRY_LOCK_SOURCE_NAME, + _ignore_expected_name(extract_poetry_lock_dependencies), + "resolved_lockfile", + ), ( "pdm.lock", _ignore_expected_name(extract_pdm_lock_dependencies), @@ -175,7 +175,10 @@ def apply_locked_dependencies(metadata: ProjectMetadata, project_dir: Path) -> N for rank, (source_name, extractor, method) in enumerate(_LOCK_SOURCES): if extractor is None: - continue # e.g. poetry.lock: applied earlier, not tried here + continue + if previous_source == source_name: + # Already extracted and set (e.g. by _try_read_poetry); keep it. + return if previous_rank is not None and rank > previous_rank: # Every remaining entry ranks below whatever's already set -- # none of them can win, so stop instead of scanning further. diff --git a/src/pitloom/extract/_pdm_lock.py b/src/pitloom/extract/_pdm_lock.py index 5e4155fb..e189bf8f 100644 --- a/src/pitloom/extract/_pdm_lock.py +++ b/src/pitloom/extract/_pdm_lock.py @@ -48,7 +48,9 @@ has_required_top_level_table, load_lock_toml, shape_validated_package, + warn_conflicting_versions, warn_non_registry_source, + warn_not_genuine_lock_file, warn_top_level_key_wrong_type, ) @@ -108,11 +110,7 @@ def extract_pdm_lock_dependencies(project_dir: Path) -> list[str] | None: if data is None: return None if not has_required_top_level_table(data, "metadata", "lock_version", str): - log.warning( - "%s: no top-level 'metadata' table with a 'lock_version' key -- " - "doesn't look like a genuine pdm.lock, ignoring", - lock_path, - ) + warn_not_genuine_lock_file(lock_path, "metadata", "lock_version", "pdm.lock") return None packages = data.get("package", []) @@ -135,11 +133,7 @@ def extract_pdm_lock_dependencies(project_dir: Path) -> list[str] | None: name, version = group[0] conflicting_versions = {v for _, v in group} if len(conflicting_versions) > 1: - log.warning( - "Skipping pdm.lock entry %r: pinned to conflicting versions (%s)", - name, - ", ".join(sorted(conflicting_versions)), - ) + warn_conflicting_versions("pdm.lock", name, conflicting_versions) continue dependencies.append(f"{name}=={version}") return dependencies diff --git a/src/pitloom/extract/_pipfile_lock.py b/src/pitloom/extract/_pipfile_lock.py index 920b8f1b..e5d995ab 100644 --- a/src/pitloom/extract/_pipfile_lock.py +++ b/src/pitloom/extract/_pipfile_lock.py @@ -49,9 +49,11 @@ has_required_top_level_table, load_lock_json, single_exact_pin, + warn_conflicting_versions, warn_missing_name, warn_missing_version, warn_non_registry_source, + warn_not_genuine_lock_file, warn_top_level_key_wrong_type, ) @@ -83,10 +85,8 @@ def extract_pipfile_lock_dependencies(project_dir: Path) -> list[str] | None: if data is None: return None if not has_required_top_level_table(data, "_meta", "pipfile-spec", int): - log.warning( - "%s: no top-level '_meta' object with a 'pipfile-spec' key -- " - "doesn't look like a genuine Pipfile.lock, ignoring", - lock_path, + warn_not_genuine_lock_file( + lock_path, "_meta", "pipfile-spec", "Pipfile.lock", container_type="object" ) return None @@ -111,11 +111,7 @@ def extract_pipfile_lock_dependencies(project_dir: Path) -> list[str] | None: name, version = group[0] conflicting_versions = {v for _, v in group} if len(conflicting_versions) > 1: - log.warning( - "Skipping Pipfile.lock entry %r: pinned to conflicting versions (%s)", - name, - ", ".join(sorted(conflicting_versions)), - ) + warn_conflicting_versions("Pipfile.lock", name, conflicting_versions) continue dependencies.append(f"{name}=={version}") return dependencies diff --git a/src/pitloom/extract/_poetry_lock.py b/src/pitloom/extract/_poetry_lock.py index 75c0a5ab..6f13a033 100644 --- a/src/pitloom/extract/_poetry_lock.py +++ b/src/pitloom/extract/_poetry_lock.py @@ -25,13 +25,17 @@ import logging from pathlib import Path +from typing import Any from pitloom.extract._lock_common import ( default_group_included, + group_versions_by_canonical_name, has_required_top_level_table, load_lock_toml, shape_validated_package, + warn_conflicting_versions, warn_non_registry_source, + warn_not_genuine_lock_file, warn_top_level_key_wrong_type, ) @@ -54,21 +58,16 @@ def extract_poetry_lock_dependencies(project_dir: Path) -> list[str] | None: packages. Packages belonging only to a non-``main`` group (``[tool.poetry.group.dev]`` - and similar) are excluded, matching the same "not a runtime dependency - of the package" policy already applied to direct dependencies -- see - "Dependency groups" in ``working-docs/implementation/poetry-support.md``. - A package listed under both ``main`` and another group still counts. + and similar) or marked ``optional = true`` (optional/extras dependencies) + are excluded from the base runtime dependency set. A package listed under + both ``main`` and another group still counts. """ lock_path = project_dir / "poetry.lock" data = load_lock_toml(lock_path) if data is None: return None if not has_required_top_level_table(data, "metadata", "lock-version", str): - log.warning( - "%s: no top-level 'metadata' table with a 'lock-version' key -- " - "doesn't look like a genuine poetry.lock, ignoring", - lock_path, - ) + warn_not_genuine_lock_file(lock_path, "metadata", "lock-version", "poetry.lock") return None packages = data.get("package", []) @@ -78,21 +77,31 @@ def extract_poetry_lock_dependencies(project_dir: Path) -> list[str] | None: ) return None + main_group_packages = [ + pkg + for pkg in (_main_group_package_or_none(raw) for raw in packages) + if pkg is not None + ] + + pairs = [(pkg["name"], pkg["version"]) for pkg in main_group_packages] + dependencies: list[str] = [] - for pkg in packages: - dep = _pinned_dep_for_package(pkg) - if dep is not None: - dependencies.append(dep) + for group in group_versions_by_canonical_name(pairs).values(): + name, version = group[0] + conflicting_versions = {v for _, v in group} + if len(conflicting_versions) > 1: + warn_conflicting_versions("poetry.lock", name, conflicting_versions) + continue + dependencies.append(f"{name}=={version}") return dependencies _NON_PEP508_SOURCE_TYPES = frozenset({"directory", "file", "git", "url"}) -def _pinned_dep_for_package(pkg: object) -> str | None: - """Return ``name==version`` for one ``[[package]]`` table entry, or - ``None`` when it's malformed, not in the ``main`` group, or sourced - from a non-PyPI location that ``name==version`` can't represent. +def _main_group_package_or_none(pkg: object) -> dict[str, Any] | None: + """Return *pkg* when it's a well-formed, non-optional, main-group, + registry-sourced entry -- ``None`` otherwise. Mirrors the skip policy :func:`pitloom.extract._poetry._poetry_dep_to_pep508` already applies to direct ``[tool.poetry.dependencies]`` entries: a @@ -104,8 +113,9 @@ def _pinned_dep_for_package(pkg: object) -> str | None: if validated is None: return None name = validated["name"] - version = validated["version"] + if validated.get("optional") is True: + return None if not default_group_included(validated, "poetry.lock", _DEFAULT_GROUP, name): return None source = validated.get("source") @@ -113,4 +123,4 @@ def _pinned_dep_for_package(pkg: object) -> str | None: if isinstance(source_type, str) and source_type in _NON_PEP508_SOURCE_TYPES: warn_non_registry_source("poetry.lock", name, source_type) return None - return f"{name}=={version}" + return validated diff --git a/src/pitloom/extract/_pylock.py b/src/pitloom/extract/_pylock.py index 4d085ef0..5152f593 100644 --- a/src/pitloom/extract/_pylock.py +++ b/src/pitloom/extract/_pylock.py @@ -38,6 +38,7 @@ group_versions_by_canonical_name, load_lock_toml, shape_validated_package, + warn_conflicting_versions, warn_non_registry_source, warn_top_level_key_wrong_type, ) @@ -166,11 +167,7 @@ def extract_pylock_dependencies(project_dir: Path) -> list[str] | None: name, version = group[0] conflicting_versions = {v for _, v in group} if len(conflicting_versions) > 1: - log.warning( - "Skipping pylock.toml entry %r: pinned to conflicting versions (%s)", - name, - ", ".join(sorted(conflicting_versions)), - ) + warn_conflicting_versions("pylock.toml", name, conflicting_versions) continue dependencies.append(f"{name}=={version}") return dependencies @@ -296,7 +293,8 @@ def _group_marker_excludes( try: # pylint: disable=protected-access tree = Marker(marker_str)._markers # noqa: SLF001 - except InvalidMarker as exc: + return _evaluate_group_node(tree, environment) is False + except (InvalidMarker, RecursionError, TypeError, ValueError) as exc: log.warning( "Skipping pylock.toml entry %r's 'marker' %r: %s -- treating " "as an unconstrained (included) marker", @@ -305,7 +303,6 @@ def _group_marker_excludes( exc, ) return False - return _evaluate_group_node(tree, environment) is False def _pinned_pair_for_package( diff --git a/src/pitloom/extract/_requirements_txt.py b/src/pitloom/extract/_requirements_txt.py index faaf37d9..d3b5731d 100644 --- a/src/pitloom/extract/_requirements_txt.py +++ b/src/pitloom/extract/_requirements_txt.py @@ -102,7 +102,7 @@ def extract_pinned_requirements_dependencies(project_dir: Path) -> list[str] | N try: raw_text = lock_path.read_text(encoding="utf-8-sig") except (OSError, UnicodeDecodeError) as exc: - log.warning("Failed to read %s: %s", lock_path, exc) + log.warning("Failed to parse %s: %s", lock_path, exc) return None pins: list[tuple[str, str]] = [] @@ -183,12 +183,13 @@ def _pinned_name_version_for_line( non-URL requirement *line*, or ``None`` (having already logged the single ``WARNING:`` naming why) when it disqualifies the whole file.""" if line.startswith(_OPTION_LINE_PREFIX): + option = line.split()[0] log.warning( - "%s:%d: option line %r means this file isn't fully pinned -- " + "%s:%d: option %r means this file isn't fully pinned -- " "ignoring requirements.txt", lock_path, lineno, - line, + option, ) return None try: diff --git a/src/pitloom/extract/_uv_lock.py b/src/pitloom/extract/_uv_lock.py index 4ed59bcf..862ec3d6 100644 --- a/src/pitloom/extract/_uv_lock.py +++ b/src/pitloom/extract/_uv_lock.py @@ -233,6 +233,7 @@ def _collect_transitive_dependencies( """ dependencies: dict[str, str] = {} visited: set[str] = set() + visited_extras: set[tuple[str, str]] = set() queue: deque[object] = deque(root_dependencies) while queue: dep_ref = queue.popleft() @@ -240,27 +241,62 @@ def _collect_transitive_dependencies( if pkg is None: continue canonical_name = canonicalize_name(pkg["name"]) - if canonical_name in visited: - continue - visited.add(canonical_name) - - pin = _pinned_dep_for_package(pkg) - if pin is not None: - dependencies[canonical_name] = pin - - nested = pkg.get("dependencies", []) - if isinstance(nested, list): - queue.extend(nested) - elif nested: - log.warning( - "Skipping uv.lock entry %r nested 'dependencies': " - "expected a list, got %s", - pkg["name"], - type(nested).__name__, + if canonical_name not in visited: + visited.add(canonical_name) + + pin = _pinned_dep_for_package(pkg) + if pin is not None: + dependencies[canonical_name] = pin + + nested = pkg.get("dependencies", []) + if isinstance(nested, list): + queue.extend(nested) + elif nested: + log.warning( + "Skipping uv.lock entry %r nested 'dependencies': " + "expected a list, got %s", + pkg["name"], + type(nested).__name__, + ) + + if isinstance(dep_ref, dict): + _enqueue_requested_extras( + dep_ref, pkg, canonical_name, visited_extras, queue ) return list(dependencies.values()) +def _enqueue_requested_extras( + dep_ref: dict[str, Any], + pkg: dict[str, Any], + canonical_name: str, + visited_extras: set[tuple[str, str]], + queue: deque[object], +) -> None: + """Enqueue dependencies from *pkg*'s ``optional-dependencies`` table + for any extra requested by *dep_ref*, skipping already-visited extras + to guard against cycles.""" + extra_val = dep_ref.get("extra") or dep_ref.get("extras") + if not extra_val: + return + requested_extras = ( + [extra_val] + if isinstance(extra_val, str) + else [e for e in extra_val if isinstance(e, str)] + ) + opt_deps_map = pkg.get("optional-dependencies", {}) + if not isinstance(opt_deps_map, dict): + return + for extra_name in requested_extras: + extra_key = (canonical_name, extra_name) + if extra_key in visited_extras: + continue + visited_extras.add(extra_key) + extra_deps = opt_deps_map.get(extra_name, []) + if isinstance(extra_deps, list): + queue.extend(extra_deps) + + def _pinned_dep_for_package(pkg: dict[str, Any]) -> str | None: """Return ``name==version`` for one top-level ``[[package]]`` entry, or ``None`` when it's non-registry-sourced or missing a version.""" diff --git a/tests/assemble/test_deps_enrichment_names_versions.py b/tests/assemble/test_deps_enrichment_names_versions.py index cee32400..3b286910 100644 --- a/tests/assemble/test_deps_enrichment_names_versions.py +++ b/tests/assemble/test_deps_enrichment_names_versions.py @@ -20,6 +20,7 @@ from __future__ import annotations +import logging from importlib.metadata import PackageNotFoundError import pytest @@ -181,7 +182,55 @@ def test_resolve_version_uses_locked_version_over_installed_when_unpinned( ) assert version == "2.31.0" + assert note == "Version resolved: Project lock file" + + +def test_resolve_version_warns_when_locked_version_violates_declared_constraint( + caplog: pytest.LogCaptureFixture, +) -> None: + """When a lock file's pinned version does not satisfy the declared constraint, + a warning is logged explaining the lockfile version overrides the constraint.""" + with caplog.at_level(logging.WARNING): + version, note = _resolve_version( + "requests", "requests<2.0", locked_version="2.31.0" + ) + + assert version == "2.31.0" + assert note == "Version resolved: Project lock file" + assert "does not satisfy declared constraint" in caplog.text + assert "requests<2.0" in caplog.text + + +def test_resolve_version_exact_pin_wins_over_conflicting_locked_version( + caplog: pytest.LogCaptureFixture, +) -> None: + """When a dependency declares an exact pin (requests==2.30.0), that pin + is authoritative and wins outright over a conflicting locked version (2.31.0), + emitting a warning about the conflict.""" + with caplog.at_level(logging.WARNING): + version, note = _resolve_version( + "requests", "requests==2.30.0", locked_version="2.31.0" + ) + + assert version == "2.30.0" + assert note is None + assert "conflicts with declared exact pin '2.30.0'" in caplog.text + assert "'2.31.0'" in caplog.text + + +def test_resolve_version_exact_pin_matching_locked_version_emits_no_warning( + caplog: pytest.LogCaptureFixture, +) -> None: + """When a dependency's exact pin matches the locked version, no conflict warning + is emitted.""" + with caplog.at_level(logging.WARNING): + version, note = _resolve_version( + "requests", "requests==2.30.0", locked_version="2.30.0" + ) + + assert version == "2.30.0" assert note is None + assert "conflicts with declared exact pin" not in caplog.text # --------------------------------------------------------------------------- @@ -299,6 +348,40 @@ def test_add_dependencies_dedupes_same_resolved_name_and_version() -> None: assert dep_comment.count("extra == 'numpy'") == 2 +def test_add_dependencies_dedupes_case_insensitive_name() -> None: + """Dependencies declared with differing case (e.g. Django vs django) + at the same version must collapse into a single software_Package node, + preserving the first-seen raw name.""" + doc_uuid = compute_doc_uuid("casetest", "1.0", []) + _clear_doc_counters(doc_uuid) + exporter = Spdx3JsonExporter() + ci = _make_ci() + main_pkg = spdx3.software_Package( + spdxId=generate_spdx_id("Package", doc_name="casetest", doc_uuid=doc_uuid), + name="casetest", + creationInfo=ci, + ) + exporter.add_package(main_pkg) + + add_dependencies( + ["Django==4.2.1", "django==4.2.1"], + "Source: pyproject.toml | Field: project.dependencies", + require_spdx_id(main_pkg), + ci, + "casetest", + doc_uuid, + exporter, + offline=True, + ) + + packages = [ + o for o in exporter.object_set.objects if isinstance(o, spdx3.software_Package) + ] + django_packages = [p for p in packages if p.name in ("Django", "django")] + assert len(django_packages) == 1 + assert django_packages[0].name == "Django" + + # --------------------------------------------------------------------------- # _enrich_from_installed -- the discarded-concluded-license-relationship bug # --------------------------------------------------------------------------- diff --git a/tests/extract/test_hatch_hook_metadata.py b/tests/extract/test_hatch_hook_metadata.py index 57dcff74..7bd0d2a9 100644 --- a/tests/extract/test_hatch_hook_metadata.py +++ b/tests/extract/test_hatch_hook_metadata.py @@ -16,6 +16,7 @@ _resolve_hatchling_license_files, metadata_from_hatchling, ) +from pitloom.extract.project import read_project # noqa: E402 from pitloom.plugins.hatch import ( # noqa: E402 _check_hatchling_sbom_support, ) @@ -435,22 +436,56 @@ def test_metadata_from_hatchling_fills_gaps_from_poetry() -> None: assert metadata.keywords == ["from-poetry", "gap-fill"] -def test_metadata_from_hatchling_does_not_leak_poetry_lock_dependencies() -> None: - """Regression: ``poetry.lock`` is a source-stage-only artifact (see - ``pitloom.extract._poetry_lock``'s module docstring) -- the real wheel - Hatchling builds never consults it, so the build hook's ``[tool.poetry]`` - gap-fill path must never populate ``locked_dependencies`` from a - ``poetry.lock`` sitting next to a Hatchling-backed project, even though - ``read_pyproject()`` (the CLI/source-stage path) legitimately does. - """ +@pytest.mark.parametrize( + ("lock_file", "content"), + [ + ( + "poetry.lock", + '[[package]]\nname = "requests"\nversion = "2.31.0"\ngroups = ["main"]\n' + '[metadata]\nlock-version = "2.1"\n', + ), + ( + "pylock.toml", + 'lock-version = "1.0"\ncreated-by = "test"\n' + '[[packages]]\nname = "requests"\nversion = "2.31.0"\n', + ), + ( + "uv.lock", + 'version = 1\nrevision = 1\nrequires-python = ">=3.10"\n' + '[[package]]\nname = "testpkg"\nversion = "0.1.0"\n' + 'source = { editable = "." }\n' + 'dependencies = [{ name = "requests" }]\n\n' + '[[package]]\nname = "requests"\nversion = "2.31.0"\n' + 'source = { registry = "https://pypi.org/simple" }\n', + ), + ( + "pdm.lock", + '[metadata]\nlock_version = "4.5.1"\n' + '[[package]]\nname = "requests"\nversion = "2.31.0"\n' + 'groups = ["default"]\n', + ), + ( + "Pipfile.lock", + '{"_meta": {"pipfile-spec": 6}, ' + '"default": {"requests": {"version": "==2.31.0"}}}', + ), + ( + "requirements.txt", + "requests==2.31.0\n", + ), + ], +) +def test_metadata_from_hatchling_does_not_leak_lock_dependencies( + lock_file: str, content: str +) -> None: + """Lock files are source-stage-only artifacts -- the real wheel Hatchling + builds never consults them, so the build hook's gap-fill path must never + populate locked_dependencies from any lock file sitting next to a + Hatchling-backed project.""" with tempfile.TemporaryDirectory() as tmp: tmp_path = Path(tmp) write_pyproject(tmp_path, POETRY_GAP_FILL_PYPROJECT) - (tmp_path / "poetry.lock").write_text( - '[[package]]\nname = "requests"\nversion = "2.31.0"\ngroups = ["main"]\n' - '[metadata]\nlock-version = "2.1"\n', - encoding="utf-8", - ) + (tmp_path / lock_file).write_text(content, encoding="utf-8") hatch_pm = hatchling_metadata_core.ProjectMetadata( str(tmp_path), PluginManager() @@ -460,9 +495,11 @@ def test_metadata_from_hatchling_does_not_leak_poetry_lock_dependencies() -> Non assert metadata.locked_dependencies == [] assert "locked_dependencies" not in metadata.provenance - # The CLI/source-stage path, by contrast, legitimately picks it up. - cli_metadata, _config = read_pyproject(tmp_path / "pyproject.toml") - assert cli_metadata.locked_dependencies == ["requests==2.31.0"] + # Companion assertion: absent the isolation boundary (via read_project's + # default path), the same directory DOES resolve the lock file -- guards + # against a vacuous pass where the lock fixture is broken or not found. + direct, _, _ = read_project(tmp_path) + assert direct.locked_dependencies == ["requests==2.31.0"] def test_check_hatchling_sbom_support_raises_when_metadata_missing() -> None: diff --git a/tests/extract/test_lock_common.py b/tests/extract/test_lock_common.py index 0ea7e6c3..c98360aa 100644 --- a/tests/extract/test_lock_common.py +++ b/tests/extract/test_lock_common.py @@ -300,6 +300,11 @@ def test_single_exact_pin_accepts_exact_operators(operator: str) -> None: assert single_exact_pin(SpecifierSet(f"{operator}2.31.0")) == "2.31.0" +def test_single_exact_pin_accepts_arbitrary_equality_non_pep440_version() -> None: + """The === operator explicitly supports non-PEP 440 version strings.""" + assert single_exact_pin(SpecifierSet("===2021.01.01-legacy")) == "2021.01.01-legacy" + + def test_single_exact_pin_rejects_wildcard() -> None: assert single_exact_pin(SpecifierSet("==2.31.*")) is None diff --git a/tests/extract/test_pdm_lock.py b/tests/extract/test_pdm_lock.py index e834b723..a7778735 100644 --- a/tests/extract/test_pdm_lock.py +++ b/tests/extract/test_pdm_lock.py @@ -104,7 +104,7 @@ def test_metadata_table_missing_lock_version_returns_none_and_warns( assert "doesn't look like a genuine pdm.lock" in caplog.text -def test_malformed_toml_returns_empty_list_and_warns( +def test_malformed_toml_returns_none_and_warns( caplog: pytest.LogCaptureFixture, ) -> None: with tempfile.TemporaryDirectory() as tmp: @@ -114,11 +114,11 @@ def test_malformed_toml_returns_empty_list_and_warns( with caplog.at_level(logging.WARNING): result = extract_pdm_lock_dependencies(tmp_path) - assert not result + assert result is None assert "Failed to parse" in caplog.text -def test_package_key_not_a_list_returns_empty_list_and_warns( +def test_package_key_not_a_list_returns_none_and_warns( caplog: pytest.LogCaptureFixture, ) -> None: with tempfile.TemporaryDirectory() as tmp: @@ -128,7 +128,7 @@ def test_package_key_not_a_list_returns_empty_list_and_warns( with caplog.at_level(logging.WARNING): result = extract_pdm_lock_dependencies(tmp_path) - assert not result + assert result is None assert "expected a list" in caplog.text diff --git a/tests/extract/test_pipfile_lock.py b/tests/extract/test_pipfile_lock.py index 519d3686..3ac51cc2 100644 --- a/tests/extract/test_pipfile_lock.py +++ b/tests/extract/test_pipfile_lock.py @@ -42,7 +42,7 @@ def test_no_lock_file_returns_none() -> None: assert extract_pipfile_lock_dependencies(Path(tmp)) is None -def test_malformed_json_returns_empty_list_and_warns( +def test_malformed_json_returns_none_and_warns( caplog: pytest.LogCaptureFixture, ) -> None: with tempfile.TemporaryDirectory() as tmp: @@ -52,7 +52,7 @@ def test_malformed_json_returns_empty_list_and_warns( with caplog.at_level(logging.WARNING): result = extract_pipfile_lock_dependencies(tmp_path) - assert not result + assert result is None assert "Failed to parse" in caplog.text @@ -99,7 +99,7 @@ def test_meta_missing_pipfile_spec_returns_none_and_warns( assert "doesn't look like a genuine Pipfile.lock" in caplog.text -def test_default_section_not_a_dict_returns_empty_list_and_warns( +def test_default_section_not_a_dict_returns_none_and_warns( caplog: pytest.LogCaptureFixture, ) -> None: with tempfile.TemporaryDirectory() as tmp: @@ -109,7 +109,7 @@ def test_default_section_not_a_dict_returns_empty_list_and_warns( with caplog.at_level(logging.WARNING): result = extract_pipfile_lock_dependencies(tmp_path) - assert not result + assert result is None assert "expected a table" in caplog.text diff --git a/tests/extract/test_poetry_lock.py b/tests/extract/test_poetry_lock.py index 57d7f309..909029cc 100644 --- a/tests/extract/test_poetry_lock.py +++ b/tests/extract/test_poetry_lock.py @@ -18,7 +18,7 @@ import pytest from pitloom.extract._poetry_lock import ( - _pinned_dep_for_package, + _main_group_package_or_none, extract_poetry_lock_dependencies, ) from pitloom.extract._pyproject import read_pyproject @@ -119,7 +119,7 @@ def test_metadata_table_missing_lock_version_returns_none_and_warns( assert "doesn't look like a genuine poetry.lock" in caplog.text -def test_malformed_toml_returns_empty_list_and_warns( +def test_malformed_toml_returns_none_and_warns( caplog: pytest.LogCaptureFixture, ) -> None: with tempfile.TemporaryDirectory() as tmp: @@ -129,7 +129,7 @@ def test_malformed_toml_returns_empty_list_and_warns( with caplog.at_level(logging.WARNING): result = extract_poetry_lock_dependencies(tmp_path) - assert not result + assert result is None assert "Failed to parse" in caplog.text @@ -202,22 +202,45 @@ def test_malformed_groups_field_skipped_and_warns( assert "'groups'" in caplog.text -def test_package_table_not_a_list_returns_empty_list() -> None: - """A ``poetry.lock`` where top-level ``package`` isn't an array of - tables (malformed/unexpected shape) must degrade to an empty list, - not raise.""" +def test_optional_package_excluded() -> None: + """A package with optional = true is an extra, not a default runtime + dependency -- must be excluded.""" with tempfile.TemporaryDirectory() as tmp: tmp_path = Path(tmp) - _write_lock(tmp_path, 'package = "not-a-list"\n') + _write_lock( + tmp_path, + '[[package]]\nname = "requests"\nversion = "2.31.0"\n' + 'optional = true\ngroups = ["main"]\n', + ) assert not extract_poetry_lock_dependencies(tmp_path) -def test_pinned_dep_for_package_non_dict_entry_returns_none() -> None: +def test_conflicting_versions_for_same_package_warns_and_excludes( + caplog: pytest.LogCaptureFixture, +) -> None: + """When a package is locked at multiple conflicting versions under main group, + warn_conflicting_versions is emitted and the package is skipped.""" + with tempfile.TemporaryDirectory() as tmp: + tmp_path = Path(tmp) + _write_lock( + tmp_path, + '[[package]]\nname = "requests"\nversion = "2.31.0"\ngroups = ["main"]\n\n' + '[[package]]\nname = "requests"\nversion = "2.32.0"\ngroups = ["main"]\n', + ) + + with caplog.at_level(logging.WARNING): + result = extract_poetry_lock_dependencies(tmp_path) + + assert not result + assert "conflicting versions" in caplog.text + + +def test_main_group_package_or_none_non_dict_entry_returns_none() -> None: """A ``[[package]]`` entry that isn't a table (defensive guard against a malformed lock file) is skipped, not a crash.""" - assert _pinned_dep_for_package("not-a-dict") is None - assert _pinned_dep_for_package(["still", "not", "a", "dict"]) is None + assert _main_group_package_or_none("not-a-dict") is None + assert _main_group_package_or_none(["still", "not", "a", "dict"]) is None def test_malformed_package_entry_skipped() -> None: @@ -295,7 +318,7 @@ def test_package_table_not_a_list_warns(caplog: pytest.LogCaptureFixture) -> Non with caplog.at_level(logging.WARNING): result = extract_poetry_lock_dependencies(tmp_path) - assert not result + assert result is None assert "expected a list" in caplog.text diff --git a/tests/extract/test_project.py b/tests/extract/test_project.py index 51a8fd17..c4f89fcf 100644 --- a/tests/extract/test_project.py +++ b/tests/extract/test_project.py @@ -287,3 +287,64 @@ def test_read_project_include_locked_dependencies_false_also_skips_poetry_lock( assert metadata.locked_dependencies == [] assert "locked_dependencies" not in metadata.provenance + + +@pytest.mark.parametrize( + ("lock_file", "content"), + [ + ( + "poetry.lock", + '[[package]]\nname = "requests"\nversion = "2.31.0"\ngroups = ["main"]\n' + '[metadata]\nlock-version = "2.1"\n', + ), + ( + "pylock.toml", + 'lock-version = "1.0"\ncreated-by = "test"\n' + '[[packages]]\nname = "requests"\nversion = "2.31.0"\n', + ), + ( + "uv.lock", + 'version = 1\nrevision = 1\nrequires-python = ">=3.10"\n' + '[[package]]\nname = "pkg"\nversion = "1.0.0"\n' + 'source = { editable = "." }\n' + 'dependencies = [{ name = "requests" }]\n\n' + '[[package]]\nname = "requests"\nversion = "2.31.0"\n' + 'source = { registry = "https://pypi.org/simple" }\n', + ), + ( + "pdm.lock", + '[metadata]\nlock_version = "4.5.1"\n' + '[[package]]\nname = "requests"\nversion = "2.31.0"\n' + 'groups = ["default"]\n', + ), + ( + "Pipfile.lock", + '{"_meta": {"pipfile-spec": 6}, ' + '"default": {"requests": {"version": "==2.31.0"}}}', + ), + ( + "requirements.txt", + "requests==2.31.0\n", + ), + ], +) +def test_read_project_include_locked_dependencies_false_skips_all_lock_formats( + tmp_path: Path, lock_file: str, content: str +) -> None: + """`include_locked_dependencies=False` must skip every supported lock format.""" + (tmp_path / "pyproject.toml").write_text( + '[project]\nname = "pkg"\nversion = "1.0.0"\n', encoding="utf-8" + ) + (tmp_path / lock_file).write_text(content, encoding="utf-8") + + metadata, _pitloom_config, _config_path = read_project( + tmp_path, include_locked_dependencies=False + ) + + assert metadata.locked_dependencies == [] + assert "locked_dependencies" not in metadata.provenance + + # Companion assertion: with include_locked_dependencies enabled (the default), + # the lock file is discovered and resolved -- guarding against a vacuous pass. + normal_metadata, _, _ = read_project(tmp_path) + assert normal_metadata.locked_dependencies == ["requests==2.31.0"] diff --git a/tests/extract/test_pylock.py b/tests/extract/test_pylock.py index 82befb89..63efc15d 100644 --- a/tests/extract/test_pylock.py +++ b/tests/extract/test_pylock.py @@ -68,7 +68,7 @@ def test_valid_lock_with_no_packages_returns_empty_list_not_none() -> None: assert extract_pylock_dependencies(tmp_path) == [] -def test_malformed_toml_returns_empty_list_and_warns( +def test_malformed_toml_returns_none_and_warns( caplog: pytest.LogCaptureFixture, ) -> None: with tempfile.TemporaryDirectory() as tmp: @@ -80,11 +80,11 @@ def test_malformed_toml_returns_empty_list_and_warns( with caplog.at_level(logging.WARNING): result = extract_pylock_dependencies(tmp_path) - assert not result + assert result is None assert "Failed to parse" in caplog.text -def test_missing_lock_version_returns_empty_list_and_warns( +def test_missing_lock_version_returns_none_and_warns( caplog: pytest.LogCaptureFixture, ) -> None: with tempfile.TemporaryDirectory() as tmp: @@ -97,7 +97,7 @@ def test_missing_lock_version_returns_empty_list_and_warns( with caplog.at_level(logging.WARNING): result = extract_pylock_dependencies(tmp_path) - assert not result + assert result is None assert "lock-version" in caplog.text @@ -209,7 +209,7 @@ def test_same_name_conflicting_versions_skipped_and_warns( assert "pinned to conflicting versions" in caplog.text -def test_packages_key_not_a_list_returns_empty_list_and_warns( +def test_packages_key_not_a_list_returns_none_and_warns( caplog: pytest.LogCaptureFixture, ) -> None: with tempfile.TemporaryDirectory() as tmp: @@ -219,10 +219,33 @@ def test_packages_key_not_a_list_returns_empty_list_and_warns( with caplog.at_level(logging.WARNING): result = extract_pylock_dependencies(tmp_path) - assert not result + assert result is None assert "expected a list" in caplog.text +def test_pathological_nested_marker_does_not_crash_and_defaults_to_included( + caplog: pytest.LogCaptureFixture, +) -> None: + """A deeply nested or pathological marker must not cause a RecursionError or crash, + and should safely degrade to treating the package as included with a warning.""" + with tempfile.TemporaryDirectory() as tmp: + tmp_path = Path(tmp) + nested = "'default' in dependency_groups" + for _ in range(300): + nested = f"({nested} and 'default' in dependency_groups)" + _write_lock( + tmp_path, + 'default-groups = ["default"]\n' + '[[packages]]\nname = "deep-marker-pkg"\nversion = "1.0.0"\n' + f'marker = "{nested}"\n', + ) + + with caplog.at_level(logging.WARNING): + result = extract_pylock_dependencies(tmp_path) + + assert result == ["deep-marker-pkg==1.0.0"] + + def test_pinned_pair_for_package_non_dict_entry_returns_none() -> None: assert _pinned_pair_for_package("not-a-dict", _NO_GROUPS_ENV) is None assert ( diff --git a/tests/extract/test_requirements_txt.py b/tests/extract/test_requirements_txt.py index f4550595..edb0ce07 100644 --- a/tests/extract/test_requirements_txt.py +++ b/tests/extract/test_requirements_txt.py @@ -122,7 +122,7 @@ def test_duplicate_name_conflicting_versions_disqualifies_whole_file( with caplog.at_level(logging.WARNING): result = extract_pinned_requirements_dependencies(tmp_path) - assert not result + assert result is None assert "conflicting versions" in caplog.text assert "requests" in caplog.text @@ -151,11 +151,11 @@ def test_duplicate_name_different_casing_conflicting_versions_disqualifies_whole with caplog.at_level(logging.WARNING): result = extract_pinned_requirements_dependencies(tmp_path) - assert not result + assert result is None assert "conflicting versions" in caplog.text -def test_undecodable_file_returns_empty_list_and_warns( +def test_undecodable_file_returns_none_and_warns( caplog: pytest.LogCaptureFixture, ) -> None: with tempfile.TemporaryDirectory() as tmp: @@ -165,8 +165,8 @@ def test_undecodable_file_returns_empty_list_and_warns( with caplog.at_level(logging.WARNING): result = extract_pinned_requirements_dependencies(tmp_path) - assert not result - assert "Failed to read" in caplog.text + assert result is None + assert "Failed to parse" in caplog.text def test_three_way_duplicate_conflicting_versions_disqualifies_whole_file( @@ -181,7 +181,7 @@ def test_three_way_duplicate_conflicting_versions_disqualifies_whole_file( with caplog.at_level(logging.WARNING): result = extract_pinned_requirements_dependencies(tmp_path) - assert not result + assert result is None assert "conflicting versions" in caplog.text @@ -243,7 +243,7 @@ def test_hash_annotated_continuation_still_disqualifies_whole_file( with caplog.at_level(logging.WARNING): result = extract_pinned_requirements_dependencies(tmp_path) - assert not result + assert result is None assert "malformed requirement line" in caplog.text @@ -257,7 +257,7 @@ def test_unpinned_bare_name_disqualifies_whole_file( with caplog.at_level(logging.WARNING): result = extract_pinned_requirements_dependencies(tmp_path) - assert not result + assert result is None assert "isn't pinned to a single exact version" in caplog.text @@ -274,7 +274,7 @@ def test_ranged_specifier_disqualifies_whole_file( with caplog.at_level(logging.WARNING): result = extract_pinned_requirements_dependencies(tmp_path) - assert not result + assert result is None assert "isn't pinned to a single exact version" in caplog.text @@ -288,7 +288,7 @@ def test_prefix_match_specifier_disqualifies_whole_file( with caplog.at_level(logging.WARNING): result = extract_pinned_requirements_dependencies(tmp_path) - assert not result + assert result is None assert "isn't pinned to a single exact version" in caplog.text @@ -314,7 +314,7 @@ def test_url_requirement_disqualifies_whole_file_even_when_tag_shaped( with caplog.at_level(logging.WARNING): result = extract_pinned_requirements_dependencies(tmp_path) - assert not result + assert result is None assert "direct URL reference" in caplog.text @@ -332,10 +332,32 @@ def test_option_line_disqualifies_whole_file( with caplog.at_level(logging.WARNING): result = extract_pinned_requirements_dependencies(tmp_path) - assert not result + assert result is None assert "isn't fully pinned" in caplog.text +def test_option_line_with_credentials_redacts_line_in_log( + caplog: pytest.LogCaptureFixture, +) -> None: + """Option lines containing credentials (e.g. basic auth in --extra-index-url) + must only log the option token, never the URL or credentials.""" + with tempfile.TemporaryDirectory() as tmp: + tmp_path = Path(tmp) + _write_requirements( + tmp_path, + "idna==3.7\n" + "--extra-index-url https://user:secret_token@custom-pypi.org/simple\n", + ) + + with caplog.at_level(logging.WARNING): + result = extract_pinned_requirements_dependencies(tmp_path) + + assert result is None + assert "--extra-index-url" in caplog.text + assert "secret_token" not in caplog.text + assert "user" not in caplog.text + + def test_bare_url_and_legacy_vcs_syntax_disqualify_as_malformed( caplog: pytest.LogCaptureFixture, ) -> None: @@ -353,7 +375,7 @@ def test_bare_url_and_legacy_vcs_syntax_disqualify_as_malformed( with caplog.at_level(logging.WARNING): result = extract_pinned_requirements_dependencies(tmp_path) - assert not result + assert result is None assert "malformed requirement line" in caplog.text @@ -367,7 +389,7 @@ def test_malformed_requirement_line_disqualifies_whole_file( with caplog.at_level(logging.WARNING): result = extract_pinned_requirements_dependencies(tmp_path) - assert not result + assert result is None assert "malformed requirement line" in caplog.text @@ -385,7 +407,7 @@ def test_first_disqualifying_line_named_in_warning( assert "urllib3" not in caplog.text -def test_unreadable_file_returns_empty_list_and_warns( +def test_unreadable_file_returns_none_and_warns( caplog: pytest.LogCaptureFixture, ) -> None: with tempfile.TemporaryDirectory() as tmp: @@ -396,8 +418,8 @@ def test_unreadable_file_returns_empty_list_and_warns( with caplog.at_level(logging.WARNING): result = extract_pinned_requirements_dependencies(tmp_path) - assert not result - assert "Failed to read" in caplog.text + assert result is None + assert "Failed to parse" in caplog.text # --- read_project() cascade integration ------------------------------- diff --git a/tests/extract/test_uv_lock.py b/tests/extract/test_uv_lock.py index b82dbe37..6f165862 100644 --- a/tests/extract/test_uv_lock.py +++ b/tests/extract/test_uv_lock.py @@ -47,7 +47,7 @@ def test_no_lock_file_returns_none() -> None: assert extract_uv_lock_dependencies(Path(tmp)) is None -def test_malformed_toml_returns_empty_list_and_warns( +def test_malformed_toml_returns_none_and_warns( caplog: pytest.LogCaptureFixture, ) -> None: with tempfile.TemporaryDirectory() as tmp: @@ -57,11 +57,11 @@ def test_malformed_toml_returns_empty_list_and_warns( with caplog.at_level(logging.WARNING): result = extract_uv_lock_dependencies(tmp_path) - assert not result + assert result is None assert "Failed to parse" in caplog.text -def test_package_key_not_a_list_returns_empty_list_and_warns( +def test_package_key_not_a_list_returns_none_and_warns( caplog: pytest.LogCaptureFixture, ) -> None: with tempfile.TemporaryDirectory() as tmp: @@ -71,11 +71,11 @@ def test_package_key_not_a_list_returns_empty_list_and_warns( with caplog.at_level(logging.WARNING): result = extract_uv_lock_dependencies(tmp_path) - assert not result + assert result is None assert "expected a list" in caplog.text -def test_no_root_package_returns_empty_list_and_warns( +def test_no_root_package_returns_none_and_warns( caplog: pytest.LogCaptureFixture, ) -> None: """A uv.lock with no `editable`/`virtual`-sourced entry has no @@ -92,7 +92,7 @@ def test_no_root_package_returns_empty_list_and_warns( with caplog.at_level(logging.WARNING): result = extract_uv_lock_dependencies(tmp_path) - assert not result + assert result is None assert "no project package found" in caplog.text @@ -123,7 +123,7 @@ def test_empty_string_expected_name_falls_back_to_pyproject_toml() -> None: ] -def test_root_dependencies_not_a_list_returns_empty_list_and_warns( +def test_root_dependencies_not_a_list_returns_none_and_warns( caplog: pytest.LogCaptureFixture, ) -> None: with tempfile.TemporaryDirectory() as tmp: @@ -137,7 +137,7 @@ def test_root_dependencies_not_a_list_returns_empty_list_and_warns( with caplog.at_level(logging.WARNING): result = extract_uv_lock_dependencies(tmp_path) - assert not result + assert result is None assert "expected a list" in caplog.text @@ -165,6 +165,28 @@ def test_simple_dependency_resolved() -> None: assert extract_uv_lock_dependencies(tmp_path) == ["requests==2.31.0"] +def test_dependency_with_extra_traverses_optional_dependencies() -> None: + """When a dependency specifies an extra (e.g. coverage[toml]), dependencies + under pkg['optional-dependencies'][extra] are traversed and resolved.""" + with tempfile.TemporaryDirectory() as tmp: + tmp_path = Path(tmp) + _write_lock( + tmp_path, + _ROOT_HEADER + + 'dependencies = [{ name = "coverage", extra = ["toml"] }]\n\n' + '[[package]]\nname = "coverage"\nversion = "7.5.0"\n' + 'source = { registry = "https://pypi.org/simple" }\n' + "[package.optional-dependencies]\n" + 'toml = [{ name = "tomli" }]\n\n' + '[[package]]\nname = "tomli"\nversion = "2.0.1"\n' + 'source = { registry = "https://pypi.org/simple" }\n', + ) + + result = extract_uv_lock_dependencies(tmp_path) + + assert sorted(result or []) == ["coverage==7.5.0", "tomli==2.0.1"] + + def test_dependency_with_marker_but_no_inline_version_still_resolved() -> None: """A `marker` field alone (conditional presence, not a version conflict) doesn't block resolution -- same "no marker evaluation, diff --git a/tests/extract/test_uv_lock_integration.py b/tests/extract/test_uv_lock_integration.py index c8434f9e..f6aeeadb 100644 --- a/tests/extract/test_uv_lock_integration.py +++ b/tests/extract/test_uv_lock_integration.py @@ -180,12 +180,18 @@ def test_real_world_fastapi_cli() -> None: names = {dep.split("==", maxsplit=1)[0] for dep in metadata.locked_dependencies} assert names == { "annotated-doc", + "anyio", "click", "colorama", + "exceptiongroup", "h11", + "httptools", + "idna", "markdown-it-py", "mdurl", "pygments", + "python-dotenv", + "pyyaml", "rich", "rich-toolkit", "shellingham", @@ -193,6 +199,9 @@ def test_real_world_fastapi_cli() -> None: "typer", "typing-extensions", "uvicorn", + "uvloop", + "watchfiles", + "websockets", } diff --git a/working-docs/design/roadmap.md b/working-docs/design/roadmap.md index ecb731e0..3f5a0ce7 100644 --- a/working-docs/design/roadmap.md +++ b/working-docs/design/roadmap.md @@ -1,6 +1,6 @@ --- Created: 2026-04-14 -Last-Modified: 2026-09-05 +Last-Modified: 2026-09-06 SPDX-FileCopyrightText: 2026-present Arthit Suriyawongkul SPDX-FileType: DOCUMENTATION SPDX-License-Identifier: CC0-1.0 @@ -164,6 +164,16 @@ table in [non-hatchling-file-discovery.md](non-hatchling-file-discovery.md)); for the two formats with their own dedicated doc. [lock-files.md](./lock-files.md) (`pixi.lock`/`conda-lock.yml` remain future work there, Phase 2). +- [ ] **CLI option `--no-locked-dependencies`** -- opt-out flag and + `[tool.pitloom] locked-dependencies = false` configuration allowing users + and CI pipelines to disable automatic lock file discovery, falling back to + direct dependencies and environment introspection. Wire across CLI, + library API, and build backend hooks. +- [ ] **Preserve lockfile hashes in `--offline` mode** -- retain package + SHA-256 digests parsed from lock files (`pylock.toml`, `uv.lock`, `pdm.lock`, + `Pipfile.lock`, etc.) so that `--offline` mode can populate SPDX 3 + `verifiedUsing` integrity checksums without requiring online PyPI JSON API + enrichment lookups. ### PEP 770 / embed-wheel From e1b801738db2ff56db530cfb967cbcc71657f0e3 Mon Sep 17 00:00:00 2001 From: Arthit Suriyawongkul Date: Sun, 6 Sep 2026 14:36:11 +0700 Subject: [PATCH 19/35] Fix test fixture timestamp bug Signed-off-by: Arthit Suriyawongkul --- tests/assemble/conftest.py | 25 +++++++++++++++++++++---- 1 file changed, 21 insertions(+), 4 deletions(-) diff --git a/tests/assemble/conftest.py b/tests/assemble/conftest.py index 8d5aed06..70a72330 100644 --- a/tests/assemble/conftest.py +++ b/tests/assemble/conftest.py @@ -26,6 +26,7 @@ import base64 import hashlib import json +import os import zipfile from collections.abc import Iterator from datetime import datetime, timezone @@ -249,11 +250,27 @@ def _rec_entry(arcname: str, payload: bytes) -> str: f"{dist_info}/RECORD,,", ] record_content = "\n".join(records).encode("utf-8") + b"\n" + fixed_time = (2026, 1, 1, 0, 0, 0) + raw_epoch = os.environ.get("SOURCE_DATE_EPOCH") + if raw_epoch: + try: + ts = int(raw_epoch) + dt = datetime.fromtimestamp(ts, tz=timezone.utc) + if dt.year >= 1980: + fixed_time = (dt.year, dt.month, dt.day, dt.hour, dt.minute, dt.second) + except (ValueError, OverflowError): + pass with zipfile.ZipFile(wheel_path, "w", compression=zipfile.ZIP_DEFLATED) as zf: - zf.writestr(f"{name}/__init__.py", init_code) - zf.writestr(f"{dist_info}/METADATA", metadata_content) - zf.writestr(f"{dist_info}/WHEEL", wheel_content) - zf.writestr(f"{dist_info}/RECORD", record_content) + for arcname, payload in ( + (f"{name}/__init__.py", init_code), + (f"{dist_info}/METADATA", metadata_content), + (f"{dist_info}/WHEEL", wheel_content), + (f"{dist_info}/RECORD", record_content), + ): + zinfo = zipfile.ZipInfo(arcname, date_time=fixed_time) + zinfo.compress_type = zipfile.ZIP_DEFLATED + zinfo.external_attr = 0o600 << 16 + zf.writestr(zinfo, payload) return wheel_path From 250dee032a1482974a54d5d0c09b38c54a709b69 Mon Sep 17 00:00:00 2001 From: Arthit Suriyawongkul Date: Sun, 6 Sep 2026 16:31:44 +0700 Subject: [PATCH 20/35] Lock file bug fixes Signed-off-by: Arthit Suriyawongkul --- src/pitloom/assemble/spdx3/deps_installed.py | 79 +++++++++------- src/pitloom/assemble/spdx3/deps_pypi.py | 31 ++++--- src/pitloom/assemble/spdx3/document.py | 18 ++-- src/pitloom/core/models.py | 2 +- src/pitloom/extract/_lock_common.py | 38 ++++++-- src/pitloom/extract/_locked_dependencies.py | 33 +++---- src/pitloom/extract/_pdm_lock.py | 2 +- src/pitloom/extract/_pipfile_lock.py | 54 ++++++----- src/pitloom/extract/_pylock.py | 25 +++-- src/pitloom/extract/_requirements_txt.py | 46 +++++++--- src/pitloom/extract/_uv_lock.py | 24 ++++- tests/assemble/conftest.py | 37 +++++--- .../test_deps_enrichment_names_versions.py | 51 +++++++++++ .../assemble/test_deps_enrichment_prefetch.py | 26 ++++++ .../assemble/test_deps_locked_dependencies.py | 30 +++++- tests/extract/test_hatch_hook_locked_deps.py | 91 +++++++++++++++++++ tests/extract/test_hatch_hook_metadata.py | 67 -------------- tests/extract/test_lock_common.py | 20 +++- tests/extract/test_locked_dependencies.py | 5 + tests/extract/test_project.py | 6 ++ tests/extract/test_requirements_txt.py | 54 +++++++++-- working-docs/design/roadmap.md | 21 ++--- 22 files changed, 514 insertions(+), 246 deletions(-) create mode 100644 tests/extract/test_hatch_hook_locked_deps.py diff --git a/src/pitloom/assemble/spdx3/deps_installed.py b/src/pitloom/assemble/spdx3/deps_installed.py index 5f3b8c5f..1c755ab4 100644 --- a/src/pitloom/assemble/spdx3/deps_installed.py +++ b/src/pitloom/assemble/spdx3/deps_installed.py @@ -16,6 +16,7 @@ from importlib.metadata import version as get_package_version from packaging.requirements import InvalidRequirement, Requirement +from packaging.version import InvalidVersion from spdx_python_model.bindings import v3_0_1 as spdx3 from pitloom.assemble.spdx3.deps_license import _apply_license @@ -51,6 +52,26 @@ def _parse_dep_name(dep: str) -> str: return dep.strip() +def _extract_exact_pin(dep: str) -> tuple[Requirement | None, str | None]: + """Parse *dep* into a Requirement and extract any exact pin (== or ===).""" + try: + req = Requirement(dep) + except InvalidRequirement: + dep_spec = dep.split(";", 1)[0] if ";" in dep else dep + for op in ("===", "=="): + if op in dep_spec: + pin = dep_spec.split(op)[1].strip() + if pin: + return None, pin + break + return None, None + + pinned = [spec.version for spec in req.specifier if spec.operator in ("==", "===")] + if pinned: + return req, pinned[0] + return req, None + + def _resolve_version( dep_name: str, dep: str, locked_version: str | None = None ) -> tuple[str, str | None]: @@ -72,42 +93,34 @@ def _resolve_version( The installed-environment lookup is a fallback for the case where neither pins an exact version. """ - req: Requirement | None = None - try: - req = Requirement(dep) - except InvalidRequirement: - unparseable = True - else: - unparseable = False - - if req is not None: - pinned = [ - spec.version for spec in req.specifier if spec.operator in ("==", "===") - ] - if pinned: - if locked_version is not None and locked_version != pinned[0]: - log.warning( - "Locked version %r for dependency %r conflicts with declared" - " exact pin %r -- using declared pin", - locked_version, - dep_name, - pinned[0], - ) - return pinned[0], None - - if locked_version is not None: - if ( - req is not None - and req.specifier - and not req.specifier.contains(locked_version, prereleases=True) - ): + req, pinned = _extract_exact_pin(dep) + if pinned is not None: + if locked_version is not None and locked_version != pinned: log.warning( - "Locked version %r for dependency %r does not satisfy declared" - " constraint %r -- using locked version", + "Locked version %r for dependency %r conflicts with declared" + " exact pin %r -- using declared pin", locked_version, dep_name, - dep, + pinned, ) + return pinned, None + + if locked_version is not None: + if req is not None and req.specifier: + satisfies = True + try: + satisfies = req.specifier.contains(locked_version, prereleases=True) + # pylint: disable-next=broad-exception-caught + except (InvalidVersion, Exception): + satisfies = False + if not satisfies: + log.warning( + "Locked version %r for dependency %r does not satisfy declared" + " constraint %r -- using locked version", + locked_version, + dep_name, + dep, + ) return locked_version, "Version resolved: Project lock file" try: @@ -117,8 +130,6 @@ def _resolve_version( except PackageNotFoundError: pass - if unparseable and "==" in dep: - return dep.split("==")[1].strip(), None return "unknown", None diff --git a/src/pitloom/assemble/spdx3/deps_pypi.py b/src/pitloom/assemble/spdx3/deps_pypi.py index b0bbbe61..72548a95 100644 --- a/src/pitloom/assemble/spdx3/deps_pypi.py +++ b/src/pitloom/assemble/spdx3/deps_pypi.py @@ -133,26 +133,27 @@ def _prefetch_pypi_release_infos( semantics -- so two dependencies that both have an unresolved version share a single fetch instead of one per occurrence. """ - canon_to_name: dict[tuple[str, str | None], tuple[str, str | None]] = {} - for name, version in name_versions: - norm_version = version if version != "unknown" else None - canon_key: tuple[str, str | None] = ( - str(canonicalize_name(name)), - norm_version, - ) - if canon_key not in canon_to_name: - canon_to_name[canon_key] = (name, norm_version) - - if not canon_to_name: + canon_keys: set[tuple[str, str | None]] = { + (str(canonicalize_name(name)), version if version != "unknown" else None) + for name, version in name_versions + } + if not canon_keys: return {} results: dict[tuple[str, str | None], dict[str, Any] | None] = {} with ThreadPoolExecutor( - max_workers=min(_PYPI_MAX_CONCURRENT_FETCHES, len(canon_to_name)) + max_workers=min(_PYPI_MAX_CONCURRENT_FETCHES, len(canon_keys)) ) as pool: futures = { - pool.submit(_fetch_pypi_release_info, orig_name, norm_ver): k - for k, (orig_name, norm_ver) in canon_to_name.items() + pool.submit(_fetch_pypi_release_info, canon_name, norm_ver): ( + canon_name, + norm_ver, + ) + for canon_name, norm_ver in canon_keys } for future, k in futures.items(): - results[k] = future.result() + try: + results[k] = future.result() + # pylint: disable-next=broad-exception-caught + except Exception: + results[k] = None return results diff --git a/src/pitloom/assemble/spdx3/document.py b/src/pitloom/assemble/spdx3/document.py index bed95246..97949dc7 100644 --- a/src/pitloom/assemble/spdx3/document.py +++ b/src/pitloom/assemble/spdx3/document.py @@ -18,7 +18,7 @@ from __future__ import annotations -from datetime import datetime +from datetime import datetime, timezone from typing import Any from packaging.utils import canonicalize_name @@ -44,6 +44,7 @@ add_dependencies, add_phantom_dependencies, ) +from pitloom.assemble.spdx3.deps_installed import _extract_exact_pin from pitloom.assemble.spdx3.deps_license import ( _add_license_noassertion, build_license_elements, @@ -115,7 +116,12 @@ def _build_main_package( main_package.software_downloadLocation = download_location if metadata.urls.get("Homepage"): main_package.software_homePage = metadata.urls.get("Homepage") - main_package.software_copyrightText = f"Copyright (c) {datetime.now().year} " + ( + created = spdx_ci.created + if isinstance(created, datetime): + created_year = created.year + else: + created_year = datetime.now(timezone.utc).year + main_package.software_copyrightText = f"Copyright (c) {created_year} " + ( metadata.authors[0].get("name", metadata.name) if metadata.authors else metadata.name @@ -219,9 +225,9 @@ def _extract_locked_version_map(locked_dependencies: list[str]) -> dict[str, str result: dict[str, str] = {} for dep in locked_dependencies: dep_name = _parse_dep_name(dep) - version, _ = _resolve_version(dep_name, dep) - if version != "unknown": - result[canonicalize_name(dep_name)] = version + _req, pinned = _extract_exact_pin(dep) + if pinned is not None: + result[canonicalize_name(dep_name)] = pinned return result @@ -410,7 +416,7 @@ def build( add_dependencies( dependencies=transitive_only, dep_provenance=metadata.provenance.get( - "locked_dependencies", "Source: lock file | Method: resolved_lockfile" + "locked_dependencies", "Source: lock file" ), main_package_spdx_id=require_spdx_id(main_package), creation_info=spdx_ci, diff --git a/src/pitloom/core/models.py b/src/pitloom/core/models.py index 03a2f4e6..97a4f8e4 100644 --- a/src/pitloom/core/models.py +++ b/src/pitloom/core/models.py @@ -64,7 +64,7 @@ def normalize_dependency_specifier(dep: str) -> str: def build_pypi_purl(name: str, version: str | None) -> str: """Return a canonical ``pkg:pypi/[@]`` Package URL.""" base = f"pkg:pypi/{canonicalize_name(name)}" - return f"{base}@{version}" if version else base + return f"{base}@{version}" if version and version != "unknown" else base def _clear_doc_counters(doc_uuid: str) -> None: diff --git a/src/pitloom/extract/_lock_common.py b/src/pitloom/extract/_lock_common.py index 0f9cf714..15b2562c 100644 --- a/src/pitloom/extract/_lock_common.py +++ b/src/pitloom/extract/_lock_common.py @@ -39,6 +39,7 @@ "group_versions_by_canonical_name", "has_required_top_level_table", "index_packages_by_name", + "is_same_version", "is_usable_version", "load_lock_json", "load_lock_toml", @@ -258,15 +259,32 @@ def group_versions_by_canonical_name( _EXACT_PIN_OPERATORS = frozenset({"==", "==="}) -def single_exact_pin(specifier_set: SpecifierSet) -> str | None: - """Return the bare version when *specifier_set* contains exactly one - non-wildcard exact-pin specifier (``==`` or PEP 440's arbitrary- - equality ``===``, e.g. ``SpecifierSet("==2.31.0")`` -> ``"2.31.0"``), - or ``None`` for anything looser than one exact pin -- a range, more - than one specifier, or a prefix-match wildcard like ``"==2.31.*"`` - (``packaging.specifiers.Specifier`` reports that as operator ``"=="`` - too, but it pins a *range* of versions, not one exact release -- - ``===`` has no wildcard form, so this check only matters for ``==``). +def is_same_version(v1: str, v2: str) -> bool: + """Return whether two version strings represent the same release. + + Uses :class:`packaging.version.Version` comparison so PEP 440 + equivalences (e.g. ``"1.0" == "1.0.0"``) compare equal rather than + triggering spurious version-conflict warnings. Falls back to exact + string comparison when either string is not a valid PEP 440 version + (e.g. arbitrary-equality ``===`` strings). + """ + try: + return Version(v1) == Version(v2) + except InvalidVersion: + return v1 == v2 + + +def single_exact_pin(specifier_set: SpecifierSet) -> tuple[str, str] | None: + """Return ``(operator, version)`` when *specifier_set* contains exactly + one non-wildcard exact-pin specifier (``==`` or PEP 440's arbitrary- + equality ``===``, e.g. ``SpecifierSet("==2.31.0")`` -> + ``("==", "2.31.0")``, ``SpecifierSet("===2021.01.01-legacy")`` -> + ``("===", "2021.01.01-legacy")``), or ``None`` for anything looser than + one exact pin -- a range, more than one specifier, or a prefix-match + wildcard like ``"==2.31.*"`` (``packaging.specifiers.Specifier`` reports + that as operator ``"=="`` too, but it pins a *range* of versions, not + one exact release -- ``===`` has no wildcard form, so this check only + matters for ``==``). Doesn't itself construct *specifier_set* from a raw string -- :mod:`pitloom.extract._pipfile_lock` and @@ -284,7 +302,7 @@ def single_exact_pin(specifier_set: SpecifierSet) -> str | None: or "*" in specifiers[0].version ): return None - return specifiers[0].version + return specifiers[0].operator, specifiers[0].version def warn_conflicting_versions( diff --git a/src/pitloom/extract/_locked_dependencies.py b/src/pitloom/extract/_locked_dependencies.py index 318baf8f..17303327 100644 --- a/src/pitloom/extract/_locked_dependencies.py +++ b/src/pitloom/extract/_locked_dependencies.py @@ -63,18 +63,14 @@ def _ignore_expected_name( return lambda project_dir, _expected_name: extractor(project_dir) -#: Full priority order (highest first) across every lock/pin source, -#: including ``poetry.lock`` even though it has no extractor here (see -#: the module docstring). Each entry is ``(source name, extractor or -#: ``None``, provenance Method tag or ``None``)``. This is the single +#: Full priority order (highest first) across every lock/pin source. +#: Each entry is ``(source name, extractor, provenance Method tag)``. +#: ``poetry.lock`` is registered with an extractor for Poetry 2.0+ and +#: non-Poetry build backends, but bypassed when ``_try_read_poetry()`` +#: already extracted it during pyproject parsing. This is the single #: place the *complete* order is declared -- both which extractors this -#: cascade tries, and where ``poetry.lock``'s already-applied result -#: ranks relative to them -- so the two can never drift apart the way -#: two independently-maintained lists could. See -#: ``working-docs/design/roadmap.md``'s "Remaining lock formats" item -#: for why this order was chosen (build-backend-agnostic and universal -#: beats tool-specific; a real resolver lock beats a merely-pinned file). -_LOCK_SOURCES: list[tuple[str, _LockExtractor | None, str | None]] = [ +#: cascade tries, and where ``poetry.lock`` ranks relative to them. +_LOCK_SOURCES: list[tuple[str, _LockExtractor, str]] = [ ( "pylock.toml", _ignore_expected_name(extract_pylock_dependencies), @@ -173,17 +169,10 @@ def apply_locked_dependencies(metadata: ProjectMetadata, project_dir: Path) -> N previous_source, ) - for rank, (source_name, extractor, method) in enumerate(_LOCK_SOURCES): - if extractor is None: - continue - if previous_source == source_name: - # Already extracted and set (e.g. by _try_read_poetry); keep it. - return - if previous_rank is not None and rank > previous_rank: - # Every remaining entry ranks below whatever's already set -- - # none of them can win, so stop instead of scanning further. - break - + sources_to_try = ( + _LOCK_SOURCES if previous_rank is None else _LOCK_SOURCES[:previous_rank] + ) + for source_name, extractor, method in sources_to_try: dependencies = extractor(project_dir, metadata.name) if dependencies is None: continue diff --git a/src/pitloom/extract/_pdm_lock.py b/src/pitloom/extract/_pdm_lock.py index e189bf8f..4dee45ad 100644 --- a/src/pitloom/extract/_pdm_lock.py +++ b/src/pitloom/extract/_pdm_lock.py @@ -70,7 +70,7 @@ #: every ordinary registry-resolved entry in this repo's two real #: pdm.lock fixtures, so including it here doesn't risk excluding a #: normal package. -_NON_REGISTRY_KEYS = ("git", "url", "path") +_NON_REGISTRY_KEYS = ("git", "hg", "svn", "bzr", "url", "path") def _default_group_package_or_none(pkg: object) -> dict[str, Any] | None: diff --git a/src/pitloom/extract/_pipfile_lock.py b/src/pitloom/extract/_pipfile_lock.py index e5d995ab..5afca1df 100644 --- a/src/pitloom/extract/_pipfile_lock.py +++ b/src/pitloom/extract/_pipfile_lock.py @@ -42,11 +42,12 @@ from pathlib import Path from packaging.specifiers import InvalidSpecifier, SpecifierSet +from packaging.utils import canonicalize_name from pitloom.extract._lock_common import ( find_first_present_key, - group_versions_by_canonical_name, has_required_top_level_table, + is_same_version, load_lock_json, single_exact_pin, warn_conflicting_versions, @@ -106,21 +107,30 @@ def extract_pipfile_lock_dependencies(project_dir: Path) -> list[str] | None: if pair is not None ] + by_canonical: dict[str, list[tuple[str, str, str]]] = {} + for name, op, version in pairs: + by_canonical.setdefault(canonicalize_name(name), []).append((name, op, version)) + dependencies: list[str] = [] - for group in group_versions_by_canonical_name(pairs).values(): - name, version = group[0] - conflicting_versions = {v for _, v in group} - if len(conflicting_versions) > 1: - warn_conflicting_versions("Pipfile.lock", name, conflicting_versions) + for group in by_canonical.values(): + name, op, version = group[0] + conflicting_versions = { + v for _, _, v in group if not is_same_version(v, version) + } + if conflicting_versions: + all_versions = {v for _, _, v in group} + warn_conflicting_versions("Pipfile.lock", name, all_versions) continue - dependencies.append(f"{name}=={version}") + dependencies.append(f"{name}{op}{version}") return dependencies -def _pinned_pair_for_package(name: object, entry: object) -> tuple[str, str] | None: - """Return ``(name, version)`` for one ``"default"``-section entry, or +def _pinned_pair_for_package( + name: object, entry: object +) -> tuple[str, str, str] | None: + """Return ``(name, op, version)`` for one ``"default"``-section entry, or ``None`` when it's malformed, non-registry-sourced, or its - ``version`` isn't a single exact ``==`` specifier. + ``version`` isn't a single exact specifier. Returning the raw pair (not the formatted ``name==version`` string) lets the caller group same-canonical-name entries via @@ -157,16 +167,17 @@ def _pinned_pair_for_package(name: object, entry: object) -> tuple[str, str] | N # for every other format's presence-only check. warn_non_registry_source("Pipfile.lock", name, non_registry_key) return None - pinned_version = _exact_pinned_version(name, entry.get("version")) - if pinned_version is None: + pin = _exact_pinned_version(name, entry.get("version")) + if pin is None: return None - return name, pinned_version + op, pinned_version = pin + return name, op, pinned_version -def _exact_pinned_version(name: str, version: object) -> str | None: - """Return the bare version string when *version* is a single exact - ``==`` PEP 440 specifier with no wildcard (e.g. ``"==2.31.0"`` -> - ``"2.31.0"``), or ``None`` (with a ``WARNING:``) when it's missing, +def _exact_pinned_version(name: str, version: object) -> tuple[str, str] | None: + """Return ``(operator, version)`` when *version* is a single exact + ``==`` or ``===`` specifier with no wildcard (e.g. ``"==2.31.0"`` -> + ``("==", "2.31.0")``), or ``None`` (with a ``WARNING:``) when it's missing, unparseable, or anything looser than one exact pin -- including a prefix-match specifier like ``"==2.31.*"``, which ``packaging.specifiers.Specifier`` also reports as operator ``"=="`` @@ -190,13 +201,12 @@ def _exact_pinned_version(name: str, version: object) -> str | None: version, ) return None - pinned_version = single_exact_pin(specifier_set) - if pinned_version is None: + pin = single_exact_pin(specifier_set) + if pin is None: log.warning( - "Skipping Pipfile.lock entry %r: 'version' %r isn't a single " - "exact '==' pin", + "Skipping Pipfile.lock entry %r: 'version' %r isn't a single exact pin", name, version, ) return None - return pinned_version + return pin diff --git a/src/pitloom/extract/_pylock.py b/src/pitloom/extract/_pylock.py index 5152f593..09234c5b 100644 --- a/src/pitloom/extract/_pylock.py +++ b/src/pitloom/extract/_pylock.py @@ -49,9 +49,9 @@ _NON_REGISTRY_SOURCE_KEYS = ("vcs", "directory", "archive") -#: PEP 751 pseudo-environment marker variables naming which -#: extras/dependency-groups are active for a given consumption -- the -#: only two this extractor's marker handling understands (see +#: PEP 751 and PEP 508 marker variables naming which extras/ +#: dependency-groups are active for a given consumption -- the only ones +#: this extractor's marker handling understands (see #: :func:`_group_marker_excludes`). Every other PEP 508 marker variable #: (``python_version``, ``sys_platform``, etc.) is deliberately left #: unevaluated, the same "no marker evaluation" limitation this format @@ -59,7 +59,7 @@ #: Pitloom's own running interpreter/platform would make the SBOM's #: contents depend on which machine generated it, violating this repo's #: determinism requirement. -_GROUP_MARKER_VARIABLES = frozenset({"extras", "dependency_groups"}) +_GROUP_MARKER_VARIABLES = frozenset({"extra", "extras", "dependency_groups"}) #: The highest ``lock-version`` this extractor understands, as #: ``(major, minor)``. PEP 751 defines only ``"1.0"`` to date. A @@ -201,13 +201,13 @@ def _evaluate_group_leaf( node: tuple[Any, Any, Any], environment: dict[str, frozenset[str]] ) -> bool | None: """Evaluate one marker leaf ``(lhs, op, rhs)`` against *environment*, - or ``None`` ("unknown") when it isn't an ``in``/``not in`` clause - naming a ``extras``/``dependency_groups`` variable -- see - :func:`_group_marker_excludes` for why every other PEP 508 marker + or ``None`` ("unknown") when it isn't an ``in``/``not in``/``==``/``!=`` + clause naming an ``extra``/``extras``/``dependency_groups`` variable -- + see :func:`_group_marker_excludes` for why every other PEP 508 marker variable is treated as unknown rather than really evaluated.""" lhs, raw_op, rhs = node op = str(raw_op) - if op not in ("in", "not in"): + if op not in ("in", "not in", "==", "!="): return None lhs_str, rhs_str = str(lhs), str(rhs) if rhs_str in _GROUP_MARKER_VARIABLES: @@ -216,8 +216,13 @@ def _evaluate_group_leaf( variable, literal = lhs_str, rhs_str else: return None - member = literal in environment[variable] - return member if op == "in" else not member + + env_key = "extras" if variable == "extra" else variable + active_set = environment.get(env_key, frozenset()) + member = literal in active_set + if op in ("in", "=="): + return member + return not member def _all3(values: list[bool | None]) -> bool | None: diff --git a/src/pitloom/extract/_requirements_txt.py b/src/pitloom/extract/_requirements_txt.py index d3b5731d..ef403ede 100644 --- a/src/pitloom/extract/_requirements_txt.py +++ b/src/pitloom/extract/_requirements_txt.py @@ -53,9 +53,10 @@ from pathlib import Path from packaging.requirements import InvalidRequirement, Requirement +from packaging.utils import canonicalize_name from pitloom.extract._lock_common import ( - group_versions_by_canonical_name, + is_same_version, single_exact_pin, ) @@ -105,7 +106,7 @@ def extract_pinned_requirements_dependencies(project_dir: Path) -> list[str] | N log.warning("Failed to parse %s: %s", lock_path, exc) return None - pins: list[tuple[str, str]] = [] + pins: list[tuple[str, str, str]] = [] for lineno, joined_line in _join_continuation_lines(raw_text): line = _COMMENT_RE.sub("", joined_line).strip() if not line: @@ -150,18 +151,31 @@ def _join_continuation_lines(raw_text: str) -> list[tuple[int, str]]: return logical_lines -def _collapse_or_none(lock_path: Path, pins: list[tuple[str, str]]) -> list[str] | None: - """Collapse *pins* to one ``name==version`` entry per PEP +def _sanitize_credentials(text: str) -> str: + """Redact username and password from URLs in *text*.""" + return re.sub(r"://([^:@/\s]+)(?::[^@/\s]*)?@", r"://***:***@", text) + + +def _collapse_or_none( + lock_path: Path, pins: list[tuple[str, str, str]] +) -> list[str] | None: + """Collapse *pins* to one ``nameversion`` entry per PEP 503-canonicalized name, preserving first-seen literal name and file order -- or ``None`` (with a ``WARNING:`` naming the name and both versions) the moment one canonicalized name repeats with two *different* versions. A plain repeated line (same name, same version) is silently collapsed to one entry. """ + by_canonical: dict[str, list[tuple[str, str, str]]] = {} + for name, op, version in pins: + by_canonical.setdefault(canonicalize_name(name), []).append((name, op, version)) + result: list[str] = [] - for group in group_versions_by_canonical_name(pins).values(): - name, version = group[0] - conflicting = next((v for _, v in group if v != version), None) + for group in by_canonical.values(): + name, op, version = group[0] + conflicting = next( + (v for _, _, v in group if not is_same_version(v, version)), None + ) if conflicting is not None: log.warning( "%s: %r pinned to conflicting versions (%s, %s) -- " @@ -172,18 +186,18 @@ def _collapse_or_none(lock_path: Path, pins: list[tuple[str, str]]) -> list[str] conflicting, ) return None - result.append(f"{name}=={version}") + result.append(f"{name}{op}{version}") return result def _pinned_name_version_for_line( lock_path: Path, lineno: int, line: str -) -> tuple[str, str] | None: - """Return ``(name, version)`` for a well-formed, exactly-pinned, +) -> tuple[str, str, str] | None: + """Return ``(name, op, version)`` for a well-formed, exactly-pinned, non-URL requirement *line*, or ``None`` (having already logged the single ``WARNING:`` naming why) when it disqualifies the whole file.""" if line.startswith(_OPTION_LINE_PREFIX): - option = line.split()[0] + option = line.split()[0].split("=")[0] log.warning( "%s:%d: option %r means this file isn't fully pinned -- " "ignoring requirements.txt", @@ -195,11 +209,12 @@ def _pinned_name_version_for_line( try: requirement = Requirement(line) except InvalidRequirement as exc: + exc_msg = _sanitize_credentials(str(exc)) log.warning( "%s:%d: malformed requirement line: %s -- ignoring requirements.txt", lock_path, lineno, - exc, + exc_msg, ) return None if requirement.url is not None: @@ -211,8 +226,8 @@ def _pinned_name_version_for_line( requirement.name, ) return None - pinned_version = single_exact_pin(requirement.specifier) - if pinned_version is None: + pinned = single_exact_pin(requirement.specifier) + if pinned is None: log.warning( "%s:%d: %r isn't pinned to a single exact version -- " "ignoring requirements.txt", @@ -221,4 +236,5 @@ def _pinned_name_version_for_line( requirement.name, ) return None - return requirement.name, pinned_version + op, version = pinned + return requirement.name, op, version diff --git a/src/pitloom/extract/_uv_lock.py b/src/pitloom/extract/_uv_lock.py index 862ec3d6..99a1ed84 100644 --- a/src/pitloom/extract/_uv_lock.py +++ b/src/pitloom/extract/_uv_lock.py @@ -125,7 +125,9 @@ def _scan_packages( def _find_root_package( - candidates: list[dict[str, Any]], expected_name: str | None + candidates: list[dict[str, Any]], + expected_name: str | None, + lock_path: Path | None = None, ) -> dict[str, Any] | None: """Return the entry in *candidates* (every ``editable``/``virtual``- sourced ``[[package]]`` entry, from :func:`_scan_packages`) that is @@ -153,10 +155,12 @@ def _find_root_package( return pkg if len(candidates) > 1: + prefix = f"{lock_path}: " if lock_path is not None else "" log.warning( - "%d candidate local/workspace package entries found in " + "%s%d candidate local/workspace package entries found in " "uv.lock but none named %r -- can't determine which is this " "project's own; ignoring uv.lock", + prefix, len(candidates), expected_name, ) @@ -288,11 +292,21 @@ def _enqueue_requested_extras( if not isinstance(opt_deps_map, dict): return for extra_name in requested_extras: - extra_key = (canonical_name, extra_name) + extra_canon = canonicalize_name(extra_name) + extra_key = (canonical_name, extra_canon) if extra_key in visited_extras: continue visited_extras.add(extra_key) - extra_deps = opt_deps_map.get(extra_name, []) + extra_deps = opt_deps_map.get(extra_name) + if extra_deps is None: + extra_deps = next( + ( + v + for k, v in opt_deps_map.items() + if canonicalize_name(k) == extra_canon + ), + [], + ) if isinstance(extra_deps, list): queue.extend(extra_deps) @@ -375,7 +389,7 @@ def extract_uv_lock_dependencies( # have done for an explicit `None` -- an empty name could never # usefully match a real workspace member's name anyway. expected_name = _expected_project_name(project_dir) - root = _find_root_package(root_candidates, expected_name) + root = _find_root_package(root_candidates, expected_name, lock_path=lock_path) if root is None: log.warning( "%s: no project package found (no 'editable'/'virtual' " diff --git a/tests/assemble/conftest.py b/tests/assemble/conftest.py index 70a72330..f2c142d0 100644 --- a/tests/assemble/conftest.py +++ b/tests/assemble/conftest.py @@ -193,6 +193,25 @@ def _spdx3_json_with_subject( ) +def _resolve_zip_time() -> tuple[int, int, int, int, int, int]: + """Return a deterministic (year, month, day, hour, min, sec) for ZipInfo. + + Honours SOURCE_DATE_EPOCH when set, clamping pre-1980 timestamps to the + ZIP-format minimum (1980, 1, 1, 0, 0, 0). + """ + raw_epoch = os.environ.get("SOURCE_DATE_EPOCH") + if raw_epoch: + try: + ts = int(raw_epoch) + dt = datetime.fromtimestamp(ts, tz=timezone.utc) + if dt.year >= 1980: + return (dt.year, dt.month, dt.day, dt.hour, dt.minute, dt.second) + return (1980, 1, 1, 0, 0, 0) + except (ValueError, OverflowError, OSError): + pass + return (2026, 1, 1, 0, 0, 0) + + def _embed_sbom_entry( wheel_path: Path, sbom_basename: str, content: str = _SAMPLE_SPDX3_JSON ) -> None: @@ -213,7 +232,12 @@ def _embed_sbom_entry( """ with zipfile.ZipFile(wheel_path, "a") as zf: dist_info = _find_dist_info_prefix(zf, wheel_path) - zf.writestr(f"{dist_info}sboms/{sbom_basename}", content) + zinfo = zipfile.ZipInfo( + f"{dist_info}sboms/{sbom_basename}", date_time=_resolve_zip_time() + ) + zinfo.compress_type = zipfile.ZIP_DEFLATED + zinfo.external_attr = 0o600 << 16 + zf.writestr(zinfo, content) def _make_dummy_wheel( @@ -250,16 +274,7 @@ def _rec_entry(arcname: str, payload: bytes) -> str: f"{dist_info}/RECORD,,", ] record_content = "\n".join(records).encode("utf-8") + b"\n" - fixed_time = (2026, 1, 1, 0, 0, 0) - raw_epoch = os.environ.get("SOURCE_DATE_EPOCH") - if raw_epoch: - try: - ts = int(raw_epoch) - dt = datetime.fromtimestamp(ts, tz=timezone.utc) - if dt.year >= 1980: - fixed_time = (dt.year, dt.month, dt.day, dt.hour, dt.minute, dt.second) - except (ValueError, OverflowError): - pass + fixed_time = _resolve_zip_time() with zipfile.ZipFile(wheel_path, "w", compression=zipfile.ZIP_DEFLATED) as zf: for arcname, payload in ( diff --git a/tests/assemble/test_deps_enrichment_names_versions.py b/tests/assemble/test_deps_enrichment_names_versions.py index 3b286910..ab3d6d64 100644 --- a/tests/assemble/test_deps_enrichment_names_versions.py +++ b/tests/assemble/test_deps_enrichment_names_versions.py @@ -134,6 +134,12 @@ def test_resolve_version_falls_back_to_naive_split_for_unparseable_dep() -> None assert version == "2.0" assert note is None + version_arb, note_arb = _resolve_version( + "not installed", "not installed===2021.01.01-legacy" + ) + assert version_arb == "2021.01.01-legacy" + assert note_arb is None + def test_resolve_version_exact_pin_wins_over_mismatched_installed_version( monkeypatch: pytest.MonkeyPatch, @@ -382,6 +388,51 @@ def test_add_dependencies_dedupes_case_insensitive_name() -> None: assert django_packages[0].name == "Django" +def test_add_dependencies_dedupes_separator_canonicalization() -> None: + """Dependencies declared with differing separators (e.g. pydantic-core vs + pydantic_core) at the same version must collapse into a single + software_Package node, preserving the first-seen raw name.""" + doc_uuid = compute_doc_uuid("sepcanonical", "1.0", []) + _clear_doc_counters(doc_uuid) + exporter = Spdx3JsonExporter() + ci = _make_ci() + main_pkg = spdx3.software_Package( + spdxId=generate_spdx_id("Package", doc_name="sepcanonical", doc_uuid=doc_uuid), + name="sepcanonical", + creationInfo=ci, + ) + exporter.add_package(main_pkg) + + add_dependencies( + ["pydantic-core==1.0.0", "pydantic_core==1.0.0"], + "Source: pyproject.toml | Field: project.dependencies", + require_spdx_id(main_pkg), + ci, + "sepcanonical", + doc_uuid, + exporter, + offline=True, + ) + + packages = [ + o for o in exporter.object_set.objects if isinstance(o, spdx3.software_Package) + ] + pydantic_packages = [ + p for p in packages if p.name in ("pydantic-core", "pydantic_core") + ] + assert len(pydantic_packages) == 1 + assert pydantic_packages[0].name == "pydantic-core" + + depends_on_rels = [ + o + for o in exporter.object_set.objects + if isinstance(o, spdx3.Relationship) + and o.relationshipType == spdx3.RelationshipType.dependsOn + and require_spdx_id(pydantic_packages[0]) in o.to + ] + assert len(depends_on_rels) == 1 + + # --------------------------------------------------------------------------- # _enrich_from_installed -- the discarded-concluded-license-relationship bug # --------------------------------------------------------------------------- diff --git a/tests/assemble/test_deps_enrichment_prefetch.py b/tests/assemble/test_deps_enrichment_prefetch.py index dfcc0d58..526f6b9d 100644 --- a/tests/assemble/test_deps_enrichment_prefetch.py +++ b/tests/assemble/test_deps_enrichment_prefetch.py @@ -89,6 +89,32 @@ def _counting_fetch(_name: str, _version: str | None) -> dict[str, Any]: assert call_count == 1 +def test_prefetch_pypi_release_infos_dedupes_canonical_name_variants( + monkeypatch: pytest.MonkeyPatch, +) -> None: + call_count = 0 + requested_names: list[str] = [] + + def _counting_fetch(name: str, _version: str | None) -> dict[str, Any]: + nonlocal call_count + call_count += 1 + requested_names.append(name) + return {"info": {}} + + monkeypatch.setattr(deps_pypi, "_fetch_pypi_release_info", _counting_fetch) + + _prefetch_pypi_release_infos( + [ + ("Flask", "1.0.0"), + ("flask", "1.0.0"), + ("pydantic-core", "2.0.0"), + ("pydantic_core", "2.0.0"), + ] + ) + assert call_count == 2 + assert set(requested_names) == {"flask", "pydantic-core"} + + def test_prefetch_pypi_release_infos_normalizes_unknown_to_none( monkeypatch: pytest.MonkeyPatch, ) -> None: diff --git a/tests/assemble/test_deps_locked_dependencies.py b/tests/assemble/test_deps_locked_dependencies.py index dad29e87..96fb461c 100644 --- a/tests/assemble/test_deps_locked_dependencies.py +++ b/tests/assemble/test_deps_locked_dependencies.py @@ -23,7 +23,11 @@ from pitloom.assemble.spdx3 import deps_installed from pitloom.assemble.spdx3.deps import add_dependencies -from pitloom.assemble.spdx3.document import _locked_dependencies_completeness, build +from pitloom.assemble.spdx3.document import ( + _extract_locked_version_map, + _locked_dependencies_completeness, + build, +) from pitloom.core.creation import CreationMetadata from pitloom.core.document import DocumentModel from pitloom.core.models import _clear_doc_counters, compute_doc_uuid @@ -403,3 +407,27 @@ def test_direct_dependency_range_resolves_to_locked_version_over_host_environmen assert packages["requests"]["software_packageVersion"] == "2.31.0" assert packages["urllib3"]["software_packageVersion"] == "2.2.0" + + +def test_extract_locked_version_map_unpinned_does_not_leak_host_environment( + monkeypatch: pytest.MonkeyPatch, +) -> None: + """_extract_locked_version_map must only extract exact pins from lock + entries, never falling back to host environment importlib.metadata.""" + monkeypatch.setattr(deps_installed, "get_package_version", lambda _name: "9.9.9") + + locked_map = _extract_locked_version_map(["unpinned-pkg", "range-dep>=1.0"]) + assert locked_map == {} + + pinned_map = _extract_locked_version_map( + [ + "pinned-pkg==2.0.0", + "custom-pkg===legacy.1", + "unparseable-pkg===legacy.2; invalid @ marker", + ] + ) + assert pinned_map == { + "pinned-pkg": "2.0.0", + "custom-pkg": "legacy.1", + "unparseable-pkg": "legacy.2", + } diff --git a/tests/extract/test_hatch_hook_locked_deps.py b/tests/extract/test_hatch_hook_locked_deps.py new file mode 100644 index 00000000..8ae7b187 --- /dev/null +++ b/tests/extract/test_hatch_hook_locked_deps.py @@ -0,0 +1,91 @@ +# SPDX-FileContributor: Arthit Suriyawongkul +# SPDX-FileCopyrightText: 2026-present Arthit Suriyawongkul +# SPDX-FileType: SOURCE +# SPDX-License-Identifier: Apache-2.0 + +"""Tests verifying that Hatchling build hook metadata extraction strictly +isolates source-stage lock files from build-stage wheel metadata. + +See also: :mod:`tests.extract.test_hatch_hook_metadata` for general Hatchling +build hook metadata extraction tests. +""" + +from __future__ import annotations + +import tempfile +from pathlib import Path + +import hatchling.metadata.core as hatchling_metadata_core +import pytest +from hatchling.plugin.manager import PluginManager + +from pitloom.extract.hatchling import metadata_from_hatchling +from pitloom.extract.project import read_project + +from .conftest import POETRY_GAP_FILL_PYPROJECT, write_pyproject + + +@pytest.mark.parametrize( + ("lock_file", "content"), + [ + ( + "poetry.lock", + '[[package]]\nname = "requests"\nversion = "2.31.0"\ngroups = ["main"]\n' + '[metadata]\nlock-version = "2.1"\n', + ), + ( + "pylock.toml", + 'lock-version = "1.0"\ncreated-by = "test"\n' + '[[packages]]\nname = "requests"\nversion = "2.31.0"\n', + ), + ( + "uv.lock", + 'version = 1\nrevision = 1\nrequires-python = ">=3.10"\n' + '[[package]]\nname = "testpkg"\nversion = "0.1.0"\n' + 'source = { editable = "." }\n' + 'dependencies = [{ name = "requests" }]\n\n' + '[[package]]\nname = "requests"\nversion = "2.31.0"\n' + 'source = { registry = "https://pypi.org/simple" }\n', + ), + ( + "pdm.lock", + '[metadata]\nlock_version = "4.5.1"\n' + '[[package]]\nname = "requests"\nversion = "2.31.0"\n' + 'groups = ["default"]\n', + ), + ( + "Pipfile.lock", + '{"_meta": {"pipfile-spec": 6}, ' + '"default": {"requests": {"version": "==2.31.0"}}}', + ), + ( + "requirements.txt", + "requests==2.31.0\n", + ), + ], +) +def test_metadata_from_hatchling_does_not_leak_lock_dependencies( + lock_file: str, content: str +) -> None: + """Lock files are source-stage-only artifacts -- the real wheel Hatchling + builds never consults them, so the build hook's gap-fill path must never + populate locked_dependencies from any lock file sitting next to a + Hatchling-backed project.""" + with tempfile.TemporaryDirectory() as tmp: + tmp_path = Path(tmp) + write_pyproject(tmp_path, POETRY_GAP_FILL_PYPROJECT) + (tmp_path / lock_file).write_text(content, encoding="utf-8") + + hatch_pm = hatchling_metadata_core.ProjectMetadata( + str(tmp_path), PluginManager() + ) + metadata = metadata_from_hatchling(hatch_pm, tmp_path) + + assert metadata.locked_dependencies == [] + assert "locked_dependencies" not in metadata.provenance + + # Companion assertion: absent the isolation boundary (via read_project's + # default path), the same directory DOES resolve the lock file -- guards + # against a vacuous pass where the lock fixture is broken or not found. + direct, _, _ = read_project(tmp_path) + assert direct.locked_dependencies == ["requests==2.31.0"] diff --git a/tests/extract/test_hatch_hook_metadata.py b/tests/extract/test_hatch_hook_metadata.py index 7bd0d2a9..0c2b4638 100644 --- a/tests/extract/test_hatch_hook_metadata.py +++ b/tests/extract/test_hatch_hook_metadata.py @@ -16,7 +16,6 @@ _resolve_hatchling_license_files, metadata_from_hatchling, ) -from pitloom.extract.project import read_project # noqa: E402 from pitloom.plugins.hatch import ( # noqa: E402 _check_hatchling_sbom_support, ) @@ -436,72 +435,6 @@ def test_metadata_from_hatchling_fills_gaps_from_poetry() -> None: assert metadata.keywords == ["from-poetry", "gap-fill"] -@pytest.mark.parametrize( - ("lock_file", "content"), - [ - ( - "poetry.lock", - '[[package]]\nname = "requests"\nversion = "2.31.0"\ngroups = ["main"]\n' - '[metadata]\nlock-version = "2.1"\n', - ), - ( - "pylock.toml", - 'lock-version = "1.0"\ncreated-by = "test"\n' - '[[packages]]\nname = "requests"\nversion = "2.31.0"\n', - ), - ( - "uv.lock", - 'version = 1\nrevision = 1\nrequires-python = ">=3.10"\n' - '[[package]]\nname = "testpkg"\nversion = "0.1.0"\n' - 'source = { editable = "." }\n' - 'dependencies = [{ name = "requests" }]\n\n' - '[[package]]\nname = "requests"\nversion = "2.31.0"\n' - 'source = { registry = "https://pypi.org/simple" }\n', - ), - ( - "pdm.lock", - '[metadata]\nlock_version = "4.5.1"\n' - '[[package]]\nname = "requests"\nversion = "2.31.0"\n' - 'groups = ["default"]\n', - ), - ( - "Pipfile.lock", - '{"_meta": {"pipfile-spec": 6}, ' - '"default": {"requests": {"version": "==2.31.0"}}}', - ), - ( - "requirements.txt", - "requests==2.31.0\n", - ), - ], -) -def test_metadata_from_hatchling_does_not_leak_lock_dependencies( - lock_file: str, content: str -) -> None: - """Lock files are source-stage-only artifacts -- the real wheel Hatchling - builds never consults them, so the build hook's gap-fill path must never - populate locked_dependencies from any lock file sitting next to a - Hatchling-backed project.""" - with tempfile.TemporaryDirectory() as tmp: - tmp_path = Path(tmp) - write_pyproject(tmp_path, POETRY_GAP_FILL_PYPROJECT) - (tmp_path / lock_file).write_text(content, encoding="utf-8") - - hatch_pm = hatchling_metadata_core.ProjectMetadata( - str(tmp_path), PluginManager() - ) - metadata = metadata_from_hatchling(hatch_pm, tmp_path) - - assert metadata.locked_dependencies == [] - assert "locked_dependencies" not in metadata.provenance - - # Companion assertion: absent the isolation boundary (via read_project's - # default path), the same directory DOES resolve the lock file -- guards - # against a vacuous pass where the lock fixture is broken or not found. - direct, _, _ = read_project(tmp_path) - assert direct.locked_dependencies == ["requests==2.31.0"] - - def test_check_hatchling_sbom_support_raises_when_metadata_missing() -> None: """If Hatchling's version can't be determined, raise a clear ``RuntimeError`` rather than letting a raw ``PackageNotFoundError`` diff --git a/tests/extract/test_lock_common.py b/tests/extract/test_lock_common.py index c98360aa..299b1a83 100644 --- a/tests/extract/test_lock_common.py +++ b/tests/extract/test_lock_common.py @@ -21,6 +21,7 @@ group_versions_by_canonical_name, has_required_top_level_table, index_packages_by_name, + is_same_version, is_usable_version, load_lock_json, load_lock_toml, @@ -297,12 +298,27 @@ def test_default_group_included_not_a_list_returns_none_and_warns( @pytest.mark.parametrize("operator", ["==", "==="]) def test_single_exact_pin_accepts_exact_operators(operator: str) -> None: - assert single_exact_pin(SpecifierSet(f"{operator}2.31.0")) == "2.31.0" + assert single_exact_pin(SpecifierSet(f"{operator}2.31.0")) == (operator, "2.31.0") def test_single_exact_pin_accepts_arbitrary_equality_non_pep440_version() -> None: """The === operator explicitly supports non-PEP 440 version strings.""" - assert single_exact_pin(SpecifierSet("===2021.01.01-legacy")) == "2021.01.01-legacy" + assert single_exact_pin(SpecifierSet("===2021.01.01-legacy")) == ( + "===", + "2021.01.01-legacy", + ) + + +def test_is_same_version_pep440_equivalences() -> None: + assert is_same_version("1.0", "1.0.0") + assert is_same_version("1.0.0", "1.0") + assert is_same_version("2.31.0", "2.31.0") + assert not is_same_version("1.0", "1.1") + + +def test_is_same_version_non_pep440_fallback() -> None: + assert is_same_version("2021.01.01-legacy", "2021.01.01-legacy") + assert not is_same_version("2021.01.01-legacy", "2021.01.02-legacy") def test_single_exact_pin_rejects_wildcard() -> None: diff --git a/tests/extract/test_locked_dependencies.py b/tests/extract/test_locked_dependencies.py index 061b4562..d9278c21 100644 --- a/tests/extract/test_locked_dependencies.py +++ b/tests/extract/test_locked_dependencies.py @@ -153,6 +153,11 @@ def test_apply_locked_dependencies_valid_empty_source_wins_over_lower_priority() "Source: pylock.toml | Method: resolved_lockfile" ) + (tmp_path / "pylock.toml").unlink() + metadata_uv = ProjectMetadata(name="demo") + apply_locked_dependencies(metadata_uv, tmp_path) + assert metadata_uv.locked_dependencies == ["requests==2.31.0"] + def test_read_project_applies_cascade_for_setup_py_only_project() -> None: """Regression: a project with no `pyproject.toml` at all -- just a diff --git a/tests/extract/test_project.py b/tests/extract/test_project.py index c4f89fcf..a565c061 100644 --- a/tests/extract/test_project.py +++ b/tests/extract/test_project.py @@ -263,6 +263,9 @@ def test_read_project_include_locked_dependencies_false_skips_cascade( assert metadata.locked_dependencies == [] assert "locked_dependencies" not in metadata.provenance + normal_metadata, _, _ = read_project(tmp_path) + assert normal_metadata.locked_dependencies == ["requests==2.31.0"] + def test_read_project_include_locked_dependencies_false_also_skips_poetry_lock( tmp_path: Path, @@ -288,6 +291,9 @@ def test_read_project_include_locked_dependencies_false_also_skips_poetry_lock( assert metadata.locked_dependencies == [] assert "locked_dependencies" not in metadata.provenance + normal_metadata, _, _ = read_project(tmp_path) + assert normal_metadata.locked_dependencies == ["requests==2.31.0"] + @pytest.mark.parametrize( ("lock_file", "content"), diff --git a/tests/extract/test_requirements_txt.py b/tests/extract/test_requirements_txt.py index edb0ce07..99be3b64 100644 --- a/tests/extract/test_requirements_txt.py +++ b/tests/extract/test_requirements_txt.py @@ -336,7 +336,15 @@ def test_option_line_disqualifies_whole_file( assert "isn't fully pinned" in caplog.text +@pytest.mark.parametrize( + "option_line", + [ + "--extra-index-url https://user:secret_token@custom-pypi.org/simple", + "--extra-index-url=https://user:secret_token@custom-pypi.org/simple", + ], +) def test_option_line_with_credentials_redacts_line_in_log( + option_line: str, caplog: pytest.LogCaptureFixture, ) -> None: """Option lines containing credentials (e.g. basic auth in --extra-index-url) @@ -345,8 +353,7 @@ def test_option_line_with_credentials_redacts_line_in_log( tmp_path = Path(tmp) _write_requirements( tmp_path, - "idna==3.7\n" - "--extra-index-url https://user:secret_token@custom-pypi.org/simple\n", + f"idna==3.7\n{option_line}\n", ) with caplog.at_level(logging.WARNING): @@ -358,6 +365,31 @@ def test_option_line_with_credentials_redacts_line_in_log( assert "user" not in caplog.text +def test_malformed_url_with_credentials_redacts_credentials_in_log( + caplog: pytest.LogCaptureFixture, +) -> None: + """URL requirements with basic auth must not leak passwords in logs.""" + with tempfile.TemporaryDirectory() as tmp: + tmp_path = Path(tmp) + _write_requirements( + tmp_path, + "https://user:secret_pass@example.com/pkg-1.0.whl\n", + ) + + with caplog.at_level(logging.WARNING): + result = extract_pinned_requirements_dependencies(tmp_path) + + assert result is None + assert "secret_pass" not in caplog.text + assert "://***:***@" in caplog.text + + +def test_arbitrary_equality_operator_preserved_in_requirements(tmp_path: Path) -> None: + _write_requirements(tmp_path, "custom-pkg===2021.01.01-legacy\n") + result = extract_pinned_requirements_dependencies(tmp_path) + assert result == ["custom-pkg===2021.01.01-legacy"] + + def test_bare_url_and_legacy_vcs_syntax_disqualify_as_malformed( caplog: pytest.LogCaptureFixture, ) -> None: @@ -472,13 +504,17 @@ def test_read_project_pipfile_lock_takes_priority_over_requirements_txt() -> Non # --- real-world fixtures ------------------------------------------------- -def test_real_world_home_assistant_core_rejects_partially_pinned_file() -> None: - """`home-assistant/core`'s real root `requirements.txt` mixes exact - pins with range specifiers -- the whole-file all-or-nothing policy - must reject it entirely, not partially include the pinned lines.""" - metadata, _config, _path = read_project( - REAL_WORLD_LOCKS / "home-assistant-core-2026.9.0" - ) +def test_real_world_home_assistant_core_rejects_partially_pinned_file( + caplog: pytest.LogCaptureFixture, +) -> None: + """`home-assistant/core`'s real root `requirements.txt` contains + constraint-file options (`-c`) and mixed pins -- the whole-file + all-or-nothing policy must reject it entirely.""" + with caplog.at_level(logging.WARNING): + metadata, _config, _path = read_project( + REAL_WORLD_LOCKS / "home-assistant-core-2026.9.0" + ) assert metadata.locked_dependencies == [] assert "locked_dependencies" not in metadata.provenance + assert "option '-c' means this file isn't fully pinned" in caplog.text diff --git a/working-docs/design/roadmap.md b/working-docs/design/roadmap.md index 3f5a0ce7..1dd573db 100644 --- a/working-docs/design/roadmap.md +++ b/working-docs/design/roadmap.md @@ -67,6 +67,11 @@ is not kept in sync with post-ship changes. Open follow-ups: [AI model id stability](#ai-model-id-stability-follow-up-to-178), [Sort-order canonicalization](#sort-order-canonicalization-follow-up-to-178) below. ([PR #178](https://github.com/bact/pitloom/pull/178)) +- [x] **Lock/pin formats as a resolved-dependency source** -- `poetry.lock`, + `pylock.toml` (PEP 751), `uv.lock`, `pdm.lock`, `Pipfile.lock`, and pinned + `requirements.txt` feed `locked_dependencies` via one shared cascade + ([#208](https://github.com/bact/pitloom/pull/208)). See + [lock-file-cascade.md](../implementation/lock-file-cascade.md). ## Adoption surfaces @@ -150,26 +155,12 @@ table in [non-hatchling-file-discovery.md](non-hatchling-file-discovery.md)); an existing installed package as a high-fidelity source when present (editable installs, virtual environments). See [metadata-sources.md](./metadata-sources.md). -- [x] **Lock/pin formats as a resolved-dependency source** -- done - (2026-08-31 through 2026-09-05): `poetry.lock`, `pylock.toml` (PEP - 751), `uv.lock`, `pdm.lock`, `Pipfile.lock`, and pinned - `requirements.txt` all feed `ProjectMetadata.locked_dependencies` - via one shared priority cascade, closing - **"Remaining lock formats as a resolved-dependency source"** - ([#208](https://github.com/bact/pitloom/pull/208)). See - [lock-file-cascade.md](../implementation/lock-file-cascade.md) (the - cascade, priority order, and per-format details) and - [poetry-support.md](../implementation/poetry-support.md)/ - [pep751-pylock-support.md](../implementation/pep751-pylock-support.md) - for the two formats with their own dedicated doc. - [lock-files.md](./lock-files.md) (`pixi.lock`/`conda-lock.yml` - remain future work there, Phase 2). - [ ] **CLI option `--no-locked-dependencies`** -- opt-out flag and `[tool.pitloom] locked-dependencies = false` configuration allowing users and CI pipelines to disable automatic lock file discovery, falling back to direct dependencies and environment introspection. Wire across CLI, library API, and build backend hooks. -- [ ] **Preserve lockfile hashes in `--offline` mode** -- retain package +- [ ] **Preserve lock file hashes in `--offline` mode** -- retain package SHA-256 digests parsed from lock files (`pylock.toml`, `uv.lock`, `pdm.lock`, `Pipfile.lock`, etc.) so that `--offline` mode can populate SPDX 3 `verifiedUsing` integrity checksums without requiring online PyPI JSON API From 6dea3558dce592e5ae49a563d5f111e779616df9 Mon Sep 17 00:00:00 2001 From: Arthit Suriyawongkul Date: Sun, 6 Sep 2026 16:56:20 +0700 Subject: [PATCH 21/35] Update lock-files design doc Signed-off-by: Arthit Suriyawongkul --- working-docs/design/lock-files.md | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/working-docs/design/lock-files.md b/working-docs/design/lock-files.md index 801356bc..61e564e6 100644 --- a/working-docs/design/lock-files.md +++ b/working-docs/design/lock-files.md @@ -1,6 +1,6 @@ --- Created: 2026-08-31 -Last-Modified: 2026-09-04 +Last-Modified: 2026-09-06 SPDX-FileCopyrightText: 2026-present Arthit Suriyawongkul SPDX-FileType: DOCUMENTATION SPDX-License-Identifier: CC0-1.0 @@ -30,9 +30,9 @@ open for the two-lock-files case: `pylock.toml` overrides an already-applied `poetry.lock`-resolved set, since it's the build-backend-agnostic interoperability standard. -See `working-docs/design/roadmap.md`'s "Remaining lock formats as a -resolved-dependency source" item for the up-to-date status of every -other format below. +See [lock-file-cascade.md](../implementation/lock-file-cascade.md) for the +implemented formats and current cascade; the remaining formats below +continue to describe future work. **Illustrative code only, not a drop-in design.** The Pydantic models and hand-rolled `SPDXRef-*`/raw-dict SPDX 3 serializer below are a sketch, From 0ce42867c19ac7f0b6a710c7f8037fa098a8f95b Mon Sep 17 00:00:00 2001 From: Arthit Suriyawongkul Date: Mon, 7 Sep 2026 03:01:59 +0700 Subject: [PATCH 22/35] Fix bugs Signed-off-by: Arthit Suriyawongkul --- docs/dependency-sources.md | 19 +- fuzz/fuzz_gguf_header.py | 5 +- fuzz/fuzz_license_expression.py | 5 +- src/pitloom/assemble/spdx3/deps.py | 1 + src/pitloom/assemble/spdx3/deps_installed.py | 102 +++++--- src/pitloom/assemble/spdx3/document.py | 35 +-- src/pitloom/extract/_poetry_lock.py | 18 +- .../assemble/test_deps_locked_dependencies.py | 25 +- tests/assemble/test_deps_resolution_pins.py | 235 ++++++++++++++++++ tests/extract/test_poetry_lock.py | 21 +- 10 files changed, 376 insertions(+), 90 deletions(-) create mode 100644 tests/assemble/test_deps_resolution_pins.py diff --git a/docs/dependency-sources.md b/docs/dependency-sources.md index 20236741..55af9aff 100644 --- a/docs/dependency-sources.md +++ b/docs/dependency-sources.md @@ -1,6 +1,6 @@ --- Created: 2026-09-04 -Last-Modified: 2026-09-05 +Last-Modified: 2026-09-07 SPDX-FileCopyrightText: 2026-present Arthit Suriyawongkul SPDX-FileType: DOCUMENTATION SPDX-License-Identifier: CC0-1.0 @@ -25,13 +25,16 @@ additionally reads its exact resolved versions and adds any dependency they introduce that your declared list doesn't already name -- your project's *transitive* dependencies, pinned exactly (e.g. `idna==3.7`). -**A direct dependency already in your declared list keeps its declared -range in the SBOM, even when a lock file has resolved it to an exact -version.** For example, if `pyproject.toml` declares `requests>=2.0` and -your lock file resolved it to `2.31.0`, the SBOM still shows -`requests>=2.0` for that entry -- only dependencies *not already -declared* (the ones the lock file alone reveals) get added, as new, -exactly-pinned entries. +**When a direct dependency is declared with a version range and a lock +file is present, Pitloom uses the lock file's exact resolved version +for the emitted SBOM package.** For example, if `pyproject.toml` +declares `requests>=2.0` and your lock file resolved it to `2.31.0`, the +package's `software_packageVersion` and PyPI PURL become `2.31.0` (with +enrichment fetching metadata for that exact release), while the +original declared range is preserved in the element's +`declared_constraint` provenance annotation. Transitive dependencies +revealed only by the lock file (e.g. `idna==3.7`) are added as new, +additive entries. ## Supported lock formats, and what counts as "resolved" diff --git a/fuzz/fuzz_gguf_header.py b/fuzz/fuzz_gguf_header.py index d2d05ab4..4b784c7d 100644 --- a/fuzz/fuzz_gguf_header.py +++ b/fuzz/fuzz_gguf_header.py @@ -33,7 +33,7 @@ sys.path.insert(0, str(Path(__file__).resolve().parent.parent / "src")) # pylint: disable=wrong-import-position -from pitloom.extract._gguf import read_gguf +from pitloom.extract._gguf import read_gguf # noqa: E402 _FUZZ_INPUT_PATH = Path(tempfile.gettempdir()) / "pitloom-fuzz-gguf-input.gguf" @@ -52,7 +52,8 @@ def _run_one(data: bytes) -> None: pass # Expected: read_gguf's own "not a valid GGUF file" signal. -def TestOneInput(data: bytes) -> None: # noqa: N802 -- atheris/libFuzzer entrypoint name +# atheris/libFuzzer entrypoint name: +def TestOneInput(data: bytes) -> None: # noqa: N802 _run_one(data) diff --git a/fuzz/fuzz_license_expression.py b/fuzz/fuzz_license_expression.py index 18b9c76d..dd6a30d9 100644 --- a/fuzz/fuzz_license_expression.py +++ b/fuzz/fuzz_license_expression.py @@ -24,7 +24,7 @@ sys.path.insert(0, str(Path(__file__).resolve().parent.parent / "src")) # pylint: disable=wrong-import-position -from pitloom.extract._license import normalize_license_expression +from pitloom.extract._license import normalize_license_expression # noqa: E402 def _run_one(data: bytes) -> None: @@ -39,7 +39,8 @@ def _run_one(data: bytes) -> None: normalize_license_expression(text) -def TestOneInput(data: bytes) -> None: # noqa: N802 -- atheris/libFuzzer entrypoint name +# atheris/libFuzzer entrypoint name: +def TestOneInput(data: bytes) -> None: # noqa: N802 _run_one(data) diff --git a/src/pitloom/assemble/spdx3/deps.py b/src/pitloom/assemble/spdx3/deps.py index 59049de2..c9a95106 100644 --- a/src/pitloom/assemble/spdx3/deps.py +++ b/src/pitloom/assemble/spdx3/deps.py @@ -172,6 +172,7 @@ def _finish_dependency_enrichment( doc_name, doc_uuid, exporter, + expected_version=dep_version, provenance_config=provenance_config, encoder=encoder, offline=offline, diff --git a/src/pitloom/assemble/spdx3/deps_installed.py b/src/pitloom/assemble/spdx3/deps_installed.py index 1c755ab4..fd046edd 100644 --- a/src/pitloom/assemble/spdx3/deps_installed.py +++ b/src/pitloom/assemble/spdx3/deps_installed.py @@ -16,6 +16,7 @@ from importlib.metadata import version as get_package_version from packaging.requirements import InvalidRequirement, Requirement +from packaging.specifiers import InvalidSpecifier, SpecifierSet from packaging.version import InvalidVersion from spdx_python_model.bindings import v3_0_1 as spdx3 @@ -32,6 +33,7 @@ from pitloom.core.provenance import ProvenanceConfig from pitloom.export.spdx3_json import Spdx3JsonExporter from pitloom.extract._extract_utils import pkg_meta_get +from pitloom.extract._lock_common import is_same_version, single_exact_pin _VERSION_OPERATORS = ("===", "~=", "!=", "==", ">=", "<=", ">", "<") _HOMEPAGE_LABELS = ("homepage", "home page", "home") @@ -52,24 +54,58 @@ def _parse_dep_name(dep: str) -> str: return dep.strip() +def _extract_pin_from_unparseable(dep: str) -> str | None: + """Extract an exact pin (== or ===) from an unparseable requirement string.""" + dep_spec = dep.split(";", 1)[0] if ";" in dep else dep + for op in ("===", "=="): + if op not in dep_spec: + continue + pin_part = dep_spec.split(op, 1)[1].strip() + if not pin_part or "*" in pin_part or "," in pin_part: + return None + try: + exact = single_exact_pin(SpecifierSet(f"{op}{pin_part}")) + if exact is not None: + return exact[1] + except InvalidSpecifier: + if op == "===": + return pin_part + return None + return None + + def _extract_exact_pin(dep: str) -> tuple[Requirement | None, str | None]: - """Parse *dep* into a Requirement and extract any exact pin (== or ===).""" + """Parse *dep* into a Requirement and extract any single exact pin (== or ===).""" try: req = Requirement(dep) + exact = single_exact_pin(req.specifier) + return (req, exact[1]) if exact is not None else (req, None) except InvalidRequirement: - dep_spec = dep.split(";", 1)[0] if ";" in dep else dep - for op in ("===", "=="): - if op in dep_spec: - pin = dep_spec.split(op)[1].strip() - if pin: - return None, pin - break - return None, None - - pinned = [spec.version for spec in req.specifier if spec.operator in ("==", "===")] - if pinned: - return req, pinned[0] - return req, None + return None, _extract_pin_from_unparseable(dep) + + +def _is_exact_pin_conflict( + req: Requirement | None, pinned: str, locked_version: str +) -> bool: + """Return True if locked_version conflicts with declared exact pin.""" + if req is not None and req.specifier: + try: + return not req.specifier.contains(locked_version, prereleases=True) + # pylint: disable-next=broad-exception-caught + except (InvalidVersion, Exception): + pass + return not is_same_version(locked_version, pinned) + + +def _satisfies_constraint(req: Requirement | None, locked_version: str) -> bool: + """Return True if locked_version satisfies req.specifier.""" + if req is None or not req.specifier: + return True + try: + return req.specifier.contains(locked_version, prereleases=True) + # pylint: disable-next=broad-exception-caught + except (InvalidVersion, Exception): + return False def _resolve_version( @@ -95,7 +131,9 @@ def _resolve_version( """ req, pinned = _extract_exact_pin(dep) if pinned is not None: - if locked_version is not None and locked_version != pinned: + if locked_version is not None and _is_exact_pin_conflict( + req, pinned, locked_version + ): log.warning( "Locked version %r for dependency %r conflicts with declared" " exact pin %r -- using declared pin", @@ -106,21 +144,14 @@ def _resolve_version( return pinned, None if locked_version is not None: - if req is not None and req.specifier: - satisfies = True - try: - satisfies = req.specifier.contains(locked_version, prereleases=True) - # pylint: disable-next=broad-exception-caught - except (InvalidVersion, Exception): - satisfies = False - if not satisfies: - log.warning( - "Locked version %r for dependency %r does not satisfy declared" - " constraint %r -- using locked version", - locked_version, - dep_name, - dep, - ) + if not _satisfies_constraint(req, locked_version): + log.warning( + "Locked version %r for dependency %r does not satisfy declared" + " constraint %r -- using locked version", + locked_version, + dep_name, + dep, + ) return locked_version, "Version resolved: Project lock file" try: @@ -143,6 +174,7 @@ def _enrich_from_installed( doc_uuid: str, exporter: Spdx3JsonExporter, *, + expected_version: str | None = None, provenance_config: ProvenanceConfig | None = None, encoder: ProvenanceEncoder | None = None, offline: bool = False, @@ -154,6 +186,16 @@ def _enrich_from_installed( except PackageNotFoundError: return set() + installed_version = pkg_meta_get(pkg_meta, "Version") + target_version = expected_version or dep_package.software_packageVersion + if ( + installed_version + and target_version + and target_version != "unknown" + and not is_same_version(installed_version, target_version) + ): + return set() + filled: set[str] = set() project_urls = _parse_project_urls(pkg_meta) diff --git a/src/pitloom/assemble/spdx3/document.py b/src/pitloom/assemble/spdx3/document.py index 97949dc7..c1664618 100644 --- a/src/pitloom/assemble/spdx3/document.py +++ b/src/pitloom/assemble/spdx3/document.py @@ -35,7 +35,6 @@ build_enrichment_fragment, build_model, ) -from pitloom.assemble.spdx3._provenance_encoders import parse_provenance_value from pitloom.assemble.spdx3.ai import add_ai_models from pitloom.assemble.spdx3.creation_info import build_creation_info from pitloom.assemble.spdx3.deps import ( @@ -180,38 +179,20 @@ def _locked_transitive_only_dependencies(metadata: ProjectMetadata) -> list[str] ] -#: `locked_dependencies` provenance `Method` tags that represent a real -#: resolver's output -- a full, hash-verifiable transitive closure, not -#: just a list of exact pins someone happened to write down. Every -#: format in `pitloom.extract._locked_dependencies`'s cascade uses this -#: tag except pinned `requirements.txt`, whose own `"pinned_requirements"` -#: tag is deliberately excluded below. -_RESOLVED_LOCKFILE_METHOD = "resolved_lockfile" - - +# pylint: disable=useless-return def _locked_dependencies_completeness(metadata: ProjectMetadata) -> str | None: """Return the `RelationshipCompleteness` value for the locked-only `dependsOn` edges :func:`_locked_transitive_only_dependencies` produces, or `None` to leave it unset. - A real resolver lock (`poetry.lock`, `pylock.toml`, `uv.lock`, - `pdm.lock`, `Pipfile.lock` -- every cascade entry tagged - `Method: resolved_lockfile`) genuinely proves the full transitive - dependency closure, so its edges are marked `complete`. Every other - case -- pinned `requirements.txt` (tagged `Method: - pinned_requirements`, just a list of exact-pin lines a human or `pip - freeze` wrote, with no resolver guarantee that every real transitive - dependency is actually present), an unrecognized future `Method` tag, - or no provenance recorded at all -- returns `None` (unset) instead: - an inclusion check (only the one tag known to prove completeness - claims it) rather than an exclusion check, so a future lock source - that forgets to record its own `Method` tag fails safe to "unset" - rather than silently defaulting to overstating completeness. + Conservatively returns ``None`` (unset): while a resolver lock represents + a resolved dependency graph, extractors may legitimately omit + unrepresentable dependencies (such as VCS/path sources, non-default groups, + or marker-ambiguous variants). Asserting ``complete`` would overstate + completeness for partial closures, so leaving it unset makes no + unverifiable claim. """ - provenance = metadata.provenance.get("locked_dependencies") - method = parse_provenance_value(provenance).get("method") if provenance else None - if method == _RESOLVED_LOCKFILE_METHOD: - return spdx3.RelationshipCompleteness.complete + del metadata return None diff --git a/src/pitloom/extract/_poetry_lock.py b/src/pitloom/extract/_poetry_lock.py index 6f13a033..99ea21f7 100644 --- a/src/pitloom/extract/_poetry_lock.py +++ b/src/pitloom/extract/_poetry_lock.py @@ -99,6 +99,20 @@ def extract_poetry_lock_dependencies(project_dir: Path) -> list[str] | None: _NON_PEP508_SOURCE_TYPES = frozenset({"directory", "file", "git", "url"}) +def _is_main_group(validated: dict[str, Any], name: str) -> bool: + """Return True if package belongs to the main/default group.""" + if "groups" in validated: + return ( + default_group_included(validated, "poetry.lock", _DEFAULT_GROUP, name) + is True + ) + if "category" in validated: + return validated.get("category") == _DEFAULT_GROUP + return ( + default_group_included(validated, "poetry.lock", _DEFAULT_GROUP, name) is True + ) + + def _main_group_package_or_none(pkg: object) -> dict[str, Any] | None: """Return *pkg* when it's a well-formed, non-optional, main-group, registry-sourced entry -- ``None`` otherwise. @@ -114,9 +128,7 @@ def _main_group_package_or_none(pkg: object) -> dict[str, Any] | None: return None name = validated["name"] - if validated.get("optional") is True: - return None - if not default_group_included(validated, "poetry.lock", _DEFAULT_GROUP, name): + if validated.get("optional") is True or not _is_main_group(validated, name): return None source = validated.get("source") source_type = source.get("type") if isinstance(source, dict) else None diff --git a/tests/assemble/test_deps_locked_dependencies.py b/tests/assemble/test_deps_locked_dependencies.py index 96fb461c..bdf7bc7b 100644 --- a/tests/assemble/test_deps_locked_dependencies.py +++ b/tests/assemble/test_deps_locked_dependencies.py @@ -101,8 +101,8 @@ def test_add_dependencies_omitted_completeness_leaves_field_unset() -> None: def test_locked_dependencies_add_transitive_only_edges() -> None: """A locked (poetry.lock-resolved) package not already a direct - dependency gets an additive ``dependsOn`` edge tagged ``complete``; - the direct dependency's own edge is untouched (no ``completeness``).""" + dependency gets an additive ``dependsOn`` edge; completeness is + conservatively left unset to avoid overstating completeness.""" project = ProjectMetadata( name="main-project", version="1.0.0", @@ -130,8 +130,8 @@ def test_locked_dependencies_add_transitive_only_edges() -> None: assert len(depends_on) == 3 # one edge per package, no duplicate for requests assert "completeness" not in depends_on[packages["requests"]["spdxId"]] - assert depends_on[packages["urllib3"]["spdxId"]]["completeness"] == "complete" - assert depends_on[packages["idna"]["spdxId"]]["completeness"] == "complete" + assert "completeness" not in depends_on[packages["urllib3"]["spdxId"]] + assert "completeness" not in depends_on[packages["idna"]["spdxId"]] def test_pinned_requirements_transitive_edges_leave_completeness_unset() -> None: @@ -171,15 +171,9 @@ def test_pinned_requirements_transitive_edges_leave_completeness_unset() -> None def test_locked_dependencies_completeness_by_method() -> None: - """Unit-level coverage of `_locked_dependencies_completeness()`'s - branches: `resolved_lockfile` (a real resolver lock) is `complete`; - everything else -- `pinned_requirements`, an unrecognized future - `Method` tag, and no provenance recorded at all -- is unset (`None`), - the same conservative "don't claim completeness we can't back up" - choice, via a positive inclusion check rather than an exclusion - check (so a future lock source that forgets to set its own `Method` - tag fails safe to unset instead of silently defaulting to - `complete`).""" + """Unit-level coverage of `_locked_dependencies_completeness()`: + conservatively returns None (unset) for all cases to avoid overstating + completeness on partial lock closures.""" resolved = ProjectMetadata( name="pkg", locked_dependencies=["idna==3.7"], @@ -205,10 +199,7 @@ def test_locked_dependencies_completeness_by_method() -> None: ) no_provenance = ProjectMetadata(name="pkg", locked_dependencies=["idna==3.7"]) - assert ( - _locked_dependencies_completeness(resolved) - == spdx3.RelationshipCompleteness.complete - ) + assert _locked_dependencies_completeness(resolved) is None assert _locked_dependencies_completeness(pinned) is None assert _locked_dependencies_completeness(unrecognized) is None assert _locked_dependencies_completeness(no_provenance) is None diff --git a/tests/assemble/test_deps_resolution_pins.py b/tests/assemble/test_deps_resolution_pins.py new file mode 100644 index 00000000..36b28e91 --- /dev/null +++ b/tests/assemble/test_deps_resolution_pins.py @@ -0,0 +1,235 @@ +# SPDX-FileContributor: Arthit Suriyawongkul +# SPDX-FileCopyrightText: 2026-present Arthit Suriyawongkul +# SPDX-FileType: SOURCE +# SPDX-License-Identifier: Apache-2.0 + +"""Tests for dependency exact pin extraction and version resolution +(:mod:`pitloom.assemble.spdx3.deps_installed`). + +See also: test_deps_enrichment_names_versions.py for display name and basic +version resolution tests; this file covers specifier operator nuances (==, ===, +wildcards, multi-specifiers) and installed metadata version mismatch isolation. +""" + +from __future__ import annotations + +import logging + +import pytest +from packaging.requirements import Requirement +from spdx_python_model.bindings import v3_0_1 as spdx3 + +import pitloom.assemble.spdx3.deps_installed as deps_installed_mod +from pitloom.assemble.spdx3.deps_installed import ( + _enrich_from_installed, + _extract_exact_pin, + _resolve_version, +) +from pitloom.core.models import _clear_doc_counters, compute_doc_uuid, generate_spdx_id +from pitloom.export.spdx3_json import Spdx3JsonExporter + +from .conftest import _FakeMetadata, _make_ci + + +def test_extract_exact_pin_accepts_single_exact_pins() -> None: + req, pin = _extract_exact_pin("requests==2.31.0") + assert isinstance(req, Requirement) + assert pin == "2.31.0" + + req_arb, pin_arb = _extract_exact_pin("legacy-pkg===2021.01.01-legacy") + assert isinstance(req_arb, Requirement) + assert pin_arb == "2021.01.01-legacy" + + +def test_extract_exact_pin_rejects_wildcards_and_ranges() -> None: + """A prefix wildcard (==1.*) or multi-clause specifier is a range, + not an exact release pin.""" + req_wild, pin_wild = _extract_exact_pin("requests==1.*") + assert isinstance(req_wild, Requirement) + assert pin_wild is None + + req_multi, pin_multi = _extract_exact_pin("requests==1.*,>=1.0") + assert isinstance(req_multi, Requirement) + assert pin_multi is None + + req_two, pin_two = _extract_exact_pin("requests==1.0,<=2.0") + assert isinstance(req_two, Requirement) + assert pin_two is None + + req_range, pin_range = _extract_exact_pin("requests>=2.0") + assert isinstance(req_range, Requirement) + assert pin_range is None + + +def test_extract_exact_pin_unparseable_requirements() -> None: + """Unparseable requirements fallback cleanly for == and === while rejecting + wildcards and multiple clauses.""" + _, pin = _extract_exact_pin("unparseable-pkg==1.0; invalid @ marker") + assert pin == "1.0" + + _, pin_arb = _extract_exact_pin( + "unparseable-pkg===2021.01.01-legacy; invalid @ marker" + ) + assert pin_arb == "2021.01.01-legacy" + + _, pin_wild = _extract_exact_pin("unparseable-pkg==1.*; invalid @ marker") + assert pin_wild is None + + _, pin_multi = _extract_exact_pin("unparseable-pkg==1.0,<=2.0; invalid @ marker") + assert pin_multi is None + + +def test_resolve_version_wildcard_prefix_defers_to_locked_version( + caplog: pytest.LogCaptureFixture, +) -> None: + """When a declared dependency uses a wildcard (pkg==1.*), it must not be + treated as an exact pin; the locked version is authoritative and emitted.""" + with caplog.at_level(logging.WARNING): + version, note = _resolve_version( + "requests", "requests==1.*", locked_version="1.2.3" + ) + + assert version == "1.2.3" + assert note == "Version resolved: Project lock file" + assert "conflicts with declared exact pin" not in caplog.text + + +def test_resolve_version_pep440_equivalent_pins_emit_no_conflict_warning( + caplog: pytest.LogCaptureFixture, +) -> None: + """Declared pkg==1.0 and locked 1.0.0 are PEP 440 equivalent and must not + trigger a false-positive conflict warning.""" + with caplog.at_level(logging.WARNING): + version, note = _resolve_version( + "requests", "requests==1.0", locked_version="1.0.0" + ) + + assert version == "1.0" + assert note is None + assert "conflicts with declared exact pin" not in caplog.text + + +def test_resolve_version_pep440_different_pins_emit_conflict_warning( + caplog: pytest.LogCaptureFixture, +) -> None: + """Declared pkg==1.0 and locked 2.0.0 genuinely conflict; declared pin wins + with a warning.""" + with caplog.at_level(logging.WARNING): + version, note = _resolve_version( + "requests", "requests==1.0", locked_version="2.0.0" + ) + + assert version == "1.0" + assert note is None + assert "conflicts with declared exact pin '1.0'" in caplog.text + + +def test_resolve_version_arbitrary_equality_pins_matching_and_conflict( + caplog: pytest.LogCaptureFixture, +) -> None: + """Declared pkg===legacy-1 and locked legacy-1 match without warning; + differing locked legacy-2 triggers conflict warning and uses declared pin.""" + caplog.clear() + with caplog.at_level(logging.WARNING): + ver_match, note_match = _resolve_version( + "legacy-pkg", + "legacy-pkg===2021.01.01-legacy", + locked_version="2021.01.01-legacy", + ) + assert ver_match == "2021.01.01-legacy" + assert note_match is None + assert "conflicts with declared exact pin" not in caplog.text + + caplog.clear() + with caplog.at_level(logging.WARNING): + ver_mismatch, note_mismatch = _resolve_version( + "legacy-pkg", + "legacy-pkg===2021.01.01-legacy", + locked_version="2021.01.02-legacy", + ) + assert ver_mismatch == "2021.01.01-legacy" + assert note_mismatch is None + assert "conflicts with declared exact pin '2021.01.01-legacy'" in caplog.text + + +def test_enrich_from_installed_skips_when_installed_version_mismatches( + monkeypatch: pytest.MonkeyPatch, +) -> None: + """If the host has another release installed (e.g. 2.28.0), but the expected + version is 2.31.0, installed metadata must NOT be attached to the SBOM package.""" + fake_meta = _FakeMetadata( + { + "Version": "2.28.0", + "Summary": "Host installed summary for 2.28.0", + "Home-page": "https://host-installed.example.com", + "License-Expression": "MIT", + } + ) + monkeypatch.setattr(deps_installed_mod, "get_pkg_metadata", lambda name: fake_meta) + + doc_uuid = compute_doc_uuid("mismatch-test", "1.0", []) + _clear_doc_counters(doc_uuid) + exporter = Spdx3JsonExporter() + ci = _make_ci() + dep_package = spdx3.software_Package( + spdxId=generate_spdx_id("Package", doc_name="mismatch-test", doc_uuid=doc_uuid), + name="requests", + creationInfo=ci, + ) + dep_package.software_packageVersion = "2.31.0" + exporter.add_package(dep_package) + + filled = _enrich_from_installed( + "requests", + dep_package, + ci, + "mismatch-test", + doc_uuid, + exporter, + expected_version="2.31.0", + ) + + assert filled == set() + assert dep_package.description is None + assert dep_package.software_homePage is None + + +def test_enrich_from_installed_accepts_matching_or_equivalent_version( + monkeypatch: pytest.MonkeyPatch, +) -> None: + """If installed version matches or is PEP 440 equivalent (e.g. 1.0.0 vs 1.0), + installed metadata is safely used.""" + fake_meta = _FakeMetadata( + { + "Version": "1.0.0", + "Summary": "Installed matching summary", + "Home-page": "https://matching.example.com", + } + ) + monkeypatch.setattr(deps_installed_mod, "get_pkg_metadata", lambda name: fake_meta) + + doc_uuid = compute_doc_uuid("match-test", "1.0", []) + _clear_doc_counters(doc_uuid) + exporter = Spdx3JsonExporter() + ci = _make_ci() + dep_package = spdx3.software_Package( + spdxId=generate_spdx_id("Package", doc_name="match-test", doc_uuid=doc_uuid), + name="requests", + creationInfo=ci, + ) + dep_package.software_packageVersion = "1.0" + exporter.add_package(dep_package) + + filled = _enrich_from_installed( + "requests", + dep_package, + ci, + "match-test", + doc_uuid, + exporter, + expected_version="1.0", + ) + + assert "originator" in filled or "license" in filled or dep_package.description + assert dep_package.description == "Installed matching summary" + assert dep_package.software_homePage == "https://matching.example.com" diff --git a/tests/extract/test_poetry_lock.py b/tests/extract/test_poetry_lock.py index 909029cc..fc65c8e8 100644 --- a/tests/extract/test_poetry_lock.py +++ b/tests/extract/test_poetry_lock.py @@ -202,6 +202,23 @@ def test_malformed_groups_field_skipped_and_warns( assert "'groups'" in caplog.text +def test_legacy_poetry_category_main_and_dev() -> None: + """Poetry 1.x locks use category = "main"|"dev" instead of groups. + category = "main" must be included, while category = "dev" must be excluded.""" + with tempfile.TemporaryDirectory() as tmp: + tmp_path = Path(tmp) + _write_lock( + tmp_path, + '[[package]]\nname = "runtime-pkg"\nversion = "1.0.0"\n' + 'category = "main"\n\n' + '[[package]]\nname = "dev-pkg"\nversion = "2.0.0"\n' + 'category = "dev"\n', + ) + + result = extract_poetry_lock_dependencies(tmp_path) + assert result == ["runtime-pkg==1.0.0"] + + def test_optional_package_excluded() -> None: """A package with optional = true is an extra, not a default runtime dependency -- must be excluded.""" @@ -459,12 +476,14 @@ def test_real_world_cleo_tool_poetry_only() -> None: def test_real_world_pastel_tool_poetry_only() -> None: + """`pastel` uses legacy Poetry 1.x `category = "dev"` for all entries; + verifies dev dependencies are excluded and locked_dependencies is empty.""" metadata, _config = read_pyproject( REAL_WORLD_LOCKS / "pastel-0.2.1" / "pyproject.toml" ) assert metadata.name == "pastel" - assert metadata.locked_dependencies + assert metadata.locked_dependencies == [] assert metadata.provenance["locked_dependencies"] == ( "Source: poetry.lock | Method: resolved_lockfile" ) From 93880085a486e68448fce75e4c4defff2ed6ed52 Mon Sep 17 00:00:00 2001 From: Arthit Suriyawongkul Date: Mon, 7 Sep 2026 05:52:30 +0700 Subject: [PATCH 23/35] Update stale docs and checks Signed-off-by: Arthit Suriyawongkul --- docs/dependency-sources.md | 7 +- skills/sbom-generate/SKILL.md | 6 +- src/pitloom/assemble/spdx3/deps.py | 14 ++- src/pitloom/assemble/spdx3/deps_installed.py | 30 +++-- src/pitloom/assemble/spdx3/document.py | 5 +- src/pitloom/extract/_pdm_lock.py | 8 +- src/pitloom/extract/_poetry_lock.py | 8 +- src/pitloom/extract/_pylock.py | 113 ++++++++++++------ src/pitloom/extract/_uv_lock.py | 18 ++- .../assemble/test_deps_locked_dependencies.py | 26 ++++ tests/assemble/test_deps_resolution_pins.py | 27 +++++ tests/extract/test_locked_dependencies.py | 34 +++++- tests/extract/test_pdm_lock.py | 14 +++ tests/extract/test_poetry_lock.py | 13 ++ tests/extract/test_pylock.py | 69 ++++++++++- tests/extract/test_pylock_markers.py | 41 +++++++ tests/extract/test_uv_lock.py | 21 ++++ working-docs/design/lock-files.md | 6 +- .../implementation/lock-file-cascade.md | 48 ++++---- 19 files changed, 414 insertions(+), 94 deletions(-) diff --git a/docs/dependency-sources.md b/docs/dependency-sources.md index 55af9aff..59691c1d 100644 --- a/docs/dependency-sources.md +++ b/docs/dependency-sources.md @@ -114,9 +114,12 @@ either. ## How to tell which source was used -Every SBOM element built from a lock-resolved dependency carries a +Every transitive SBOM package introduced by a lock file carries a provenance annotation naming the file and method Pitloom used, e.g. -`Source: pylock.toml | Method: resolved_lockfile`. The cascade stops at +`Source: pylock.toml | Method: resolved_lockfile`. Direct dependencies +retain their declared source (e.g. `Source: pyproject.toml`), with +`Version resolved: Project lock file` noting when a declared range was +resolved to an exact version by the lock file. The cascade stops at the first usable source it tries, so it doesn't itself check whether a still-lower-priority lock file is *also* present on disk -- the one case it does detect and annotate is `poetry.lock`, since that one is diff --git a/skills/sbom-generate/SKILL.md b/skills/sbom-generate/SKILL.md index cfd562a7..09b441a8 100644 --- a/skills/sbom-generate/SKILL.md +++ b/skills/sbom-generate/SKILL.md @@ -135,8 +135,10 @@ When a lock file is present: back to host environment introspection. - Transitive dependencies from the lock file are emitted as SPDX 3 `software_Package` elements connected via `dependsOn` relationships. -- For resolver lock files (formats 1–5), relationships are marked with - `completeness: complete`. +- Relationship completeness is conservatively left unset (`None`) to + avoid overstating completeness for partial closures (e.g. omitted + VCS/path dependencies or marker-ambiguous variants). + ## Embed an SBOM into a wheel (PEP 770) diff --git a/src/pitloom/assemble/spdx3/deps.py b/src/pitloom/assemble/spdx3/deps.py index c9a95106..1eb32ce1 100644 --- a/src/pitloom/assemble/spdx3/deps.py +++ b/src/pitloom/assemble/spdx3/deps.py @@ -43,6 +43,7 @@ from pitloom.core.project import PhantomDependency from pitloom.core.provenance import ProvenanceConfig from pitloom.export.spdx3_json import Spdx3JsonExporter, require_spdx_id, sha256_hash +from pitloom.extract._lock_common import is_same_version __all__ = [ "_DOWNLOAD_LABELS", @@ -278,7 +279,18 @@ def add_dependencies( grouped: dict[tuple[str, str], list[tuple[str, str, str | None]]] = {} for dep, dep_name, dep_version, version_note in resolved: - canon_key = (canonicalize_name(dep_name), dep_version) + canon_name = canonicalize_name(dep_name) + matched_key = next( + ( + (k_name, k_ver) + for k_name, k_ver in grouped + if k_name == canon_name and is_same_version(k_ver, dep_version) + ), + None, + ) + canon_key = ( + matched_key if matched_key is not None else (canon_name, dep_version) + ) grouped.setdefault(canon_key, []).append((dep, dep_name, version_note)) for (_canon_name, dep_version), declared in grouped.items(): diff --git a/src/pitloom/assemble/spdx3/deps_installed.py b/src/pitloom/assemble/spdx3/deps_installed.py index fd046edd..f95e49f7 100644 --- a/src/pitloom/assemble/spdx3/deps_installed.py +++ b/src/pitloom/assemble/spdx3/deps_installed.py @@ -57,11 +57,13 @@ def _parse_dep_name(dep: str) -> str: def _extract_pin_from_unparseable(dep: str) -> str | None: """Extract an exact pin (== or ===) from an unparseable requirement string.""" dep_spec = dep.split(";", 1)[0] if ";" in dep else dep + if not dep_spec or "," in dep_spec: + return None for op in ("===", "=="): if op not in dep_spec: continue pin_part = dep_spec.split(op, 1)[1].strip() - if not pin_part or "*" in pin_part or "," in pin_part: + if not pin_part or "*" in pin_part: return None try: exact = single_exact_pin(SpecifierSet(f"{op}{pin_part}")) @@ -109,16 +111,18 @@ def _satisfies_constraint(req: Requirement | None, locked_version: str) -> bool: def _resolve_version( - dep_name: str, dep: str, locked_version: str | None = None + dep_name: str, + dep: str, + *, + locked_version: str | None = None, + warn: bool = True, ) -> tuple[str, str | None]: - """Return ``(version_string, resolved_from)`` for a dependency. + """Resolve the authoritative version string and provenance note for *dep*. - An exact ``==``/``===`` pin already present in *dep* -- e.g. a resolved - ``poetry.lock`` entry, or any dependency the project itself pins - exactly -- is authoritative and checked first: it reflects a decision - already resolved by the dependency's own source and must never be - silently overridden by whatever happens to be installed in Pitloom's - own execution environment or a conflicting lock file entry. + Honours the "explicit pin beats local environment" rule: an exact pin + (``==`` or ``===``) declared directly on the dependency is authoritative; + it cannot be silently overridden by whatever happens to be installed in + Pitloom's own execution environment or a conflicting lock file entry. Likewise, a *locked_version* provided by a project lock file (PEP 751 ``pylock.toml``, ``uv.lock``, ``poetry.lock``, etc.) for a direct dependency @@ -131,8 +135,10 @@ def _resolve_version( """ req, pinned = _extract_exact_pin(dep) if pinned is not None: - if locked_version is not None and _is_exact_pin_conflict( - req, pinned, locked_version + if ( + warn + and locked_version is not None + and _is_exact_pin_conflict(req, pinned, locked_version) ): log.warning( "Locked version %r for dependency %r conflicts with declared" @@ -144,7 +150,7 @@ def _resolve_version( return pinned, None if locked_version is not None: - if not _satisfies_constraint(req, locked_version): + if warn and not _satisfies_constraint(req, locked_version): log.warning( "Locked version %r for dependency %r does not satisfy declared" " constraint %r -- using locked version", diff --git a/src/pitloom/assemble/spdx3/document.py b/src/pitloom/assemble/spdx3/document.py index c1664618..718a943f 100644 --- a/src/pitloom/assemble/spdx3/document.py +++ b/src/pitloom/assemble/spdx3/document.py @@ -230,13 +230,14 @@ def _prefetch_combined_release_info( else None ) dep_version, _version_note = _resolve_version( - dep_name, dep, locked_version=locked_ver + dep_name, dep, locked_version=locked_ver, warn=False ) name_version_pairs.append((dep_name, dep_version)) for dep in transitive_only: dep_name = _parse_dep_name(dep) - dep_version, _version_note = _resolve_version(dep_name, dep) + dep_version, _version_note = _resolve_version(dep_name, dep, warn=False) name_version_pairs.append((dep_name, dep_version)) + return _prefetch_pypi_release_infos(name_version_pairs) diff --git a/src/pitloom/extract/_pdm_lock.py b/src/pitloom/extract/_pdm_lock.py index 4dee45ad..5a65e30b 100644 --- a/src/pitloom/extract/_pdm_lock.py +++ b/src/pitloom/extract/_pdm_lock.py @@ -46,6 +46,7 @@ find_first_present_key, group_versions_by_canonical_name, has_required_top_level_table, + is_same_version, load_lock_toml, shape_validated_package, warn_conflicting_versions, @@ -131,9 +132,10 @@ def extract_pdm_lock_dependencies(project_dir: Path) -> list[str] | None: dependencies: list[str] = [] for group in group_versions_by_canonical_name(pairs).values(): name, version = group[0] - conflicting_versions = {v for _, v in group} - if len(conflicting_versions) > 1: - warn_conflicting_versions("pdm.lock", name, conflicting_versions) + conflicting_versions = {v for _, v in group if not is_same_version(v, version)} + if conflicting_versions: + all_versions = {v for _, v in group} + warn_conflicting_versions("pdm.lock", name, all_versions) continue dependencies.append(f"{name}=={version}") return dependencies diff --git a/src/pitloom/extract/_poetry_lock.py b/src/pitloom/extract/_poetry_lock.py index 99ea21f7..b99af512 100644 --- a/src/pitloom/extract/_poetry_lock.py +++ b/src/pitloom/extract/_poetry_lock.py @@ -31,6 +31,7 @@ default_group_included, group_versions_by_canonical_name, has_required_top_level_table, + is_same_version, load_lock_toml, shape_validated_package, warn_conflicting_versions, @@ -88,9 +89,10 @@ def extract_poetry_lock_dependencies(project_dir: Path) -> list[str] | None: dependencies: list[str] = [] for group in group_versions_by_canonical_name(pairs).values(): name, version = group[0] - conflicting_versions = {v for _, v in group} - if len(conflicting_versions) > 1: - warn_conflicting_versions("poetry.lock", name, conflicting_versions) + conflicting_versions = {v for _, v in group if not is_same_version(v, version)} + if conflicting_versions: + all_versions = {v for _, v in group} + warn_conflicting_versions("poetry.lock", name, all_versions) continue dependencies.append(f"{name}=={version}") return dependencies diff --git a/src/pitloom/extract/_pylock.py b/src/pitloom/extract/_pylock.py index 09234c5b..85a32cb2 100644 --- a/src/pitloom/extract/_pylock.py +++ b/src/pitloom/extract/_pylock.py @@ -32,10 +32,12 @@ from typing import Any from packaging.markers import InvalidMarker, Marker +from packaging.utils import canonicalize_name from pitloom.extract._lock_common import ( find_first_present_key, group_versions_by_canonical_name, + is_same_version, load_lock_toml, shape_validated_package, warn_conflicting_versions, @@ -83,33 +85,10 @@ def _parse_lock_version(lock_version: str) -> tuple[int, int] | None: return int(parts[0]), int(parts[1]) -def extract_pylock_dependencies(project_dir: Path) -> list[str] | None: - """Read ``pylock.toml`` next to ``pyproject.toml`` and return its - resolved packages as exact-pin PEP 508 strings. - - Returns ``None`` when no ``pylock.toml`` is present, it can't be - parsed, or its declared ``lock-version`` is unsupported -- this is - optional enrichment, never a requirement, and ``None`` (as opposed - to a valid-but-empty ``[]``) tells :mod:`pitloom.extract._locked_dependencies`'s - cascade this source doesn't apply here, so a lower-priority source - can still be tried, rather than a genuinely dependency-free lock - file being confused with an absent/unusable one. - - Unlike ``poetry.lock``, PEP 751 has no ``groups``-style per-package - membership *field*: a ``pylock.toml`` can bundle more than one - dependency-group's packages in a single flattened ``[[packages]]`` - list, distinguished only by an optional per-package ``marker`` string - referencing the pseudo-environment variables ``extras``/ - ``dependency_groups`` (e.g. ``"'dev' in dependency_groups"``). This - extractor filters to the file's own declared ``default-groups`` (no - extras) the same way ``poetry.lock``/``pdm.lock`` filter to their - ``main``/``default`` group -- see :func:`_group_marker_excludes`. - """ - lock_path = project_dir / "pylock.toml" - data = load_lock_toml(lock_path) - if data is None: - return None - +def _extract_validated_packages( + lock_path: Path, data: dict[str, Any] +) -> list[object] | None: + """Validate top-level PEP 751 keys and return the packages list, or None.""" raw_lock_version = data.get("lock-version") parsed_version = ( _parse_lock_version(raw_lock_version) @@ -148,14 +127,65 @@ def extract_pylock_dependencies(project_dir: Path) -> list[str] | None: supported_minor, ) - packages = data.get("packages", []) + created_by = data.get("created-by") + if not isinstance(created_by, str) or not created_by.strip(): + log.warning( + "%s: missing or malformed top-level 'created-by' key " + "(expected a non-empty string) -- ignoring pylock.toml", + lock_path, + ) + return None + + if "packages" not in data: + log.warning( + "%s: missing top-level 'packages' key (expected a list) -- " + "ignoring pylock.toml", + lock_path, + ) + return None + + packages = data["packages"] if not isinstance(packages, list): warn_top_level_key_wrong_type( lock_path, "packages", packages, "a list", "pylock.toml" ) return None + return packages + + +def extract_pylock_dependencies(project_dir: Path) -> list[str] | None: + """Read ``pylock.toml`` next to ``pyproject.toml`` and return its + resolved packages as exact-pin PEP 508 strings. + + Returns ``None`` when no ``pylock.toml`` is present, it can't be + parsed, or its declared ``lock-version`` is unsupported -- this is + optional enrichment, never a requirement, and ``None`` (as opposed + to a valid-but-empty ``[]``) tells :mod:`pitloom.extract._locked_dependencies`'s + cascade this source doesn't apply here, so a lower-priority source + can still be tried, rather than a genuinely dependency-free lock + file being confused with an absent/unusable one. + + Unlike ``poetry.lock``, PEP 751 has no ``groups``-style per-package + membership *field*: a ``pylock.toml`` can bundle more than one + dependency-group's packages in a single flattened ``[[packages]]`` + list, distinguished only by an optional per-package ``marker`` string + referencing the pseudo-environment variables ``extras``/ + ``dependency_groups`` (e.g. ``"'dev' in dependency_groups"``). This + extractor filters to the file's own declared ``default-groups`` (no + extras) the same way ``poetry.lock``/``pdm.lock`` filter to their + ``main``/``default`` group -- see :func:`_group_marker_excludes`. + """ + lock_path = project_dir / "pylock.toml" + data = load_lock_toml(lock_path) + if data is None: + return None + + packages = _extract_validated_packages(lock_path, data) + if packages is None: + return None environment = _default_group_environment(lock_path, data) + pairs = [ pair for pair in (_pinned_pair_for_package(pkg, environment) for pkg in packages) @@ -165,9 +195,10 @@ def extract_pylock_dependencies(project_dir: Path) -> list[str] | None: dependencies: list[str] = [] for group in group_versions_by_canonical_name(pairs).values(): name, version = group[0] - conflicting_versions = {v for _, v in group} - if len(conflicting_versions) > 1: - warn_conflicting_versions("pylock.toml", name, conflicting_versions) + conflicting_versions = {v for _, v in group if not is_same_version(v, version)} + if conflicting_versions: + all_versions = {v for _, v in group} + warn_conflicting_versions("pylock.toml", name, all_versions) continue dependencies.append(f"{name}=={version}") return dependencies @@ -194,7 +225,10 @@ def _default_group_environment( default_groups, ) default_groups = [] - return {"dependency_groups": frozenset(default_groups), "extras": frozenset()} + return { + "dependency_groups": frozenset(canonicalize_name(g) for g in default_groups), + "extras": frozenset(), + } def _evaluate_group_leaf( @@ -219,7 +253,7 @@ def _evaluate_group_leaf( env_key = "extras" if variable == "extra" else variable active_set = environment.get(env_key, frozenset()) - member = literal in active_set + member = canonicalize_name(literal) in active_set if op in ("in", "=="): return member return not member @@ -342,8 +376,17 @@ def _pinned_pair_for_package( name = validated["name"] version = validated["version"] marker = validated.get("marker") - if isinstance(marker, str) and _group_marker_excludes(marker, environment, name): - return None + if marker is not None: + if not isinstance(marker, str): + log.warning( + "Skipping malformed pylock.toml [[packages]] entry %r: " + "'marker' is %s, expected a string", + name, + type(marker).__name__, + ) + return None + if _group_marker_excludes(marker, environment, name): + return None non_registry_source = find_first_present_key(validated, _NON_REGISTRY_SOURCE_KEYS) if non_registry_source is not None: warn_non_registry_source("pylock.toml", name, non_registry_source) diff --git a/src/pitloom/extract/_uv_lock.py b/src/pitloom/extract/_uv_lock.py index 99a1ed84..de2449a5 100644 --- a/src/pitloom/extract/_uv_lock.py +++ b/src/pitloom/extract/_uv_lock.py @@ -283,14 +283,22 @@ def _enqueue_requested_extras( extra_val = dep_ref.get("extra") or dep_ref.get("extras") if not extra_val: return - requested_extras = ( - [extra_val] - if isinstance(extra_val, str) - else [e for e in extra_val if isinstance(e, str)] - ) + if isinstance(extra_val, str): + requested_extras = [extra_val] + elif isinstance(extra_val, list): + requested_extras = [e for e in extra_val if isinstance(e, str)] + else: + log.warning( + "Skipping uv.lock entry %r requested 'extra'/'extras': " + "expected a string or list, got %s", + pkg.get("name", canonical_name), + type(extra_val).__name__, + ) + return opt_deps_map = pkg.get("optional-dependencies", {}) if not isinstance(opt_deps_map, dict): return + for extra_name in requested_extras: extra_canon = canonicalize_name(extra_name) extra_key = (canonical_name, extra_canon) diff --git a/tests/assemble/test_deps_locked_dependencies.py b/tests/assemble/test_deps_locked_dependencies.py index bdf7bc7b..16200bcd 100644 --- a/tests/assemble/test_deps_locked_dependencies.py +++ b/tests/assemble/test_deps_locked_dependencies.py @@ -422,3 +422,29 @@ def test_extract_locked_version_map_unpinned_does_not_leak_host_environment( "custom-pkg": "legacy.1", "unparseable-pkg": "legacy.2", } + + +def test_add_dependencies_groups_pep440_equivalent_versions() -> None: + """Declared dependencies with PEP 440 equivalent versions (e.g. 2.31 and 2.31.0) + must group under the same Package node rather than creating duplicate nodes.""" + doc_uuid = compute_doc_uuid("equiv-versions", "1.0", []) + _clear_doc_counters(doc_uuid) + exporter = Spdx3JsonExporter() + ci = _make_ci() + + add_dependencies( + ["requests==2.31", "requests==2.31.0"], + "Source: pyproject.toml", + "http://spdx.org/spdxdocs/main-pkg", + ci, + "equiv-versions", + doc_uuid, + exporter, + offline=True, + ) + + pkg_nodes = [ + o for o in exporter.object_set.objects if isinstance(o, spdx3.software_Package) + ] + assert len(pkg_nodes) == 1 + assert pkg_nodes[0].name == "requests" diff --git a/tests/assemble/test_deps_resolution_pins.py b/tests/assemble/test_deps_resolution_pins.py index 36b28e91..600f023b 100644 --- a/tests/assemble/test_deps_resolution_pins.py +++ b/tests/assemble/test_deps_resolution_pins.py @@ -78,6 +78,11 @@ def test_extract_exact_pin_unparseable_requirements() -> None: _, pin_multi = _extract_exact_pin("unparseable-pkg==1.0,<=2.0; invalid @ marker") assert pin_multi is None + _, pin_multi_rev = _extract_exact_pin( + "unparseable-pkg<=2.0,==1.0; invalid @ marker" + ) + assert pin_multi_rev is None + def test_resolve_version_wildcard_prefix_defers_to_locked_version( caplog: pytest.LogCaptureFixture, @@ -233,3 +238,25 @@ def test_enrich_from_installed_accepts_matching_or_equivalent_version( assert "originator" in filled or "license" in filled or dep_package.description assert dep_package.description == "Installed matching summary" assert dep_package.software_homePage == "https://matching.example.com" + + +def test_prefetch_suppresses_conflict_warnings( + monkeypatch: pytest.MonkeyPatch, + caplog: pytest.LogCaptureFixture, +) -> None: + """During online prefetch, version resolution must not duplicate conflict + warnings that the later dependency emission pass will log.""" + from pitloom.assemble.spdx3.document import _prefetch_combined_release_info + + monkeypatch.setattr( + "pitloom.assemble.spdx3.document._prefetch_pypi_release_infos", + lambda pairs: {}, + ) + + caplog.clear() + with caplog.at_level(logging.WARNING): + _prefetch_combined_release_info( + ["requests==1.0"], [], locked_versions={"requests": "2.0.0"} + ) + + assert "conflicts with declared exact pin" not in caplog.text diff --git a/tests/extract/test_locked_dependencies.py b/tests/extract/test_locked_dependencies.py index d9278c21..e8bbdbd4 100644 --- a/tests/extract/test_locked_dependencies.py +++ b/tests/extract/test_locked_dependencies.py @@ -133,7 +133,8 @@ def test_apply_locked_dependencies_valid_empty_source_wins_over_lower_priority() with tempfile.TemporaryDirectory() as tmp: tmp_path = Path(tmp) (tmp_path / "pylock.toml").write_text( - 'lock-version = "1.0"\ncreated-by = "test"\n', encoding="utf-8" + 'lock-version = "1.0"\ncreated-by = "test"\npackages = []\n', + encoding="utf-8", ) (tmp_path / "uv.lock").write_text( 'version = 1\nrevision = 1\nrequires-python = ">=3.10"\n' @@ -159,6 +160,37 @@ def test_apply_locked_dependencies_valid_empty_source_wins_over_lower_priority() assert metadata_uv.locked_dependencies == ["requests==2.31.0"] +def test_apply_locked_dependencies_truncated_pylock_falls_back_to_lower_priority( + caplog: pytest.LogCaptureFixture, +) -> None: + """A truncated pylock.toml (e.g. missing 'packages') is not a valid empty lock; + it must warn and let the cascade fall back to lower-priority sources.""" + with tempfile.TemporaryDirectory() as tmp: + tmp_path = Path(tmp) + (tmp_path / "pylock.toml").write_text( + 'lock-version = "1.0"\ncreated-by = "test"\n', encoding="utf-8" + ) + (tmp_path / "uv.lock").write_text( + 'version = 1\nrevision = 1\nrequires-python = ">=3.10"\n' + '[[package]]\nname = "demo"\nversion = "1.0.0"\n' + 'source = { editable = "." }\n' + 'dependencies = [{ name = "requests" }]\n\n' + '[[package]]\nname = "requests"\nversion = "2.31.0"\n' + 'source = { registry = "https://pypi.org/simple" }\n', + encoding="utf-8", + ) + metadata = ProjectMetadata(name="demo") + + with caplog.at_level(logging.WARNING): + apply_locked_dependencies(metadata, tmp_path) + + assert metadata.locked_dependencies == ["requests==2.31.0"] + assert metadata.provenance["locked_dependencies"] == ( + "Source: uv.lock | Method: resolved_lockfile" + ) + assert "missing top-level 'packages' key" in caplog.text + + def test_read_project_applies_cascade_for_setup_py_only_project() -> None: """Regression: a project with no `pyproject.toml` at all -- just a bare `setup.py`, the realistic pairing for `Pipfile.lock`/pinned diff --git a/tests/extract/test_pdm_lock.py b/tests/extract/test_pdm_lock.py index a7778735..6b1010c2 100644 --- a/tests/extract/test_pdm_lock.py +++ b/tests/extract/test_pdm_lock.py @@ -359,6 +359,20 @@ def test_same_name_conflicting_versions_skipped_and_warns( assert "pinned to conflicting versions" in caplog.text +def test_same_name_equivalent_versions_not_conflicted() -> None: + with tempfile.TemporaryDirectory() as tmp: + tmp_path = Path(tmp) + _write_lock( + tmp_path, + '[[package]]\nname = "pkg"\nversion = "1.0"\n' + 'groups = ["default"]\n\n' + '[[package]]\nname = "pkg"\nversion = "1.0.0"\n' + 'groups = ["default"]\n', + ) + + assert extract_pdm_lock_dependencies(tmp_path) == ["pkg==1.0"] + + # --- read_project() cascade integration ----------------------------------- diff --git a/tests/extract/test_poetry_lock.py b/tests/extract/test_poetry_lock.py index fc65c8e8..9236c5da 100644 --- a/tests/extract/test_poetry_lock.py +++ b/tests/extract/test_poetry_lock.py @@ -253,6 +253,19 @@ def test_conflicting_versions_for_same_package_warns_and_excludes( assert "conflicting versions" in caplog.text +def test_equivalent_versions_for_same_package_not_conflicted() -> None: + with tempfile.TemporaryDirectory() as tmp: + tmp_path = Path(tmp) + _write_lock( + tmp_path, + '[[package]]\nname = "requests"\nversion = "2.31"\ngroups = ["main"]\n\n' + '[[package]]\nname = "requests"\nversion = "2.31.0"\ngroups = ["main"]\n', + ) + + result = extract_poetry_lock_dependencies(tmp_path) + assert result == ["requests==2.31"] + + def test_main_group_package_or_none_non_dict_entry_returns_none() -> None: """A ``[[package]]`` entry that isn't a table (defensive guard against a malformed lock file) is skipped, not a crash.""" diff --git a/tests/extract/test_pylock.py b/tests/extract/test_pylock.py index 63efc15d..b4c0f230 100644 --- a/tests/extract/test_pylock.py +++ b/tests/extract/test_pylock.py @@ -63,11 +63,64 @@ def test_valid_lock_with_no_packages_returns_empty_list_not_none() -> None: winning (if empty) result rather than "not present".""" with tempfile.TemporaryDirectory() as tmp: tmp_path = Path(tmp) - _write_lock(tmp_path) + _write_lock(tmp_path, "packages = []\n") assert extract_pylock_dependencies(tmp_path) == [] +def test_missing_created_by_returns_none_and_warns( + caplog: pytest.LogCaptureFixture, +) -> None: + with tempfile.TemporaryDirectory() as tmp: + tmp_path = Path(tmp) + (tmp_path / "pylock.toml").write_text( + 'lock-version = "1.0"\npackages = []\n', + encoding="utf-8", + ) + + with caplog.at_level(logging.WARNING): + result = extract_pylock_dependencies(tmp_path) + + assert result is None + assert "created-by" in caplog.text + + +def test_empty_created_by_returns_none_and_warns( + caplog: pytest.LogCaptureFixture, +) -> None: + with tempfile.TemporaryDirectory() as tmp: + tmp_path = Path(tmp) + (tmp_path / "pylock.toml").write_text( + 'lock-version = "1.0"\ncreated-by = " "\npackages = []\n', + encoding="utf-8", + ) + + with caplog.at_level(logging.WARNING): + result = extract_pylock_dependencies(tmp_path) + + assert result is None + assert "created-by" in caplog.text + + +def test_missing_packages_key_returns_none_and_warns( + caplog: pytest.LogCaptureFixture, +) -> None: + """A file with only lock-version and created-by is truncated, not a valid + empty lockfile -- must return None and warn so fallback cascades continue.""" + with tempfile.TemporaryDirectory() as tmp: + tmp_path = Path(tmp) + (tmp_path / "pylock.toml").write_text( + 'lock-version = "1.0"\ncreated-by = "test"\n', + encoding="utf-8", + ) + + with caplog.at_level(logging.WARNING): + result = extract_pylock_dependencies(tmp_path) + + assert result is None + assert "packages" in caplog.text + + def test_malformed_toml_returns_none_and_warns( caplog: pytest.LogCaptureFixture, ) -> None: @@ -127,6 +180,7 @@ def test_unsupported_major_lock_version_returns_none_and_warns( tmp_path = Path(tmp) (tmp_path / "pylock.toml").write_text( 'lock-version = "2.0"\n' + 'created-by = "test"\n' '[[packages]]\nname = "requests"\nversion = "2.31.0"\n', encoding="utf-8", ) @@ -148,6 +202,7 @@ def test_newer_minor_lock_version_still_parsed_with_warning( tmp_path = Path(tmp) (tmp_path / "pylock.toml").write_text( 'lock-version = "1.5"\n' + 'created-by = "test"\n' '[[packages]]\nname = "requests"\nversion = "2.31.0"\n', encoding="utf-8", ) @@ -179,6 +234,18 @@ def test_same_name_same_version_duplicate_entries_deduped() -> None: assert extract_pylock_dependencies(tmp_path) == ["httpx==0.28.1"] +def test_same_name_equivalent_versions_not_conflicted() -> None: + with tempfile.TemporaryDirectory() as tmp: + tmp_path = Path(tmp) + _write_lock( + tmp_path, + '[[packages]]\nname = "pkg"\nversion = "1.0"\n\n' + '[[packages]]\nname = "pkg"\nversion = "1.0.0"\n', + ) + + assert extract_pylock_dependencies(tmp_path) == ["pkg==1.0"] + + def test_same_name_conflicting_versions_skipped_and_warns( caplog: pytest.LogCaptureFixture, ) -> None: diff --git a/tests/extract/test_pylock_markers.py b/tests/extract/test_pylock_markers.py index adfc1a21..ef81911b 100644 --- a/tests/extract/test_pylock_markers.py +++ b/tests/extract/test_pylock_markers.py @@ -247,3 +247,44 @@ def test_malformed_marker_string_included_and_warns( assert result == ["broken==1.0.0"] assert "'marker'" in caplog.text + + +def test_default_group_canonicalized_name_matching() -> None: + """Under PEP 735 / PEP 503, dependency group and extra names are + case-insensitive and treat '-', '_', and '.' as equivalent. + Verifies that 'main_deps' matches 'main-deps' and 'Dev_Group' + matches 'dev-group'.""" + with tempfile.TemporaryDirectory() as tmp: + tmp_path = Path(tmp) + _write_lock( + tmp_path, + 'default-groups = ["main-deps", "Dev_Group"]\n' + '[[packages]]\nname = "pkg1"\nversion = "1.0.0"\n' + "marker = \"'main_deps' in dependency_groups\"\n\n" + '[[packages]]\nname = "pkg2"\nversion = "2.0.0"\n' + "marker = \"'dev-group' in dependency_groups\"\n\n" + '[[packages]]\nname = "pkg3"\nversion = "3.0.0"\n' + "marker = \"'other-group' in dependency_groups\"\n", + ) + + assert extract_pylock_dependencies(tmp_path) == [ + "pkg1==1.0.0", + "pkg2==2.0.0", + ] + + +def test_malformed_non_string_marker_skipped_and_warns( + caplog: pytest.LogCaptureFixture, +) -> None: + with tempfile.TemporaryDirectory() as tmp: + tmp_path = Path(tmp) + _write_lock( + tmp_path, + '[[packages]]\nname = "broken"\nversion = "1.0.0"\nmarker = 123\n', + ) + + with caplog.at_level(logging.WARNING): + result = extract_pylock_dependencies(tmp_path) + + assert result == [] + assert "'marker' is int" in caplog.text diff --git a/tests/extract/test_uv_lock.py b/tests/extract/test_uv_lock.py index 6f165862..da007939 100644 --- a/tests/extract/test_uv_lock.py +++ b/tests/extract/test_uv_lock.py @@ -412,3 +412,24 @@ def test_dependency_missing_version_skipped_and_warns( assert not result assert "missing" in caplog.text.lower() + + +def test_dependency_with_malformed_scalar_extra_warns_and_skips( + caplog: pytest.LogCaptureFixture, +) -> None: + """A malformed truthy scalar such as extra = 1 must not crash with TypeError; + it should log a warning and skip the invalid extra reference.""" + with tempfile.TemporaryDirectory() as tmp: + tmp_path = Path(tmp) + _write_lock( + tmp_path, + _ROOT_HEADER + 'dependencies = [{ name = "coverage", extra = 1 }]\n\n' + '[[package]]\nname = "coverage"\nversion = "7.5.0"\n' + 'source = { registry = "https://pypi.org/simple" }\n', + ) + + with caplog.at_level(logging.WARNING): + result = extract_uv_lock_dependencies(tmp_path) + + assert result == ["coverage==7.5.0"] + assert "expected a string or list" in caplog.text diff --git a/working-docs/design/lock-files.md b/working-docs/design/lock-files.md index 61e564e6..4261f144 100644 --- a/working-docs/design/lock-files.md +++ b/working-docs/design/lock-files.md @@ -1,6 +1,6 @@ --- Created: 2026-08-31 -Last-Modified: 2026-09-06 +Last-Modified: 2026-09-07 SPDX-FileCopyrightText: 2026-present Arthit Suriyawongkul SPDX-FileType: DOCUMENTATION SPDX-License-Identifier: CC0-1.0 @@ -14,7 +14,7 @@ See also: [poetry-support.md](../implementation/poetry-support.md)'s roadmap's own priority order, as a scoped follow-on to Poetry wheel-file discovery rather than as part of a general lock-file initiative. Its design (source-stage-only scoping, direct/transitive dedup, additive -`dependsOn` edges tagged `RelationshipCompleteness.complete`) came from +`dependsOn` edges with `RelationshipCompleteness` left conservatively unset) came from [sbom-lifecycle-stages.md](sbom-lifecycle-stages.md)'s source/build/deployed staging model, which this document's priority table doesn't use -- worth reconciling if the two priority framings diverge as more formats land. @@ -23,7 +23,7 @@ reconciling if the two priority framings diverge as more formats land. `pylock.toml` (PEP 751, Phase 1's headline item) support shipped (2026-09-02), reusing `poetry.lock`'s established shape (`ProjectMetadata.locked_dependencies`, additive `dependsOn` edges, -`completeness` tagging, source-stage-only scoping) rather than this +conservative unset `completeness`, source-stage-only scoping) rather than this document's illustrative Pydantic/CycloneDX sketch. It also settles the "which lock file wins" question this document's intro previously left open for the two-lock-files case: `pylock.toml` overrides an diff --git a/working-docs/implementation/lock-file-cascade.md b/working-docs/implementation/lock-file-cascade.md index 1b257d5e..de0c6105 100644 --- a/working-docs/implementation/lock-file-cascade.md +++ b/working-docs/implementation/lock-file-cascade.md @@ -1,6 +1,6 @@ --- Created: 2026-09-04 -Last-Modified: 2026-09-05 +Last-Modified: 2026-09-07 SPDX-FileCopyrightText: 2026-present Arthit Suriyawongkul SPDX-FileType: DOCUMENTATION SPDX-License-Identifier: CC0-1.0 @@ -39,16 +39,20 @@ ordered cascade -- all six formats now registered in it. ## The cascade ```python -_LockExtractor = Callable[[Path, str | None], list[str]] +_LockExtractor = Callable[[Path, str | None], list[str] | None] -_LOCK_SOURCES: list[tuple[str, _LockExtractor | None, str | None]] = [ +_LOCK_SOURCES: list[tuple[str, _LockExtractor, str]] = [ ( "pylock.toml", _ignore_expected_name(extract_pylock_dependencies), "resolved_lockfile", ), ("uv.lock", extract_uv_lock_dependencies, "resolved_lockfile"), - ("poetry.lock", None, None), + ( + "poetry.lock", + _ignore_expected_name(extract_poetry_lock_dependencies), + "resolved_lockfile", + ), ( "pdm.lock", _ignore_expected_name(extract_pdm_lock_dependencies), @@ -75,8 +79,8 @@ Each entry pairs a source name, an extractor matching the uniform of exact-pin PEP 508 strings), and a provenance `Method` tag. Only `uv.lock`'s own extractor uses *expected_name* (to disambiguate a shared workspace lock's multiple local package entries without re-reading -`pyproject.toml` a second time); `pylock.toml`'s, `pdm.lock`'s, and -`Pipfile.lock`'s extractors keep their simpler, single-`project_dir` +`pyproject.toml` a second time); `pylock.toml`'s, `poetry.lock`'s, `pdm.lock`'s, +and `Pipfile.lock`'s extractors keep their simpler, single-`project_dir` signature and are wrapped with `_ignore_expected_name()` when registered in `_LOCK_SOURCES` above, rather than widening every format's own signature for a need only one of them has. `apply_locked_dependencies()` @@ -104,35 +108,32 @@ generated SBOM tell "a resolver actually produced this" from "this merely happened to already be a fully pinned list" -- see [docs/dependency-sources.md](../../docs/dependency-sources.md). -**`poetry.lock` has no extractor here (`None`, `None`), but it *is* in -the table.** It's still applied earlier, gated inside -`_try_read_poetry()`'s `include_locked_dependencies` build-stage flag, -since `poetry.lock` only ever makes sense alongside a `[tool.poetry]` -table -- which requires `pyproject.toml` to exist regardless, so it -needs no `read_project()`-level generalization of its own. What changed -once a format *below* `poetry.lock` in the priority order (`pdm.lock`) -joined the cascade: `poetry.lock` needed a fixed rank in the *same* -list, not just an informal "runs before this cascade" note -- see the -next section for why. +**`poetry.lock` is registered with an extractor in the cascade table.** +When `pyproject.toml` is a Poetry 1.x project, `_try_read_poetry()` extracts +it earlier during project parsing; when the cascade runs, the deduplication +guard recognizes that `poetry.lock` was already extracted and avoids re-parsing +it. For standard PEP 621 or non-Poetry projects accompanied by a `poetry.lock`, +the cascade's registered extractor parses it directly. ## Priority order Same order [docs/dependency-sources.md](../../docs/dependency-sources.md) documents for users, restated here as the exact rank list `_LOCK_SOURCES` must match. Highest to lowest, per -`working-docs/design/roadmap.md`'s "Remaining lock formats" item and +`working-docs/design/roadmap.md`'s completed "Lock/pin formats" item and `lock-files.md`'s phase reasoning (build-backend-agnostic and universal beats tool-specific; a real resolver lock beats a merely-pinned file): 1. `pylock.toml` (PEP 751) -- the interoperability standard. 2. `uv.lock` -3. `poetry.lock` (via `_try_read_poetry()`, not this cascade -- see above) +3. `poetry.lock` 4. `pdm.lock` 5. `Pipfile.lock` -- JSON, not TOML; see its own notes below. 6. pinned `requirements.txt` -- weakest signal, lowest rank; not a real lock file at all, only usable when every line is already an exact `==` pin. See its own notes below. + ## Why `poetry.lock` needs a fixed rank, not just "runs first" Caught while adding `pdm.lock` (rank 4, below `poetry.lock` at rank 3): @@ -145,12 +146,11 @@ and `uv.lock`, ranks 1-2), but silently wrong the moment an entry ranks `poetry.lock`'s already-applied result, even though `pdm.lock` is supposed to lose that comparison. -The fix: `poetry.lock` is a real entry in `_LOCK_SOURCES` (extractor -`None`, since it's applied elsewhere), so its rank is looked up the same -way as everything else instead of being assumed. `apply_locked_dependencies()` -first resolves the rank of whatever source (if any) already populated -`metadata.provenance["locked_dependencies"]` -- today that can only be -`poetry.lock`, via `_try_read_poetry()`, which runs before this cascade +The fix: `poetry.lock` is a real entry in `_LOCK_SOURCES`, so its rank is +looked up the same way as everything else instead of being assumed. +`apply_locked_dependencies()` first resolves the rank of whatever source +(if any) already populated `metadata.provenance["locked_dependencies"]` +(such as `poetry.lock` via `_try_read_poetry()` for Poetry 1.x projects) -- then, walking `_LOCK_SOURCES` in order, stops (`break`) the moment it reaches an entry ranked *below* that already-set source, since nothing from there on could legitimately win. `tests/extract/test_pdm_lock.py::test_read_project_pdm_lock_never_overrides_poetry_lock` From 974e6d7a10b390eab7dff1263e3057f90d7b705d Mon Sep 17 00:00:00 2001 From: Arthit Suriyawongkul Date: Mon, 7 Sep 2026 11:49:09 +0700 Subject: [PATCH 24/35] Normalize PURL Signed-off-by: Arthit Suriyawongkul --- src/pitloom/assemble/_generators.py | 2 +- src/pitloom/assemble/spdx3/deps_installed.py | 12 +- src/pitloom/assemble/spdx3/deps_license.py | 75 +++++++++ src/pitloom/assemble/spdx3/deps_pypi.py | 15 +- src/pitloom/assemble/spdx3/document.py | 77 ++++------ src/pitloom/core/models.py | 4 +- src/pitloom/export/spdx3_json.py | 5 + src/pitloom/extract/_pylock.py | 76 ++++++--- src/pitloom/extract/_uv_lock.py | 17 +- .../test_deps_enrichment_names_versions.py | 1 + ...test_deps_enrichment_originator_license.py | 39 ++++- .../assemble/test_deps_enrichment_prefetch.py | 43 ++++++ .../test_deps_enrichment_pypi_fallback.py | 10 +- .../assemble/test_deps_locked_dependencies.py | 18 +++ tests/assemble/test_deps_resolution_pins.py | 145 +++++++++++++++++- .../core/generator/test_generator_project.py | 129 +++++++++++++++- tests/export/test_spdx3_json.py | 20 +++ tests/extract/test_pylock.py | 10 +- tests/extract/test_pylock_markers.py | 59 +++++-- tests/extract/test_uv_lock_transitive.py | 109 ++++++++++++- .../implementation/lock-file-cascade.md | 2 +- 21 files changed, 741 insertions(+), 127 deletions(-) diff --git a/src/pitloom/assemble/_generators.py b/src/pitloom/assemble/_generators.py index 19463cc8..cced37a1 100644 --- a/src/pitloom/assemble/_generators.py +++ b/src/pitloom/assemble/_generators.py @@ -191,7 +191,7 @@ def generate_project_sbom( project_metadata.version, project_metadata.license_files, ) - project_metadata.files = project_files + project_metadata = dataclasses.replace(project_metadata, files=project_files) search_root = target_path ai_models = ( diff --git a/src/pitloom/assemble/spdx3/deps_installed.py b/src/pitloom/assemble/spdx3/deps_installed.py index f95e49f7..5ec7ec18 100644 --- a/src/pitloom/assemble/spdx3/deps_installed.py +++ b/src/pitloom/assemble/spdx3/deps_installed.py @@ -62,17 +62,17 @@ def _extract_pin_from_unparseable(dep: str) -> str | None: for op in ("===", "=="): if op not in dep_spec: continue - pin_part = dep_spec.split(op, 1)[1].strip() + prefix, pin_part = dep_spec.split(op, 1) + if any(other in prefix for other in _VERSION_OPERATORS): + return None + pin_part = pin_part.strip() if not pin_part or "*" in pin_part: return None try: exact = single_exact_pin(SpecifierSet(f"{op}{pin_part}")) - if exact is not None: - return exact[1] + return exact[1] if exact is not None else None except InvalidSpecifier: - if op == "===": - return pin_part - return None + return None return None diff --git a/src/pitloom/assemble/spdx3/deps_license.py b/src/pitloom/assemble/spdx3/deps_license.py index 8464bf67..5179ace0 100644 --- a/src/pitloom/assemble/spdx3/deps_license.py +++ b/src/pitloom/assemble/spdx3/deps_license.py @@ -22,6 +22,7 @@ parse_provenance_value, ) from pitloom.core.models import build_relationship, generate_spdx_id +from pitloom.core.project import ProjectMetadata from pitloom.core.provenance import ProvenanceConfig from pitloom.export.spdx3_json import Spdx3JsonExporter, require_spdx_id from pitloom.extract._license import ( @@ -403,3 +404,77 @@ def _add_license_noassertion( doc_uuid, ) ) + + +# pylint: disable=too-many-arguments,too-many-positional-arguments +def attach_main_package_license( + metadata: ProjectMetadata, + main_package: spdx3.software_Package, + spdx_ci: spdx3.CreationInfo, + spdx_doc: spdx3.SpdxDocument, + doc_uuid: str, + exporter: Spdx3JsonExporter, + *, + provenance_config: ProvenanceConfig | None = None, + encoder: ProvenanceEncoder | None = None, +) -> None: + """Attach declared and/or concluded license elements and relationships for + the main Python project package.""" + if metadata.license_name: + spdx_doc.profileConformance.append(spdx3.ProfileIdentifierType.simpleLicensing) + rel_declared, rel_concluded = build_license_elements( + license_id=metadata.license_name, + package_spdx_id=require_spdx_id(main_package), + license_provenance=metadata.provenance.get( + "license", "Source: pyproject.toml | Field: project.license" + ), + creation_info=spdx_ci, + doc_name=metadata.name, + doc_uuid=doc_uuid, + exporter=exporter, + concluded_license_id=metadata.license_concluded, + concluded_license_provenance=metadata.provenance.get("license_concluded"), + provenance_config=provenance_config, + encoder=encoder, + ) + if rel_declared: + exporter.add_relationship(rel_declared) + if rel_concluded: + exporter.add_relationship(rel_concluded) + elif metadata.license_concluded: + spdx_doc.profileConformance.append(spdx3.ProfileIdentifierType.simpleLicensing) + _add_license_noassertion( + main_package, + spdx_ci, + metadata.name, + doc_uuid, + exporter, + provenance_config=provenance_config, + encoder=encoder, + ) + _rel_dec, rel_concluded = build_license_elements( + license_id=metadata.license_concluded, + package_spdx_id=require_spdx_id(main_package), + license_provenance=metadata.provenance.get( + "license_concluded", + "Source: LICENSE | Method: licenseid_detection", + ), + creation_info=spdx_ci, + doc_name=metadata.name, + doc_uuid=doc_uuid, + exporter=exporter, + provenance_config=provenance_config, + encoder=encoder, + ) + if rel_concluded: + exporter.add_relationship(rel_concluded) + else: + _add_license_noassertion( + main_package, + spdx_ci, + metadata.name, + doc_uuid, + exporter, + provenance_config=provenance_config, + encoder=encoder, + ) diff --git a/src/pitloom/assemble/spdx3/deps_pypi.py b/src/pitloom/assemble/spdx3/deps_pypi.py index 72548a95..22bcb16c 100644 --- a/src/pitloom/assemble/spdx3/deps_pypi.py +++ b/src/pitloom/assemble/spdx3/deps_pypi.py @@ -112,11 +112,20 @@ def _extract_release_hash(release_info: dict[str, Any]) -> str | None: by_type = {u.get("packagetype"): u for u in urls if isinstance(u, dict)} entry = by_type.get("bdist_wheel") or by_type.get("sdist") if entry is None and urls: - entry = urls[0] + entry = next((u for u in urls if isinstance(u, dict)), None) if entry is None: return None - digest = (entry.get("digests") or {}).get("sha256") - return digest or None + digests = entry.get("digests") + if not isinstance(digests, dict): + return None + digest = digests.get("sha256") + if ( + isinstance(digest, str) + and len(digest) == 64 + and all(c in "0123456789abcdefABCDEF" for c in digest) + ): + return digest.lower() + return None def _prefetch_pypi_release_infos( diff --git a/src/pitloom/assemble/spdx3/document.py b/src/pitloom/assemble/spdx3/document.py index 718a943f..4a445e05 100644 --- a/src/pitloom/assemble/spdx3/document.py +++ b/src/pitloom/assemble/spdx3/document.py @@ -44,10 +44,7 @@ add_phantom_dependencies, ) from pitloom.assemble.spdx3.deps_installed import _extract_exact_pin -from pitloom.assemble.spdx3.deps_license import ( - _add_license_noassertion, - build_license_elements, -) +from pitloom.assemble.spdx3.deps_license import attach_main_package_license from pitloom.assemble.spdx3.deps_pypi import _prefetch_pypi_release_infos from pitloom.assemble.spdx3.provenance import ( ProvenanceEncoder, @@ -65,6 +62,7 @@ from pitloom.core.provenance import ProvenanceConfig from pitloom.enrich.base import EnrichmentResult from pitloom.export.spdx3_json import Spdx3JsonExporter, require_spdx_id, sha256_hash +from pitloom.extract._lock_common import is_same_version, warn_conflicting_versions from pitloom.ids import IdRegistry __all__ = [ @@ -174,7 +172,7 @@ def _locked_transitive_only_dependencies(metadata: ProjectMetadata) -> list[str] } return [ dep - for dep in metadata.locked_dependencies + for dep in (metadata.locked_dependencies or []) if canonicalize_name(_parse_dep_name(dep)) not in direct_names ] @@ -196,7 +194,9 @@ def _locked_dependencies_completeness(metadata: ProjectMetadata) -> str | None: return None -def _extract_locked_version_map(locked_dependencies: list[str]) -> dict[str, str]: +def _extract_locked_version_map( + locked_dependencies: list[str] | None, +) -> dict[str, str]: """Map canonical package names to their exact locked version string. Enables direct dependencies declared as ranges (e.g. ``requests>=2.0``) @@ -204,11 +204,18 @@ def _extract_locked_version_map(locked_dependencies: list[str]) -> dict[str, str to introspecting Pitloom's host environment. """ result: dict[str, str] = {} - for dep in locked_dependencies: + for dep in locked_dependencies or []: dep_name = _parse_dep_name(dep) _req, pinned = _extract_exact_pin(dep) if pinned is not None: - result[canonicalize_name(dep_name)] = pinned + canon = canonicalize_name(dep_name) + if canon in result and not is_same_version(result[canon], pinned): + warn_conflicting_versions( + "locked dependencies", + dep_name, + [result[canon], pinned], + ) + result[canon] = pinned return result @@ -327,44 +334,16 @@ def build( ) # --- License --- - if metadata.license_name: - spdx_doc.profileConformance.append(spdx3.ProfileIdentifierType.simpleLicensing) - rel_declared, rel_concluded = build_license_elements( - license_id=metadata.license_name, - package_spdx_id=require_spdx_id(main_package), - license_provenance=metadata.provenance.get( - "license", "Source: pyproject.toml | Field: project.license" - ), - creation_info=spdx_ci, - doc_name=metadata.name, - doc_uuid=doc_uuid, - exporter=exporter, - # G2: only the pyproject.toml [project]-path extractor populates - # license_concluded (independent directory scan) -- None here for - # any other backend, which keeps this the original single-value - # behavior unchanged. - concluded_license_id=metadata.license_concluded, - concluded_license_provenance=metadata.provenance.get("license_concluded"), - provenance_config=prov_cfg, - encoder=encoder, - ) - if rel_declared: - exporter.add_relationship(rel_declared) - if rel_concluded: - exporter.add_relationship(rel_concluded) - else: - # No license declared anywhere pitloom looked -- assert that - # explicitly rather than silently omitting the field; see - # add_dependencies' identical NOASSERTION policy for dependencies. - _add_license_noassertion( - main_package, - spdx_ci, - metadata.name, - doc_uuid, - exporter, - provenance_config=prov_cfg, - encoder=encoder, - ) + attach_main_package_license( + metadata=metadata, + main_package=main_package, + spdx_ci=spdx_ci, + spdx_doc=spdx_doc, + doc_uuid=doc_uuid, + exporter=exporter, + provenance_config=prov_cfg, + encoder=encoder, + ) # --- Locked (e.g. poetry.lock-resolved) transitive-only dependencies --- transitive_only = _locked_transitive_only_dependencies(metadata) @@ -466,4 +445,10 @@ def build( enrichment_results_by_model=enrichment_results_by_model, ) + if ( + spdx3.ProfileIdentifierType.simpleLicensing not in spdx_doc.profileConformance + and exporter.has_licenses + ): + spdx_doc.profileConformance.append(spdx3.ProfileIdentifierType.simpleLicensing) + return exporter diff --git a/src/pitloom/core/models.py b/src/pitloom/core/models.py index 97a4f8e4..bc6527e7 100644 --- a/src/pitloom/core/models.py +++ b/src/pitloom/core/models.py @@ -64,7 +64,9 @@ def normalize_dependency_specifier(dep: str) -> str: def build_pypi_purl(name: str, version: str | None) -> str: """Return a canonical ``pkg:pypi/[@]`` Package URL.""" base = f"pkg:pypi/{canonicalize_name(name)}" - return f"{base}@{version}" if version and version != "unknown" else base + if version and version != "unknown": + return f"{base}@{version.replace('+', '%2B')}" + return base def _clear_doc_counters(doc_uuid: str) -> None: diff --git a/src/pitloom/export/spdx3_json.py b/src/pitloom/export/spdx3_json.py index ef1a1406..9fba9907 100644 --- a/src/pitloom/export/spdx3_json.py +++ b/src/pitloom/export/spdx3_json.py @@ -317,6 +317,11 @@ def find_license(self, license_id: str) -> str | None: """ return self._license_index.get(license_id) + @property + def has_licenses(self) -> bool: + """Return True if any real (non-NOASSERTION) license text has been added.""" + return any(k != "NOASSERTION" for k in self._license_index) + def add_license( self, simple_licensing_text: spdx3.simplelicensing_SimpleLicensingText ) -> None: diff --git a/src/pitloom/extract/_pylock.py b/src/pitloom/extract/_pylock.py index 85a32cb2..15ba3784 100644 --- a/src/pitloom/extract/_pylock.py +++ b/src/pitloom/extract/_pylock.py @@ -38,9 +38,12 @@ find_first_present_key, group_versions_by_canonical_name, is_same_version, + is_usable_version, load_lock_toml, - shape_validated_package, warn_conflicting_versions, + warn_malformed_entry_not_table, + warn_missing_name, + warn_missing_version, warn_non_registry_source, warn_top_level_key_wrong_type, ) @@ -241,22 +244,33 @@ def _evaluate_group_leaf( variable is treated as unknown rather than really evaluated.""" lhs, raw_op, rhs = node op = str(raw_op) - if op not in ("in", "not in", "==", "!="): - return None lhs_str, rhs_str = str(lhs), str(rhs) if rhs_str in _GROUP_MARKER_VARIABLES: variable, literal = rhs_str, lhs_str + is_reversed = False elif lhs_str in _GROUP_MARKER_VARIABLES: variable, literal = lhs_str, rhs_str + is_reversed = True else: return None - env_key = "extras" if variable == "extra" else variable - active_set = environment.get(env_key, frozenset()) + if variable in ("dependency_groups", "extras"): + # Under PEP 751, dependency_groups and extras are sets of strings. + # Membership is tested strictly via `literal in variable` or + # `literal not in variable` (set membership). Set-to-string equality + # (`!=` / `==`) and reversed `set in string` are not membership. + if is_reversed or op not in ("in", "not in"): + return None + active_set = environment.get(variable, frozenset()) + member = canonicalize_name(literal) in active_set + return member if op == "in" else not member + + # variable == "extra" (PEP 508 singular string variable) + if op not in ("==", "!=", "in", "not in"): + return None + active_set = environment.get("extras", frozenset()) member = canonicalize_name(literal) in active_set - if op in ("in", "=="): - return member - return not member + return member if op in ("==", "in") else not member def _all3(values: list[bool | None]) -> bool | None: @@ -344,6 +358,21 @@ def _group_marker_excludes( return False +def _is_marker_excluded( + marker: Any, environment: dict[str, frozenset[str]], name: str +) -> bool: + """Return True if marker is non-string (malformed) or excludes the entry.""" + if not isinstance(marker, str): + log.warning( + "Skipping malformed pylock.toml [[packages]] entry %r: " + "'marker' is %s, expected a string", + name, + type(marker).__name__, + ) + return True + return _group_marker_excludes(marker, environment, name) + + def _pinned_pair_for_package( pkg: object, environment: dict[str, frozenset[str]] ) -> tuple[str, str] | None: @@ -370,25 +399,22 @@ def _pinned_pair_for_package( gated on different, unevaluated ``python_version``/``sys_platform`` markers can both survive to this point. """ - validated = shape_validated_package(pkg, "pylock.toml", "[[packages]]") - if validated is None: + if not isinstance(pkg, dict): + warn_malformed_entry_not_table("pylock.toml", "[[packages]]", pkg) return None - name = validated["name"] - version = validated["version"] - marker = validated.get("marker") - if marker is not None: - if not isinstance(marker, str): - log.warning( - "Skipping malformed pylock.toml [[packages]] entry %r: " - "'marker' is %s, expected a string", - name, - type(marker).__name__, - ) - return None - if _group_marker_excludes(marker, environment, name): - return None - non_registry_source = find_first_present_key(validated, _NON_REGISTRY_SOURCE_KEYS) + name = pkg.get("name") + if not isinstance(name, str) or not name.strip(): + warn_missing_name("Skipping malformed pylock.toml [[packages]] entry", name) + return None + non_registry_source = find_first_present_key(pkg, _NON_REGISTRY_SOURCE_KEYS) if non_registry_source is not None: warn_non_registry_source("pylock.toml", name, non_registry_source) return None + version = pkg.get("version") + if not is_usable_version(version): + warn_missing_version("pylock.toml", name) + return None + marker = pkg.get("marker") + if marker is not None and _is_marker_excluded(marker, environment, name): + return None return name, version diff --git a/src/pitloom/extract/_uv_lock.py b/src/pitloom/extract/_uv_lock.py index de2449a5..dfb6d8bc 100644 --- a/src/pitloom/extract/_uv_lock.py +++ b/src/pitloom/extract/_uv_lock.py @@ -169,16 +169,13 @@ def _find_root_package( def _resolved_package_for_dependency( - dep_ref: object, by_name: dict[str, list[dict[str, Any]]] + dep_ref: dict[str, Any], by_name: dict[str, list[dict[str, Any]]] ) -> dict[str, Any] | None: """Return the single, unambiguous ``[[package]]`` entry that one ``dependencies``-list reference resolves to -- the root package's own, or one already-visited package's own nested reference during the transitive walk in :func:`_collect_transitive_dependencies` -- or ``None`` when it can't be resolved that way.""" - if not isinstance(dep_ref, dict): - warn_malformed_entry_not_table("uv.lock", "dependency reference", dep_ref) - return None name = dep_ref.get("name") if not isinstance(name, str) or not name: warn_missing_name("Skipping malformed uv.lock dependency reference", name) @@ -241,6 +238,9 @@ def _collect_transitive_dependencies( queue: deque[object] = deque(root_dependencies) while queue: dep_ref = queue.popleft() + if not isinstance(dep_ref, dict): + warn_malformed_entry_not_table("uv.lock", "dependency reference", dep_ref) + continue pkg = _resolved_package_for_dependency(dep_ref, by_name) if pkg is None: continue @@ -252,10 +252,10 @@ def _collect_transitive_dependencies( if pin is not None: dependencies[canonical_name] = pin - nested = pkg.get("dependencies", []) + nested = pkg.get("dependencies") if isinstance(nested, list): queue.extend(nested) - elif nested: + elif nested is not None: log.warning( "Skipping uv.lock entry %r nested 'dependencies': " "expected a list, got %s", @@ -263,10 +263,7 @@ def _collect_transitive_dependencies( type(nested).__name__, ) - if isinstance(dep_ref, dict): - _enqueue_requested_extras( - dep_ref, pkg, canonical_name, visited_extras, queue - ) + _enqueue_requested_extras(dep_ref, pkg, canonical_name, visited_extras, queue) return list(dependencies.values()) diff --git a/tests/assemble/test_deps_enrichment_names_versions.py b/tests/assemble/test_deps_enrichment_names_versions.py index ab3d6d64..9b809c26 100644 --- a/tests/assemble/test_deps_enrichment_names_versions.py +++ b/tests/assemble/test_deps_enrichment_names_versions.py @@ -250,6 +250,7 @@ def test_build_pypi_purl_omits_version_when_none() -> None: def test_build_pypi_purl_includes_version_when_known() -> None: assert build_pypi_purl("auditwheel", "6.7.0") == "pkg:pypi/auditwheel@6.7.0" + assert build_pypi_purl("torch", "2.0.0+cpu") == "pkg:pypi/torch@2.0.0%2Bcpu" def test_add_dependencies_sets_name_only_purl_for_unresolved_version( diff --git a/tests/assemble/test_deps_enrichment_originator_license.py b/tests/assemble/test_deps_enrichment_originator_license.py index af7d16a8..09920099 100644 --- a/tests/assemble/test_deps_enrichment_originator_license.py +++ b/tests/assemble/test_deps_enrichment_originator_license.py @@ -294,20 +294,31 @@ def test_extract_pypi_license_absent_returns_none() -> None: def test_extract_release_hash_prefers_wheel() -> None: + sdist_hash = "a" * 64 + wheel_hash = "b" * 64 release_info = { "urls": [ - {"packagetype": "sdist", "digests": {"sha256": "sdist-hash"}}, - {"packagetype": "bdist_wheel", "digests": {"sha256": "wheel-hash"}}, + {"packagetype": "sdist", "digests": {"sha256": sdist_hash}}, + {"packagetype": "bdist_wheel", "digests": {"sha256": wheel_hash}}, ] } - assert _extract_release_hash(release_info) == "wheel-hash" + assert _extract_release_hash(release_info) == wheel_hash def test_extract_release_hash_falls_back_to_sdist() -> None: + sdist_hash = "a" * 64 release_info = { - "urls": [{"packagetype": "sdist", "digests": {"sha256": "sdist-hash"}}] + "urls": [{"packagetype": "sdist", "digests": {"sha256": sdist_hash}}] } - assert _extract_release_hash(release_info) == "sdist-hash" + assert _extract_release_hash(release_info) == sdist_hash + + +def test_extract_release_hash_falls_back_to_first_url() -> None: + egg_hash = "c" * 64 + release_info = { + "urls": [{"packagetype": "bdist_egg", "digests": {"sha256": egg_hash}}] + } + assert _extract_release_hash(release_info) == egg_hash def test_extract_release_hash_no_urls_returns_none() -> None: @@ -315,6 +326,24 @@ def test_extract_release_hash_no_urls_returns_none() -> None: assert _extract_release_hash({}) is None +def test_extract_release_hash_validates_hex_sha256() -> None: + # Non-dict digests + assert _extract_release_hash({"urls": [{"digests": "not-a-dict"}]}) is None + assert _extract_release_hash({"urls": [{"digests": [1, 2, 3]}]}) is None + assert _extract_release_hash({"urls": [{"digests": 123}]}) is None + # Invalid length or characters + assert ( + _extract_release_hash({"urls": [{"digests": {"sha256": "too-short"}}]}) is None + ) + assert _extract_release_hash({"urls": [{"digests": {"sha256": "g" * 64}}]}) is None + # Uppercase normalized to lowercase + upper_hash = ("A" * 32) + ("B" * 32) + assert ( + _extract_release_hash({"urls": [{"digests": {"sha256": upper_hash}}]}) + == upper_hash.lower() + ) + + # --------------------------------------------------------------------------- # _resolve_address_entry -- no-email and all-empty edge cases # --------------------------------------------------------------------------- diff --git a/tests/assemble/test_deps_enrichment_prefetch.py b/tests/assemble/test_deps_enrichment_prefetch.py index 526f6b9d..849028c5 100644 --- a/tests/assemble/test_deps_enrichment_prefetch.py +++ b/tests/assemble/test_deps_enrichment_prefetch.py @@ -35,6 +35,7 @@ _fetch_pypi_release_info, _prefetch_pypi_release_infos, ) +from pitloom.assemble.spdx3.document import _prefetch_combined_release_info from pitloom.core.models import _clear_doc_counters, compute_doc_uuid, generate_spdx_id from pitloom.export.spdx3_json import Spdx3JsonExporter, require_spdx_id @@ -354,3 +355,45 @@ def test_resolve_remote_authors_file_success_and_branches( content_type_method="auto", ) assert locator == "https://github.com/foo" + + +def test_prefetch_pypi_release_infos_worker_exception( + monkeypatch: pytest.MonkeyPatch, +) -> None: + """_prefetch_pypi_release_infos safely captures exceptions from worker threads.""" + + def _fail_fetch(name: str, version: str | None) -> dict[str, Any] | None: + raise RuntimeError("Worker thread error") + + monkeypatch.setattr(deps_pypi, "_fetch_pypi_release_info", _fail_fetch) + results = _prefetch_pypi_release_infos([("requests", "2.31.0")]) + assert results == {("requests", "2.31.0"): None} + + +def test_prefetch_combined_release_info_with_transitive_only( + monkeypatch: pytest.MonkeyPatch, +) -> None: + """_prefetch_combined_release_info resolves and prefetches transitive-only deps.""" + calls: list[tuple[str, str]] = [] + + def _mock_prefetch(pairs: Any) -> dict[Any, Any]: + calls.extend(pairs) + return {} + + monkeypatch.setattr( + "pitloom.assemble.spdx3.document._prefetch_pypi_release_infos", + _mock_prefetch, + ) + _prefetch_combined_release_info( + ["requests>=2.0"], ["urllib3==2.0.0"], locked_versions={"requests": "2.31.0"} + ) + assert ("requests", "2.31.0") in calls + assert ("urllib3", "2.0.0") in calls + + # Also test locked_versions=None branch + calls.clear() + _prefetch_combined_release_info( + ["requests==2.31.0"], ["urllib3==2.0.0"], locked_versions=None + ) + assert ("requests", "2.31.0") in calls + assert ("urllib3", "2.0.0") in calls diff --git a/tests/assemble/test_deps_enrichment_pypi_fallback.py b/tests/assemble/test_deps_enrichment_pypi_fallback.py index 8bf3be88..769d43ed 100644 --- a/tests/assemble/test_deps_enrichment_pypi_fallback.py +++ b/tests/assemble/test_deps_enrichment_pypi_fallback.py @@ -39,6 +39,8 @@ from .conftest import _make_ci +_real_fetch_pypi = deps_pypi._fetch_pypi_release_info + # --------------------------------------------------------------------------- # _resolve_metadata_url -- deterministic label priority, not hash-order # --------------------------------------------------------------------------- @@ -310,7 +312,6 @@ def test_add_license_noassertion_is_deduped() -> None: # --------------------------------------------------------------------------- -@pytest.mark.pypi_network def test_fetch_pypi_release_info_versioned_url( monkeypatch: pytest.MonkeyPatch, ) -> None: @@ -321,34 +322,37 @@ def _fake_fetch_json(url: str, *, timeout: float) -> dict[str, object]: captured["timeout"] = timeout return {"info": {}} + monkeypatch.setattr(deps_pypi, "_fetch_pypi_release_info", _real_fetch_pypi) monkeypatch.setattr(deps_pypi, "fetch_json", _fake_fetch_json) result = deps_pypi._fetch_pypi_release_info("requests", "2.31.0") assert captured["url"] == "https://pypi.org/pypi/requests/2.31.0/json" assert result == {"info": {}} -@pytest.mark.pypi_network def test_fetch_pypi_release_info_unversioned_url( monkeypatch: pytest.MonkeyPatch, ) -> None: captured: dict[str, object] = {} def _fake_fetch_json(url: str, *, timeout: float) -> dict[str, object]: + del timeout captured["url"] = url return {"info": {}} + monkeypatch.setattr(deps_pypi, "_fetch_pypi_release_info", _real_fetch_pypi) monkeypatch.setattr(deps_pypi, "fetch_json", _fake_fetch_json) deps_pypi._fetch_pypi_release_info("requests", None) assert captured["url"] == "https://pypi.org/pypi/requests/json" -@pytest.mark.pypi_network def test_fetch_pypi_release_info_returns_none_on_value_error( monkeypatch: pytest.MonkeyPatch, ) -> None: def _raise(url: str, *, timeout: float) -> dict[str, object]: + del url, timeout raise ValueError("network error") + monkeypatch.setattr(deps_pypi, "_fetch_pypi_release_info", _real_fetch_pypi) monkeypatch.setattr(deps_pypi, "fetch_json", _raise) assert deps_pypi._fetch_pypi_release_info("requests", None) is None diff --git a/tests/assemble/test_deps_locked_dependencies.py b/tests/assemble/test_deps_locked_dependencies.py index 16200bcd..e1c11e82 100644 --- a/tests/assemble/test_deps_locked_dependencies.py +++ b/tests/assemble/test_deps_locked_dependencies.py @@ -26,6 +26,7 @@ from pitloom.assemble.spdx3.document import ( _extract_locked_version_map, _locked_dependencies_completeness, + _locked_transitive_only_dependencies, build, ) from pitloom.core.creation import CreationMetadata @@ -448,3 +449,20 @@ def test_add_dependencies_groups_pep440_equivalent_versions() -> None: ] assert len(pkg_nodes) == 1 assert pkg_nodes[0].name == "requests" + + +def test_extract_locked_version_map_warns_on_conflicting_duplicates( + caplog: pytest.LogCaptureFixture, +) -> None: + """Conflicting duplicate package entries in locked_dependencies must warn.""" + caplog.set_level("WARNING") + locked_map = _extract_locked_version_map(["requests==2.31.0", "requests==2.28.0"]) + assert locked_map["requests"] == "2.28.0" + assert "pinned to conflicting versions" in caplog.text + + +def test_locked_transitive_only_dependencies_handles_none_locked() -> None: + """None locked_dependencies must safely return empty list without TypeError.""" + meta = ProjectMetadata(name="testpkg", dependencies=["requests>=2.0"]) + meta.locked_dependencies = None # type: ignore[assignment] + assert _locked_transitive_only_dependencies(meta) == [] diff --git a/tests/assemble/test_deps_resolution_pins.py b/tests/assemble/test_deps_resolution_pins.py index 600f023b..30e7fe5e 100644 --- a/tests/assemble/test_deps_resolution_pins.py +++ b/tests/assemble/test_deps_resolution_pins.py @@ -11,12 +11,16 @@ wildcards, multi-specifiers) and installed metadata version mismatch isolation. """ +# pylint: disable=protected-access + from __future__ import annotations import logging +from unittest.mock import MagicMock import pytest from packaging.requirements import Requirement +from packaging.version import InvalidVersion from spdx_python_model.bindings import v3_0_1 as spdx3 import pitloom.assemble.spdx3.deps_installed as deps_installed_mod @@ -25,6 +29,7 @@ _extract_exact_pin, _resolve_version, ) +from pitloom.assemble.spdx3.document import _prefetch_combined_release_info from pitloom.core.models import _clear_doc_counters, compute_doc_uuid, generate_spdx_id from pitloom.export.spdx3_json import Spdx3JsonExporter @@ -246,8 +251,6 @@ def test_prefetch_suppresses_conflict_warnings( ) -> None: """During online prefetch, version resolution must not duplicate conflict warnings that the later dependency emission pass will log.""" - from pitloom.assemble.spdx3.document import _prefetch_combined_release_info - monkeypatch.setattr( "pitloom.assemble.spdx3.document._prefetch_pypi_release_infos", lambda pairs: {}, @@ -260,3 +263,141 @@ def test_prefetch_suppresses_conflict_warnings( ) assert "conflicts with declared exact pin" not in caplog.text + + +def test_extract_exact_pin_unparseable_invalid_specifier_returns_none() -> None: + """When an unparseable requirement has == with an invalid specifier, return None.""" + _, pin = _extract_exact_pin("invalid name @ == bad-version") + assert pin is None + + +def test_extract_exact_pin_unparseable_arbitrary_equality_invalid_specifier() -> None: + """When an unparseable requirement has === with an invalid specifier containing + whitespace, it must return None to avoid polluting SBOM with invalid versions.""" + _, pin = _extract_exact_pin("invalid name @ === foo bar") + assert pin is None + + +def test_is_exact_pin_conflict_invalid_version_in_contains() -> None: + """When req.specifier.contains raises InvalidVersion, fall through to + is_same_version.""" + mock_req = MagicMock(spec=Requirement) + mock_req.specifier = MagicMock() + mock_req.specifier.contains.side_effect = InvalidVersion("invalid version") + + assert not deps_installed_mod._is_exact_pin_conflict(mock_req, "1.0", "1.0") + assert deps_installed_mod._is_exact_pin_conflict(mock_req, "1.0", "2.0") + + +def test_satisfies_constraint_req_none_or_empty_specifier() -> None: + """_satisfies_constraint returns True when req is None or has no specifier.""" + assert deps_installed_mod._satisfies_constraint(None, "1.0.0") + req_no_spec = Requirement("requests") + assert deps_installed_mod._satisfies_constraint(req_no_spec, "1.0.0") + + +def test_satisfies_constraint_invalid_version_returns_false() -> None: + """When req.specifier.contains raises InvalidVersion, return False.""" + mock_req = MagicMock(spec=Requirement) + mock_req.specifier = MagicMock() + mock_req.specifier.contains.side_effect = InvalidVersion("invalid version") + assert not deps_installed_mod._satisfies_constraint( + mock_req, "not-a-pep440-version" + ) + + +def test_resolve_version_warn_false_suppresses_unsatisfied_warning( + caplog: pytest.LogCaptureFixture, +) -> None: + """When warn=False, locked version conflict with specifier must not warn.""" + caplog.clear() + with caplog.at_level(logging.WARNING): + version, note = _resolve_version( + "requests", "requests>=2.0", locked_version="1.0", warn=False + ) + assert version == "1.0" + assert note == "Version resolved: Project lock file" + assert "does not satisfy declared constraint" not in caplog.text + + +def test_resolve_version_warn_true_logs_unsatisfied_warning( + caplog: pytest.LogCaptureFixture, +) -> None: + """When warn=True, locked version conflict with specifier logs warning.""" + caplog.clear() + with caplog.at_level(logging.WARNING): + version, note = _resolve_version( + "requests", "requests>=2.0", locked_version="1.0", warn=True + ) + assert version == "1.0" + assert note == "Version resolved: Project lock file" + assert "does not satisfy declared constraint" in caplog.text + + +def test_parse_dep_name_unparseable_without_operators() -> None: + """_parse_dep_name returns stripped string when no operator is present.""" + assert deps_installed_mod._parse_dep_name(" invalid package name ") == ( + "invalid package name" + ) + + +def test_extract_pin_from_unparseable_no_exact_operators() -> None: + """_extract_pin_from_unparseable returns None when neither == nor === is present.""" + assert ( + deps_installed_mod._extract_pin_from_unparseable("invalid pkg >= 1.0") is None + ) + + +def test_extract_pin_from_unparseable_compound_operator_without_comma() -> None: + """Requirement containing multiple operators without commas is rejected.""" + assert ( + deps_installed_mod._extract_pin_from_unparseable("invalid pkg > 1.0 == 2.0") + is None + ) + + +def test_is_exact_pin_conflict_none_req() -> None: + """_is_exact_pin_conflict with req=None falls back to is_same_version.""" + assert not deps_installed_mod._is_exact_pin_conflict(None, "1.0", "1.0") + assert deps_installed_mod._is_exact_pin_conflict(None, "1.0", "2.0") + + +def test_enrich_from_installed_download_url_and_unknown_version( + monkeypatch: pytest.MonkeyPatch, +) -> None: + """_enrich_from_installed sets downloadLocation when present and handles + unknown version.""" + fake_meta = _FakeMetadata( + { + "Version": "1.0", + "Download-URL": "https://example.com/download.tar.gz", + } + ) + monkeypatch.setattr(deps_installed_mod, "get_pkg_metadata", lambda name: fake_meta) + + doc_uuid = compute_doc_uuid("dl-test", "1.0", []) + _clear_doc_counters(doc_uuid) + exporter = Spdx3JsonExporter() + ci = _make_ci() + dep_package = spdx3.software_Package( + spdxId=generate_spdx_id("Package", doc_name="dl-test", doc_uuid=doc_uuid), + name="foo", + creationInfo=ci, + ) + dep_package.software_packageVersion = "unknown" + exporter.add_package(dep_package) + + _enrich_from_installed( + "foo", + dep_package, + ci, + "dl-test", + doc_uuid, + exporter, + expected_version="1.0", + ) + + assert ( + dep_package.software_downloadLocation == "https://example.com/download.tar.gz" + ) + assert not getattr(dep_package, "software_packageUrl", None) diff --git a/tests/core/generator/test_generator_project.py b/tests/core/generator/test_generator_project.py index 0642f6ed..2b7e8ba0 100644 --- a/tests/core/generator/test_generator_project.py +++ b/tests/core/generator/test_generator_project.py @@ -19,16 +19,20 @@ import json import tempfile +from datetime import datetime, timezone, tzinfo +from importlib.metadata import PackageNotFoundError from pathlib import Path +from unittest.mock import MagicMock import pytest from spdx_python_model.bindings import v3_0_1 as spdx3 from pitloom.assemble import generate_project_sbom -from pitloom.assemble.spdx3.document import _magika_version, build +from pitloom.assemble.spdx3.document import _build_main_package, _magika_version, build from pitloom.core.creation import CreationMetadata, Creator from pitloom.core.document import DocumentModel from pitloom.core.project import ProjectFile, ProjectMetadata +from tests.assemble.conftest import _FakeMetadata def test_generate_project_sbom_basic() -> None: @@ -163,6 +167,26 @@ def test_build_main_package_purl_normalizes_name() -> None: assert main_package["software_packageUrl"] == "pkg:pypi/my-package@2.0.0" +def test_build_main_package_copyright_year_fallback_when_created_not_datetime( + monkeypatch: pytest.MonkeyPatch, +) -> None: + """When spdx_ci.created is not a datetime, fall back to current UTC year.""" + + class MockDatetime(datetime): + @classmethod + def now(cls, tz: tzinfo | None = None) -> MockDatetime: + return cls(2026, 3, 15, 12, 0, tzinfo=timezone.utc) + + monkeypatch.setattr("pitloom.assemble.spdx3.document.datetime", MockDatetime) + project = ProjectMetadata(name="fallback-year-pkg") + doc = DocumentModel(project=project, creation_metadata=CreationMetadata()) + spdx_ci = MagicMock(spec=spdx3.CreationInfo) + spdx_ci.created = None + pkg = _build_main_package(doc, spdx_ci, [], "00000000-0000-0000-0000-000000000000") + assert pkg.software_copyrightText is not None + assert "Copyright (c) 2026" in pkg.software_copyrightText + + def test_magika_version_is_cached(monkeypatch: pytest.MonkeyPatch) -> None: """_magika_version() must only hit importlib.metadata once per process.""" _magika_version.cache_clear() @@ -263,8 +287,6 @@ def test_magika_version_falls_back_to_unknown_when_package_not_found( ) -> None: """_magika_version() must fall back to "unknown" when ``magika`` isn't installed (importlib.metadata.version() raises PackageNotFoundError).""" - from importlib.metadata import PackageNotFoundError - _magika_version.cache_clear() def _raise_not_found(name: str) -> str: @@ -307,3 +329,104 @@ def test_add_package_files_skips_relationships_when_build_relationship_none( if e.get("type") == "Relationship" and e.get("relationshipType") == "contains" ] assert contains_rels == [] + + +def test_build_document_ai_model_license_adds_simple_licensing_profile() -> None: + """When an AI model has a license and the project has no license, + simpleLicensing profile must be added to profileConformance.""" + from pitloom.core.ai_metadata import AiModelMetadata + + project = ProjectMetadata(name="ai-lic-project", version="1.0.0", license_name=None) + ai_model = AiModelMetadata(name="test-model", license="Apache-2.0") + doc = DocumentModel( + project=project, + creation_metadata=CreationMetadata(), + ai_models=[ai_model], + ) + exporter = build(doc, offline=True) + spdx_doc = next( + o for o in exporter.object_set.objects if isinstance(o, spdx3.SpdxDocument) + ) + assert spdx3.ProfileIdentifierType.ai in spdx_doc.profileConformance + assert spdx3.ProfileIdentifierType.simpleLicensing in spdx_doc.profileConformance + + +def test_build_concluded_license_without_declared_license() -> None: + """When license_name is None but license_concluded is present, + concluded license relationship must be emitted and simpleLicensing added.""" + project = ProjectMetadata( + name="concluded-only", + version="1.0.0", + license_name=None, + license_concluded="MIT", + ) + doc = DocumentModel(project=project, creation_metadata=CreationMetadata()) + exporter = build(doc, offline=True) + graph = json.loads(exporter.to_json())["@graph"] + + spdx_doc = next(e for e in graph if e.get("type") == "SpdxDocument") + assert "simpleLicensing" in spdx_doc["profileConformance"] + + rels = [e for e in graph if e.get("type") == "Relationship"] + concluded_rels = [ + r for r in rels if r.get("relationshipType") == "hasConcludedLicense" + ] + assert len(concluded_rels) == 1 + licenses = { + e["spdxId"]: e.get("simplelicensing_licenseText") + for e in graph + if e.get("type") == "simplelicensing_SimpleLicensingText" + } + assert licenses[concluded_rels[0]["to"][0]] == "MIT" + + +def test_generate_project_sbom_does_not_mutate_caller_files( + tmp_path: Path, +) -> None: + """generate_project_sbom must not mutate caller's files list in place.""" + (tmp_path / "pyproject.toml").write_text( + '[project]\nname = "caller-name"\nversion = "1.0.0"\n' + ) + initial_files: list[ProjectFile] = [] + caller_meta = ProjectMetadata( + name="caller-name", + version="1.0.0", + files=initial_files, + ) + from pitloom.core.config import PitloomConfig + + generate_project_sbom( + tmp_path, project_metadata=caller_meta, pitloom_config=PitloomConfig() + ) + assert initial_files == [] + + +def test_build_dependency_license_adds_simple_licensing_profile( + monkeypatch: pytest.MonkeyPatch, +) -> None: + """When a dependency has a resolved license, simpleLicensing must be in + profileConformance. + """ + fake_meta = _FakeMetadata( + { + "Name": "requests", + "Version": "2.31.0", + "License": "Apache-2.0", + } + ) + monkeypatch.setattr( + "pitloom.assemble.spdx3.deps_installed.get_pkg_metadata", + lambda _name: fake_meta, + ) + project = ProjectMetadata( + name="nolicense-with-dep", + version="1.0.0", + license_name=None, + dependencies=["requests==2.31.0"], + ) + doc = DocumentModel(project=project, creation_metadata=CreationMetadata()) + exporter = build(doc, offline=True) + spdx_doc = next( + o for o in exporter.object_set.objects if isinstance(o, spdx3.SpdxDocument) + ) + assert spdx3.ProfileIdentifierType.simpleLicensing in spdx_doc.profileConformance diff --git a/tests/export/test_spdx3_json.py b/tests/export/test_spdx3_json.py index 0109d8d4..a7be1943 100644 --- a/tests/export/test_spdx3_json.py +++ b/tests/export/test_spdx3_json.py @@ -270,6 +270,26 @@ def test_add_license_without_license_text_is_not_indexed() -> None: assert license_text in exporter.object_set.objects +def test_has_licenses_detects_real_license() -> None: + ci = _creation_info() + exporter = Spdx3JsonExporter() + assert not exporter.has_licenses + + noassert = spdx3.simplelicensing_SimpleLicensingText( + spdxId="urn:x#Lic-NoAssert", creationInfo=ci + ) + noassert.simplelicensing_licenseText = "NOASSERTION" + exporter.add_license(noassert) + assert not exporter.has_licenses + + lic = spdx3.simplelicensing_SimpleLicensingText( + spdxId="urn:x#Lic-1", creationInfo=ci + ) + lic.simplelicensing_licenseText = "MIT" + exporter.add_license(lic) + assert exporter.has_licenses + + # --- Spdx3JsonExporter: to_json()/to_file() --- diff --git a/tests/extract/test_pylock.py b/tests/extract/test_pylock.py index b4c0f230..9e7ee6f7 100644 --- a/tests/extract/test_pylock.py +++ b/tests/extract/test_pylock.py @@ -374,17 +374,18 @@ def test_version_validated_even_when_group_marker_excludes_package( @pytest.mark.parametrize("source_key", ["vcs", "directory", "archive"]) +@pytest.mark.parametrize("has_version", [True, False]) def test_non_registry_sourced_package_excluded( - source_key: str, caplog: pytest.LogCaptureFixture + source_key: str, has_version: bool, caplog: pytest.LogCaptureFixture ) -> None: """A package pinned via `vcs`/`directory`/`archive` has no meaningful - PyPI version pin -- excluded the same way poetry.lock's equivalent - non-registry sources are excluded.""" + PyPI version pin -- excluded whether a version is present or omitted.""" + version_line = 'version = "0.1.0"\n' if has_version else "" with tempfile.TemporaryDirectory() as tmp: tmp_path = Path(tmp) _write_lock( tmp_path, - '[[packages]]\nname = "local-dep"\nversion = "0.1.0"\n' + f'[[packages]]\nname = "local-dep"\n{version_line}' f'[packages.{source_key}]\nurl = "https://example.com"\n', ) @@ -393,6 +394,7 @@ def test_non_registry_sourced_package_excluded( assert not result assert "local-dep" in caplog.text + assert "cannot be represented as a PEP 508 specifier" in caplog.text def test_sdist_sourced_package_included() -> None: diff --git a/tests/extract/test_pylock_markers.py b/tests/extract/test_pylock_markers.py index ef81911b..bc6c5cd1 100644 --- a/tests/extract/test_pylock_markers.py +++ b/tests/extract/test_pylock_markers.py @@ -207,23 +207,20 @@ def test_or_combined_group_clauses_true_when_one_group_active() -> None: assert extract_pylock_dependencies(tmp_path) == ["black==26.1.0"] -def test_reversed_operand_group_clause_evaluated() -> None: - """PEP 751 always writes the group/extras variable on the *right* of - `in` (e.g. `"'dev' in dependency_groups"`) in real output, but PEP - 508 grammar allows either operand order -- `_evaluate_group_leaf`'s - `elif lhs_str in _GROUP_MARKER_VARIABLES` branch (variable on the - left) must still be reachable and correct, not just the more common - literal-on-left form tested elsewhere.""" +def test_reversed_operand_extra_clause_evaluated() -> None: + """PEP 508 allows either operand order for extra comparisons -- + `'dev' == extra` (variable on the right) must evaluate identically to + `extra == 'dev'`, and both are correctly excluded when extras is empty.""" with tempfile.TemporaryDirectory() as tmp: tmp_path = Path(tmp) _write_lock( tmp_path, - 'default-groups = ["dev"]\n' + 'default-groups = ["default"]\n' '[[packages]]\nname = "black"\nversion = "26.1.0"\n' - "marker = \"dependency_groups in 'dev'\"\n", + "marker = \"'dev' == extra\"\n", ) - assert extract_pylock_dependencies(tmp_path) == ["black==26.1.0"] + assert extract_pylock_dependencies(tmp_path) == [] def test_malformed_marker_string_included_and_warns( @@ -288,3 +285,45 @@ def test_malformed_non_string_marker_skipped_and_warns( assert result == [] assert "'marker' is int" in caplog.text + + +def test_marker_not_in_and_not_equal_operators() -> None: + """Markers using 'not in' and '!=' operators must evaluate correctly against + the active environment.""" + with tempfile.TemporaryDirectory() as tmp: + tmp_path = Path(tmp) + _write_lock( + tmp_path, + 'default-groups = ["default"]\n' + '[[packages]]\nname = "included-not-in"\nversion = "1.0.0"\n' + "marker = \"'dev' not in dependency_groups\"\n\n" + '[[packages]]\nname = "excluded-not-in"\nversion = "1.0.0"\n' + "marker = \"'default' not in dependency_groups\"\n\n" + '[[packages]]\nname = "included-not-equal"\nversion = "1.0.0"\n' + "marker = \"extra != 'dev'\"\n\n" + '[[packages]]\nname = "excluded-equal"\nversion = "1.0.0"\n' + "marker = \"extra == 'dev'\"\n", + ) + + deps = extract_pylock_dependencies(tmp_path) + assert deps is not None + assert set(deps) == {"included-not-in==1.0.0", "included-not-equal==1.0.0"} + + +def test_marker_invalid_operators_treated_as_unknown() -> None: + """Clauses with invalid operators on dependency_groups (e.g. ==) or extra + (e.g. >=) return None ("unknown") and do not exclude the package.""" + with tempfile.TemporaryDirectory() as tmp: + tmp_path = Path(tmp) + _write_lock( + tmp_path, + 'default-groups = ["default"]\n' + '[[packages]]\nname = "group-eq"\nversion = "1.0.0"\n' + "marker = \"dependency_groups == 'default'\"\n\n" + '[[packages]]\nname = "extra-gte"\nversion = "1.0.0"\n' + "marker = \"extra >= '1.0'\"\n", + ) + + deps = extract_pylock_dependencies(tmp_path) + assert deps is not None + assert set(deps) == {"group-eq==1.0.0", "extra-gte==1.0.0"} diff --git a/tests/extract/test_uv_lock_transitive.py b/tests/extract/test_uv_lock_transitive.py index 93e2b26f..4c07e897 100644 --- a/tests/extract/test_uv_lock_transitive.py +++ b/tests/extract/test_uv_lock_transitive.py @@ -112,14 +112,12 @@ def test_nested_dependencies_not_a_list_skipped_and_warns( assert "nested 'dependencies'" in caplog.text -def test_nested_dependencies_falsy_non_list_silently_skipped( +def test_nested_dependencies_falsy_non_list_warns_and_skipped( caplog: pytest.LogCaptureFixture, ) -> None: - """A falsy non-list `dependencies` value (e.g. `false` -- TOML has - no `null`, so this is the practical malformed-but-empty shape) - behaves like a missing/empty key, not like the truthy-malformed case - above -- no `WARNING:`, and nothing to walk into, but the package's - own pin is still resolved.""" + """A falsy non-list `dependencies` value (e.g. `false`) is malformed + schema -- emits a `WARNING:` and skips walking it, but resolves the + package's own pin.""" with tempfile.TemporaryDirectory() as tmp: tmp_path = Path(tmp) _write_lock( @@ -134,7 +132,7 @@ def test_nested_dependencies_falsy_non_list_silently_skipped( result = extract_uv_lock_dependencies(tmp_path) assert result == ["requests==2.31.0"] - assert "nested 'dependencies'" not in caplog.text + assert "nested 'dependencies'" in caplog.text def test_dependency_with_no_source_table_still_included() -> None: @@ -150,3 +148,100 @@ def test_dependency_with_no_source_table_still_included() -> None: ) assert extract_uv_lock_dependencies(tmp_path) == ["no-source==1.2.3"] + + +def test_transitive_walk_extra_scalar_string() -> None: + """A scalar string `extra = 'socks'` is handled properly and enqueues + optional deps.""" + with tempfile.TemporaryDirectory() as tmp: + tmp_path = Path(tmp) + _write_lock( + tmp_path, + _ROOT_HEADER + 'dependencies = [{ name = "requests", extra = "socks" }]\n\n' + '[[package]]\nname = "requests"\nversion = "2.31.0"\n' + 'source = { registry = "https://pypi.org/simple" }\n' + "[package.optional-dependencies]\n" + 'socks = [{ name = "PySocks" }]\n\n' + '[[package]]\nname = "PySocks"\nversion = "1.7.1"\n' + 'source = { registry = "https://pypi.org/simple" }\n', + ) + + result = extract_uv_lock_dependencies(tmp_path) + assert result is not None + assert set(result) == {"requests==2.31.0", "PySocks==1.7.1"} + + +def test_transitive_walk_optional_dependencies_non_dict() -> None: + """When package.optional-dependencies is not a table, it is ignored safely.""" + with tempfile.TemporaryDirectory() as tmp: + tmp_path = Path(tmp) + _write_lock( + tmp_path, + _ROOT_HEADER + 'dependencies = [{ name = "requests", extra = "socks" }]\n\n' + '[[package]]\nname = "requests"\nversion = "2.31.0"\n' + 'source = { registry = "https://pypi.org/simple" }\n' + 'optional-dependencies = "not-a-table"\n', + ) + + result = extract_uv_lock_dependencies(tmp_path) + assert result == ["requests==2.31.0"] + + +def test_transitive_walk_extra_already_visited_cycle_prevention() -> None: + """Duplicate/cyclic extra requests must be skipped via visited_extras check.""" + with tempfile.TemporaryDirectory() as tmp: + tmp_path = Path(tmp) + _write_lock( + tmp_path, + _ROOT_HEADER + "dependencies = [\n" + ' { name = "requests", extra = "socks" },\n' + ' { name = "requests", extra = "socks" },\n' + "]\n\n" + '[[package]]\nname = "requests"\nversion = "2.31.0"\n' + 'source = { registry = "https://pypi.org/simple" }\n' + "[package.optional-dependencies]\n" + 'socks = [{ name = "PySocks" }]\n\n' + '[[package]]\nname = "PySocks"\nversion = "1.7.1"\n' + 'source = { registry = "https://pypi.org/simple" }\n', + ) + + result = extract_uv_lock_dependencies(tmp_path) + assert result is not None + assert set(result) == {"requests==2.31.0", "PySocks==1.7.1"} + + +def test_transitive_walk_extra_name_canonicalization() -> None: + """Extra names differing in casing or punctuation (- vs _) match canonically.""" + with tempfile.TemporaryDirectory() as tmp: + tmp_path = Path(tmp) + _write_lock( + tmp_path, + _ROOT_HEADER + 'dependencies = [{ name = "pkg", extra = "foo_bar" }]\n\n' + '[[package]]\nname = "pkg"\nversion = "1.0.0"\n' + 'source = { registry = "https://pypi.org/simple" }\n' + "[package.optional-dependencies]\n" + '"foo-bar" = [{ name = "dep" }]\n\n' + '[[package]]\nname = "dep"\nversion = "1.0.0"\n' + 'source = { registry = "https://pypi.org/simple" }\n', + ) + + result = extract_uv_lock_dependencies(tmp_path) + assert result is not None + assert set(result) == {"pkg==1.0.0", "dep==1.0.0"} + + +def test_transitive_walk_extra_deps_non_list() -> None: + """When an entry in optional-dependencies is not a list, it is not queued.""" + with tempfile.TemporaryDirectory() as tmp: + tmp_path = Path(tmp) + _write_lock( + tmp_path, + _ROOT_HEADER + 'dependencies = [{ name = "pkg", extra = "socks" }]\n\n' + '[[package]]\nname = "pkg"\nversion = "1.0.0"\n' + 'source = { registry = "https://pypi.org/simple" }\n' + "[package.optional-dependencies]\n" + 'socks = "not-a-list"\n', + ) + + result = extract_uv_lock_dependencies(tmp_path) + assert result == ["pkg==1.0.0"] diff --git a/working-docs/implementation/lock-file-cascade.md b/working-docs/implementation/lock-file-cascade.md index de0c6105..ce93687a 100644 --- a/working-docs/implementation/lock-file-cascade.md +++ b/working-docs/implementation/lock-file-cascade.md @@ -111,7 +111,7 @@ merely happened to already be a fully pinned list" -- see **`poetry.lock` is registered with an extractor in the cascade table.** When `pyproject.toml` is a Poetry 1.x project, `_try_read_poetry()` extracts it earlier during project parsing; when the cascade runs, the deduplication -guard recognizes that `poetry.lock` was already extracted and avoids re-parsing +guard recognises that `poetry.lock` was already extracted and avoids re-parsing it. For standard PEP 621 or non-Poetry projects accompanied by a `poetry.lock`, the cascade's registered extractor parses it directly. From 95e787243475232f1ebb71bfce52bba3742bd77e Mon Sep 17 00:00:00 2001 From: Arthit Suriyawongkul Date: Mon, 7 Sep 2026 22:59:36 +0700 Subject: [PATCH 25/35] Update docs Signed-off-by: Arthit Suriyawongkul --- docs/dependency-sources.md | 39 +++++++++- src/pitloom/assemble/_generators.py | 8 +-- src/pitloom/assemble/spdx3/deps.py | 11 +-- src/pitloom/extract/_lock_common.py | 34 +++++++-- src/pitloom/extract/_pdm_lock.py | 9 ++- src/pitloom/extract/_pipfile_lock.py | 11 ++- src/pitloom/extract/_pylock.py | 2 +- src/pitloom/extract/_requirements_txt.py | 28 ++++---- src/pitloom/extract/_uv_lock.py | 19 +++-- .../implementation/lock-file-cascade.md | 72 +++++++++++++------ working-docs/implementation/poetry-support.md | 48 ++++++++----- 11 files changed, 194 insertions(+), 87 deletions(-) diff --git a/docs/dependency-sources.md b/docs/dependency-sources.md index 59691c1d..5146aed5 100644 --- a/docs/dependency-sources.md +++ b/docs/dependency-sources.md @@ -41,11 +41,11 @@ additive entries. | Priority | Format | File | What's included | | :---: | :--- | :--- | :--- | | 1 (highest) | PEP 751 | `pylock.toml` | Every resolved package the file records for its declared `default-groups` (a package needed only for a non-default dependency-group/extra, per its own `marker` field, is excluded). | -| 2 | uv | `uv.lock` | Your project's own main/runtime dependencies, walked transitively (dependencies of dependencies, and so on) -- not `optional-dependencies` extras or `dev-dependencies` groups. A dependency pinned to more than one version for different Python versions is skipped, not guessed at, and nothing depending only on it is walked into either -- see below. | +| 2 | uv | `uv.lock` | Your project's own main/runtime dependencies, walked transitively (dependencies of dependencies, and so on), including any extra a real dependency requests on another package (e.g. `uvicorn[standard]` pulls in `uvicorn`'s own `standard` extra). Your project's *own* `optional-dependencies`/`dev-dependencies` groups (the ones a user would have to opt into, e.g. `pip install yourpkg[dev]`) are not included. A dependency pinned to more than one version for different Python versions is skipped, not guessed at, and nothing depending only on it is walked into either -- see below. | | 3 | Poetry | `poetry.lock` | Packages in the `main` dependency group only (not `[tool.poetry.group.*]` dev/extra groups). | | 4 | PDM | `pdm.lock` | Packages in the `default` dependency group only. | -| 5 | Pipenv | `Pipfile.lock` | Packages in the `default` section only (not `develop`). A package whose resolved `version` isn't a single exact `==` pin is skipped, not guessed at. | -| 6 (lowest) | -- | pinned `requirements.txt` | Not a real lock file -- only used when *every* line in the file is already an exact `==` pin. If even one line is unpinned, ranged, a pip option (`-e`, `-r`, `--hash`, ...), a URL-based requirement (even one that looks like it points at a tagged release), or one package name is pinned to two conflicting versions, the **whole file** is skipped, not just that line -- see below. | +| 5 | Pipenv | `Pipfile.lock` | Packages in the `default` section only (not `develop`). A package whose resolved `version` isn't a single exact `==`/`===` pin is skipped, not guessed at. | +| 6 (lowest) | -- | pinned `requirements.txt` | Not a real lock file -- only used when *every* line in the file is already a single exact `==`/`===` pin. If even one line is unpinned, ranged, a pip option (`-e`, `-r`, `--hash`, ...), a URL-based requirement (even one that looks like it points at a tagged release), or one package name is pinned to two conflicting versions, the **whole file** is skipped, not just that line -- see below. | `requirements.txt`'s entry is tagged `Method: pinned_requirements` in its provenance annotation (see "How to tell which source was used" @@ -85,6 +85,39 @@ list rather than added with a possibly-wrong version. Check stderr for a `WARNING:` naming the skipped package if a dependency you expected is missing. +## Version comparison: PEP 440, not SemVer + +**Pitloom compares dependency versions using [PEP 440][pep-440] equality, +not SemVer.** "Same version" means the two version strings normalize to +the identical release under PEP 440 -- trailing-zero components are +padded and compared, so `1.0`, `1.0.0`, and `1.0.0.0` are all the same +version. It does **not** mean "the latest release compatible with 1.0" +or any other range/caret-style resolution: `1.0` and `1.0.1` are +different versions under this comparison, exactly as they'd differ under +strict string equality, even though a SemVer-style `^1.0.0` range would +consider `1.0.1` compatible. + +This comparison is what decides whether two version strings for the same +package are treated as agreeing or genuinely conflicting. It shows up in +two places: + +- **A lock file's own duplicate entries.** If one lock file records the + same package name more than once (e.g. a platform-specific variant), + entries that normalize to the same PEP 440 release are silently + collapsed into one; entries that don't get a `WARNING:` naming both + versions, and that package is left out of the transitive list + entirely rather than guessed at. +- **A declared range vs. the lock file's resolved version.** When a + direct dependency is unpinned or declared as a range, the lock file's + resolved version is used (see above). When it's already pinned + exactly (e.g. `requests==2.31.0`) and the lock file separately + resolved it to a version that doesn't normalize the same way (e.g. + `2.31.1`), Pitloom logs a `WARNING:` but keeps the *declared* pin -- + the lock's differing value never silently overrides an exact pin the + project itself declared. + +[pep-440]: https://peps.python.org/pep-0440/ + ## Which commands use lock files at all Lock-file resolution only ever applies to a **Source SBOM** diff --git a/src/pitloom/assemble/_generators.py b/src/pitloom/assemble/_generators.py index cced37a1..c9ca1c40 100644 --- a/src/pitloom/assemble/_generators.py +++ b/src/pitloom/assemble/_generators.py @@ -181,10 +181,10 @@ def generate_project_sbom( # real wheel build -- it never reproduces the # `.dist-info/licenses/...` entries a real build would add for # `[project.license-files]`. Resolve those directly so they still - # show up in the SBOM's file list. Must happen before the - # `project_metadata.files = project_files` assignment below, since - # that overwrite is the only place `project_files` becomes the - # metadata's authoritative file list for this (directory) target. + # show up in the SBOM's file list, before the `dataclasses.replace` + # below makes `project_files` the metadata's authoritative file + # list for this (directory) target -- replace, not in-place + # mutation, so the caller's own `project_metadata` is untouched. project_files = project_files + resolve_license_file_entries( target_path, project_metadata.name, diff --git a/src/pitloom/assemble/spdx3/deps.py b/src/pitloom/assemble/spdx3/deps.py index 1eb32ce1..3caca9f0 100644 --- a/src/pitloom/assemble/spdx3/deps.py +++ b/src/pitloom/assemble/spdx3/deps.py @@ -235,10 +235,13 @@ def add_dependencies( dependencies. Multiple declared dependency strings that resolve to the same - ``(name, version)`` -- e.g. the same package listed under more than one - ``pyproject.toml`` extra, each split by a ``python_version`` marker -- - collapse into a single ``software_Package`` node. Their raw declared - strings are preserved together in that node's provenance comment. + package -- same PEP 503-canonicalized name (``Django``/``django``) and + the same PEP 440 version (``"1.0"``/``"1.0.0"``), e.g. the same + package listed under more than one ``pyproject.toml`` extra, each + split by a ``python_version`` marker -- collapse into a single + ``software_Package`` node, keeping the first-seen literal name for + display. Their raw declared strings are preserved together in that + node's provenance comment. *completeness*, when given (e.g. ``spdx3.RelationshipCompleteness.complete`` for a lock-resolved transitive-dependency call), is set on every diff --git a/src/pitloom/extract/_lock_common.py b/src/pitloom/extract/_lock_common.py index 15b2562c..28ee528b 100644 --- a/src/pitloom/extract/_lock_common.py +++ b/src/pitloom/extract/_lock_common.py @@ -36,6 +36,7 @@ "POETRY_LOCK_SOURCE_NAME", "default_group_included", "find_first_present_key", + "group_pin_triples_by_canonical_name", "group_versions_by_canonical_name", "has_required_top_level_table", "index_packages_by_name", @@ -241,10 +242,9 @@ def group_versions_by_canonical_name( A caller decides what a multi-entry group means for its own format: :mod:`pitloom.extract._pdm_lock` collapses a group to one entry when every version agrees (its per-extra duplicate records always do) and - skips just that name otherwise; :mod:`pitloom.extract._requirements_txt` - treats any group with more than one distinct version as disqualifying - its whole file, since it has no per-format definition of "expected - duplication" the way an extra-variant lock entry does. + skips just that name otherwise. See :func:`group_pin_triples_by_canonical_name` + for the sibling version used where the pin's operator (``==`` vs ``===``) + also needs to survive grouping. """ by_canonical: dict[str, list[tuple[str, str]]] = {} for name, version in pairs: @@ -252,6 +252,32 @@ def group_versions_by_canonical_name( return by_canonical +def group_pin_triples_by_canonical_name( + triples: Iterable[tuple[str, str, str]], +) -> dict[str, list[tuple[str, str, str]]]: + """Group ``(name, operator, version)`` pins by PEP 503-canonicalized + *name*, preserving file order both across and within groups -- the + ``===``-aware sibling of :func:`group_versions_by_canonical_name`, + for :mod:`pitloom.extract._pipfile_lock` and + :mod:`pitloom.extract._requirements_txt`, whose ``version`` field is + already a PEP 440 specifier that can carry either exact-pin operator + and must keep it through to the formatted ``nameversion`` output. + + A caller decides what a multi-entry group means for its own format: + :mod:`pitloom.extract._pipfile_lock` skips just the conflicting name + and keeps the rest; :mod:`pitloom.extract._requirements_txt` treats + any group with more than one distinct version as disqualifying its + whole file, since it has no per-format definition of "expected + duplication" the way an extra-variant lock entry does. + """ + by_canonical: dict[str, list[tuple[str, str, str]]] = {} + for name, operator, version in triples: + by_canonical.setdefault(canonicalize_name(name), []).append( + (name, operator, version) + ) + return by_canonical + + #: PEP 440 operators that pin to exactly one release: ``==`` (the #: ordinary case) and ``===`` (arbitrary-equality, for a legacy/ #: non-normalizable version string a resolver would otherwise reject -- diff --git a/src/pitloom/extract/_pdm_lock.py b/src/pitloom/extract/_pdm_lock.py index 5a65e30b..0a6e4d84 100644 --- a/src/pitloom/extract/_pdm_lock.py +++ b/src/pitloom/extract/_pdm_lock.py @@ -30,9 +30,12 @@ ``httpx`` entry alongside an ``httpx`` entry with ``extras = ["socks"]``) that always agree on ``version`` -- collapsed here via :func:`pitloom.extract._lock_common.group_versions_by_canonical_name`, -also shared with :mod:`pitloom.extract._requirements_txt`. Only a name -whose entries actually *disagree* on version is treated as ambiguous and -skipped, matching ``uv.lock``'s "don't guess" policy for that case. +also shared with :mod:`pitloom.extract._poetry_lock` and +:mod:`pitloom.extract._pylock`. Only a name whose entries actually +*disagree* on version (compared via +:func:`pitloom.extract._lock_common.is_same_version`'s PEP 440 +equality, not raw string equality) is treated as ambiguous and skipped, +matching ``uv.lock``'s "don't guess" policy for that case. """ from __future__ import annotations diff --git a/src/pitloom/extract/_pipfile_lock.py b/src/pitloom/extract/_pipfile_lock.py index 5afca1df..a1826af9 100644 --- a/src/pitloom/extract/_pipfile_lock.py +++ b/src/pitloom/extract/_pipfile_lock.py @@ -31,7 +31,8 @@ specifier string (typically ``"==x.y.z"``, since ``pipenv lock`` resolves to an exact pin) rather than a bare version number the way every other format's ``version`` field is -- this extractor validates -it's a single exact ``==`` specifier with no wildcard before using it, +it's a single exact ``==``/``===`` specifier with no wildcard before +using it (see :func:`pitloom.extract._lock_common.single_exact_pin`), not a range, a prefix-match specifier like ``"==x.y.*"``, or a malformed string coerced into looking like one. """ @@ -42,10 +43,10 @@ from pathlib import Path from packaging.specifiers import InvalidSpecifier, SpecifierSet -from packaging.utils import canonicalize_name from pitloom.extract._lock_common import ( find_first_present_key, + group_pin_triples_by_canonical_name, has_required_top_level_table, is_same_version, load_lock_json, @@ -107,12 +108,8 @@ def extract_pipfile_lock_dependencies(project_dir: Path) -> list[str] | None: if pair is not None ] - by_canonical: dict[str, list[tuple[str, str, str]]] = {} - for name, op, version in pairs: - by_canonical.setdefault(canonicalize_name(name), []).append((name, op, version)) - dependencies: list[str] = [] - for group in by_canonical.values(): + for group in group_pin_triples_by_canonical_name(pairs).values(): name, op, version = group[0] conflicting_versions = { v for _, _, v in group if not is_same_version(v, version) diff --git a/src/pitloom/extract/_pylock.py b/src/pitloom/extract/_pylock.py index 15ba3784..806b4639 100644 --- a/src/pitloom/extract/_pylock.py +++ b/src/pitloom/extract/_pylock.py @@ -385,7 +385,7 @@ def _pinned_pair_for_package( including it here would misrepresent it as an ordinary published release (wrong PURL, bogus PyPI enrichment lookup) -- mirrors ``poetry.lock``'s equivalent ``directory``/``file``/``git``/``url`` - skip in :func:`pitloom.extract._poetry_lock._pinned_dep_for_package`. + skip in :func:`pitloom.extract._poetry_lock._main_group_package_or_none`. A registry-resolved package sourced via ``sdist``/``wheels`` (or with no source table at all) is always included when it has a version. diff --git a/src/pitloom/extract/_requirements_txt.py b/src/pitloom/extract/_requirements_txt.py index ef403ede..473c25ae 100644 --- a/src/pitloom/extract/_requirements_txt.py +++ b/src/pitloom/extract/_requirements_txt.py @@ -24,16 +24,19 @@ metadata (often hashes); a plain ``requirements.txt`` is just a list of lines a human or ``pip freeze`` wrote, with no such guarantee. Pitloom only trusts it as a resolved-dependency source when it can prove, line -by line, that *every* real dependency line is already an exact ``==`` -pin -- if even one line isn't, the **entire file** is ignored with one +by line, that *every* real dependency line is already a single exact +``==``/``===`` pin (see :func:`pitloom.extract._lock_common.single_exact_pin`) +-- if even one line isn't, the **entire file** is ignored with one ``WARNING:`` naming the first disqualifying line, never partially included. The same whole-file rejection applies if one name (compared PEP 503-canonicalized, so ``Flask`` and ``flask`` count as the same -name) repeats with two different pinned versions; a repeat with the -same version is silently collapsed to one entry. Its provenance -``Method`` tag is ``"pinned_requirements"``, distinct from -every other source's ``"resolved_lockfile"``, so a reader of the -generated SBOM can tell the two kinds of evidence apart. +name) repeats with two versions that don't compare equal under PEP 440 +(see :func:`pitloom.extract._lock_common.is_same_version`, e.g. +``"1.0"``/``"1.0.0"`` don't conflict but ``"1.0"``/``"1.0.1"`` do); a +repeat with the same PEP 440 version is silently collapsed to one +entry. Its provenance ``Method`` tag is ``"pinned_requirements"``, +distinct from every other source's ``"resolved_lockfile"``, so a +reader of the generated SBOM can tell the two kinds of evidence apart. **A URL-based line (``name @ https://...`` or ``git+https://...``) is a PEP 508 direct reference, not a PEP 440 version specifier, and always @@ -53,9 +56,9 @@ from pathlib import Path from packaging.requirements import InvalidRequirement, Requirement -from packaging.utils import canonicalize_name from pitloom.extract._lock_common import ( + group_pin_triples_by_canonical_name, is_same_version, single_exact_pin, ) @@ -82,7 +85,8 @@ def extract_pinned_requirements_dependencies(project_dir: Path) -> list[str] | None: """Read ``requirements.txt`` next to ``pyproject.toml``/``setup.py`` and return every dependency as an exact-pin PEP 508 string, but only - when *every* real line in the file is already an exact ``==`` pin. + when *every* real line in the file is already a single exact + ``==``/``===`` pin. Returns ``None`` when no ``requirements.txt`` is present, it can't be read/decoded, or any line disqualifies the whole file (an option @@ -166,12 +170,8 @@ def _collapse_or_none( *different* versions. A plain repeated line (same name, same version) is silently collapsed to one entry. """ - by_canonical: dict[str, list[tuple[str, str, str]]] = {} - for name, op, version in pins: - by_canonical.setdefault(canonicalize_name(name), []).append((name, op, version)) - result: list[str] = [] - for group in by_canonical.values(): + for group in group_pin_triples_by_canonical_name(pins).values(): name, op, version = group[0] conflicting = next( (v for _, _, v in group if not is_same_version(v, version)), None diff --git a/src/pitloom/extract/_uv_lock.py b/src/pitloom/extract/_uv_lock.py index dfb6d8bc..6422faf0 100644 --- a/src/pitloom/extract/_uv_lock.py +++ b/src/pitloom/extract/_uv_lock.py @@ -32,13 +32,18 @@ it reads the *project's own* ``[[package]]`` entry (identified by ``source.editable``/``source.virtual``, uv's markers for "this is a local project, not a PyPI download") and only its ``dependencies`` list -(main/runtime only -- ``optional-dependencies``/``dev-dependencies`` are -extras and dev groups, excluded the same way ``poetry.lock``'s -non-``main`` groups are), then resolves each referenced name against -the flat table *only* when exactly one candidate exists for that name. -An ambiguous (multiple-version) or marker-conditional (inline -``version`` on the dependency reference itself) name is skipped with a -``WARNING:``, not guessed. +(main/runtime only -- the project's *own* ``optional-dependencies``/ +``dev-dependencies`` groups are extras and dev groups a user would have +to opt into, excluded the same way ``poetry.lock``'s non-``main`` groups +are), then resolves each referenced name against the flat table *only* +when exactly one candidate exists for that name. An ambiguous +(multiple-version) or marker-conditional (inline ``version`` on the +dependency reference itself) name is skipped with a ``WARNING:``, not +guessed. A dependency reference that names a specific extra on the +package it points at (e.g. ``uvicorn[standard]``) still walks *that +package's own* ``optional-dependencies[extra]`` list, since an extra a +real dependency requests is part of what actually gets installed -- see +:func:`_enqueue_requested_extras`. """ from __future__ import annotations diff --git a/working-docs/implementation/lock-file-cascade.md b/working-docs/implementation/lock-file-cascade.md index ce93687a..fd6b0941 100644 --- a/working-docs/implementation/lock-file-cascade.md +++ b/working-docs/implementation/lock-file-cascade.md @@ -150,10 +150,11 @@ The fix: `poetry.lock` is a real entry in `_LOCK_SOURCES`, so its rank is looked up the same way as everything else instead of being assumed. `apply_locked_dependencies()` first resolves the rank of whatever source (if any) already populated `metadata.provenance["locked_dependencies"]` -(such as `poetry.lock` via `_try_read_poetry()` for Poetry 1.x projects) --- then, walking `_LOCK_SOURCES` in order, stops (`break`) the moment it -reaches an entry ranked *below* that already-set source, since nothing -from there on could legitimately win. `tests/extract/test_pdm_lock.py::test_read_project_pdm_lock_never_overrides_poetry_lock` +(such as `poetry.lock` via `_try_read_poetry()` for Poetry 1.x projects), +then only tries the entries strictly above that rank +(`sources_to_try = _LOCK_SOURCES[:previous_rank]`) -- nothing at or +below the already-set source's rank could legitimately win, so it's +never even called. `tests/extract/test_pdm_lock.py::test_read_project_pdm_lock_never_overrides_poetry_lock` is the regression test for this; `test_read_project_uv_lock_still_overrides_pdm_lock` confirms the higher-ranked entries' behaviour didn't change. @@ -184,16 +185,24 @@ a real environment, `_uv_lock.py`: project" from a PyPI download) instead of scanning every `[[package]]` entry directly. 2. Breadth-first walks the dependency graph starting from that entry's - own `dependencies` list (main/runtime -- `optional-dependencies`/ - `dev-dependencies` are extras and dev groups, excluded the same way - `poetry.lock`'s non-`main` groups are), in `_collect_transitive_dependencies()`. - This isn't just the root's *immediate* dependencies: each resolved - package's own `dependencies` list is walked too, since the installed - set is the closure over that graph, not just its first layer (e.g. a - CLI tool's direct dependency on a web framework that itself pulls in - several more packages). PEP 503-canonicalized names guard against - revisiting the same package twice (a diamond dependency shared by two - branches) or looping on a cycle. + own `dependencies` list (main/runtime only -- the *root* package's + own `optional-dependencies` and `dev-dependencies` groups are the + project's own extras/dev groups, excluded the same way `poetry.lock`'s + non-`main` groups are, and never seed the walk), in + `_collect_transitive_dependencies()`. This isn't just the root's + *immediate* dependencies: each resolved package's own `dependencies` + list is walked too, since the installed set is the closure over that + graph, not just its first layer (e.g. a CLI tool's direct dependency + on a web framework that itself pulls in several more packages). A + dependency reference that names a specific `extra`/`extras` on the + package it points at (e.g. `uvicorn[standard]`) additionally walks + *that package's own* `optional-dependencies[extra]` list -- + `_enqueue_requested_extras()` -- since an extra requested by a real + dependency (not the root project's own, unrequested extras) is part + of what actually gets installed. PEP 503-canonicalized names guard + against revisiting the same package twice (a diamond dependency + shared by two branches) or looping on a cycle; a separate + `(name, extra)` set guards the extras walk the same way. 3. Resolves each referenced name against the flat table only when exactly one candidate exists for that name; an ambiguous (multiple-version) or marker-conditional (inline `version` on the @@ -307,10 +316,20 @@ similar in spirit: - **Grouping a flat package list by name.** `_uv_lock.py`'s ambiguity check groups full `[[package]]` table entries by their raw `name` field -- `pitloom.extract._lock_common.index_packages_by_name()`. - `_pdm_lock.py` and `_requirements_txt.py` need the narrower "group - just a `(name, version)` pair by *canonicalized* name" shape instead - (their conflict check has to treat `Flask`/`flask` as the same - package) -- `pitloom.extract._lock_common.group_versions_by_canonical_name()`. + Every format whose conflict check only needs a `(name, version)` + pair grouped by *canonicalized* name (`Flask`/`flask` must count as + the same package) uses + `pitloom.extract._lock_common.group_versions_by_canonical_name()` + instead: `_poetry_lock.py`, `_pdm_lock.py`, and `_pylock.py`. + `_pipfile_lock.py` and `_requirements_txt.py` need the pin's operator + (`==` vs `===`) to survive grouping too, since their `version` field + is a full specifier rather than a bare version number (see below) -- + they use the `(name, operator, version)` sibling, + `pitloom.extract._lock_common.group_pin_triples_by_canonical_name()`. + Every one of these groupings compares versions with + `pitloom.extract._lock_common.is_same_version()` (PEP 440 equality, + e.g. `"1.0"` == `"1.0.0"`), not raw string equality, so two spellings + of the same release never trigger a false-positive conflict warning. - **Validating a `version` field is a usable PEP 440 version.** `pitloom.extract._lock_common.is_usable_version()` checks a field is a non-empty string *and* parses as a valid `packaging.version.Version` @@ -334,7 +353,7 @@ similar in spirit: name, source_key)`. Each extractor still does its own lookup of *which* key triggered it (see below) and only calls this once it has the answer. -- **Judging whether a specifier is a single exact `==` pin.** +- **Judging whether a specifier is a single exact `==`/`===` pin.** `_pipfile_lock.py` and `_requirements_txt.py` both need this -- Pipfile.lock's `version` field and a `requirements.txt` line's specifier are both full PEP 440 specifier strings, not bare version @@ -345,7 +364,12 @@ similar in spirit: Pipfile.lock, `Requirement(...).specifier` for `requirements.txt`) and catches its own parse failure with its own `WARNING:` wording, since the two call sites want different messages for "unparseable" vs. - "parseable but not a single exact pin." + "parseable but not a single exact pin." Returns `(operator, version)`, + not just `version` -- `===` (PEP 440 arbitrary-equality, for a legacy + version string that doesn't parse as a normal `Version` at all) is as + valid a single exact pin as `==`, and the caller needs the operator + back to format `f"{name}{operator}{version}"` rather than assuming + `==`. What's deliberately **not** shared: the per-entry lookup for which key marks a non-registry source, and what the `groups`/`dependencies` @@ -450,9 +474,11 @@ unaffected -- purely additive. 2. Add one entry to `_LOCK_SOURCES` in `_locked_dependencies.py`, at the priority position from the table above -- **including if it ranks below `poetry.lock`** (pinned `requirements.txt`, rank 6, does). - No extra code is needed for that case: the rank check in - `apply_locked_dependencies()` already treats every entry in - `_LOCK_SOURCES` (poetry.lock's placeholder included) uniformly. + No extra code is needed for that case: `apply_locked_dependencies()` + slices `_LOCK_SOURCES` down to whatever rank is already resolved + (`sources_to_try = _LOCK_SOURCES[:previous_rank]`), so every entry -- + `poetry.lock` included -- is treated uniformly regardless of where + it sits in the table. 3. No changes needed anywhere else -- `read_project()`'s wiring, provenance formatting, the override note, and UUID seeding are all already generic across every entry in the table. diff --git a/working-docs/implementation/poetry-support.md b/working-docs/implementation/poetry-support.md index 2970d440..ef02a74c 100644 --- a/working-docs/implementation/poetry-support.md +++ b/working-docs/implementation/poetry-support.md @@ -186,17 +186,25 @@ it). `_poetry_lock.py`'s `extract_poetry_lock_dependencies()` reads `[[package]]` tables from a sibling `poetry.lock`, keeping only packages -whose `groups` includes `"main"` (excluding dev/other-group-only -packages, the same "not a runtime dependency" policy already applied to -`[tool.poetry.group.*]` above), as exact-pin `name==version` strings. A -package resolved from a `directory`/`file`/`git`/`url` source (per -`[package.source].type`) is excluded the same way -`_poetry_dep_to_pep508()` excludes it from direct dependencies -- it has -no meaningful PyPI version pin, so including it would misrepresent it as -an ordinary published release. A malformed lock (an unparseable -top-level `package` key, or an individual `[[package]]` entry missing -`name`/`version`) is skipped with a `WARNING:`, not silently dropped, per -this repo's "no silent deviations" rule. +in the main/default group -- `groups` includes `"main"` (Poetry +1.2+'s dependency-groups feature) or, for a legacy Poetry 1.x lock with +no `groups` field at all, `category == "main"` -- and not marked +`optional = true` (an extra, not a default runtime dependency). This +excludes dev/other-group-only and extras packages, the same "not a +runtime dependency" policy already applied to `[tool.poetry.group.*]` +above, as exact-pin `name==version` (or `name===version` for a legacy +non-normalizable pin) strings. A package resolved from a +`directory`/`file`/`git`/`url` source (per `[package.source].type`) is +excluded the same way `_poetry_dep_to_pep508()` excludes it from direct +dependencies -- it has no meaningful PyPI version pin, so including it +would misrepresent it as an ordinary published release. A malformed +lock (an unparseable top-level `package` key, or an individual +`[[package]]` entry missing `name`/`version`) is skipped with a +`WARNING:`, not silently dropped, per this repo's "no silent +deviations" rule; two `[[package]]` entries for the same +PEP 503-canonicalized name that disagree on version (compared under +PEP 440 equality, not raw string equality) are skipped with a +`WARNING:` too, rather than one silently overwriting the other. Wired into `_try_read_poetry()`, which takes an `include_locked_dependencies` keyword (default `true`): both @@ -223,9 +231,14 @@ itself couldn't be extracted. A locked package's exact-pin version is authoritative when resolving what to report in the SBOM -- `_resolve_version()` (`assemble/spdx3/deps_installed.py`) checks a dependency string's own -`==`/`===` pin before falling back to introspecting whatever happens to -be installed in Pitloom's own execution environment, which has no -relationship to the target project's environment. +`==`/`===` pin first (winning outright, with a `WARNING:` if it +disagrees with a locked version), then any locked version for a +direct dependency declared as a range, before falling back to +introspecting whatever happens to be installed in Pitloom's own +execution environment, which has no relationship to the target +project's environment. This priority order is generic across every +lock format, not `poetry.lock`-specific -- see +[lock-file-cascade.md](lock-file-cascade.md). In the assembled SPDX 3 graph, a locked package already covered by a direct `[tool.poetry.dependencies]` entry gets no duplicate edge -- @@ -270,9 +283,10 @@ case for Poetry support (issue [#62]). It has: sources are skipped because they cannot be expressed as PEP 508 specifiers, logging a `WARNING:` naming the dependency and the source kind (`_poetry_dep_to_pep508()` in `_poetry.py`). `poetry.lock` entries - resolved from the equivalent `directory`/`file`/`git`/`url` sources are - excluded from `locked_dependencies` for the same reason - (`_pinned_dep_for_package()` in `_poetry_lock.py`). + resolved from the equivalent `directory`/`file`/`git`/`url` sources, or + marked `optional = true` (an extra, not a default runtime dependency), + are excluded from `locked_dependencies` for the same reason + (`_main_group_package_or_none()` in `_poetry_lock.py`). - **`[tool.poetry.extras]`** -- optional extras are not yet mapped to `ProjectMetadata`. This is a schema-wide gap, not Poetry-specific: `ProjectMetadata` has no extras/optional-dependencies field for any From 86114710eeb4cc59062fb02e899d03630db38b9e Mon Sep 17 00:00:00 2001 From: Arthit Suriyawongkul Date: Tue, 8 Sep 2026 11:07:29 +0700 Subject: [PATCH 26/35] Fix comments and minor bugs Signed-off-by: Arthit Suriyawongkul --- AGENTS.md | 87 +++++++++++++++++++ src/pitloom/assemble/spdx3/deps_installed.py | 48 +++++++--- src/pitloom/assemble/spdx3/deps_pypi.py | 38 ++++++-- src/pitloom/assemble/spdx3/document.py | 69 ++++++++++++--- src/pitloom/extract/_lock_common.py | 55 +++++++----- src/pitloom/extract/_locked_dependencies.py | 4 +- src/pitloom/extract/_pipfile_lock.py | 8 +- src/pitloom/extract/_poetry.py | 26 ++++-- src/pitloom/extract/_poetry_lock.py | 20 ++++- src/pitloom/extract/_pylock.py | 9 +- src/pitloom/extract/_pyproject.py | 10 ++- src/pitloom/extract/_requirements_txt.py | 13 ++- src/pitloom/extract/_setuptools_cfg.py | 56 ++++++++++-- src/pitloom/extract/_setuptools_py.py | 42 ++++++--- src/pitloom/extract/hatchling.py | 40 +++++++-- ...test_deps_enrichment_originator_license.py | 34 ++++++++ .../assemble/test_deps_locked_dependencies.py | 38 +++++++- tests/assemble/test_deps_resolution_pins.py | 51 +++++++++-- tests/extract/conftest.py | 26 ++++-- tests/extract/test_lock_common.py | 24 +++++ tests/extract/test_poetry_lock.py | 21 +++++ tests/extract/test_poetry_parsing.py | 24 +++++ tests/extract/test_pylock_markers.py | 19 ++++ tests/extract/test_setuptools_cfg.py | 12 +++ tests/extract/test_setuptools_py.py | 15 ++++ working-docs/implementation/poetry-support.md | 10 ++- 26 files changed, 680 insertions(+), 119 deletions(-) diff --git a/AGENTS.md b/AGENTS.md index b7444bdf..eb37e74f 100644 --- a/AGENTS.md +++ b/AGENTS.md @@ -75,6 +75,40 @@ shape described, not just the module where each was first found. `dict.get(key, [])`, or `if some_container:`, ask whether the empty case and the absent case are supposed to behave the same -- they usually aren't. + - **Same bug, provenance-flavoured: gate a "was this field explicitly + declared" check on presence of the raw source key, never on the + resolved value's truthiness.** A metadata producer building a + `provenance` dict for a container field (`keywords`, `urls`, + `dependencies`, `authors`, `license_files`, ...) with `if parsed_value: + provenance["field"] = ...` silently fails to record provenance for an + explicitly-declared-but-empty value (`dependencies = []`, + `install_requires =`) -- indistinguishable downstream from the field + never having been mentioned at all. This recurred independently across + five separate `ProjectMetadata` producers in one PR + (`_pyproject.py`, `_setuptools_py.py`, `_setuptools_cfg.py`, + `_poetry.py`, `hatchling.py`) before all five were fixed to check + presence in the raw source (`"key" in raw_dict`/`kwargs`/`core.config`) + instead. When adding a new container field or a new metadata producer, + grep every existing producer's `provenance[...]` assignments for the + same field and match whichever check style they already settled on. + - **The presence signal must survive every merge/inheritance boundary, + or the fix is cosmetic.** `merge_project_metadata()` only treats an + empty container as authoritative when *its own* provenance key says + so -- so a producer that resolves the presence check correctly but + is never checked against real merge call sites can still lose the + signal in practice. The same masking happens one layer down: + `configparser`'s `[DEFAULT]`-section value inheritance makes + `"key" in cfg.items(section)` true even when *that section* never + declared `key` -- a presence check must read the section's own keys + only (not the merged view) or a shared default gets misread as an + explicit per-section declaration. And an upstream resolver that + silently collapses "couldn't fully resolve" to the same empty + container as "genuinely empty" (e.g. an AST list literal with one + unresolvable element silently dropping just that element instead of + invalidating the whole literal) reintroduces the exact ambiguity a + presence check downstream is trying to eliminate -- propagate + "unresolvable" as its own outcome, distinct from both "absent" and + "empty". - **Compare domain identifiers the way the ecosystem/spec does, not as raw strings.** A raw `==`/dict-key comparison silently fails to match values that a spec treats as equivalent (e.g. PEP 503 package-name @@ -82,6 +116,20 @@ shape described, not just the module where each was first found. Whenever two identifiers of the same kind are compared or one is used as a dict key, canonicalize both sides first per the format/spec that defines them, rather than assuming byte-for-byte equality is enough. + - **Version equality is PEP 440, never SemVer, and the two must not be + conflated in an explanation or a docstring.** Pitloom's own + `is_same_version()` compares two version strings via + `packaging.version.Version` equality: `"1.0"` == `"1.0.0"` == + `"1.0.0.0"` (trailing-zero-padded normalization), a fixed, narrow + notion of "the same release" -- not "the latest release compatible + with 1.0" and not a caret/tilde-style range (`^1.0.0` accepting + `1.0.1`). The two are easy to blur in prose (a reader's SemVer + intuition reads "same version" as "compatible version"), so an + explanation of a version-equality check must say "PEP 440 equality", + not bare "same version", and must not describe it in range/ + compatibility terms. See "Version comparison: PEP 440, not SemVer" + in `docs/dependency-sources.md` for the user-facing version of this + same distinction. - **A private third-party API (`obj._attr`) does not owe you any structural guarantee beyond what it happens to return today.** E.g. `packaging.markers.Marker()._markers` does not pre-group same- @@ -115,6 +163,45 @@ shape described, not just the module where each was first found. a cascade, fallback, or precedence order: re-read the current implementation before repeating or extending a prior description of its behavior, rather than assuming an existing doc still matches it). +- **Picking one candidate from an unordered collection needs an explicit, + stable tie-break whenever the result must be deterministic** (see "SBOM + output" above). `{u.get("packagetype"): u for u in urls}`-style + dict-comprehension overwrite, or "first item in a list", silently makes + the choice depend on whatever order an external API/dict/set happens to + produce -- not a contract Pitloom controls. Sort candidates by a stable + key (filename, name, version) before picking one; never rely on + insertion/iteration order as the tie-break. +- **Reusing a helper outside the contract it was actually built for + silently narrows behavior.** A helper written for one caller's specific + shape (`single_exact_pin()`: a lock file's `version` field, which is + always *exactly one* PEP 440 specifier clause) can look like a + reasonable fit for a superficially similar but looser case (a general + PEP 508 dependency string, which may legally combine an exact `==` + clause with another, non-conflicting clause, e.g. `foo==1.0,!=1.0.dev0`) + -- and silently reject valid input the narrower helper was never asked + to handle. Before reusing a helper in a new call site, check its + docstring's stated preconditions against what the new call site can + actually receive, not just whether the return type matches. +- **A dedup/conflict-exclusion fix must check every path that can produce + an entry for the same identity, not just the path the original bug was + in.** A fix that partitions input into "the bucket the bug lived in" + (now correctly deduplicated) and "everything else, passed through + unfiltered" can silently reintroduce the exact double-emission bug it + was meant to fix, via the passthrough bucket, the moment the same + identity (e.g. a canonicalized package name) can appear in *both* + buckets. After adding conflict-exclusion logic for one shape of + duplicate, ask whether the same identity could also reach the output + through an entirely different, unfiltered code path. +- **A test fixture/mock that models an external system's shape must be + updated in lockstep with what the production code under test actually + inspects.** A duck-typed stand-in (e.g. a fake Hatchling `core.config`) + that only populates the one field an earlier version of the code + happened to check gives false confidence once the code is fixed to + check more fields the same way -- the fixture still returns all-green + because it was never asked to model the new field, not because the fix + is correct. When broadening a check across several fields, broaden the + fixture that backs its tests across the same fields in the same change, + or the new branches go untested despite "the tests pass." ## CLI output diff --git a/src/pitloom/assemble/spdx3/deps_installed.py b/src/pitloom/assemble/spdx3/deps_installed.py index 5ec7ec18..9e2f1ab9 100644 --- a/src/pitloom/assemble/spdx3/deps_installed.py +++ b/src/pitloom/assemble/spdx3/deps_installed.py @@ -77,13 +77,27 @@ def _extract_pin_from_unparseable(dep: str) -> str | None: def _extract_exact_pin(dep: str) -> tuple[Requirement | None, str | None]: - """Parse *dep* into a Requirement and extract any single exact pin (== or ===).""" + """Parse *dep* into a Requirement and extract an exact pin (== or ===), + if any of its (possibly several) specifier clauses is one. + + Unlike :func:`pitloom.extract._lock_common.single_exact_pin` (which + requires the *entire* specifier set to be one exact pin -- correct for + a lock file's own ``version`` field, always a single specifier), a + general PEP 508 dependency string can legitimately combine an exact + pin with another clause (e.g. ``foo==1.2.3,!=1.2.3.dev0``) and still be + fully determined by that pin. Requiring the specifier set to contain + *only* the pin would wrongly treat such a dependency as unpinned, + letting a conflicting *locked_version* override a declared exact + version -- the opposite of "explicit pin beats local environment". + """ try: req = Requirement(dep) - exact = single_exact_pin(req.specifier) - return (req, exact[1]) if exact is not None else (req, None) except InvalidRequirement: return None, _extract_pin_from_unparseable(dep) + for spec in req.specifier: + if spec.operator in ("==", "===") and "*" not in spec.version: + return req, spec.version + return req, None def _is_exact_pin_conflict( @@ -93,20 +107,22 @@ def _is_exact_pin_conflict( if req is not None and req.specifier: try: return not req.specifier.contains(locked_version, prereleases=True) - # pylint: disable-next=broad-exception-caught - except (InvalidVersion, Exception): + except InvalidVersion: pass return not is_same_version(locked_version, pinned) -def _satisfies_constraint(req: Requirement | None, locked_version: str) -> bool: - """Return True if locked_version satisfies req.specifier.""" - if req is None or not req.specifier: +def _satisfies_constraint(req: Requirement | None, locked_version: str) -> bool | None: + """Return whether locked_version satisfies req.specifier, or ``None`` + when that can't be determined at all (dep was unparseable, so its + declared constraint -- if any -- is unknown, not merely absent).""" + if req is None: + return None + if not req.specifier: return True try: return req.specifier.contains(locked_version, prereleases=True) - # pylint: disable-next=broad-exception-caught - except (InvalidVersion, Exception): + except InvalidVersion: return False @@ -150,7 +166,17 @@ def _resolve_version( return pinned, None if locked_version is not None: - if warn and not _satisfies_constraint(req, locked_version): + satisfies = _satisfies_constraint(req, locked_version) + if warn and satisfies is None: + log.warning( + "Dependency %r declared as %r couldn't be parsed -- its" + " constraint (if any) can't be verified against locked" + " version %r, using it anyway", + dep_name, + dep, + locked_version, + ) + elif warn and not satisfies: log.warning( "Locked version %r for dependency %r does not satisfy declared" " constraint %r -- using locked version", diff --git a/src/pitloom/assemble/spdx3/deps_pypi.py b/src/pitloom/assemble/spdx3/deps_pypi.py index 22bcb16c..136ae12a 100644 --- a/src/pitloom/assemble/spdx3/deps_pypi.py +++ b/src/pitloom/assemble/spdx3/deps_pypi.py @@ -107,15 +107,37 @@ def _fetch_pypi_release_info(name: str, version: str | None) -> dict[str, Any] | def _extract_release_hash(release_info: dict[str, Any]) -> str | None: """Return the hex SHA-256 digest of the release's wheel (preferred) or - sdist artifact from a PyPI JSON API response, or ``None``.""" - urls = release_info.get("urls") or [] - by_type = {u.get("packagetype"): u for u in urls if isinstance(u, dict)} - entry = by_type.get("bdist_wheel") or by_type.get("sdist") - if entry is None and urls: - entry = next((u for u in urls if isinstance(u, dict)), None) - if entry is None: + sdist artifact from a PyPI JSON API response, or ``None``. + + A release commonly ships several ``bdist_wheel`` entries (one per + platform/ABI tag); picking one requires a deterministic tie-break -- + by filename, since neither PyPI's JSON API nor this repo defines any + other stable ordering -- so the same release always resolves to the + same hash across builds, per this repo's "SBOMs must be bit-for-bit + identical" requirement. Relying on whatever order the ``urls`` array + happens to arrive in would make the choice depend on an API response + order this repo has no contract with. + """ + urls = [u for u in (release_info.get("urls") or []) if isinstance(u, dict)] + by_type: dict[str, list[dict[str, Any]]] = {} + for url_entry in urls: + packagetype = url_entry.get("packagetype") + if isinstance(packagetype, str): + by_type.setdefault(packagetype, []).append(url_entry) + + def _first_by_filename(candidates: list[dict[str, Any]]) -> dict[str, Any] | None: + if not candidates: + return None + return min(candidates, key=lambda u: str(u.get("filename", ""))) + + selected = _first_by_filename(by_type.get("bdist_wheel", [])) or _first_by_filename( + by_type.get("sdist", []) + ) + if selected is None: + selected = _first_by_filename(urls) + if selected is None: return None - digests = entry.get("digests") + digests = selected.get("digests") if not isinstance(digests, dict): return None digest = digests.get("sha256") diff --git a/src/pitloom/assemble/spdx3/document.py b/src/pitloom/assemble/spdx3/document.py index 4a445e05..e16b8538 100644 --- a/src/pitloom/assemble/spdx3/document.py +++ b/src/pitloom/assemble/spdx3/document.py @@ -154,6 +154,62 @@ def _build_main_package( return main_package +def _deduplicated_locked_dependencies( + locked_dependencies: list[str] | None, +) -> list[str]: + """Collapse *locked_dependencies* to one entry per PEP 503-canonicalized + name among its exact-pinned entries, preserving order. + + A canonical name whose *pinned* entries disagree on PEP 440 version is + a genuine conflict (e.g. two lock formats layered by hand into the + same ``ProjectMetadata``, or a future extractor that forgets to + dedupe before returning) -- warned via :func:`warn_conflicting_versions` + and excluded entirely, the same "skip the ambiguous name, don't guess" + policy every extractor already applies to its own duplicate entries. + Neither of this function's two callers (:func:`_extract_locked_version_map`, + :func:`_locked_transitive_only_dependencies`) could otherwise safely + pick a winner between two conflicting entries on its own -- and picking + different winners in each would silently emit the ambiguous package + twice, once per winner, into the assembled SPDX graph. + + An entry with no exact pin at all (unpinned, ranged, or unparseable -- + every shipped extractor always emits an exact pin, but this guards a + future one that doesn't) has no version to compare and passes through + unfiltered: only :func:`_extract_locked_version_map` needs a pin, and + it already discards a pin-less entry on its own via + :func:`_extract_exact_pin`'s own ``None`` return. A passthrough entry + is dropped, though, when its canonical name also has a pinned entry + elsewhere in *locked_dependencies* -- the pin is strictly more + informative, and keeping both would double-emit the same package + (one from the pinned entry, one from the passthrough one). + """ + by_canonical: dict[str, list[tuple[str, str]]] = {} + passthrough: list[str] = [] + for dep in locked_dependencies or []: + _req, pinned = _extract_exact_pin(dep) + if pinned is None: + passthrough.append(dep) + continue + canon = canonicalize_name(_parse_dep_name(dep)) + by_canonical.setdefault(canon, []).append((dep, pinned)) + + deduplicated: list[str] = [ + dep + for dep in passthrough + if canonicalize_name(_parse_dep_name(dep)) not in by_canonical + ] + for group in by_canonical.values(): + dep, version = group[0] + conflicting_versions = {v for _, v in group if not is_same_version(v, version)} + if conflicting_versions: + warn_conflicting_versions( + "locked dependencies", _parse_dep_name(dep), {v for _, v in group} + ) + continue + deduplicated.append(dep) + return deduplicated + + def _locked_transitive_only_dependencies(metadata: ProjectMetadata) -> list[str]: """Return *metadata*'s locked (e.g. ``poetry.lock``-resolved) dependencies that aren't already a direct dependency, so a package declared both @@ -172,7 +228,7 @@ def _locked_transitive_only_dependencies(metadata: ProjectMetadata) -> list[str] } return [ dep - for dep in (metadata.locked_dependencies or []) + for dep in _deduplicated_locked_dependencies(metadata.locked_dependencies) if canonicalize_name(_parse_dep_name(dep)) not in direct_names ] @@ -204,18 +260,11 @@ def _extract_locked_version_map( to introspecting Pitloom's host environment. """ result: dict[str, str] = {} - for dep in locked_dependencies or []: + for dep in _deduplicated_locked_dependencies(locked_dependencies): dep_name = _parse_dep_name(dep) _req, pinned = _extract_exact_pin(dep) if pinned is not None: - canon = canonicalize_name(dep_name) - if canon in result and not is_same_version(result[canon], pinned): - warn_conflicting_versions( - "locked dependencies", - dep_name, - [result[canon], pinned], - ) - result[canon] = pinned + result[canonicalize_name(dep_name)] = pinned return result diff --git a/src/pitloom/extract/_lock_common.py b/src/pitloom/extract/_lock_common.py index 28ee528b..de9ab8c7 100644 --- a/src/pitloom/extract/_lock_common.py +++ b/src/pitloom/extract/_lock_common.py @@ -22,7 +22,7 @@ import logging from collections.abc import Callable, Iterable, Mapping from pathlib import Path -from typing import Any, TypeGuard +from typing import Any, TypeGuard, TypeVar from packaging.specifiers import SpecifierSet from packaging.utils import canonicalize_name @@ -226,12 +226,14 @@ def is_usable_version(version: object) -> TypeGuard[str]: return True -def group_versions_by_canonical_name( - pairs: Iterable[tuple[str, str]], -) -> dict[str, list[tuple[str, str]]]: - """Group ``(name, version)`` pairs by PEP 503-canonicalized *name*, - preserving each pair's original literal name/version and file order - both across and within groups. +_CanonicalGroupT = TypeVar("_CanonicalGroupT", bound=tuple[str, ...]) + + +def _group_by_canonical_name( + items: Iterable[_CanonicalGroupT], +) -> dict[str, list[_CanonicalGroupT]]: + """Group tuples by PEP 503-canonicalized *name* (each tuple's first + element), preserving file order both across and within groups. Comparing canonicalized (lowercased, ``-``/``_``/``.``-folded) names is required, not optional: ``Flask==1.0`` and ``flask==2.0`` name the @@ -239,26 +241,40 @@ def group_versions_by_canonical_name( resolve to more than one version" must group them together or the check silently never fires for a mixed-case duplicate. + Generic over tuple arity so :func:`group_versions_by_canonical_name`'s + ``(name, version)`` pairs and :func:`group_pin_triples_by_canonical_name`'s + ``(name, operator, version)`` triples share one implementation instead + of two copies of the same loop. + """ + by_canonical: dict[str, list[_CanonicalGroupT]] = {} + for item in items: + by_canonical.setdefault(canonicalize_name(item[0]), []).append(item) + return by_canonical + + +def group_versions_by_canonical_name( + pairs: Iterable[tuple[str, str]], +) -> dict[str, list[tuple[str, str]]]: + """Group ``(name, version)`` pairs by PEP 503-canonicalized *name* -- + see :func:`_group_by_canonical_name`. + A caller decides what a multi-entry group means for its own format: :mod:`pitloom.extract._pdm_lock` collapses a group to one entry when every version agrees (its per-extra duplicate records always do) and skips just that name otherwise. See :func:`group_pin_triples_by_canonical_name` - for the sibling version used where the pin's operator (``==`` vs ``===``) - also needs to survive grouping. + for the sibling used where the pin's operator (``==`` vs ``===``) also + needs to survive grouping. """ - by_canonical: dict[str, list[tuple[str, str]]] = {} - for name, version in pairs: - by_canonical.setdefault(canonicalize_name(name), []).append((name, version)) - return by_canonical + return _group_by_canonical_name(pairs) def group_pin_triples_by_canonical_name( triples: Iterable[tuple[str, str, str]], ) -> dict[str, list[tuple[str, str, str]]]: """Group ``(name, operator, version)`` pins by PEP 503-canonicalized - *name*, preserving file order both across and within groups -- the - ``===``-aware sibling of :func:`group_versions_by_canonical_name`, - for :mod:`pitloom.extract._pipfile_lock` and + *name* -- see :func:`_group_by_canonical_name`. The ``===``-aware + sibling of :func:`group_versions_by_canonical_name`, for + :mod:`pitloom.extract._pipfile_lock` and :mod:`pitloom.extract._requirements_txt`, whose ``version`` field is already a PEP 440 specifier that can carry either exact-pin operator and must keep it through to the formatted ``nameversion`` output. @@ -270,12 +286,7 @@ def group_pin_triples_by_canonical_name( whole file, since it has no per-format definition of "expected duplication" the way an extra-variant lock entry does. """ - by_canonical: dict[str, list[tuple[str, str, str]]] = {} - for name, operator, version in triples: - by_canonical.setdefault(canonicalize_name(name), []).append( - (name, operator, version) - ) - return by_canonical + return _group_by_canonical_name(triples) #: PEP 440 operators that pin to exactly one release: ``==`` (the diff --git a/src/pitloom/extract/_locked_dependencies.py b/src/pitloom/extract/_locked_dependencies.py index 17303327..3f78f5ee 100644 --- a/src/pitloom/extract/_locked_dependencies.py +++ b/src/pitloom/extract/_locked_dependencies.py @@ -127,8 +127,8 @@ def apply_locked_dependencies(metadata: ProjectMetadata, project_dir: Path) -> N :func:`_ignore_expected_name` in :data:`_LOCK_SOURCES` above instead. If *metadata* already carries a ``locked_dependencies`` result and a - higher-or-equal-priority source here wins, that source replaces it - and a ``WARNING:`` names the override -- and, per this repo's "no + higher-priority source here wins, that source replaces it and a + ``WARNING:`` names the override -- and, per this repo's "no silent deviations" principle, the fact that a source was superseded is also recorded in the resulting ``provenance["locked_dependencies"]`` string itself (as a trailing ``| Note: supersedes ``), not only diff --git a/src/pitloom/extract/_pipfile_lock.py b/src/pitloom/extract/_pipfile_lock.py index a1826af9..2c206869 100644 --- a/src/pitloom/extract/_pipfile_lock.py +++ b/src/pitloom/extract/_pipfile_lock.py @@ -131,7 +131,7 @@ def _pinned_pair_for_package( Returning the raw pair (not the formatted ``name==version`` string) lets the caller group same-canonical-name entries via - :func:`pitloom.extract._lock_common.group_versions_by_canonical_name` + :func:`pitloom.extract._lock_common.group_pin_triples_by_canonical_name` and skip a name that resolves to more than one distinct version -- unlike every sibling format, this extractor's input is a JSON object keyed directly by literal (not canonicalized) name, so a hand-edited @@ -143,6 +143,12 @@ def _pinned_pair_for_package( warn_missing_name("Skipping malformed Pipfile.lock entry", name) return None if not isinstance(entry, dict): + # Not warn_malformed_entry_not_table(): unlike every sibling + # format's [[package]]-style list (where the entry itself must be + # a table before a name can even be read out of it), Pipfile.lock + # keys each entry by name up front -- naming which key was + # malformed is more useful here than the shared helper's generic + # positional entry_label (e.g. "[[package]]") would be. log.warning( "Skipping malformed Pipfile.lock entry %r: expected a table, got %s", name, diff --git a/src/pitloom/extract/_poetry.py b/src/pitloom/extract/_poetry.py index 1cd9574e..9b50ecad 100644 --- a/src/pitloom/extract/_poetry.py +++ b/src/pitloom/extract/_poetry.py @@ -134,25 +134,35 @@ def extract_poetry_metadata( if readme: prov["readme"] = "Source: pyproject.toml | Field: tool.poetry.readme" prov.update(license_prov) - if authors: + # A container field's provenance is gated on the raw key's *presence* + # in [tool.poetry], not on whether parsing it produced a non-empty + # result -- an explicitly declared but empty `keywords = []` is a + # genuine, authoritative "zero" that merge_project_metadata() must not + # silently fill in from a lower-priority source, the same None-vs-[] + # distinction _pyproject.py's [project]-table path already applies. + if "authors" in poetry: prov["authors"] = "Source: pyproject.toml | Field: tool.poetry.authors" - prov["copyright_text"] = ( - "Source: Pitloom generator | Method: inferred_from_authors" - ) - if urls: + if authors: + prov["copyright_text"] = ( + "Source: Pitloom generator | Method: inferred_from_authors" + ) + if any(key in poetry for key in ("homepage", "repository", "documentation")): prov["urls"] = ( "Source: pyproject.toml" " | Field: tool.poetry.homepage/repository/documentation" ) - if dependencies: + if "dependencies" in poetry: prov["dependencies"] = ( "Source: pyproject.toml | Field: tool.poetry.dependencies" ) - if requires_python: + if ( + isinstance(poetry.get("dependencies"), dict) + and "python" in poetry["dependencies"] + ): prov["requires_python"] = ( "Source: pyproject.toml | Field: tool.poetry.dependencies.python" ) - if keywords: + if "keywords" in poetry: prov["keywords"] = "Source: pyproject.toml | Field: tool.poetry.keywords" return ProjectMetadata( diff --git a/src/pitloom/extract/_poetry_lock.py b/src/pitloom/extract/_poetry_lock.py index b99af512..a4381450 100644 --- a/src/pitloom/extract/_poetry_lock.py +++ b/src/pitloom/extract/_poetry_lock.py @@ -102,14 +102,30 @@ def extract_poetry_lock_dependencies(project_dir: Path) -> list[str] | None: def _is_main_group(validated: dict[str, Any], name: str) -> bool: - """Return True if package belongs to the main/default group.""" + """Return True if package belongs to the main/default group. + + Modern Poetry (1.2+) locks use ``groups``; legacy pre-1.5 locks use + ``category`` instead -- the two are schema-version-exclusive and + never coexist in a genuine lock file, so ``groups`` taking precedence + when both happen to be present (e.g. a hand-merged/corrupted file) is + an arbitrary but harmless tie-break. + """ if "groups" in validated: return ( default_group_included(validated, "poetry.lock", _DEFAULT_GROUP, name) is True ) if "category" in validated: - return validated.get("category") == _DEFAULT_GROUP + category = validated.get("category") + if not isinstance(category, str): + log.warning( + "Skipping malformed poetry.lock entry %r: 'category' is %s, " + "expected a string", + name, + type(category).__name__, + ) + return False + return category == _DEFAULT_GROUP return ( default_group_included(validated, "poetry.lock", _DEFAULT_GROUP, name) is True ) diff --git a/src/pitloom/extract/_pylock.py b/src/pitloom/extract/_pylock.py index 806b4639..38ccc848 100644 --- a/src/pitloom/extract/_pylock.py +++ b/src/pitloom/extract/_pylock.py @@ -265,7 +265,14 @@ def _evaluate_group_leaf( member = canonicalize_name(literal) in active_set return member if op == "in" else not member - # variable == "extra" (PEP 508 singular string variable) + # variable == "extra" (PEP 508 singular string variable). Equality + # commutes, so is_reversed doesn't matter for ==/!=. in/not in do care + # about operand order the same way the plural branch above does -- + # `extra in 'devtools'` (variable on the left) tests substring + # containment in the wrong direction, not membership, so it's + # rejected as unknown rather than guessed at. + if op in ("in", "not in") and is_reversed: + return None if op not in ("==", "!=", "in", "not in"): return None active_set = environment.get("extras", frozenset()) diff --git a/src/pitloom/extract/_pyproject.py b/src/pitloom/extract/_pyproject.py index fbaaf459..a46fa036 100644 --- a/src/pitloom/extract/_pyproject.py +++ b/src/pitloom/extract/_pyproject.py @@ -228,12 +228,18 @@ def read_pyproject( ) license_files = [p.as_posix() for p in (std.license_files or [])] + project_data = data.get("project", {}) provenance = _build_provenance( - data.get("project", {}), version_source, license_prov, description_source + project_data, version_source, license_prov, description_source ) if license_concluded and license_concluded_prov: provenance["license_concluded"] = license_concluded_prov - if license_files: + # Presence-gated, not truthy-gated: an explicit `license-files = []` + # is a genuine, authoritative "zero" that merge_project_metadata() + # must not silently fill in from a lower-priority source, the same + # None-vs-[] distinction _build_provenance() already applies to its + # own fields. + if "license-files" in project_data: provenance["license_files"] = ( "Source: pyproject.toml | Field: project.license-files" ) diff --git a/src/pitloom/extract/_requirements_txt.py b/src/pitloom/extract/_requirements_txt.py index 473c25ae..aa2b7e2e 100644 --- a/src/pitloom/extract/_requirements_txt.py +++ b/src/pitloom/extract/_requirements_txt.py @@ -173,17 +173,16 @@ def _collapse_or_none( result: list[str] = [] for group in group_pin_triples_by_canonical_name(pins).values(): name, op, version = group[0] - conflicting = next( - (v for _, _, v in group if not is_same_version(v, version)), None - ) - if conflicting is not None: + conflicting_versions = { + v for _, _, v in group if not is_same_version(v, version) + } + if conflicting_versions: log.warning( - "%s: %r pinned to conflicting versions (%s, %s) -- " + "%s: %r pinned to conflicting versions (%s) -- " "ignoring requirements.txt", lock_path, name, - version, - conflicting, + ", ".join(sorted({version, *conflicting_versions})), ) return None result.append(f"{name}{op}{version}") diff --git a/src/pitloom/extract/_setuptools_cfg.py b/src/pitloom/extract/_setuptools_cfg.py index 7c0a6242..ed43fc7c 100644 --- a/src/pitloom/extract/_setuptools_cfg.py +++ b/src/pitloom/extract/_setuptools_cfg.py @@ -33,10 +33,38 @@ class _NoProjectNameError(ValueError): def _section_dict(cfg: configparser.ConfigParser, section: str) -> dict[str, str]: - """Return a section's items as a plain dict, or empty dict if absent.""" + """Return a section's items as a plain dict, or empty dict if absent. + + Includes ``[DEFAULT]``-inherited values (``cfg.items()``'s normal, + intended behaviour) -- correct for *resolving* a value, but not for + asking whether *this section* declared a key: see + :func:`_section_declares_key` for that question. + """ return dict(cfg.items(section)) if cfg.has_section(section) else {} +def _section_declares_key( + cfg: configparser.ConfigParser, section: str, key: str +) -> bool: + """Return whether *section* itself declares *key*, ignoring any value + only inherited from ``[DEFAULT]``. + + ``key in cfg[section]``/``cfg.items(section)`` both merge in + ``[DEFAULT]`` by design (real, intended value-resolution behaviour) -- + but that makes them unusable for "was this explicitly declared here" + provenance-presence checks: a ``[DEFAULT]`` value shared across + sections would make every section's container field look explicitly + (and emptily) declared, even one that never mentions the key at all. + ``cfg._sections`` is the one place holding each section's own keys + with no ``[DEFAULT]`` merge -- an accepted, stable use of + :mod:`configparser`'s implementation, since the public API has no + equivalent "this section's own keys only" accessor. + """ + # pylint: disable-next=protected-access + sections: dict[str, dict[str, str]] = cfg._sections # type: ignore[attr-defined] + return key in sections.get(section, {}) + + def _resolve_cfg_version_file_directive( value: str, project_dir: Path ) -> tuple[str | None, str | None]: @@ -241,18 +269,30 @@ def read_setup_cfg( prov["readme"] = "Source: setup.cfg | Field: metadata.long_description" if license_name: prov["license"] = "Source: setup.cfg | Field: metadata.license" - if authors: + # Provenance for a container field is gated on the raw key's *presence* + # in the file, not on whether parsing it produced a non-empty result -- + # an explicitly-declared-but-empty value (e.g. `install_requires =`) + # is a genuine, authoritative "zero" that merge_project_metadata() must + # not silently fill in from a lower-priority source, the same + # None-vs-[] distinction _pyproject.py's [project]-table path already + # applies to `keywords`/`urls`/`dependencies`/`authors`. + if _section_declares_key(cfg, "metadata", "author") or _section_declares_key( + cfg, "metadata", "author_email" + ): prov["authors"] = "Source: setup.cfg | Field: metadata.author/author_email" - prov["copyright_text"] = ( - "Source: Pitloom generator | Method: inferred_from_authors" - ) - if urls: + if authors: + prov["copyright_text"] = ( + "Source: Pitloom generator | Method: inferred_from_authors" + ) + if _section_declares_key(cfg, "metadata", "url") or _section_declares_key( + cfg, "metadata", "project_urls" + ): prov["urls"] = "Source: setup.cfg | Field: metadata.url/project_urls" - if dependencies: + if _section_declares_key(cfg, "options", "install_requires"): prov["dependencies"] = "Source: setup.cfg | Field: options.install_requires" if requires_python: prov["requires_python"] = "Source: setup.cfg | Field: options.python_requires" - if keywords: + if _section_declares_key(cfg, "metadata", "keywords"): prov["keywords"] = "Source: setup.cfg | Field: metadata.keywords" project_metadata = ProjectMetadata( diff --git a/src/pitloom/extract/_setuptools_py.py b/src/pitloom/extract/_setuptools_py.py index 98e44ab8..6f8e6b6e 100644 --- a/src/pitloom/extract/_setuptools_py.py +++ b/src/pitloom/extract/_setuptools_py.py @@ -46,10 +46,16 @@ def _ast_literal(node: ast.expr) -> Any: """ if isinstance(node, ast.Constant): return node.value - if isinstance(node, ast.List): - return [v for elt in node.elts if (v := _ast_literal(elt)) is not None] - if isinstance(node, ast.Tuple): - return [v for elt in node.elts if (v := _ast_literal(elt)) is not None] + if isinstance(node, (ast.List, ast.Tuple)): + # All-or-nothing, unlike the dict branch below: silently dropping + # just the unresolvable elements would misrepresent a list like + # `install_requires=[SOME_CONSTANT]` as the literal empty list + # `[]` -- a "no dependencies" claim indistinguishable from a + # genuinely empty `install_requires=[]`, which downstream + # presence-based provenance treats as authoritative. `None` here + # correctly propagates as "not a resolvable literal" instead. + values = [_ast_literal(elt) for elt in node.elts] + return None if any(v is None for v in values) else values if isinstance(node, ast.Dict): result: dict[str, Any] = {} for key, value in zip(node.keys, node.values, strict=False): @@ -126,12 +132,22 @@ def _build_setup_py_provenance( has_readme: bool, has_license: bool, has_authors: bool, + authors: list[dict[str, str]], has_urls: bool, has_dependencies: bool, has_requires_python: bool, has_keywords: bool, ) -> dict[str, str]: - """Build provenance dictionary for extracted setup.py fields.""" + """Build provenance dictionary for extracted setup.py fields. + + A container field's provenance is gated on *presence* of its own + setup() kwarg (``has_urls``, ``has_dependencies``, etc.), not on + whether parsing it produced a non-empty result -- an explicitly + declared but empty ``install_requires=[]`` is a genuine, authoritative + "zero" that ``merge_project_metadata()`` must not silently fill in + from a lower-priority source, the same None-vs-[] distinction + ``_pyproject.py``'s ``[project]``-table path already applies. + """ prov: dict[str, str] = {"name": "Source: setup.py | Field: setup(name=...)"} if has_version: prov["version"] = "Source: setup.py | Field: setup(version=...)" @@ -143,9 +159,10 @@ def _build_setup_py_provenance( prov["license"] = "Source: setup.py | Field: setup(license=...)" if has_authors: prov["authors"] = "Source: setup.py | Field: setup(author=...)" - prov["copyright_text"] = ( - "Source: Pitloom generator | Method: inferred_from_authors" - ) + if authors: + prov["copyright_text"] = ( + "Source: Pitloom generator | Method: inferred_from_authors" + ) if has_urls: prov["urls"] = "Source: setup.py | Field: setup(url=...)" if has_dependencies: @@ -208,11 +225,12 @@ def read_setup_py( has_description=bool(description), has_readme=bool(readme), has_license=bool(license_name), - has_authors=bool(authors), - has_urls=bool(urls), - has_dependencies=bool(dependencies), + has_authors="author" in kwargs or "author_email" in kwargs, + authors=authors, + has_urls="url" in kwargs or "project_urls" in kwargs, + has_dependencies="install_requires" in kwargs, has_requires_python=bool(requires_python), - has_keywords=bool(keywords), + has_keywords="keywords" in kwargs, ) project_metadata = ProjectMetadata( diff --git a/src/pitloom/extract/hatchling.py b/src/pitloom/extract/hatchling.py index 1ae7bb2f..18080592 100644 --- a/src/pitloom/extract/hatchling.py +++ b/src/pitloom/extract/hatchling.py @@ -92,6 +92,22 @@ def _field_provenance(field_name: str) -> str: return f"{_PROVENANCE_SOURCE} | Field: project.{field_name}" +def _hatchling_field_declared(core: Any, project_key: str) -> bool: + """Return whether ``[project.]`` was actually declared, + via ``core.config`` (the raw, unprocessed ``[project]`` table) -- + never a container field's own *parsed* truthiness, which can't + distinguish "declared but empty" (e.g. ``dependencies = []``, a + genuine, authoritative zero) from "not declared at all" (fall back to + a lower-priority source in :func:`pitloom.core.project.merge_project_metadata`). + ``core.config`` access can raise ``OSError`` the same way the + property accessors it backs can (see :func:`_resolve_hatchling_readme`). + """ + try: + return project_key in core.config + except OSError: + return False + + def _resolve_hatchling_readme(core: Any) -> str | None: """Extract readme string or path safely from Hatchling core metadata.""" try: @@ -123,7 +139,7 @@ def _resolve_hatchling_license( provenance["license_concluded"] = license_concluded_prov license_files = _resolve_hatchling_license_files(core) - if license_files: + if _hatchling_field_declared(core, "license-files"): provenance["license_files"] = _field_provenance("license-files") return license_name, license_concluded, license_files @@ -175,23 +191,33 @@ def metadata_from_hatchling( readme = _resolve_hatchling_readme(core) requires_python = core.requires_python or None + # Provenance for a container field is gated on presence in the raw + # [project] table (`_hatchling_field_declared`), not on the resolved + # value's truthiness -- an explicitly declared but empty + # `dependencies = []` is a genuine, authoritative "zero" that + # merge_project_metadata() must not silently fill in from a + # lower-priority source ([tool.poetry] gap-fill below). authors = _authors_from_data(core.authors_data or {}) - if authors: + if _hatchling_field_declared(core, "authors"): provenance["authors"] = _field_provenance("authors") - provenance["copyright_text"] = ( - "Source: Pitloom generator | Method: inferred_from_authors" - ) + if authors: + provenance["copyright_text"] = ( + "Source: Pitloom generator | Method: inferred_from_authors" + ) urls = dict(core.urls or {}) - if urls: + if _hatchling_field_declared(core, "urls"): provenance["urls"] = _field_provenance("urls") dependencies = [ normalize_dependency_specifier(dep) for dep in (core.dependencies or []) ] - if dependencies: + if _hatchling_field_declared(core, "dependencies"): provenance["dependencies"] = _field_provenance("dependencies") + if _hatchling_field_declared(core, "keywords"): + provenance["keywords"] = _field_provenance("keywords") + license_name, license_concluded, license_files = _resolve_hatchling_license( core, project_dir, provenance ) diff --git a/tests/assemble/test_deps_enrichment_originator_license.py b/tests/assemble/test_deps_enrichment_originator_license.py index 09920099..f89c013a 100644 --- a/tests/assemble/test_deps_enrichment_originator_license.py +++ b/tests/assemble/test_deps_enrichment_originator_license.py @@ -305,6 +305,40 @@ def test_extract_release_hash_prefers_wheel() -> None: assert _extract_release_hash(release_info) == wheel_hash +def test_extract_release_hash_multiple_wheels_deterministic_by_filename() -> None: + """A release with several platform wheels must always pick the same + one (sorted by filename), regardless of the PyPI JSON API's own + array order -- required for bit-for-bit-identical SBOMs across builds.""" + macos_hash = "a" * 64 + linux_hash = "b" * 64 + windows_hash = "c" * 64 + forward_order = { + "urls": [ + { + "packagetype": "bdist_wheel", + "filename": "pkg-1.0-cp310-cp310-macosx_10_9_x86_64.whl", + "digests": {"sha256": macos_hash}, + }, + { + "packagetype": "bdist_wheel", + "filename": "pkg-1.0-cp310-cp310-manylinux_2_17_x86_64.whl", + "digests": {"sha256": linux_hash}, + }, + { + "packagetype": "bdist_wheel", + "filename": "pkg-1.0-cp310-cp310-win_amd64.whl", + "digests": {"sha256": windows_hash}, + }, + ] + } + reversed_order = {"urls": list(reversed(forward_order["urls"]))} + + result_forward = _extract_release_hash(forward_order) + result_reversed = _extract_release_hash(reversed_order) + + assert result_forward == result_reversed == macos_hash + + def test_extract_release_hash_falls_back_to_sdist() -> None: sdist_hash = "a" * 64 release_info = { diff --git a/tests/assemble/test_deps_locked_dependencies.py b/tests/assemble/test_deps_locked_dependencies.py index e1c11e82..44775ee5 100644 --- a/tests/assemble/test_deps_locked_dependencies.py +++ b/tests/assemble/test_deps_locked_dependencies.py @@ -454,13 +454,47 @@ def test_add_dependencies_groups_pep440_equivalent_versions() -> None: def test_extract_locked_version_map_warns_on_conflicting_duplicates( caplog: pytest.LogCaptureFixture, ) -> None: - """Conflicting duplicate package entries in locked_dependencies must warn.""" + """Conflicting duplicate package entries in locked_dependencies must warn + and be excluded entirely -- neither conflicting version is guessed at.""" caplog.set_level("WARNING") locked_map = _extract_locked_version_map(["requests==2.31.0", "requests==2.28.0"]) - assert locked_map["requests"] == "2.28.0" + assert "requests" not in locked_map assert "pinned to conflicting versions" in caplog.text +def test_locked_transitive_only_dependencies_excludes_conflicting_duplicates( + caplog: pytest.LogCaptureFixture, +) -> None: + """A conflicting duplicate name in locked_dependencies must not reach the + assembled SBOM as two separate dependsOn edges for the same package.""" + caplog.set_level("WARNING") + meta = ProjectMetadata( + name="testpkg", + dependencies=["requests>=2.0"], + locked_dependencies=["bar==1.0", "bar==2.0", "requests==2.31.0"], + ) + assert _locked_transitive_only_dependencies(meta) == [] + assert "pinned to conflicting versions" in caplog.text + + +def test_locked_transitive_only_dependencies_keeps_unpinned_entries() -> None: + """An unpinned/ranged locked_dependencies entry has no version to + compare or conflict on, so it must pass through unfiltered rather + than being silently dropped by the pin-only conflict-dedup step -- + only _extract_locked_version_map needs a pin, not this function.""" + meta = ProjectMetadata( + name="testpkg", + dependencies=[], + locked_dependencies=["unpinned-pkg", "range-dep>=1.0", "pinned==1.0"], + ) + + assert set(_locked_transitive_only_dependencies(meta)) == { + "unpinned-pkg", + "range-dep>=1.0", + "pinned==1.0", + } + + def test_locked_transitive_only_dependencies_handles_none_locked() -> None: """None locked_dependencies must safely return empty list without TypeError.""" meta = ProjectMetadata(name="testpkg", dependencies=["requests>=2.0"]) diff --git a/tests/assemble/test_deps_resolution_pins.py b/tests/assemble/test_deps_resolution_pins.py index 30e7fe5e..0a3814e8 100644 --- a/tests/assemble/test_deps_resolution_pins.py +++ b/tests/assemble/test_deps_resolution_pins.py @@ -47,8 +47,12 @@ def test_extract_exact_pin_accepts_single_exact_pins() -> None: def test_extract_exact_pin_rejects_wildcards_and_ranges() -> None: - """A prefix wildcard (==1.*) or multi-clause specifier is a range, - not an exact release pin.""" + """A prefix wildcard (==1.*), alone or combined with another clause, + is a range, never an exact release pin -- but a genuine exact ==/=== + clause combined with another (non-wildcard) clause still fully + determines the version (e.g. `==1.0,<=2.0` -- the `<=2.0` is + redundant, not conflicting) and must still be recognized as pinned, + per "explicit pin beats local environment".""" req_wild, pin_wild = _extract_exact_pin("requests==1.*") assert isinstance(req_wild, Requirement) assert pin_wild is None @@ -59,7 +63,7 @@ def test_extract_exact_pin_rejects_wildcards_and_ranges() -> None: req_two, pin_two = _extract_exact_pin("requests==1.0,<=2.0") assert isinstance(req_two, Requirement) - assert pin_two is None + assert pin_two == "1.0" req_range, pin_range = _extract_exact_pin("requests>=2.0") assert isinstance(req_range, Requirement) @@ -162,6 +166,38 @@ def test_resolve_version_arbitrary_equality_pins_matching_and_conflict( assert "conflicts with declared exact pin '2021.01.01-legacy'" in caplog.text +def test_resolve_version_compound_specifier_with_exact_pin_wins_over_locked( + caplog: pytest.LogCaptureFixture, +) -> None: + """A compound specifier that embeds a genuine exact pin (e.g. + `foo>=1.0,==1.5`) must still be recognized as pinned -- the declared + pin wins over a conflicting locked_version, with a warning, the same + as a bare `==1.5` would.""" + with caplog.at_level(logging.WARNING): + version, note = _resolve_version("foo", "foo>=1.0,==1.5", locked_version="2.0") + + assert version == "1.5" + assert note is None + assert "conflicts with declared exact pin '1.5'" in caplog.text + + +def test_resolve_version_unparseable_dep_with_locked_version_warns( + caplog: pytest.LogCaptureFixture, +) -> None: + """An unparseable dependency string combined with a locked_version must + still warn -- its declared constraint (if any) couldn't be verified, + which is different from "no constraint to violate" and must not be + silently treated the same.""" + with caplog.at_level(logging.WARNING): + version, note = _resolve_version( + "foo", "foo (garbled >= syntax", locked_version="9.9.9" + ) + + assert version == "9.9.9" + assert note == "Version resolved: Project lock file" + assert "couldn't be parsed" in caplog.text + + def test_enrich_from_installed_skips_when_installed_version_mismatches( monkeypatch: pytest.MonkeyPatch, ) -> None: @@ -290,10 +326,13 @@ def test_is_exact_pin_conflict_invalid_version_in_contains() -> None: def test_satisfies_constraint_req_none_or_empty_specifier() -> None: - """_satisfies_constraint returns True when req is None or has no specifier.""" - assert deps_installed_mod._satisfies_constraint(None, "1.0.0") + """_satisfies_constraint returns True when req has no specifier (genuinely + unconstrained), but None (unknown, not satisfied) when req itself is None + (the dependency string was unparseable, so its constraint can't be + verified at all -- distinct from "no constraint").""" + assert deps_installed_mod._satisfies_constraint(None, "1.0.0") is None req_no_spec = Requirement("requests") - assert deps_installed_mod._satisfies_constraint(req_no_spec, "1.0.0") + assert deps_installed_mod._satisfies_constraint(req_no_spec, "1.0.0") is True def test_satisfies_constraint_invalid_version_returns_false() -> None: diff --git a/tests/extract/conftest.py b/tests/extract/conftest.py index 01f7c9f6..3505c781 100644 --- a/tests/extract/conftest.py +++ b/tests/extract/conftest.py @@ -109,15 +109,27 @@ def _fake_hatch_metadata( ``_fake_hatch_metadata(core={"license_expression": "MIT"})``. The fake ``core.config`` (the raw, unprocessed ``[project]`` table -- - see :func:`pitloom.extract.hatchling._resolve_hatchling_license_files`) - gets a ``"license-files"`` key exactly when *core* explicitly overrides - ``license_files``, mirroring how a real declared field would show up in - both places at once. + see :func:`pitloom.extract.hatchling._hatchling_field_declared`) gets + the corresponding ``[project]`` key exactly for whichever container + fields *core* explicitly overrides, mirroring how a real declared + field would show up in both places at once -- every container field + ``metadata_from_hatchling()`` gates provenance on presence for + (``authors``/``urls``/``dependencies``/``keywords``/``license-files``), + not just ``license_files``. """ merged_core = {"raw_name": name, **_FAKE_CORE_DEFAULTS, **(core or {})} - config: dict[str, Any] = {} - if core is not None and "license_files" in core: - config["license-files"] = merged_core["license_files"] + core_attr_to_config_key = { + "authors_data": "authors", + "urls": "urls", + "dependencies": "dependencies", + "keywords": "keywords", + "license_files": "license-files", + } + config: dict[str, Any] = { + core_attr_to_config_key[attr]: merged_core[attr] + for attr in (core or {}) + if attr in core_attr_to_config_key and merged_core[attr] is not None + } return SimpleNamespace( name=name, version=version, diff --git a/tests/extract/test_lock_common.py b/tests/extract/test_lock_common.py index 299b1a83..90f9294f 100644 --- a/tests/extract/test_lock_common.py +++ b/tests/extract/test_lock_common.py @@ -18,6 +18,7 @@ from pitloom.extract._lock_common import ( default_group_included, find_first_present_key, + group_pin_triples_by_canonical_name, group_versions_by_canonical_name, has_required_top_level_table, index_packages_by_name, @@ -161,6 +162,29 @@ def test_group_versions_by_canonical_name_empty_input_returns_empty_dict() -> No assert not group_versions_by_canonical_name([]) +def test_group_pin_triples_by_canonical_name_groups_case_and_separator_variants() -> ( + None +): + """The ``(name, operator, version)`` sibling of + ``group_versions_by_canonical_name`` must canonicalize the same way, + and preserve each triple's operator (``==``/``===``) through grouping.""" + triples = [ + ("Flask", "==", "2.0"), + ("flask", "===", "2.0-legacy"), + ("idna", "==", "3.7"), + ] + + result = group_pin_triples_by_canonical_name(triples) + + assert list(result.keys()) == ["flask", "idna"] + assert result["flask"] == [("Flask", "==", "2.0"), ("flask", "===", "2.0-legacy")] + assert result["idna"] == [("idna", "==", "3.7")] + + +def test_group_pin_triples_by_canonical_name_empty_input_returns_empty_dict() -> None: + assert not group_pin_triples_by_canonical_name([]) + + def test_find_first_present_key_returns_first_match_in_key_order() -> None: """Order is determined by *keys*, not by the mapping's own key order -- callers rely on this to report a stable, predictable diff --git a/tests/extract/test_poetry_lock.py b/tests/extract/test_poetry_lock.py index 9236c5da..57c0393a 100644 --- a/tests/extract/test_poetry_lock.py +++ b/tests/extract/test_poetry_lock.py @@ -219,6 +219,27 @@ def test_legacy_poetry_category_main_and_dev() -> None: assert result == ["runtime-pkg==1.0.0"] +def test_legacy_poetry_malformed_category_skipped_and_warns( + caplog: pytest.LogCaptureFixture, +) -> None: + """A non-string 'category' (corrupted/hand-edited legacy lock) must be + skipped with a WARNING:, the same as a malformed 'groups' field is -- + not silently excluded with no diagnostic.""" + with tempfile.TemporaryDirectory() as tmp: + tmp_path = Path(tmp) + _write_lock( + tmp_path, + '[[package]]\nname = "bad-category-pkg"\nversion = "1.0.0"\n' + 'category = ["main"]\n', + ) + + with caplog.at_level(logging.WARNING): + result = extract_poetry_lock_dependencies(tmp_path) + + assert result == [] + assert "'category' is list, expected a string" in caplog.text + + def test_optional_package_excluded() -> None: """A package with optional = true is an extra, not a default runtime dependency -- must be excluded.""" diff --git a/tests/extract/test_poetry_parsing.py b/tests/extract/test_poetry_parsing.py index 74a4dec9..04a55a34 100644 --- a/tests/extract/test_poetry_parsing.py +++ b/tests/extract/test_poetry_parsing.py @@ -334,6 +334,30 @@ def test_extract_provenance_sources() -> None: assert "inferred_from_authors" in metadata.provenance.get("copyright_text", "") +def test_extract_provenance_empty_declared_dependencies() -> None: + """An explicitly declared but empty [tool.poetry.dependencies] (besides + the always-present `python` key) must still record provenance for + `dependencies` -- merge_project_metadata() relies on that presence to + treat the empty list as authoritative, not absent.""" + data = { + "tool": { + "poetry": { + "name": "my-pkg", + "version": "1.0.0", + "dependencies": {"python": "^3.10"}, + "keywords": [], + } + } + } + with tempfile.TemporaryDirectory() as d: + metadata = extract_poetry_metadata(data, Path(d)) + assert metadata.dependencies == [] + assert metadata.keywords == [] + assert "dependencies" in metadata.provenance + assert "keywords" in metadata.provenance + assert "requires_python" in metadata.provenance + + def test_convert_caret_and_tilde_edge_cases() -> None: """_convert_caret and _convert_tilde handle zero/short/invalid versions.""" from pitloom.extract._poetry import ( diff --git a/tests/extract/test_pylock_markers.py b/tests/extract/test_pylock_markers.py index bc6c5cd1..1c3ff83e 100644 --- a/tests/extract/test_pylock_markers.py +++ b/tests/extract/test_pylock_markers.py @@ -327,3 +327,22 @@ def test_marker_invalid_operators_treated_as_unknown() -> None: deps = extract_pylock_dependencies(tmp_path) assert deps is not None assert set(deps) == {"group-eq==1.0.0", "extra-gte==1.0.0"} + + +def test_marker_reversed_in_operand_on_singular_extra_treated_as_unknown() -> None: + """`extra in 'devtools'` (the singular string variable on the left of + `in`) tests substring containment in the wrong direction, not set + membership -- same as the plural extras/dependency_groups branch + already rejects a reversed `in`/`not in` operand, this must return + unknown (included) rather than guessing at a result.""" + with tempfile.TemporaryDirectory() as tmp: + tmp_path = Path(tmp) + _write_lock( + tmp_path, + 'default-groups = ["default"]\n' + '[[packages]]\nname = "reversed-in"\nversion = "1.0.0"\n' + "marker = \"extra in 'devtools'\"\n", + ) + + deps = extract_pylock_dependencies(tmp_path) + assert deps == ["reversed-in==1.0.0"] diff --git a/tests/extract/test_setuptools_cfg.py b/tests/extract/test_setuptools_cfg.py index 2ccde4f0..2c63e83c 100644 --- a/tests/extract/test_setuptools_cfg.py +++ b/tests/extract/test_setuptools_cfg.py @@ -307,6 +307,18 @@ def test_read_setup_cfg_provenance() -> None: assert "inferred_from_authors" in metadata.provenance.get("copyright_text", "") +def test_read_setup_cfg_empty_install_requires_gets_provenance() -> None: + """An explicitly declared but empty install_requires must still record + provenance -- merge_project_metadata() relies on that presence to + treat the empty list as authoritative, not absent.""" + content = "[metadata]\nname = pkg\nversion = 1.0\n[options]\ninstall_requires =\n" + with tempfile.TemporaryDirectory() as d: + (Path(d) / "setup.cfg").write_text(content) + metadata, _ = read_setup_cfg(Path(d)) + assert metadata.dependencies == [] + assert "dependencies" in metadata.provenance + + def test_resolve_cfg_version_edge_cases(tmp_path: Path) -> None: """_resolve_cfg_version handles empty strings, invalid attrs, and directives.""" from pitloom.extract._setuptools_cfg import _resolve_cfg_version diff --git a/tests/extract/test_setuptools_py.py b/tests/extract/test_setuptools_py.py index 188efcb5..f197668a 100644 --- a/tests/extract/test_setuptools_py.py +++ b/tests/extract/test_setuptools_py.py @@ -149,6 +149,21 @@ def test_read_setup_py_provenance() -> None: assert "setup.py" in metadata.provenance["authors"] +def test_read_setup_py_empty_install_requires_gets_provenance() -> None: + """An explicitly declared but empty install_requires=[] must still + record provenance -- merge_project_metadata() relies on that + presence to treat the empty list as authoritative, not absent.""" + content = ( + "from setuptools import setup\n" + "setup(name='pkg', version='1.0', install_requires=[])\n" + ) + with tempfile.TemporaryDirectory() as d: + (Path(d) / "setup.py").write_text(content) + metadata, _ = read_setup_py(Path(d)) + assert metadata.dependencies == [] + assert "dependencies" in metadata.provenance + + def test_read_setup_py_returns_default_pitloom_config() -> None: """setup.py provides no pitloom config -- defaults are returned.""" content = "from setuptools import setup\nsetup(name='pkg', version='1.0')\n" diff --git a/working-docs/implementation/poetry-support.md b/working-docs/implementation/poetry-support.md index ef02a74c..aa28184b 100644 --- a/working-docs/implementation/poetry-support.md +++ b/working-docs/implementation/poetry-support.md @@ -192,8 +192,10 @@ no `groups` field at all, `category == "main"` -- and not marked `optional = true` (an extra, not a default runtime dependency). This excludes dev/other-group-only and extras packages, the same "not a runtime dependency" policy already applied to `[tool.poetry.group.*]` -above, as exact-pin `name==version` (or `name===version` for a legacy -non-normalizable pin) strings. A package resolved from a +above, as exact-pin `name==version` strings -- `poetry.lock`'s +`version` field is a bare version number, not a PEP 440 specifier, so +(unlike `Pipfile.lock`/`requirements.txt` below) there's no operator to +preserve and this extractor always emits `==`. A package resolved from a `directory`/`file`/`git`/`url` source (per `[package.source].type`) is excluded the same way `_poetry_dep_to_pep508()` excludes it from direct dependencies -- it has no meaningful PyPI version pin, so including it @@ -238,7 +240,9 @@ introspecting whatever happens to be installed in Pitloom's own execution environment, which has no relationship to the target project's environment. This priority order is generic across every lock format, not `poetry.lock`-specific -- see -[lock-file-cascade.md](lock-file-cascade.md). +[docs/dependency-sources.md](../../docs/dependency-sources.md)'s "Two +kinds of dependency information" and "Version comparison: PEP 440, not +SemVer" sections. In the assembled SPDX 3 graph, a locked package already covered by a direct `[tool.poetry.dependencies]` entry gets no duplicate edge -- From c974b81782b82716d8653410cbc13018b1499983 Mon Sep 17 00:00:00 2001 From: Arthit Suriyawongkul Date: Tue, 8 Sep 2026 11:55:36 +0700 Subject: [PATCH 27/35] Add diagram and fix minor bugs Signed-off-by: Arthit Suriyawongkul --- src/pitloom/assemble/spdx3/document.py | 75 +++++++-- src/pitloom/extract/_poetry.py | 13 +- .../assemble/test_deps_locked_dependencies.py | 43 ++++++ tests/extract/test_poetry_parsing.py | 22 +++ working-docs/design/architecture-overview.md | 5 + .../implementation/end-to-end-flow.md | 142 ++++++++++++++++++ 6 files changed, 283 insertions(+), 17 deletions(-) create mode 100644 working-docs/implementation/end-to-end-flow.md diff --git a/src/pitloom/assemble/spdx3/document.py b/src/pitloom/assemble/spdx3/document.py index e16b8538..dd6648f0 100644 --- a/src/pitloom/assemble/spdx3/document.py +++ b/src/pitloom/assemble/spdx3/document.py @@ -184,33 +184,49 @@ def _deduplicated_locked_dependencies( (one from the pinned entry, one from the passthrough one). """ by_canonical: dict[str, list[tuple[str, str]]] = {} - passthrough: list[str] = [] for dep in locked_dependencies or []: _req, pinned = _extract_exact_pin(dep) if pinned is None: - passthrough.append(dep) continue canon = canonicalize_name(_parse_dep_name(dep)) by_canonical.setdefault(canon, []).append((dep, pinned)) - deduplicated: list[str] = [ - dep - for dep in passthrough - if canonicalize_name(_parse_dep_name(dep)) not in by_canonical - ] - for group in by_canonical.values(): + excluded: set[str] = set() + resolved: dict[str, str] = {} + for group_canon, group in by_canonical.items(): dep, version = group[0] conflicting_versions = {v for _, v in group if not is_same_version(v, version)} if conflicting_versions: warn_conflicting_versions( "locked dependencies", _parse_dep_name(dep), {v for _, v in group} ) + excluded.add(group_canon) + else: + resolved[group_canon] = dep + + deduplicated: list[str] = [] + emitted: set[str] = set() + for dep in locked_dependencies or []: + canon = canonicalize_name(_parse_dep_name(dep)) + if canon in excluded: + continue + if canon in resolved: + if canon in emitted: + continue + emitted.add(canon) + deduplicated.append(resolved[canon]) continue + # No pinned entry anywhere for this canonical name -- pass + # through as-is, at its own original position. deduplicated.append(dep) return deduplicated -def _locked_transitive_only_dependencies(metadata: ProjectMetadata) -> list[str]: +def _locked_transitive_only_dependencies( + metadata: ProjectMetadata, + *, + deduplicated_locked: list[str] | None = None, +) -> list[str]: """Return *metadata*'s locked (e.g. ``poetry.lock``-resolved) dependencies that aren't already a direct dependency, so a package declared both directly and in the lock gets one ``dependsOn`` edge, not two. @@ -222,13 +238,25 @@ def _locked_transitive_only_dependencies(metadata: ProjectMetadata) -> list[str] treat those as different packages and double-emit the edge this function exists to avoid. See ``_try_read_poetry()`` in ``pitloom.extract._pyproject`` for why this is source-stage-only. + + *deduplicated_locked*, when given, is used as-is instead of calling + :func:`_deduplicated_locked_dependencies` again -- :func:`build` computes + it once and shares it with :func:`_extract_locked_version_map` so a + genuine name/version conflict in ``locked_dependencies`` only logs + :func:`warn_conflicting_versions`'s warning once per document, not once + per caller. """ direct_names = { canonicalize_name(_parse_dep_name(dep)) for dep in metadata.dependencies } + locked = ( + deduplicated_locked + if deduplicated_locked is not None + else _deduplicated_locked_dependencies(metadata.locked_dependencies) + ) return [ dep - for dep in _deduplicated_locked_dependencies(metadata.locked_dependencies) + for dep in locked if canonicalize_name(_parse_dep_name(dep)) not in direct_names ] @@ -252,15 +280,26 @@ def _locked_dependencies_completeness(metadata: ProjectMetadata) -> str | None: def _extract_locked_version_map( locked_dependencies: list[str] | None, + *, + deduplicated: list[str] | None = None, ) -> dict[str, str]: """Map canonical package names to their exact locked version string. Enables direct dependencies declared as ranges (e.g. ``requests>=2.0``) to resolve to their authoritative locked version rather than falling back to introspecting Pitloom's host environment. + + *deduplicated*, when given, is used as-is instead of calling + :func:`_deduplicated_locked_dependencies` again -- see + :func:`_locked_transitive_only_dependencies`'s matching parameter for why. """ result: dict[str, str] = {} - for dep in _deduplicated_locked_dependencies(locked_dependencies): + locked = ( + deduplicated + if deduplicated is not None + else _deduplicated_locked_dependencies(locked_dependencies) + ) + for dep in locked: dep_name = _parse_dep_name(dep) _req, pinned = _extract_exact_pin(dep) if pinned is not None: @@ -395,8 +434,18 @@ def build( ) # --- Locked (e.g. poetry.lock-resolved) transitive-only dependencies --- - transitive_only = _locked_transitive_only_dependencies(metadata) - locked_versions = _extract_locked_version_map(metadata.locked_dependencies) + # Deduplicated once and shared below: a genuine name/version conflict + # in metadata.locked_dependencies must warn exactly once per document, + # not once per function that would otherwise recompute it. + deduplicated_locked = _deduplicated_locked_dependencies( + metadata.locked_dependencies + ) + transitive_only = _locked_transitive_only_dependencies( + metadata, deduplicated_locked=deduplicated_locked + ) + locked_versions = _extract_locked_version_map( + metadata.locked_dependencies, deduplicated=deduplicated_locked + ) release_info_cache = ( None if offline diff --git a/src/pitloom/extract/_poetry.py b/src/pitloom/extract/_poetry.py index 9b50ecad..fc07d1b4 100644 --- a/src/pitloom/extract/_poetry.py +++ b/src/pitloom/extract/_poetry.py @@ -155,10 +155,15 @@ def extract_poetry_metadata( prov["dependencies"] = ( "Source: pyproject.toml | Field: tool.poetry.dependencies" ) - if ( - isinstance(poetry.get("dependencies"), dict) - and "python" in poetry["dependencies"] - ): + # requires_python is a scalar (str | None), not a container -- unlike + # keywords/urls/dependencies/authors above, there's no "explicitly + # declared but empty" state worth preserving: `python = "*"` means + # "no constraint", which is correctly None, and provenance must + # follow that resolved value (truthy-gated), not the raw key's mere + # presence -- otherwise a `python = "*"` entry sets provenance for a + # field that stays None, which can misattribute a real value a + # lower-priority source supplies later via merge_project_metadata(). + if requires_python: prov["requires_python"] = ( "Source: pyproject.toml | Field: tool.poetry.dependencies.python" ) diff --git a/tests/assemble/test_deps_locked_dependencies.py b/tests/assemble/test_deps_locked_dependencies.py index 44775ee5..4fb3f9ef 100644 --- a/tests/assemble/test_deps_locked_dependencies.py +++ b/tests/assemble/test_deps_locked_dependencies.py @@ -24,6 +24,7 @@ from pitloom.assemble.spdx3 import deps_installed from pitloom.assemble.spdx3.deps import add_dependencies from pitloom.assemble.spdx3.document import ( + _deduplicated_locked_dependencies, _extract_locked_version_map, _locked_dependencies_completeness, _locked_transitive_only_dependencies, @@ -495,6 +496,48 @@ def test_locked_transitive_only_dependencies_keeps_unpinned_entries() -> None: } +def test_deduplicated_locked_dependencies_preserves_original_order() -> None: + """The docstring promises 'preserving order' -- a deduplicated pinned + entry must land at its first occurrence's original position, not be + moved to the end (or the front) relative to unrelated entries.""" + assert _deduplicated_locked_dependencies(["foo==1.0", "bar", "baz==2.0"]) == [ + "foo==1.0", + "bar", + "baz==2.0", + ] + # A name repeated later (agreeing pin) still resolves at its FIRST + # occurrence's position, not its last. + assert _deduplicated_locked_dependencies(["foo==1.0", "bar", "foo==1.0.0"]) == [ + "foo==1.0", + "bar", + ] + + +def test_build_warns_conflicting_locked_duplicates_only_once( + caplog: pytest.LogCaptureFixture, +) -> None: + """build() shares one _deduplicated_locked_dependencies() result between + _locked_transitive_only_dependencies() and _extract_locked_version_map() + instead of each recomputing it -- a genuine conflict must log + 'pinned to conflicting versions' exactly once per document, not once + per caller.""" + project = ProjectMetadata( + name="main-project", + version="1.0.0", + dependencies=["requests>=2.0"], + locked_dependencies=["bar==1.0", "bar==2.0", "requests==2.31.0"], + provenance={ + "locked_dependencies": "Source: poetry.lock | Method: resolved_lockfile" + }, + ) + doc = DocumentModel(project=project, creation_metadata=CreationMetadata()) + + caplog.set_level("WARNING") + build(doc, offline=True) + + assert caplog.text.count("pinned to conflicting versions") == 1 + + def test_locked_transitive_only_dependencies_handles_none_locked() -> None: """None locked_dependencies must safely return empty list without TypeError.""" meta = ProjectMetadata(name="testpkg", dependencies=["requests>=2.0"]) diff --git a/tests/extract/test_poetry_parsing.py b/tests/extract/test_poetry_parsing.py index 04a55a34..505316e4 100644 --- a/tests/extract/test_poetry_parsing.py +++ b/tests/extract/test_poetry_parsing.py @@ -358,6 +358,28 @@ def test_extract_provenance_empty_declared_dependencies() -> None: assert "requires_python" in metadata.provenance +def test_extract_provenance_wildcard_python_leaves_requires_python_unset() -> None: + """`python = "*"` (no real constraint) resolves requires_python to + None -- unlike the container fields, provenance must follow that + resolved value, not the raw `python` key's mere presence, or a + misattributed provenance tag could survive a later + merge_project_metadata() call that fills requires_python from a + different, real source.""" + data = { + "tool": { + "poetry": { + "name": "my-pkg", + "version": "1.0.0", + "dependencies": {"python": "*"}, + } + } + } + with tempfile.TemporaryDirectory() as d: + metadata = extract_poetry_metadata(data, Path(d)) + assert metadata.requires_python is None + assert "requires_python" not in metadata.provenance + + def test_convert_caret_and_tilde_edge_cases() -> None: """_convert_caret and _convert_tilde handle zero/short/invalid versions.""" from pitloom.extract._poetry import ( diff --git a/working-docs/design/architecture-overview.md b/working-docs/design/architecture-overview.md index 16e08ed7..94626b43 100644 --- a/working-docs/design/architecture-overview.md +++ b/working-docs/design/architecture-overview.md @@ -288,6 +288,11 @@ See `working-docs/design/sbom-fragments/fragment-merge-design.md`. ### Data flow: extraction -> document model -> assembly +See [end-to-end-flow.md](../implementation/end-to-end-flow.md) for a +generic, stage-level version of this diagram -- the version here is the fuller, +more implementation/integration-specific picture, including the still-planned +pieces. + ```text Information sources ─────────────────── diff --git a/working-docs/implementation/end-to-end-flow.md b/working-docs/implementation/end-to-end-flow.md new file mode 100644 index 00000000..f8727236 --- /dev/null +++ b/working-docs/implementation/end-to-end-flow.md @@ -0,0 +1,142 @@ +--- +Created: 2026-09-08 +Last-Modified: 2026-09-08 +SPDX-FileCopyrightText: 2026-present Arthit Suriyawongkul +SPDX-FileType: DOCUMENTATION +SPDX-License-Identifier: CC0-1.0 +--- + +# End-to-end flow: extraction to SBOM + +A one-page map of how a call becomes an SBOM. Deliberately generic -- +it names stable stages and top-level types, not individual extractor +modules or lock formats, so a new lock format or a new AI-model +extractor doesn't require updating this diagram. For per-topic detail, +follow the "See also" links below instead of expanding this page. + +See also: [architecture-overview.md](../design/architecture-overview.md) +for the fuller (and more speculative/planned-integration-heavy) +picture this page distills; [sbom-lifecycle-stages.md](sbom-lifecycle-stages.md) +for the source-stage vs. build-stage distinction that decides which +entry point runs; [lock-file-cascade.md](lock-file-cascade.md) and +[metadata-provenance.md](../../docs/metadata-provenance.md) for two of +the stages below in depth. + +## The diagram + +```mermaid +flowchart LR + subgraph Sources["Sources"] + direction TB + S1["Project config
(pyproject.toml, setup.cfg/.py)"] + S2["Lock / pin files
(one per supported format)"] + S3["Built wheel
(or installed env)"] + S4["AI model / dataset files,
tracking platforms, SBOM fragments"] + S5["Externally-generated SBOM
(embed-wheel --sbom)"] + end + + subgraph Extract["Extraction -- pitloom.extract.*"] + direction TB + E1["Metadata extractors
(one per source format)"] + E2["Lock/pin priority cascade
(highest-priority usable source wins)"] + end + + subgraph Model["Format-neutral model -- pitloom.core.*"] + direction TB + M1["ProjectMetadata
(+ locked_dependencies, + provenance)"] + M2["AiModelMetadata / DatasetMetadata /
FragmentConfig"] + M3["DocumentModel"] + end + + subgraph Assemble["Assembly -- pitloom.assemble.spdx3.*"] + A1["build(doc)
packages, relationships, licenses,
provenance annotations"] + end + + subgraph Output["Output"] + direction TB + O1["Spdx3JsonExporter"] + O2["SPDX 3 JSON-LD SBOM"] + O3["Embed into wheel archive
(PEP 770, .dist-info/sboms/)"] + end + + S1 --> E1 + S2 --> E2 + S3 --> E1 + S4 --> E1 + + E1 --> M1 + E2 -. "overlays onto" .-> M1 + E1 --> M2 + + M1 --> M3 + M2 --> M3 + + M3 --> A1 + A1 --> O1 + O1 --> O2 + + O2 -. "embed-wheel" .-> O3 + S5 -. "embed-wheel --sbom
(skips extraction/assembly)" .-> O3 + S3 -. "same wheel file, rewritten" .-> O3 +``` + +## Reading it + +- **Sources -> Extraction** is many-to-many by design: every supported + project-config format, lock format, and AI-model/dataset format gets + its own extractor module, but they all converge on the same two model + types one level up. Adding a seventh lock format or a new AI-model + format changes this layer's *inside*, never this diagram. +- **The lock/pin cascade is a distinct step from ordinary extraction**: + it doesn't produce its own `ProjectMetadata` -- it overlays + `locked_dependencies` and a `provenance["locked_dependencies"]` + annotation onto whichever metadata the config-format extractor + already produced. See + [lock-file-cascade.md](lock-file-cascade.md). +- **`ProjectMetadata`/`DocumentModel` are the seam.** Every extractor's + job is to populate one of these two dataclasses; every assembler's + job is to read them. Neither side needs to know how the other is + implemented -- that's what makes this diagram stable across either + side changing internally. +- **Two real entry points converge on the same seam**: the CLI + (`pitloom.__main__`) and the public library API + (`generate_project_sbom()`, etc.) both resolve metadata via + `pitloom.extract.project.read_project()` and both call + `pitloom.assemble.spdx3.document.build()`; the Hatchling build hook + (`pitloom.plugins.hatch`) resolves metadata via + `pitloom.extract.hatchling.metadata_from_hatchling()` instead (a + build-stage source, never a lock file -- see + [sbom-lifecycle-stages.md](sbom-lifecycle-stages.md)) but converges + on the same `build()` call. All three end up in the same box in this + diagram. +- **Provenance rides alongside data at every stage**, not as an + afterthought bolted on at the end: each extractor records where a + field came from in `ProjectMetadata.provenance`, `build()` reads it + to annotate relationships (e.g. `RelationshipCompleteness`, + `declared_constraint`), and the final SBOM carries those annotations + for a human or downstream tool to inspect. See + [metadata-provenance.md](../../docs/metadata-provenance.md). +- **`embed-wheel` is the same pipeline with one extra terminal, not a + separate pipeline.** `loom embed-wheel ` builds an SBOM the + usual way (from the wheel alone, or from the wheel plus a + `--project-dir` source tree -- either way with + `include_locked_dependencies=False`, since embedding is build-stage + and a source-stage lock file's resolved dependencies must never leak + into it, per [sbom-lifecycle-stages.md](sbom-lifecycle-stages.md)), + then writes the result into that *same* wheel's + `.dist-info/sboms/` directory instead of (or alongside) emitting a + standalone file. `embed-wheel --sbom ` skips extraction and + assembly entirely and embeds an already-built SBOM as-is, after + cross-checking its declared subject name/version against the + wheel's own metadata. + +## When this diagram *should* change + +Only when a stage itself changes shape -- not when something moves +inside a stage. Concretely: a new top-level model type replacing +`ProjectMetadata`/`DocumentModel`; a new stage inserted between +extraction and assembly; the assembler starting to consume something +other than `DocumentModel`; or a new output format alongside SPDX 3 +JSON-LD. Adding, removing, or reordering-within-priority a lock +format, an extractor module, or an enrichment step does not warrant an +update here -- that detail belongs in the linked per-topic docs. From 8052f7d7c462e141609851c3fcb3ba7100f1b7c9 Mon Sep 17 00:00:00 2001 From: Arthit Suriyawongkul Date: Tue, 8 Sep 2026 12:34:42 +0700 Subject: [PATCH 28/35] Refactor for reuse / file size Signed-off-by: Arthit Suriyawongkul --- .../assemble/spdx3/_document_locked_deps.py | 216 +++++++++++++++++ src/pitloom/assemble/spdx3/deps_installed.py | 2 +- src/pitloom/assemble/spdx3/document.py | 224 ++---------------- src/pitloom/extract/_extract_utils.py | 22 ++ src/pitloom/extract/_lock_common.py | 120 +--------- src/pitloom/extract/_lock_common_warnings.py | 141 +++++++++++ src/pitloom/extract/_poetry.py | 12 +- src/pitloom/extract/_pylock.py | 22 +- src/pitloom/extract/_pyproject.py | 9 +- src/pitloom/extract/_setuptools_py.py | 11 +- src/pitloom/extract/hatchling.py | 8 +- .../assemble/test_deps_enrichment_prefetch.py | 2 +- tests/assemble/test_deps_resolution_pins.py | 2 +- tests/extract/test_hatch_hook_metadata.py | 12 + tests/extract/test_pylock.py | 26 ++ 15 files changed, 498 insertions(+), 331 deletions(-) create mode 100644 src/pitloom/assemble/spdx3/_document_locked_deps.py create mode 100644 src/pitloom/extract/_lock_common_warnings.py diff --git a/src/pitloom/assemble/spdx3/_document_locked_deps.py b/src/pitloom/assemble/spdx3/_document_locked_deps.py new file mode 100644 index 00000000..2c6b59b1 --- /dev/null +++ b/src/pitloom/assemble/spdx3/_document_locked_deps.py @@ -0,0 +1,216 @@ +# SPDX-FileContributor: Arthit Suriyawongkul +# SPDX-FileCopyrightText: 2026-present Arthit Suriyawongkul +# SPDX-FileType: SOURCE +# SPDX-License-Identifier: Apache-2.0 + +"""Locked (e.g. ``poetry.lock``-resolved) dependency handling for +:func:`pitloom.assemble.spdx3.document.build` -- deduplication, conflict +detection, the exact-locked-version map, and the combined PyPI release-info +prefetch. Split out of :mod:`pitloom.assemble.spdx3.document` to keep that +module under this repo's file-size soft limit; every name here is +re-exported from there, so existing imports of these names from +``pitloom.assemble.spdx3.document`` keep working. + +See also: :mod:`pitloom.extract._lock_common` for the shared +canonical-name-grouping and version-equality helpers this module builds on. +""" + +from __future__ import annotations + +from typing import Any + +from packaging.utils import canonicalize_name + +from pitloom.assemble.spdx3.deps import _parse_dep_name, _resolve_version +from pitloom.assemble.spdx3.deps_installed import _extract_exact_pin +from pitloom.assemble.spdx3.deps_pypi import _prefetch_pypi_release_infos +from pitloom.core.project import ProjectMetadata +from pitloom.extract._lock_common import ( + _group_by_canonical_name, + is_same_version, + warn_conflicting_versions, +) + + +def _dedup_and_locked_versions( + locked_dependencies: list[str] | None, +) -> tuple[list[str], dict[str, str]]: + """Collapse *locked_dependencies* to one entry per PEP 503-canonicalized + name among its exact-pinned entries (preserving order), and map each + surviving canonical name to its pinned version -- computed together so + :func:`_extract_exact_pin` parses each entry's pin only once. + + A canonical name whose *pinned* entries disagree on PEP 440 version is + a genuine conflict (e.g. two lock formats layered by hand into the + same ``ProjectMetadata``, or a future extractor that forgets to + dedupe before returning) -- warned via :func:`warn_conflicting_versions` + and excluded entirely, the same "skip the ambiguous name, don't guess" + policy every extractor already applies to its own duplicate entries. + Neither of this function's callers (:func:`_extract_locked_version_map`, + :func:`_locked_transitive_only_dependencies`) could otherwise safely + pick a winner between two conflicting entries on its own -- and picking + different winners in each would silently emit the ambiguous package + twice, once per winner, into the assembled SPDX graph. + + An entry with no exact pin at all (unpinned, ranged, or unparseable -- + every shipped extractor always emits an exact pin, but this guards a + future one that doesn't) has no version to compare and passes through + the returned list unfiltered, but contributes nothing to the version + map: only :func:`_extract_locked_version_map` needs a pin, and it + already discards a pin-less entry on its own via + :func:`_extract_exact_pin`'s own ``None`` return. A passthrough entry + is dropped from the list, though, when its canonical name also has a + pinned entry elsewhere in *locked_dependencies* -- the pin is strictly + more informative, and keeping both would double-emit the same package + (one from the pinned entry, one from the passthrough one). + """ + pinned_triples: list[tuple[str, str, str]] = [] + for dep in locked_dependencies or []: + _req, pinned = _extract_exact_pin(dep) + if pinned is not None: + pinned_triples.append((_parse_dep_name(dep), dep, pinned)) + by_canonical = _group_by_canonical_name(pinned_triples) + + excluded: set[str] = set() + resolved: dict[str, str] = {} + resolved_versions: dict[str, str] = {} + for group_canon, group in by_canonical.items(): + name, dep, version = group[0] + conflicting_versions = { + v for _, _, v in group if not is_same_version(v, version) + } + if conflicting_versions: + warn_conflicting_versions( + "locked dependencies", name, {v for _, _, v in group} + ) + excluded.add(group_canon) + else: + resolved[group_canon] = dep + resolved_versions[group_canon] = version + + deduplicated: list[str] = [] + emitted: set[str] = set() + for dep in locked_dependencies or []: + canon = canonicalize_name(_parse_dep_name(dep)) + if canon in excluded: + continue + if canon in resolved: + if canon in emitted: + continue + emitted.add(canon) + deduplicated.append(resolved[canon]) + continue + # No pinned entry anywhere for this canonical name -- pass + # through as-is, at its own original position. + deduplicated.append(dep) + return deduplicated, resolved_versions + + +def _deduplicated_locked_dependencies( + locked_dependencies: list[str] | None, +) -> list[str]: + """Collapse *locked_dependencies* to one entry per PEP 503-canonicalized + name among its exact-pinned entries, preserving order -- see + :func:`_dedup_and_locked_versions`, which this delegates to. + """ + return _dedup_and_locked_versions(locked_dependencies)[0] + + +def _locked_transitive_only_dependencies( + metadata: ProjectMetadata, + *, + deduplicated_locked: list[str] | None = None, +) -> list[str]: + """Return *metadata*'s locked (e.g. ``poetry.lock``-resolved) dependencies + that aren't already a direct dependency, so a package declared both + directly and in the lock gets one ``dependsOn`` edge, not two. + + Names are compared PEP 503-canonicalized (lowercased, ``-``/``_``/``.`` + folded to ``-``) since a lock file's resolved package names are + normalized while the author's ``pyproject.toml`` spelling (e.g. + ``"Django"``) may not be -- comparing raw, unnormalized names would + treat those as different packages and double-emit the edge this + function exists to avoid. See ``_try_read_poetry()`` in + ``pitloom.extract._pyproject`` for why this is source-stage-only. + + *deduplicated_locked*, when given, is used as-is instead of calling + :func:`_deduplicated_locked_dependencies` again -- :func:`build` computes + both it and the locked version map once via :func:`_dedup_and_locked_versions` + so a genuine name/version conflict in ``locked_dependencies`` only logs + :func:`warn_conflicting_versions`'s warning once per document, not once + per caller. + """ + direct_names = { + canonicalize_name(_parse_dep_name(dep)) for dep in metadata.dependencies + } + locked = ( + deduplicated_locked + if deduplicated_locked is not None + else _deduplicated_locked_dependencies(metadata.locked_dependencies) + ) + return [ + dep + for dep in locked + if canonicalize_name(_parse_dep_name(dep)) not in direct_names + ] + + +# pylint: disable=useless-return +def _locked_dependencies_completeness(metadata: ProjectMetadata) -> str | None: + """Return the `RelationshipCompleteness` value for the locked-only + `dependsOn` edges :func:`_locked_transitive_only_dependencies` + produces, or `None` to leave it unset. + + Conservatively returns ``None`` (unset): while a resolver lock represents + a resolved dependency graph, extractors may legitimately omit + unrepresentable dependencies (such as VCS/path sources, non-default groups, + or marker-ambiguous variants). Asserting ``complete`` would overstate + completeness for partial closures, so leaving it unset makes no + unverifiable claim. + """ + del metadata + return None + + +def _extract_locked_version_map( + locked_dependencies: list[str] | None, +) -> dict[str, str]: + """Map canonical package names to their exact locked version string. + + Enables direct dependencies declared as ranges (e.g. ``requests>=2.0``) + to resolve to their authoritative locked version rather than falling back + to introspecting Pitloom's host environment. See + :func:`_dedup_and_locked_versions`, which this delegates to -- :func:`build` + calls that shared helper directly instead of this function, so a + dependency's pin is parsed once per document, not once per caller. + """ + return _dedup_and_locked_versions(locked_dependencies)[1] + + +def _prefetch_combined_release_info( + dependencies: list[str], + transitive_only: list[str], + locked_versions: dict[str, str] | None = None, +) -> dict[tuple[str, str | None], dict[str, Any] | None]: + """Prefetch PyPI release info once for every dependency a document will + emit -- direct and lock-resolved-transitive alike -- so the result can + be shared across both :func:`add_dependencies` calls in :func:`build` + instead of each call paying for its own network round-trip.""" + name_version_pairs = [] + for dep in dependencies: + dep_name = _parse_dep_name(dep) + locked_ver = ( + locked_versions.get(canonicalize_name(dep_name)) + if locked_versions is not None + else None + ) + dep_version, _version_note = _resolve_version( + dep_name, dep, locked_version=locked_ver, warn=False + ) + name_version_pairs.append((dep_name, dep_version)) + for dep in transitive_only: + dep_name = _parse_dep_name(dep) + dep_version, _version_note = _resolve_version(dep_name, dep, warn=False) + name_version_pairs.append((dep_name, dep_version)) + + return _prefetch_pypi_release_infos(name_version_pairs) diff --git a/src/pitloom/assemble/spdx3/deps_installed.py b/src/pitloom/assemble/spdx3/deps_installed.py index 9e2f1ab9..76e1d50b 100644 --- a/src/pitloom/assemble/spdx3/deps_installed.py +++ b/src/pitloom/assemble/spdx3/deps_installed.py @@ -166,7 +166,7 @@ def _resolve_version( return pinned, None if locked_version is not None: - satisfies = _satisfies_constraint(req, locked_version) + satisfies = warn and _satisfies_constraint(req, locked_version) if warn and satisfies is None: log.warning( "Dependency %r declared as %r couldn't be parsed -- its" diff --git a/src/pitloom/assemble/spdx3/document.py b/src/pitloom/assemble/spdx3/document.py index dd6648f0..cfc196a1 100644 --- a/src/pitloom/assemble/spdx3/document.py +++ b/src/pitloom/assemble/spdx3/document.py @@ -8,10 +8,11 @@ Public entry point / facade: the project-SBOM assembly (:func:`build`) and its two shared helpers (:func:`_build_creation_bundle`, :func:`_build_main_package`) live here; file-element assembly, single-model -assembly, and deployed-environment assembly are split into -:mod:`pitloom.assemble.spdx3._document_files`, -:mod:`pitloom.assemble.spdx3._document_model`, and -:mod:`pitloom.assemble.spdx3._document_deployed` respectively, and +assembly, deployed-environment assembly, and locked-dependency handling are +split into :mod:`pitloom.assemble.spdx3._document_files`, +:mod:`pitloom.assemble.spdx3._document_model`, +:mod:`pitloom.assemble.spdx3._document_deployed`, and +:mod:`pitloom.assemble.spdx3._document_locked_deps` respectively, and re-exported below so every previously-public name is still importable from this module. """ @@ -21,7 +22,6 @@ from datetime import datetime, timezone from typing import Any -from packaging.utils import canonicalize_name from spdx_python_model.bindings import v3_0_1 as spdx3 from pitloom.assemble.spdx3._document_deployed import build_deployed @@ -30,6 +30,14 @@ _emit_file_header_metadata, _magika_version, ) +from pitloom.assemble.spdx3._document_locked_deps import ( + _dedup_and_locked_versions, + _deduplicated_locked_dependencies, + _extract_locked_version_map, + _locked_dependencies_completeness, + _locked_transitive_only_dependencies, + _prefetch_combined_release_info, +) from pitloom.assemble.spdx3._document_model import ( _ai_model_identity, build_enrichment_fragment, @@ -37,15 +45,8 @@ ) from pitloom.assemble.spdx3.ai import add_ai_models from pitloom.assemble.spdx3.creation_info import build_creation_info -from pitloom.assemble.spdx3.deps import ( - _parse_dep_name, - _resolve_version, - add_dependencies, - add_phantom_dependencies, -) -from pitloom.assemble.spdx3.deps_installed import _extract_exact_pin +from pitloom.assemble.spdx3.deps import add_dependencies, add_phantom_dependencies from pitloom.assemble.spdx3.deps_license import attach_main_package_license -from pitloom.assemble.spdx3.deps_pypi import _prefetch_pypi_release_infos from pitloom.assemble.spdx3.provenance import ( ProvenanceEncoder, emit_provenance, @@ -58,18 +59,21 @@ compute_doc_uuid, generate_spdx_id, ) -from pitloom.core.project import ProjectMetadata from pitloom.core.provenance import ProvenanceConfig from pitloom.enrich.base import EnrichmentResult from pitloom.export.spdx3_json import Spdx3JsonExporter, require_spdx_id, sha256_hash -from pitloom.extract._lock_common import is_same_version, warn_conflicting_versions from pitloom.ids import IdRegistry __all__ = [ "_ai_model_identity", "_add_package_files", + "_deduplicated_locked_dependencies", "_emit_file_header_metadata", + "_extract_locked_version_map", + "_locked_dependencies_completeness", + "_locked_transitive_only_dependencies", "_magika_version", + "_prefetch_combined_release_info", "build", "build_deployed", "build_enrichment_fragment", @@ -154,188 +158,6 @@ def _build_main_package( return main_package -def _deduplicated_locked_dependencies( - locked_dependencies: list[str] | None, -) -> list[str]: - """Collapse *locked_dependencies* to one entry per PEP 503-canonicalized - name among its exact-pinned entries, preserving order. - - A canonical name whose *pinned* entries disagree on PEP 440 version is - a genuine conflict (e.g. two lock formats layered by hand into the - same ``ProjectMetadata``, or a future extractor that forgets to - dedupe before returning) -- warned via :func:`warn_conflicting_versions` - and excluded entirely, the same "skip the ambiguous name, don't guess" - policy every extractor already applies to its own duplicate entries. - Neither of this function's two callers (:func:`_extract_locked_version_map`, - :func:`_locked_transitive_only_dependencies`) could otherwise safely - pick a winner between two conflicting entries on its own -- and picking - different winners in each would silently emit the ambiguous package - twice, once per winner, into the assembled SPDX graph. - - An entry with no exact pin at all (unpinned, ranged, or unparseable -- - every shipped extractor always emits an exact pin, but this guards a - future one that doesn't) has no version to compare and passes through - unfiltered: only :func:`_extract_locked_version_map` needs a pin, and - it already discards a pin-less entry on its own via - :func:`_extract_exact_pin`'s own ``None`` return. A passthrough entry - is dropped, though, when its canonical name also has a pinned entry - elsewhere in *locked_dependencies* -- the pin is strictly more - informative, and keeping both would double-emit the same package - (one from the pinned entry, one from the passthrough one). - """ - by_canonical: dict[str, list[tuple[str, str]]] = {} - for dep in locked_dependencies or []: - _req, pinned = _extract_exact_pin(dep) - if pinned is None: - continue - canon = canonicalize_name(_parse_dep_name(dep)) - by_canonical.setdefault(canon, []).append((dep, pinned)) - - excluded: set[str] = set() - resolved: dict[str, str] = {} - for group_canon, group in by_canonical.items(): - dep, version = group[0] - conflicting_versions = {v for _, v in group if not is_same_version(v, version)} - if conflicting_versions: - warn_conflicting_versions( - "locked dependencies", _parse_dep_name(dep), {v for _, v in group} - ) - excluded.add(group_canon) - else: - resolved[group_canon] = dep - - deduplicated: list[str] = [] - emitted: set[str] = set() - for dep in locked_dependencies or []: - canon = canonicalize_name(_parse_dep_name(dep)) - if canon in excluded: - continue - if canon in resolved: - if canon in emitted: - continue - emitted.add(canon) - deduplicated.append(resolved[canon]) - continue - # No pinned entry anywhere for this canonical name -- pass - # through as-is, at its own original position. - deduplicated.append(dep) - return deduplicated - - -def _locked_transitive_only_dependencies( - metadata: ProjectMetadata, - *, - deduplicated_locked: list[str] | None = None, -) -> list[str]: - """Return *metadata*'s locked (e.g. ``poetry.lock``-resolved) dependencies - that aren't already a direct dependency, so a package declared both - directly and in the lock gets one ``dependsOn`` edge, not two. - - Names are compared PEP 503-canonicalized (lowercased, ``-``/``_``/``.`` - folded to ``-``) since a lock file's resolved package names are - normalized while the author's ``pyproject.toml`` spelling (e.g. - ``"Django"``) may not be -- comparing raw, unnormalized names would - treat those as different packages and double-emit the edge this - function exists to avoid. See ``_try_read_poetry()`` in - ``pitloom.extract._pyproject`` for why this is source-stage-only. - - *deduplicated_locked*, when given, is used as-is instead of calling - :func:`_deduplicated_locked_dependencies` again -- :func:`build` computes - it once and shares it with :func:`_extract_locked_version_map` so a - genuine name/version conflict in ``locked_dependencies`` only logs - :func:`warn_conflicting_versions`'s warning once per document, not once - per caller. - """ - direct_names = { - canonicalize_name(_parse_dep_name(dep)) for dep in metadata.dependencies - } - locked = ( - deduplicated_locked - if deduplicated_locked is not None - else _deduplicated_locked_dependencies(metadata.locked_dependencies) - ) - return [ - dep - for dep in locked - if canonicalize_name(_parse_dep_name(dep)) not in direct_names - ] - - -# pylint: disable=useless-return -def _locked_dependencies_completeness(metadata: ProjectMetadata) -> str | None: - """Return the `RelationshipCompleteness` value for the locked-only - `dependsOn` edges :func:`_locked_transitive_only_dependencies` - produces, or `None` to leave it unset. - - Conservatively returns ``None`` (unset): while a resolver lock represents - a resolved dependency graph, extractors may legitimately omit - unrepresentable dependencies (such as VCS/path sources, non-default groups, - or marker-ambiguous variants). Asserting ``complete`` would overstate - completeness for partial closures, so leaving it unset makes no - unverifiable claim. - """ - del metadata - return None - - -def _extract_locked_version_map( - locked_dependencies: list[str] | None, - *, - deduplicated: list[str] | None = None, -) -> dict[str, str]: - """Map canonical package names to their exact locked version string. - - Enables direct dependencies declared as ranges (e.g. ``requests>=2.0``) - to resolve to their authoritative locked version rather than falling back - to introspecting Pitloom's host environment. - - *deduplicated*, when given, is used as-is instead of calling - :func:`_deduplicated_locked_dependencies` again -- see - :func:`_locked_transitive_only_dependencies`'s matching parameter for why. - """ - result: dict[str, str] = {} - locked = ( - deduplicated - if deduplicated is not None - else _deduplicated_locked_dependencies(locked_dependencies) - ) - for dep in locked: - dep_name = _parse_dep_name(dep) - _req, pinned = _extract_exact_pin(dep) - if pinned is not None: - result[canonicalize_name(dep_name)] = pinned - return result - - -def _prefetch_combined_release_info( - dependencies: list[str], - transitive_only: list[str], - locked_versions: dict[str, str] | None = None, -) -> dict[tuple[str, str | None], dict[str, Any] | None]: - """Prefetch PyPI release info once for every dependency a document will - emit -- direct and lock-resolved-transitive alike -- so the result can - be shared across both :func:`add_dependencies` calls in :func:`build` - instead of each call paying for its own network round-trip.""" - name_version_pairs = [] - for dep in dependencies: - dep_name = _parse_dep_name(dep) - locked_ver = ( - locked_versions.get(canonicalize_name(dep_name)) - if locked_versions is not None - else None - ) - dep_version, _version_note = _resolve_version( - dep_name, dep, locked_version=locked_ver, warn=False - ) - name_version_pairs.append((dep_name, dep_version)) - for dep in transitive_only: - dep_name = _parse_dep_name(dep) - dep_version, _version_note = _resolve_version(dep_name, dep, warn=False) - name_version_pairs.append((dep_name, dep_version)) - - return _prefetch_pypi_release_infos(name_version_pairs) - - # pylint: disable=too-many-locals # pylint: disable-next=too-many-arguments def build( @@ -436,16 +258,14 @@ def build( # --- Locked (e.g. poetry.lock-resolved) transitive-only dependencies --- # Deduplicated once and shared below: a genuine name/version conflict # in metadata.locked_dependencies must warn exactly once per document, - # not once per function that would otherwise recompute it. - deduplicated_locked = _deduplicated_locked_dependencies( + # not once per function that would otherwise recompute it, and each + # entry's pin is parsed once, not once per consumer. + deduplicated_locked, locked_versions = _dedup_and_locked_versions( metadata.locked_dependencies ) transitive_only = _locked_transitive_only_dependencies( metadata, deduplicated_locked=deduplicated_locked ) - locked_versions = _extract_locked_version_map( - metadata.locked_dependencies, deduplicated=deduplicated_locked - ) release_info_cache = ( None if offline diff --git a/src/pitloom/extract/_extract_utils.py b/src/pitloom/extract/_extract_utils.py index 22a14b58..0543949c 100644 --- a/src/pitloom/extract/_extract_utils.py +++ b/src/pitloom/extract/_extract_utils.py @@ -75,6 +75,28 @@ def record_dict_field_provenance( ) +def field_declared(container: Any, key: str) -> bool: + """Return whether *key* is present in *container*, never the resolved + value's truthiness. + + The one canonical presence check for the provenance-gating pattern + documented in AGENTS.md's "Recurring bug patterns": a metadata + producer must record provenance for a container field (``keywords``, + ``dependencies``, ``authors``, ...) based on whether its raw source + key was declared at all, not on whether the parsed value is truthy -- + ``dependencies = []`` is a declared, authoritative empty list, not an + absent field. A bare ``key in container`` is enough for a plain + ``dict``; some sources (e.g. Hatchling's ``core.config``) can raise + ``OSError`` from the same underlying access their property accessors + do, so that failure is treated as "not declared" rather than + propagating. + """ + try: + return key in container + except OSError: + return False + + def get_first(d: dict[str, Any], *keys: str) -> Any: """Return the value for the first matching key in *d*, or ``None``.""" for k in keys: diff --git a/src/pitloom/extract/_lock_common.py b/src/pitloom/extract/_lock_common.py index de9ab8c7..42663ea6 100644 --- a/src/pitloom/extract/_lock_common.py +++ b/src/pitloom/extract/_lock_common.py @@ -14,6 +14,10 @@ names, its own group/source-key conventions) stays in that format's own module; only what's shared across two or more formats -- loading the lock file, grouping entries by name, judging a specifier -- lives here. + +See also: :mod:`pitloom.extract._lock_common_warnings` for the shared +``WARNING:`` message helpers, split out to keep this module under this +repo's file-size soft limit and re-exported below. """ from __future__ import annotations @@ -28,6 +32,15 @@ from packaging.utils import canonicalize_name from packaging.version import InvalidVersion, Version +from pitloom.extract._lock_common_warnings import ( + warn_conflicting_versions, + warn_malformed_entry_not_table, + warn_missing_name, + warn_missing_version, + warn_non_registry_source, + warn_not_genuine_lock_file, + warn_top_level_key_wrong_type, +) from pitloom.extract._toml_io import TOMLDecodeError, load_toml_file log = logging.getLogger(__name__) @@ -342,113 +355,6 @@ def single_exact_pin(specifier_set: SpecifierSet) -> tuple[str, str] | None: return specifiers[0].operator, specifiers[0].version -def warn_conflicting_versions( - lock_file: str, name: str, conflicting_versions: Iterable[str] -) -> None: - """Log the standard ``WARNING:`` when multiple variants of a package disagree - on version in a lock file.""" - log.warning( - "Skipping %s entry %r: pinned to conflicting versions (%s)", - lock_file, - name, - ", ".join(sorted(conflicting_versions)), - ) - - -def warn_not_genuine_lock_file( - lock_path: Path, - table_key: str, - required_key: str, - lock_file: str, - container_type: str = "table", -) -> None: - """Log the standard ``WARNING:`` when a lock file lacks required top-level - metadata.""" - log.warning( - "%s: no top-level %r %s with a %r key -- " - "doesn't look like a genuine %s, ignoring", - lock_path, - table_key, - container_type, - required_key, - lock_file, - ) - - -def warn_non_registry_source(lock_file: str, name: str, source_key: str) -> None: - """Log the standard ``WARNING:`` for a non-registry-sourced entry - (VCS, local path, archive/URL -- anything a bare ``name==version`` - pin can't represent), naming *lock_file* (e.g. ``"uv.lock"``), - *name* (the package), and *source_key* (which non-registry marker - was found). Shared by every extractor that has a non-registry-source - concept (`_poetry_lock.py`, `_pylock.py`, `_uv_lock.py`, - `_pdm_lock.py`, `_pipfile_lock.py`) so the wording stays identical - across formats. - """ - log.warning( - "Skipping %s entry %r: %s-sourced dependencies cannot be " - "represented as a PEP 508 specifier", - lock_file, - name, - source_key, - ) - - -def warn_top_level_key_wrong_type( - lock_path: Path, key: str, value: object, expected: str, lock_file: str -) -> None: - """Log the shared ``": top-level '' key is , - expected -- ignoring "`` warning for a top-level - lock-file key of the wrong shape (a ``packages``/``package`` key - that isn't a list, a ``default`` key that isn't a table) -- shared - across formats the same way :func:`warn_non_registry_source` is - shared for the non-registry-source case. - """ - log.warning( - "%s: top-level '%s' key is %s, expected %s -- ignoring %s", - lock_path, - key, - type(value).__name__, - expected, - lock_file, - ) - - -def warn_missing_version(lock_file: str, name: str) -> None: - """Log the shared ``"Skipping entry '': missing or - non-string 'version'"`` warning -- identical across every format - that validates its ``version`` field via :func:`is_usable_version`.""" - log.warning( - "Skipping %s entry %r: missing or non-string 'version'", - lock_file, - name, - ) - - -def warn_malformed_entry_not_table( - lock_file: str, entry_label: str, value: object -) -> None: - """Log the shared ``"Skipping malformed - entry: expected a table, got "`` warning for a top-level - ``[[package]]``/``[[packages]]``-style entry that isn't a table -- - shared across every format with this malformed-entry shape.""" - log.warning( - "Skipping malformed %s %s entry: expected a table, got %s", - lock_file, - entry_label, - type(value).__name__, - ) - - -def warn_missing_name(context: str, name: object) -> None: - """Log the shared ``": missing or non-string 'name' - (name=)"`` warning tail -- *context* supplies each call site's - own lead-in (which format, which kind of entry) since that part - genuinely differs per site, while the recurring "missing or - non-string 'name'" wording itself doesn't.""" - log.warning("%s: missing or non-string 'name' (name=%r)", context, name) - - def find_first_present_key( mapping: Mapping[str, object], keys: Iterable[str] ) -> str | None: diff --git a/src/pitloom/extract/_lock_common_warnings.py b/src/pitloom/extract/_lock_common_warnings.py new file mode 100644 index 00000000..03491061 --- /dev/null +++ b/src/pitloom/extract/_lock_common_warnings.py @@ -0,0 +1,141 @@ +# SPDX-FileContributor: Arthit Suriyawongkul +# SPDX-FileCopyrightText: 2026-present Arthit Suriyawongkul +# SPDX-FileType: SOURCE +# SPDX-License-Identifier: Apache-2.0 + +"""Shared ``WARNING:`` message helpers for lock/pin file extractors. + +Split out of :mod:`pitloom.extract._lock_common` (which re-exports every +name here) to keep that module under this repo's file-size soft limit. +Every extractor's own malformed-entry/non-registry-source/missing-field +warning is worded identically by routing through one of these instead of +hand-rolling a similarly-worded message per format -- see AGENTS.md's +"Recurring bug patterns" for why wording drift across siblings is worth +avoiding. +""" + +from __future__ import annotations + +import logging +from collections.abc import Iterable +from pathlib import Path + +log = logging.getLogger(__name__) + +__all__ = [ + "warn_conflicting_versions", + "warn_malformed_entry_not_table", + "warn_missing_name", + "warn_missing_version", + "warn_non_registry_source", + "warn_not_genuine_lock_file", + "warn_top_level_key_wrong_type", +] + + +def warn_conflicting_versions( + lock_file: str, name: str, conflicting_versions: Iterable[str] +) -> None: + """Log the standard ``WARNING:`` when multiple variants of a package disagree + on version in a lock file.""" + log.warning( + "Skipping %s entry %r: pinned to conflicting versions (%s)", + lock_file, + name, + ", ".join(sorted(conflicting_versions)), + ) + + +def warn_not_genuine_lock_file( + lock_path: Path, + table_key: str, + required_key: str, + lock_file: str, + container_type: str = "table", +) -> None: + """Log the standard ``WARNING:`` when a lock file lacks required top-level + metadata.""" + log.warning( + "%s: no top-level %r %s with a %r key -- " + "doesn't look like a genuine %s, ignoring", + lock_path, + table_key, + container_type, + required_key, + lock_file, + ) + + +def warn_non_registry_source(lock_file: str, name: str, source_key: str) -> None: + """Log the standard ``WARNING:`` for a non-registry-sourced entry + (VCS, local path, archive/URL -- anything a bare ``name==version`` + pin can't represent), naming *lock_file* (e.g. ``"uv.lock"``), + *name* (the package), and *source_key* (which non-registry marker + was found). Shared by every extractor that has a non-registry-source + concept (`_poetry_lock.py`, `_pylock.py`, `_uv_lock.py`, + `_pdm_lock.py`, `_pipfile_lock.py`) so the wording stays identical + across formats. + """ + log.warning( + "Skipping %s entry %r: %s-sourced dependencies cannot be " + "represented as a PEP 508 specifier", + lock_file, + name, + source_key, + ) + + +def warn_top_level_key_wrong_type( + lock_path: Path, key: str, value: object, expected: str, lock_file: str +) -> None: + """Log the shared ``": top-level '' key is , + expected -- ignoring "`` warning for a top-level + lock-file key of the wrong shape (a ``packages``/``package`` key + that isn't a list, a ``default`` key that isn't a table) -- shared + across formats the same way :func:`warn_non_registry_source` is + shared for the non-registry-source case. + """ + log.warning( + "%s: top-level '%s' key is %s, expected %s -- ignoring %s", + lock_path, + key, + type(value).__name__, + expected, + lock_file, + ) + + +def warn_missing_version(lock_file: str, name: str) -> None: + """Log the shared ``"Skipping entry '': missing or + non-string 'version'"`` warning -- identical across every format + that validates its ``version`` field via + :func:`pitloom.extract._lock_common.is_usable_version`.""" + log.warning( + "Skipping %s entry %r: missing or non-string 'version'", + lock_file, + name, + ) + + +def warn_malformed_entry_not_table( + lock_file: str, entry_label: str, value: object +) -> None: + """Log the shared ``"Skipping malformed + entry: expected a table, got "`` warning for a top-level + ``[[package]]``/``[[packages]]``-style entry that isn't a table -- + shared across every format with this malformed-entry shape.""" + log.warning( + "Skipping malformed %s %s entry: expected a table, got %s", + lock_file, + entry_label, + type(value).__name__, + ) + + +def warn_missing_name(context: str, name: object) -> None: + """Log the shared ``": missing or non-string 'name' + (name=)"`` warning tail -- *context* supplies each call site's + own lead-in (which format, which kind of entry) since that part + genuinely differs per site, while the recurring "missing or + non-string 'name'" wording itself doesn't.""" + log.warning("%s: missing or non-string 'name' (name=%r)", context, name) diff --git a/src/pitloom/extract/_poetry.py b/src/pitloom/extract/_poetry.py index fc07d1b4..e211131f 100644 --- a/src/pitloom/extract/_poetry.py +++ b/src/pitloom/extract/_poetry.py @@ -63,6 +63,7 @@ from typing import Any from pitloom.core.project import ProjectMetadata +from pitloom.extract._extract_utils import field_declared from pitloom.extract._license import ( detect_license_for_project, resolve_license_concluded, @@ -140,18 +141,21 @@ def extract_poetry_metadata( # genuine, authoritative "zero" that merge_project_metadata() must not # silently fill in from a lower-priority source, the same None-vs-[] # distinction _pyproject.py's [project]-table path already applies. - if "authors" in poetry: + if field_declared(poetry, "authors"): prov["authors"] = "Source: pyproject.toml | Field: tool.poetry.authors" if authors: prov["copyright_text"] = ( "Source: Pitloom generator | Method: inferred_from_authors" ) - if any(key in poetry for key in ("homepage", "repository", "documentation")): + if any( + field_declared(poetry, key) + for key in ("homepage", "repository", "documentation") + ): prov["urls"] = ( "Source: pyproject.toml" " | Field: tool.poetry.homepage/repository/documentation" ) - if "dependencies" in poetry: + if field_declared(poetry, "dependencies"): prov["dependencies"] = ( "Source: pyproject.toml | Field: tool.poetry.dependencies" ) @@ -167,7 +171,7 @@ def extract_poetry_metadata( prov["requires_python"] = ( "Source: pyproject.toml | Field: tool.poetry.dependencies.python" ) - if "keywords" in poetry: + if field_declared(poetry, "keywords"): prov["keywords"] = "Source: pyproject.toml | Field: tool.poetry.keywords" return ProjectMetadata( diff --git a/src/pitloom/extract/_pylock.py b/src/pitloom/extract/_pylock.py index 38ccc848..3bbf23c3 100644 --- a/src/pitloom/extract/_pylock.py +++ b/src/pitloom/extract/_pylock.py @@ -405,6 +405,16 @@ def _pinned_pair_for_package( :func:`_group_marker_excludes`), so two entries for the same package gated on different, unevaluated ``python_version``/``sys_platform`` markers can both survive to this point. + + A marker-excluded entry that's *also* non-registry-sourced is dropped + silently, with no ``non-registry source`` warning: it's excluded + either way, and warning about a source type that's about to be + discarded regardless would be noise a maintainer can't act on -- + mirrors ``poetry.lock``'s/``pdm.lock``'s equivalents, which check + group membership before the source type for the same reason. A + malformed ``version`` is still reported even for a marker-excluded + entry, though: unlike the source-type check, that's a data-quality + problem in the lock file itself, not a consequence of exclusion. """ if not isinstance(pkg, dict): warn_malformed_entry_not_table("pylock.toml", "[[packages]]", pkg) @@ -413,15 +423,17 @@ def _pinned_pair_for_package( if not isinstance(name, str) or not name.strip(): warn_missing_name("Skipping malformed pylock.toml [[packages]] entry", name) return None + marker = pkg.get("marker") + marker_excluded = marker is not None and _is_marker_excluded( + marker, environment, name + ) non_registry_source = find_first_present_key(pkg, _NON_REGISTRY_SOURCE_KEYS) if non_registry_source is not None: - warn_non_registry_source("pylock.toml", name, non_registry_source) + if not marker_excluded: + warn_non_registry_source("pylock.toml", name, non_registry_source) return None version = pkg.get("version") if not is_usable_version(version): warn_missing_version("pylock.toml", name) return None - marker = pkg.get("marker") - if marker is not None and _is_marker_excluded(marker, environment, name): - return None - return name, version + return None if marker_excluded else (name, version) diff --git a/src/pitloom/extract/_pyproject.py b/src/pitloom/extract/_pyproject.py index a46fa036..3a966c5c 100644 --- a/src/pitloom/extract/_pyproject.py +++ b/src/pitloom/extract/_pyproject.py @@ -25,6 +25,7 @@ from pitloom.core.config import PitloomConfig, parse_pitloom_config from pitloom.core.models import normalize_dependency_specifier from pitloom.core.project import ProjectMetadata, merge_project_metadata +from pitloom.extract._extract_utils import field_declared from pitloom.extract._license import ( _looks_like_spdx_license_expression, _looks_like_spdx_license_id, @@ -239,7 +240,7 @@ def read_pyproject( # must not silently fill in from a lower-priority source, the same # None-vs-[] distinction _build_provenance() already applies to its # own fields. - if "license-files" in project_data: + if field_declared(project_data, "license-files"): provenance["license_files"] = ( "Source: pyproject.toml | Field: project.license-files" ) @@ -298,7 +299,7 @@ def _build_provenance( } if version_source: prov["version"] = version_source - elif "version" in project_data: + elif field_declared(project_data, "version"): prov["version"] = "Source: pyproject.toml | Field: project.version" for field_key, source in _FIELD_PROVENANCE.items(): @@ -310,9 +311,9 @@ def _build_provenance( # fallback is needed for that case. if license_prov_override: prov["license"] = license_prov_override - elif field_key in project_data: + elif field_declared(project_data, field_key): prov["license"] = source - elif field_key in project_data: + elif field_declared(project_data, field_key): prov[field_key] = source if project_data.get("authors"): diff --git a/src/pitloom/extract/_setuptools_py.py b/src/pitloom/extract/_setuptools_py.py index 6f8e6b6e..0f253623 100644 --- a/src/pitloom/extract/_setuptools_py.py +++ b/src/pitloom/extract/_setuptools_py.py @@ -18,6 +18,7 @@ from pitloom.core.config import PitloomConfig from pitloom.core.project import ProjectMetadata +from pitloom.extract._extract_utils import field_declared def iter_setup_calls(tree: ast.AST) -> Iterator[ast.Call]: @@ -225,12 +226,14 @@ def read_setup_py( has_description=bool(description), has_readme=bool(readme), has_license=bool(license_name), - has_authors="author" in kwargs or "author_email" in kwargs, + has_authors=field_declared(kwargs, "author") + or field_declared(kwargs, "author_email"), authors=authors, - has_urls="url" in kwargs or "project_urls" in kwargs, - has_dependencies="install_requires" in kwargs, + has_urls=field_declared(kwargs, "url") + or field_declared(kwargs, "project_urls"), + has_dependencies=field_declared(kwargs, "install_requires"), has_requires_python=bool(requires_python), - has_keywords="keywords" in kwargs, + has_keywords=field_declared(kwargs, "keywords"), ) project_metadata = ProjectMetadata( diff --git a/src/pitloom/extract/hatchling.py b/src/pitloom/extract/hatchling.py index 18080592..c002b6e9 100644 --- a/src/pitloom/extract/hatchling.py +++ b/src/pitloom/extract/hatchling.py @@ -23,6 +23,7 @@ from pitloom.core.models import normalize_dependency_specifier from pitloom.core.project import ProjectMetadata, merge_project_metadata +from pitloom.extract._extract_utils import field_declared from pitloom.extract._license import ( detect_license_for_project, resolve_license_concluded, @@ -100,12 +101,15 @@ def _hatchling_field_declared(core: Any, project_key: str) -> bool: genuine, authoritative zero) from "not declared at all" (fall back to a lower-priority source in :func:`pitloom.core.project.merge_project_metadata`). ``core.config`` access can raise ``OSError`` the same way the - property accessors it backs can (see :func:`_resolve_hatchling_readme`). + property accessors it backs can (see :func:`_resolve_hatchling_readme`), + so the lookup goes through :func:`field_declared` rather than a bare + ``in`` check. """ try: - return project_key in core.config + config = core.config except OSError: return False + return field_declared(config, project_key) def _resolve_hatchling_readme(core: Any) -> str | None: diff --git a/tests/assemble/test_deps_enrichment_prefetch.py b/tests/assemble/test_deps_enrichment_prefetch.py index 849028c5..2a97dc3a 100644 --- a/tests/assemble/test_deps_enrichment_prefetch.py +++ b/tests/assemble/test_deps_enrichment_prefetch.py @@ -381,7 +381,7 @@ def _mock_prefetch(pairs: Any) -> dict[Any, Any]: return {} monkeypatch.setattr( - "pitloom.assemble.spdx3.document._prefetch_pypi_release_infos", + "pitloom.assemble.spdx3._document_locked_deps._prefetch_pypi_release_infos", _mock_prefetch, ) _prefetch_combined_release_info( diff --git a/tests/assemble/test_deps_resolution_pins.py b/tests/assemble/test_deps_resolution_pins.py index 0a3814e8..5c80251d 100644 --- a/tests/assemble/test_deps_resolution_pins.py +++ b/tests/assemble/test_deps_resolution_pins.py @@ -288,7 +288,7 @@ def test_prefetch_suppresses_conflict_warnings( """During online prefetch, version resolution must not duplicate conflict warnings that the later dependency emission pass will log.""" monkeypatch.setattr( - "pitloom.assemble.spdx3.document._prefetch_pypi_release_infos", + "pitloom.assemble.spdx3._document_locked_deps._prefetch_pypi_release_infos", lambda pairs: {}, ) diff --git a/tests/extract/test_hatch_hook_metadata.py b/tests/extract/test_hatch_hook_metadata.py index 0c2b4638..ba2b90e7 100644 --- a/tests/extract/test_hatch_hook_metadata.py +++ b/tests/extract/test_hatch_hook_metadata.py @@ -65,6 +65,18 @@ def test_metadata_from_hatchling_maps_license_files() -> None: ) +def test_metadata_from_hatchling_empty_declared_dependencies_gets_provenance() -> None: + """An explicitly declared but empty ``[project.dependencies]`` must + still record provenance -- merge_project_metadata() relies on that + presence to treat the empty list as authoritative, not absent. Guards + _hatchling_field_declared() against regressing to a truthiness check + (``if dependencies:``) on the resolved list.""" + hatch_meta = _fake_hatch_metadata(core={"dependencies": []}) + metadata = metadata_from_hatchling(hatch_meta, Path(".")) + assert metadata.dependencies == [] + assert "dependencies" in metadata.provenance + + def test_metadata_from_hatchling_no_license_files() -> None: """Absent ``[project.license-files]`` must resolve to an empty list, not ``None`` or a missing field.""" diff --git a/tests/extract/test_pylock.py b/tests/extract/test_pylock.py index 9e7ee6f7..bc14000a 100644 --- a/tests/extract/test_pylock.py +++ b/tests/extract/test_pylock.py @@ -397,6 +397,32 @@ def test_non_registry_sourced_package_excluded( assert "cannot be represented as a PEP 508 specifier" in caplog.text +@pytest.mark.parametrize("source_key", ["vcs", "directory", "archive"]) +def test_marker_excluded_non_registry_sourced_package_is_silent( + source_key: str, caplog: pytest.LogCaptureFixture +) -> None: + """A package that is both marker-excluded (e.g. dev-only) and + non-registry-sourced must be dropped with no warning at all -- it's + excluded either way, and warning about a source type that's being + discarded regardless is noise, matching poetry.lock's/pdm.lock's + equivalents, which check group membership before source type.""" + with tempfile.TemporaryDirectory() as tmp: + tmp_path = Path(tmp) + _write_lock( + tmp_path, + 'default-groups = ["default"]\n' + '[[packages]]\nname = "dev-only-vcs"\n' + "marker = \"'dev' in dependency_groups\"\n" + f'[packages.{source_key}]\nurl = "https://example.com"\n', + ) + + with caplog.at_level(logging.WARNING): + result = extract_pylock_dependencies(tmp_path) + + assert not result + assert caplog.text == "" + + def test_sdist_sourced_package_included() -> None: with tempfile.TemporaryDirectory() as tmp: tmp_path = Path(tmp) From b0b9bba4973d134a5beb0b475a5b78312aa28ef7 Mon Sep 17 00:00:00 2001 From: Arthit Suriyawongkul Date: Tue, 8 Sep 2026 16:38:18 +0700 Subject: [PATCH 29/35] Fix consistency / function boundary Signed-off-by: Arthit Suriyawongkul --- .../assemble/spdx3/_document_locked_deps.py | 40 ++++++++++++++----- src/pitloom/assemble/spdx3/deps_installed.py | 37 ++++++++--------- src/pitloom/extract/_lock_common.py | 21 ++++++---- src/pitloom/extract/_pylock.py | 8 +++- src/pitloom/extract/_setuptools_py.py | 39 +++++++++++++----- tests/extract/test_setuptools_py.py | 35 ++++++++++++++++ 6 files changed, 132 insertions(+), 48 deletions(-) diff --git a/src/pitloom/assemble/spdx3/_document_locked_deps.py b/src/pitloom/assemble/spdx3/_document_locked_deps.py index 2c6b59b1..bf5aa005 100644 --- a/src/pitloom/assemble/spdx3/_document_locked_deps.py +++ b/src/pitloom/assemble/spdx3/_document_locked_deps.py @@ -26,19 +26,45 @@ from pitloom.assemble.spdx3.deps_pypi import _prefetch_pypi_release_infos from pitloom.core.project import ProjectMetadata from pitloom.extract._lock_common import ( - _group_by_canonical_name, + group_by_canonical_name, is_same_version, warn_conflicting_versions, ) +def _canon_names_and_pins( + locked_dependencies: list[str] | None, +) -> tuple[dict[str, str], list[tuple[str, str, str]]]: + """Parse every entry in *locked_dependencies* exactly once, returning + each dep string's PEP 503-canonicalized name (``canon_by_dep``, + keyed by the original dep string) alongside the ``(name, dep, pinned)`` + triples for entries that carry an exact pin. + + Split out of :func:`_dedup_and_locked_versions` only to keep that + function's local-variable count under this repo's complexity ceiling. + """ + canon_by_dep: dict[str, str] = {} + pinned_triples: list[tuple[str, str, str]] = [] + for dep in locked_dependencies or []: + req, pinned = _extract_exact_pin(dep) + # req.name is already resolved by _extract_exact_pin's own parse -- + # only fall back to re-parsing via _parse_dep_name for a dep string + # that Requirement() itself couldn't parse (req is None). + name = req.name if req is not None else _parse_dep_name(dep) + canon_by_dep[dep] = canonicalize_name(name) + if pinned is not None: + pinned_triples.append((name, dep, pinned)) + return canon_by_dep, pinned_triples + + def _dedup_and_locked_versions( locked_dependencies: list[str] | None, ) -> tuple[list[str], dict[str, str]]: """Collapse *locked_dependencies* to one entry per PEP 503-canonicalized name among its exact-pinned entries (preserving order), and map each surviving canonical name to its pinned version -- computed together so - :func:`_extract_exact_pin` parses each entry's pin only once. + each entry is parsed via :func:`_extract_exact_pin` only once, and its + canonical name derived from that same parse rather than re-parsed. A canonical name whose *pinned* entries disagree on PEP 440 version is a genuine conflict (e.g. two lock formats layered by hand into the @@ -64,12 +90,8 @@ def _dedup_and_locked_versions( more informative, and keeping both would double-emit the same package (one from the pinned entry, one from the passthrough one). """ - pinned_triples: list[tuple[str, str, str]] = [] - for dep in locked_dependencies or []: - _req, pinned = _extract_exact_pin(dep) - if pinned is not None: - pinned_triples.append((_parse_dep_name(dep), dep, pinned)) - by_canonical = _group_by_canonical_name(pinned_triples) + canon_by_dep, pinned_triples = _canon_names_and_pins(locked_dependencies) + by_canonical = group_by_canonical_name(pinned_triples) excluded: set[str] = set() resolved: dict[str, str] = {} @@ -91,7 +113,7 @@ def _dedup_and_locked_versions( deduplicated: list[str] = [] emitted: set[str] = set() for dep in locked_dependencies or []: - canon = canonicalize_name(_parse_dep_name(dep)) + canon = canon_by_dep[dep] if canon in excluded: continue if canon in resolved: diff --git a/src/pitloom/assemble/spdx3/deps_installed.py b/src/pitloom/assemble/spdx3/deps_installed.py index 76e1d50b..ea8c7af2 100644 --- a/src/pitloom/assemble/spdx3/deps_installed.py +++ b/src/pitloom/assemble/spdx3/deps_installed.py @@ -166,24 +166,25 @@ def _resolve_version( return pinned, None if locked_version is not None: - satisfies = warn and _satisfies_constraint(req, locked_version) - if warn and satisfies is None: - log.warning( - "Dependency %r declared as %r couldn't be parsed -- its" - " constraint (if any) can't be verified against locked" - " version %r, using it anyway", - dep_name, - dep, - locked_version, - ) - elif warn and not satisfies: - log.warning( - "Locked version %r for dependency %r does not satisfy declared" - " constraint %r -- using locked version", - locked_version, - dep_name, - dep, - ) + if warn: + satisfies = _satisfies_constraint(req, locked_version) + if satisfies is None: + log.warning( + "Dependency %r declared as %r couldn't be parsed -- its" + " constraint (if any) can't be verified against locked" + " version %r, using it anyway", + dep_name, + dep, + locked_version, + ) + elif not satisfies: + log.warning( + "Locked version %r for dependency %r does not satisfy declared" + " constraint %r -- using locked version", + locked_version, + dep_name, + dep, + ) return locked_version, "Version resolved: Project lock file" try: diff --git a/src/pitloom/extract/_lock_common.py b/src/pitloom/extract/_lock_common.py index 42663ea6..882ff82e 100644 --- a/src/pitloom/extract/_lock_common.py +++ b/src/pitloom/extract/_lock_common.py @@ -49,6 +49,7 @@ "POETRY_LOCK_SOURCE_NAME", "default_group_included", "find_first_present_key", + "group_by_canonical_name", "group_pin_triples_by_canonical_name", "group_versions_by_canonical_name", "has_required_top_level_table", @@ -242,7 +243,7 @@ def is_usable_version(version: object) -> TypeGuard[str]: _CanonicalGroupT = TypeVar("_CanonicalGroupT", bound=tuple[str, ...]) -def _group_by_canonical_name( +def group_by_canonical_name( items: Iterable[_CanonicalGroupT], ) -> dict[str, list[_CanonicalGroupT]]: """Group tuples by PEP 503-canonicalized *name* (each tuple's first @@ -255,9 +256,13 @@ def _group_by_canonical_name( check silently never fires for a mixed-case duplicate. Generic over tuple arity so :func:`group_versions_by_canonical_name`'s - ``(name, version)`` pairs and :func:`group_pin_triples_by_canonical_name`'s - ``(name, operator, version)`` triples share one implementation instead - of two copies of the same loop. + ``(name, version)`` pairs, :func:`group_pin_triples_by_canonical_name`'s + ``(name, operator, version)`` triples, and any other caller's own + ``(name, ...)`` tuple shape share one implementation instead of a + per-caller copy of the same loop -- public (no leading underscore) + since :mod:`pitloom.assemble.spdx3._document_locked_deps` groups a + third, differently-shaped ``(name, dep, pinned)`` triple that fits + neither typed wrapper below. """ by_canonical: dict[str, list[_CanonicalGroupT]] = {} for item in items: @@ -269,7 +274,7 @@ def group_versions_by_canonical_name( pairs: Iterable[tuple[str, str]], ) -> dict[str, list[tuple[str, str]]]: """Group ``(name, version)`` pairs by PEP 503-canonicalized *name* -- - see :func:`_group_by_canonical_name`. + see :func:`group_by_canonical_name`. A caller decides what a multi-entry group means for its own format: :mod:`pitloom.extract._pdm_lock` collapses a group to one entry when @@ -278,14 +283,14 @@ def group_versions_by_canonical_name( for the sibling used where the pin's operator (``==`` vs ``===``) also needs to survive grouping. """ - return _group_by_canonical_name(pairs) + return group_by_canonical_name(pairs) def group_pin_triples_by_canonical_name( triples: Iterable[tuple[str, str, str]], ) -> dict[str, list[tuple[str, str, str]]]: """Group ``(name, operator, version)`` pins by PEP 503-canonicalized - *name* -- see :func:`_group_by_canonical_name`. The ``===``-aware + *name* -- see :func:`group_by_canonical_name`. The ``===``-aware sibling of :func:`group_versions_by_canonical_name`, for :mod:`pitloom.extract._pipfile_lock` and :mod:`pitloom.extract._requirements_txt`, whose ``version`` field is @@ -299,7 +304,7 @@ def group_pin_triples_by_canonical_name( whole file, since it has no per-format definition of "expected duplication" the way an extra-variant lock entry does. """ - return _group_by_canonical_name(triples) + return group_by_canonical_name(triples) #: PEP 440 operators that pin to exactly one release: ``==`` (the diff --git a/src/pitloom/extract/_pylock.py b/src/pitloom/extract/_pylock.py index 3bbf23c3..e97cb0cf 100644 --- a/src/pitloom/extract/_pylock.py +++ b/src/pitloom/extract/_pylock.py @@ -413,8 +413,12 @@ def _pinned_pair_for_package( mirrors ``poetry.lock``'s/``pdm.lock``'s equivalents, which check group membership before the source type for the same reason. A malformed ``version`` is still reported even for a marker-excluded - entry, though: unlike the source-type check, that's a data-quality - problem in the lock file itself, not a consequence of exclusion. + entry that has *no* non-registry source, though: unlike the + source-type check, that's a data-quality problem in the lock file + itself, not a consequence of exclusion. When a non-registry source + *is* present, the version is never checked at all -- a non-registry + pin has no meaningful version regardless, so there's nothing to + validate. """ if not isinstance(pkg, dict): warn_malformed_entry_not_table("pylock.toml", "[[packages]]", pkg) diff --git a/src/pitloom/extract/_setuptools_py.py b/src/pitloom/extract/_setuptools_py.py index 0f253623..c8fef877 100644 --- a/src/pitloom/extract/_setuptools_py.py +++ b/src/pitloom/extract/_setuptools_py.py @@ -39,11 +39,22 @@ def iter_setup_calls(tree: ast.AST) -> Iterator[ast.Call]: yield node +#: Sentinel for "not a resolvable literal" (a variable, function call, +#: f-string, ...), distinct from a genuine literal ``None`` constant +#: (``Constant(value=None)``) -- conflating the two would make a real +#: ``[None]`` list element indistinguishable from an unresolvable one, and +#: (via :func:`_extract_setup_kwargs`) would make a kwarg whose value +#: couldn't be resolved indistinguishable from a kwarg never written at +#: all. +_UNRESOLVABLE = object() + + def _ast_literal(node: ast.expr) -> Any: """Extract a Python literal value from an AST expression. - Returns ``None`` for non-literal expressions (variables, function calls, - f-strings, etc.) rather than raising. + Returns :data:`_UNRESOLVABLE` for non-literal expressions (variables, + function calls, f-strings, etc.) rather than raising -- never ``None`` + for that case, so a genuine literal ``None`` stays distinguishable. """ if isinstance(node, ast.Constant): return node.value @@ -53,10 +64,11 @@ def _ast_literal(node: ast.expr) -> Any: # `install_requires=[SOME_CONSTANT]` as the literal empty list # `[]` -- a "no dependencies" claim indistinguishable from a # genuinely empty `install_requires=[]`, which downstream - # presence-based provenance treats as authoritative. `None` here - # correctly propagates as "not a resolvable literal" instead. + # presence-based provenance treats as authoritative. + # `_UNRESOLVABLE` here correctly propagates "not a resolvable + # literal" instead, without colliding with a real `None` element. values = [_ast_literal(elt) for elt in node.elts] - return None if any(v is None for v in values) else values + return _UNRESOLVABLE if any(v is _UNRESOLVABLE for v in values) else values if isinstance(node, ast.Dict): result: dict[str, Any] = {} for key, value in zip(node.keys, node.values, strict=False): @@ -65,16 +77,22 @@ def _ast_literal(node: ast.expr) -> Any: k = _ast_literal(key) v = _ast_literal(value) if isinstance(k, str): - result[k] = v + result[k] = None if v is _UNRESOLVABLE else v return result - return None + return _UNRESOLVABLE def _extract_setup_kwargs(tree: ast.Module) -> dict[str, Any]: """Extract keyword arguments from a ``setup()`` or ``setuptools.setup()`` call. - Returns the first matching call's kwargs as a dict. Non-literal values - (variables, function calls) are omitted from the result. + Returns the first matching call's kwargs as a dict. A kwarg whose value + isn't a resolvable literal (a variable, function call, ...) is still + present in the result, with value ``None`` -- only the unresolved + *value* is dropped, never the key's presence. Keeping the key lets a + presence check (:func:`pitloom.extract._extract_utils.field_declared`) + correctly tell "explicitly declared, but not statically resolvable" + apart from "never mentioned at all" -- collapsing the two would silently + lose provenance for a kwarg like ``install_requires=SOME_VARIABLE``. """ node = next(iter_setup_calls(tree), None) if node is None: @@ -83,8 +101,7 @@ def _extract_setup_kwargs(tree: ast.Module) -> dict[str, Any]: for kw in node.keywords: if kw.arg is not None: # skip **expansion value = _ast_literal(kw.value) - if value is not None: - kwargs[kw.arg] = value + kwargs[kw.arg] = None if value is _UNRESOLVABLE else value return kwargs diff --git a/tests/extract/test_setuptools_py.py b/tests/extract/test_setuptools_py.py index f197668a..112f375f 100644 --- a/tests/extract/test_setuptools_py.py +++ b/tests/extract/test_setuptools_py.py @@ -164,6 +164,25 @@ def test_read_setup_py_empty_install_requires_gets_provenance() -> None: assert "dependencies" in metadata.provenance +def test_read_setup_py_unresolvable_install_requires_still_gets_provenance() -> None: + """A declared but statically-unresolvable install_requires (a module- + level variable, not a literal) must still record provenance -- the + kwarg was genuinely written, even though its value can't be resolved + by AST parsing alone. Dropping the key entirely (as an earlier bug + did) would make this indistinguishable from install_requires never + being mentioned at all.""" + content = ( + "from setuptools import setup\n" + "DEPS = ['requests']\n" + "setup(name='pkg', version='1.0', install_requires=DEPS)\n" + ) + with tempfile.TemporaryDirectory() as d: + (Path(d) / "setup.py").write_text(content) + metadata, _ = read_setup_py(Path(d)) + assert metadata.dependencies == [] + assert "dependencies" in metadata.provenance + + def test_read_setup_py_returns_default_pitloom_config() -> None: """setup.py provides no pitloom config -- defaults are returned.""" content = "from setuptools import setup\nsetup(name='pkg', version='1.0')\n" @@ -208,6 +227,22 @@ def test_read_setup_py_long_description_and_tuples() -> None: assert "readme" in metadata.provenance +def test_ast_literal_distinguishes_none_element_from_unresolvable() -> None: + """A literal `None` list element must resolve to the list `[None]`, + not be treated the same as an unresolvable element (a Name/Call node) + -- both used to collapse to the same `None` return, silently + invalidating a list that happens to contain a real `None`.""" + import ast # pylint: disable=import-outside-toplevel + + from pitloom.extract._setuptools_py import _UNRESOLVABLE, _ast_literal + + literal_none_list = ast.parse("[None]", mode="eval").body + assert _ast_literal(literal_none_list) == [None] + + unresolvable_list = ast.parse("[SOME_VAR]", mode="eval").body + assert _ast_literal(unresolvable_list) is _UNRESOLVABLE + + def test_ast_literal_dict_unpacking_and_calls() -> None: """_extract_setup_kwargs handles dict unpacking and non-setup calls.""" import ast # pylint: disable=import-outside-toplevel From e22566f8e405be39b345e435f94910e485fbb986 Mon Sep 17 00:00:00 2001 From: Arthit Suriyawongkul Date: Tue, 8 Sep 2026 17:43:42 +0700 Subject: [PATCH 30/35] Fix requires-python bug Signed-off-by: Arthit Suriyawongkul --- AGENTS.md | 55 +++++++---- .../assemble/spdx3/_document_locked_deps.py | 19 ++-- src/pitloom/core/project.py | 20 ++-- src/pitloom/extract/_poetry.py | 46 ++++++---- src/pitloom/extract/_pyproject.py | 7 ++ src/pitloom/extract/_sdist.py | 5 + src/pitloom/extract/_setuptools_cfg.py | 2 +- src/pitloom/extract/_setuptools_py.py | 38 +++++--- src/pitloom/extract/hatchling.py | 5 + .../assemble/test_deps_locked_dependencies.py | 6 +- tests/core/test_project_metadata.py | 29 ++++++ tests/extract/conftest.py | 11 ++- tests/extract/test_hatch_hook_metadata.py | 24 +++++ tests/extract/test_poetry_parsing.py | 92 ++++++++++++++++--- tests/extract/test_pyproject.py | 59 ++++++++++++ tests/extract/test_setuptools_cfg.py | 12 +++ tests/extract/test_setuptools_integration.py | 18 ++++ tests/extract/test_setuptools_py.py | 37 ++++++-- 18 files changed, 390 insertions(+), 95 deletions(-) diff --git a/AGENTS.md b/AGENTS.md index eb37e74f..fc2fffff 100644 --- a/AGENTS.md +++ b/AGENTS.md @@ -92,23 +92,44 @@ shape described, not just the module where each was first found. grep every existing producer's `provenance[...]` assignments for the same field and match whichever check style they already settled on. - **The presence signal must survive every merge/inheritance boundary, - or the fix is cosmetic.** `merge_project_metadata()` only treats an - empty container as authoritative when *its own* provenance key says - so -- so a producer that resolves the presence check correctly but - is never checked against real merge call sites can still lose the - signal in practice. The same masking happens one layer down: - `configparser`'s `[DEFAULT]`-section value inheritance makes - `"key" in cfg.items(section)` true even when *that section* never - declared `key` -- a presence check must read the section's own keys - only (not the merged view) or a shared default gets misread as an - explicit per-section declaration. And an upstream resolver that - silently collapses "couldn't fully resolve" to the same empty - container as "genuinely empty" (e.g. an AST list literal with one - unresolvable element silently dropping just that element instead of - invalidating the whole literal) reintroduces the exact ambiguity a - presence check downstream is trying to eliminate -- propagate - "unresolvable" as its own outcome, distinct from both "absent" and - "empty". + or the fix is cosmetic.** `merge_project_metadata()` only treats a + falsy value (empty container **or** `None`/other falsy scalar) as + authoritative when *its own* provenance key says so -- so a producer + that resolves the presence check correctly but is never checked + against real merge call sites can still lose the signal in practice. + The same masking happens one layer down: `configparser`'s `[DEFAULT]`- + section value inheritance makes `"key" in cfg.items(section)` true even + when *that section* never declared `key` -- a presence check must + read the section's own keys only (not the merged view) or a shared + default gets misread as an explicit per-section declaration. And an + upstream resolver that silently collapses "couldn't fully resolve" to + the same empty container as "genuinely empty" (e.g. an AST list + literal with one unresolvable element silently dropping just that + element instead of invalidating the whole literal) reintroduces the + exact ambiguity a presence check downstream is trying to eliminate -- + propagate "unresolvable" as its own outcome, distinct from both + "absent" and "empty". + - **The provenance dict's tri-state signal isn't container-specific -- + it's the same rule for a scalar that can legitimately resolve to + `None`.** `merge_project_metadata()`'s "was this explicitly declared" + check special-cased `primary_value is None` unconditionally, so no + scalar field's `None` could ever be protected as a deliberate answer, + only an absent one -- even when its own producer had confirmed + provenance for it. Three producers worked around this instead of + fixing it: `_poetry.py`'s `python = "*"` (Poetry's "explicitly no + constraint" convention), `_setuptools_cfg.py`'s `python_requires =`, + and `_setuptools_py.py`'s `python_requires=""` all truthy-gated their + own `provenance["requires_python"]` write -- correctly avoiding a + provenance/value mismatch (claiming "declared" for a field the merge + would then silently overwrite anyway), but at the cost of that field + never being protected against a lower-priority source's real, + possibly wrong, constraint. A producer that gates a scalar's + provenance on the *resolved value's truthiness* rather than the *raw + source key's presence* is treating a symptom of an asymmetric merge + condition, not a genuine ambiguity in its own data -- the fix belongs + in the shared merge function once, so every field type (present or + future, scalar or container) gets the same rule, not a per-producer + workaround that has to be independently rediscovered next time. - **Compare domain identifiers the way the ecosystem/spec does, not as raw strings.** A raw `==`/dict-key comparison silently fails to match values that a spec treats as equivalent (e.g. PEP 503 package-name diff --git a/src/pitloom/assemble/spdx3/_document_locked_deps.py b/src/pitloom/assemble/spdx3/_document_locked_deps.py index bf5aa005..f2b785af 100644 --- a/src/pitloom/assemble/spdx3/_document_locked_deps.py +++ b/src/pitloom/assemble/spdx3/_document_locked_deps.py @@ -7,9 +7,11 @@ :func:`pitloom.assemble.spdx3.document.build` -- deduplication, conflict detection, the exact-locked-version map, and the combined PyPI release-info prefetch. Split out of :mod:`pitloom.assemble.spdx3.document` to keep that -module under this repo's file-size soft limit; every name here is -re-exported from there, so existing imports of these names from -``pitloom.assemble.spdx3.document`` keep working. +module under this repo's file-size soft limit; every name that was already +public from that module before the split is re-exported from there, so +existing imports of those names from ``pitloom.assemble.spdx3.document`` +keep working. ``_dedup_and_locked_versions`` and ``_canon_names_and_pins`` +are new, module-internal to this split and not re-exported. See also: :mod:`pitloom.extract._lock_common` for the shared canonical-name-grouping and version-equality helpers this module builds on. @@ -35,10 +37,13 @@ def _canon_names_and_pins( locked_dependencies: list[str] | None, ) -> tuple[dict[str, str], list[tuple[str, str, str]]]: - """Parse every entry in *locked_dependencies* exactly once, returning - each dep string's PEP 503-canonicalized name (``canon_by_dep``, - keyed by the original dep string) alongside the ``(name, dep, pinned)`` - triples for entries that carry an exact pin. + """Parse every entry in *locked_dependencies* via ``Requirement()`` + at most once each (twice only for a dep string ``Requirement()`` + itself can't parse, where :func:`_parse_dep_name`'s own fallback + re-attempts it before falling back further), returning each dep + string's PEP 503-canonicalized name (``canon_by_dep``, keyed by the + original dep string) alongside the ``(name, dep, pinned)`` triples + for entries that carry an exact pin. Split out of :func:`_dedup_and_locked_versions` only to keep that function's local-variable count under this repo's complexity ceiling. diff --git a/src/pitloom/core/project.py b/src/pitloom/core/project.py index 6cc320c6..31c5a008 100644 --- a/src/pitloom/core/project.py +++ b/src/pitloom/core/project.py @@ -157,12 +157,16 @@ def merge_project_metadata( conflict, rather than replaced wholesale. Every other field: *primary*'s value when present, else *secondary*'s. - An empty container (``dependencies``, ``keywords``, ``urls``, etc.) - with provenance confirming it was explicitly declared in *primary* is - authoritative and preserved. Default-constructed empty containers (absent - from *primary*'s provenance) or ``None`` values are treated as absent and - filled from *secondary*. A non-empty *primary* list replaces *secondary*'s - wholesale, it is never unioned with it. If a future + This rule is uniform across scalar and container fields, with no + field-type-specific case: a falsy value -- an empty container + (``dependencies``, ``keywords``, ``urls``, etc.) or a scalar's + ``None`` (e.g. ``requires_python`` left unset by an explicit + ``python = "*"``) -- with provenance confirming it was explicitly + declared in *primary* is authoritative and preserved, exactly like a + truthy value would be. A falsy value absent from *primary*'s + provenance is treated as not-yet-resolved and filled from + *secondary*. A non-empty *primary* list replaces + *secondary*'s wholesale, it is never unioned with it. If a future ``locked_dependencies`` source needs union-not-replace semantics (e.g. combining two lock-derived dependency sets), that is a deliberate deviation from every sibling list field here and belongs in a dedicated @@ -183,8 +187,6 @@ def merge_project_metadata( continue primary_value = getattr(primary, f.name) provenance_key = _PROVENANCE_KEY_ALIASES.get(f.name, f.name) - if primary_value is None or ( - not primary_value and provenance_key not in primary.provenance - ): + if not primary_value and provenance_key not in primary.provenance: setattr(merged, f.name, getattr(secondary, f.name)) return merged diff --git a/src/pitloom/extract/_poetry.py b/src/pitloom/extract/_poetry.py index e211131f..99c0c091 100644 --- a/src/pitloom/extract/_poetry.py +++ b/src/pitloom/extract/_poetry.py @@ -117,13 +117,16 @@ def extract_poetry_metadata( poetry, project_dir ) - raw_keywords = poetry.get("keywords", []) - keywords: list[str] = raw_keywords if isinstance(raw_keywords, list) else [] + keywords = poetry.get("keywords", []) + if not isinstance(keywords, list): + keywords = [] authors = _parse_poetry_authors(poetry.get("authors", [])) urls = _parse_poetry_urls(poetry) - dependencies, requires_python = _parse_poetry_deps(poetry.get("dependencies", {})) + dependencies, requires_python, python_declared = _parse_poetry_deps( + poetry.get("dependencies", {}) + ) prov: dict[str, str] = { "name": "Source: pyproject.toml | Field: tool.poetry.name", @@ -159,15 +162,9 @@ def extract_poetry_metadata( prov["dependencies"] = ( "Source: pyproject.toml | Field: tool.poetry.dependencies" ) - # requires_python is a scalar (str | None), not a container -- unlike - # keywords/urls/dependencies/authors above, there's no "explicitly - # declared but empty" state worth preserving: `python = "*"` means - # "no constraint", which is correctly None, and provenance must - # follow that resolved value (truthy-gated), not the raw key's mere - # presence -- otherwise a `python = "*"` entry sets provenance for a - # field that stays None, which can misattribute a real value a - # lower-priority source supplies later via merge_project_metadata(). - if requires_python: + # Presence-gated like the container fields above, not truthy-gated -- + # see AGENTS.md's "provenance dict's tri-state signal" bullet. + if python_declared: prov["requires_python"] = ( "Source: pyproject.toml | Field: tool.poetry.dependencies.python" ) @@ -277,31 +274,42 @@ def _parse_poetry_authors(authors: list[Any]) -> list[dict[str, str]]: def _parse_poetry_deps( deps: Any, -) -> tuple[list[str], str | None]: +) -> tuple[list[str], str | None, bool]: """Convert ``[tool.poetry.dependencies]`` to a PEP 508 list plus requires-python. - The ``python`` key is extracted as ``requires_python``; all other entries - are converted to PEP 508 strings on a best-effort basis. + The ``python`` key is extracted as ``requires_python`` (matched + case-insensitively); all other entries are converted to PEP 508 + strings on a best-effort basis. Returns: - ``(dependencies, requires_python)`` where ``requires_python`` may be - ``None`` when the ``python`` key is absent. + ``(dependencies, requires_python, python_declared)`` -- + ``requires_python`` may be ``None`` when the ``python`` key is + absent OR when it's present but resolves to no constraint (e.g. + ``python = "*"``); ``python_declared`` distinguishes those two + cases so a caller can gate provenance on presence, not on + ``requires_python``'s truthiness -- an explicit "no constraint" + is a genuine, authoritative answer that + :func:`pitloom.core.project.merge_project_metadata` must not + silently override from a lower-priority source, the same + None-vs-absent distinction already applied to container fields. """ if not isinstance(deps, dict): - return [], None + return [], None, False requires_python: str | None = None + python_declared = False dependencies: list[str] = [] for pkg, constraint in deps.items(): if pkg.lower() == "python": requires_python = _poetry_constraint_to_pep440(constraint) + python_declared = True continue dep = _poetry_dep_to_pep508(pkg, constraint) if dep is not None: dependencies.append(dep) - return dependencies, requires_python + return dependencies, requires_python, python_declared def _convert_caret(ver: str) -> str: diff --git a/src/pitloom/extract/_pyproject.py b/src/pitloom/extract/_pyproject.py index 3a966c5c..28f998f4 100644 --- a/src/pitloom/extract/_pyproject.py +++ b/src/pitloom/extract/_pyproject.py @@ -244,6 +244,13 @@ def read_pyproject( provenance["license_files"] = ( "Source: pyproject.toml | Field: project.license-files" ) + # Presence-gated, not truthy-gated -- see AGENTS.md's "tri-state + # signal" bullet: `requires-python = ""` resolves to an empty (falsy) + # SpecifierSet, PEP 621's equivalent of Poetry's `python = "*"`. + if field_declared(project_data, "requires-python"): + provenance["requires_python"] = ( + "Source: pyproject.toml | Field: project.requires-python" + ) metadata = ProjectMetadata( name=std.name, diff --git a/src/pitloom/extract/_sdist.py b/src/pitloom/extract/_sdist.py index ed391c15..ce375776 100644 --- a/src/pitloom/extract/_sdist.py +++ b/src/pitloom/extract/_sdist.py @@ -40,6 +40,11 @@ def _parse_pkg_info(pkg_info_text: str, source_label: str) -> ProjectMetadata: requires_python=requires_python, ) metadata.provenance["name"] = source_label + # Unlike Poetry's `python = "*"` or setup.cfg's `python_requires =`, + # PKG-INFO/Core-Metadata has no convention where an empty header value + # means "explicitly no constraint" rather than just missing data, so + # truthy-gating these below is not the same presence-vs-truthy bug + # fixed elsewhere -- see AGENTS.md's "tri-state signal" bullet. if version: metadata.provenance["version"] = source_label if summary: diff --git a/src/pitloom/extract/_setuptools_cfg.py b/src/pitloom/extract/_setuptools_cfg.py index ed43fc7c..445bb55f 100644 --- a/src/pitloom/extract/_setuptools_cfg.py +++ b/src/pitloom/extract/_setuptools_cfg.py @@ -290,7 +290,7 @@ def read_setup_cfg( prov["urls"] = "Source: setup.cfg | Field: metadata.url/project_urls" if _section_declares_key(cfg, "options", "install_requires"): prov["dependencies"] = "Source: setup.cfg | Field: options.install_requires" - if requires_python: + if _section_declares_key(cfg, "options", "python_requires"): prov["requires_python"] = "Source: setup.cfg | Field: options.python_requires" if _section_declares_key(cfg, "metadata", "keywords"): prov["keywords"] = "Source: setup.cfg | Field: metadata.keywords" diff --git a/src/pitloom/extract/_setuptools_py.py b/src/pitloom/extract/_setuptools_py.py index c8fef877..3d70527f 100644 --- a/src/pitloom/extract/_setuptools_py.py +++ b/src/pitloom/extract/_setuptools_py.py @@ -12,6 +12,7 @@ from __future__ import annotations import ast +import logging from collections.abc import Iterator from pathlib import Path from typing import Any @@ -20,6 +21,8 @@ from pitloom.core.project import ProjectMetadata from pitloom.extract._extract_utils import field_declared +log = logging.getLogger(__name__) + def iter_setup_calls(tree: ast.AST) -> Iterator[ast.Call]: """Yield every ``setup()``/``x.setup()``-named call in *tree*, in @@ -42,10 +45,7 @@ def iter_setup_calls(tree: ast.AST) -> Iterator[ast.Call]: #: Sentinel for "not a resolvable literal" (a variable, function call, #: f-string, ...), distinct from a genuine literal ``None`` constant #: (``Constant(value=None)``) -- conflating the two would make a real -#: ``[None]`` list element indistinguishable from an unresolvable one, and -#: (via :func:`_extract_setup_kwargs`) would make a kwarg whose value -#: couldn't be resolved indistinguishable from a kwarg never written at -#: all. +#: ``[None]`` list element indistinguishable from an unresolvable one. _UNRESOLVABLE = object() @@ -86,13 +86,13 @@ def _extract_setup_kwargs(tree: ast.Module) -> dict[str, Any]: """Extract keyword arguments from a ``setup()`` or ``setuptools.setup()`` call. Returns the first matching call's kwargs as a dict. A kwarg whose value - isn't a resolvable literal (a variable, function call, ...) is still - present in the result, with value ``None`` -- only the unresolved - *value* is dropped, never the key's presence. Keeping the key lets a - presence check (:func:`pitloom.extract._extract_utils.field_declared`) - correctly tell "explicitly declared, but not statically resolvable" - apart from "never mentioned at all" -- collapsing the two would silently - lose provenance for a kwarg like ``install_requires=SOME_VARIABLE``. + isn't a resolvable literal (a variable, function call, ...) is omitted + from the result -- Pitloom has no actual value to report for it, so + treating it as "declared" would assert a confidently wrong empty + container (e.g. ``install_requires=[]``) instead of leaving the field + open for ``merge_project_metadata()`` to fill from a lower-priority + source. A ``WARNING:`` names the dropped kwarg so this isn't a silent + deviation. """ node = next(iter_setup_calls(tree), None) if node is None: @@ -101,7 +101,15 @@ def _extract_setup_kwargs(tree: ast.Module) -> dict[str, Any]: for kw in node.keywords: if kw.arg is not None: # skip **expansion value = _ast_literal(kw.value) - kwargs[kw.arg] = None if value is _UNRESOLVABLE else value + if value is _UNRESOLVABLE: + log.warning( + "setup.py: %r is declared but its value isn't a" + " statically resolvable literal -- treating it as" + " undeclared and falling back to a lower-priority source", + kw.arg, + ) + continue + kwargs[kw.arg] = value return kwargs @@ -239,6 +247,10 @@ def read_setup_py( ) prov = _build_setup_py_provenance( + # version/description/readme/license have no meaningful "explicitly + # declared but empty" state (unlike install_requires/keywords/ + # python_requires below) -- see AGENTS.md's "tri-state signal" + # bullet -- so truthy-gating them is not the same bug. has_version=bool(version), has_description=bool(description), has_readme=bool(readme), @@ -249,7 +261,7 @@ def read_setup_py( has_urls=field_declared(kwargs, "url") or field_declared(kwargs, "project_urls"), has_dependencies=field_declared(kwargs, "install_requires"), - has_requires_python=bool(requires_python), + has_requires_python=field_declared(kwargs, "python_requires"), has_keywords=field_declared(kwargs, "keywords"), ) diff --git a/src/pitloom/extract/hatchling.py b/src/pitloom/extract/hatchling.py index c002b6e9..f699dade 100644 --- a/src/pitloom/extract/hatchling.py +++ b/src/pitloom/extract/hatchling.py @@ -194,6 +194,11 @@ def metadata_from_hatchling( readme = _resolve_hatchling_readme(core) requires_python = core.requires_python or None + # Presence-gated, not truthy-gated -- see AGENTS.md's "tri-state + # signal" bullet: an explicit `requires-python = ""` is PEP 621's + # equivalent of Poetry's `python = "*"`. + if _hatchling_field_declared(core, "requires-python"): + provenance["requires_python"] = _field_provenance("requires-python") # Provenance for a container field is gated on presence in the raw # [project] table (`_hatchling_field_declared`), not on the resolved diff --git a/tests/assemble/test_deps_locked_dependencies.py b/tests/assemble/test_deps_locked_dependencies.py index 4fb3f9ef..65e660cf 100644 --- a/tests/assemble/test_deps_locked_dependencies.py +++ b/tests/assemble/test_deps_locked_dependencies.py @@ -516,9 +516,9 @@ def test_deduplicated_locked_dependencies_preserves_original_order() -> None: def test_build_warns_conflicting_locked_duplicates_only_once( caplog: pytest.LogCaptureFixture, ) -> None: - """build() shares one _deduplicated_locked_dependencies() result between - _locked_transitive_only_dependencies() and _extract_locked_version_map() - instead of each recomputing it -- a genuine conflict must log + """build() shares one _dedup_and_locked_versions() result between + _locked_transitive_only_dependencies() and its own locked-version-map + unpacking instead of each recomputing it -- a genuine conflict must log 'pinned to conflicting versions' exactly once per document, not once per caller.""" project = ProjectMetadata( diff --git a/tests/core/test_project_metadata.py b/tests/core/test_project_metadata.py index 8afa251b..cd84c2d1 100644 --- a/tests/core/test_project_metadata.py +++ b/tests/core/test_project_metadata.py @@ -158,6 +158,35 @@ def test_merge_project_metadata_license_concluded_preserved() -> None: assert merged.license_concluded == "Apache-2.0" +def test_merge_project_metadata_explicit_none_scalar_preserved() -> None: + """A scalar field resolved to None with confirmed provenance (e.g. + Poetry's `python = "*"`, meaning "explicitly no constraint") is a + deliberate, authoritative answer -- not absent -- and must not be + overwritten by secondary's real value, the same None-vs-[] distinction + already applied to empty containers above.""" + primary = ProjectMetadata( + name="pkg", + requires_python=None, + provenance={ + "requires_python": ( + "Source: pyproject.toml | Field: tool.poetry.dependencies.python" + ) + }, + ) + secondary = ProjectMetadata( + name="pkg", + requires_python=">=3.8", + provenance={ + "requires_python": "Source: setup.py | Field: setup(python_requires=...)" + }, + ) + merged = merge_project_metadata(primary, secondary) + assert merged.requires_python is None + assert merged.provenance["requires_python"] == ( + "Source: pyproject.toml | Field: tool.poetry.dependencies.python" + ) + + def test_merge_project_metadata_does_not_mutate_inputs() -> None: """Neither *primary* nor *secondary* is modified by the merge.""" primary = ProjectMetadata(name="pkg", provenance={"name": "Source: primary"}) diff --git a/tests/extract/conftest.py b/tests/extract/conftest.py index 3505c781..1163e2f4 100644 --- a/tests/extract/conftest.py +++ b/tests/extract/conftest.py @@ -110,12 +110,12 @@ def _fake_hatch_metadata( The fake ``core.config`` (the raw, unprocessed ``[project]`` table -- see :func:`pitloom.extract.hatchling._hatchling_field_declared`) gets - the corresponding ``[project]`` key exactly for whichever container - fields *core* explicitly overrides, mirroring how a real declared - field would show up in both places at once -- every container field + the corresponding ``[project]`` key exactly for whichever fields + *core* explicitly overrides, mirroring how a real declared field + would show up in both places at once -- every field ``metadata_from_hatchling()`` gates provenance on presence for - (``authors``/``urls``/``dependencies``/``keywords``/``license-files``), - not just ``license_files``. + (``authors``/``urls``/``dependencies``/``keywords``/``license-files``/ + ``requires-python``), not just ``license_files``. """ merged_core = {"raw_name": name, **_FAKE_CORE_DEFAULTS, **(core or {})} core_attr_to_config_key = { @@ -124,6 +124,7 @@ def _fake_hatch_metadata( "dependencies": "dependencies", "keywords": "keywords", "license_files": "license-files", + "requires_python": "requires-python", } config: dict[str, Any] = { core_attr_to_config_key[attr]: merged_core[attr] diff --git a/tests/extract/test_hatch_hook_metadata.py b/tests/extract/test_hatch_hook_metadata.py index ba2b90e7..df51b818 100644 --- a/tests/extract/test_hatch_hook_metadata.py +++ b/tests/extract/test_hatch_hook_metadata.py @@ -86,6 +86,30 @@ def test_metadata_from_hatchling_no_license_files() -> None: assert "license_files" not in metadata.provenance +def test_metadata_from_hatchling_explicit_empty_requires_python_gets_provenance() -> ( + None +): + """An explicit `requires-python = ""` (PEP 621's equivalent of Poetry's + `python = "*"`) resolves to None but must still record provenance -- + merge_project_metadata() relies on that presence to treat the None as + an authoritative "no constraint", not absent.""" + hatch_meta = _fake_hatch_metadata(core={"requires_python": ""}) + metadata = metadata_from_hatchling(hatch_meta, Path(".")) + assert metadata.requires_python is None + assert metadata.provenance["requires_python"] == ( + "Source: Hatchling build backend | Field: project.requires-python" + ) + + +def test_metadata_from_hatchling_no_requires_python_declared() -> None: + """No ``[project.requires-python]`` key: resolves to None, and no + provenance is recorded -- distinct from an explicit empty string.""" + hatch_meta = _fake_hatch_metadata() + metadata = metadata_from_hatchling(hatch_meta, Path(".")) + assert metadata.requires_python is None + assert "requires_python" not in metadata.provenance + + def test_metadata_from_hatchling_no_license_files_with_real_core( tmp_path: Path, ) -> None: diff --git a/tests/extract/test_poetry_parsing.py b/tests/extract/test_poetry_parsing.py index 505316e4..afbb3855 100644 --- a/tests/extract/test_poetry_parsing.py +++ b/tests/extract/test_poetry_parsing.py @@ -15,6 +15,7 @@ import pytest +from pitloom.core.project import ProjectMetadata, merge_project_metadata from pitloom.extract._poetry import ( _parse_poetry_authors, _parse_poetry_deps, @@ -195,29 +196,44 @@ def test_parse_authors_unmatched_email_bracket_skipped() -> None: def test_parse_deps_python_extracted() -> None: deps = {"python": "^3.10", "requests": "^2.28"} - packages, requires_python = _parse_poetry_deps(deps) + packages, requires_python, python_declared = _parse_poetry_deps(deps) assert requires_python == ">=3.10,<4.0.0" + assert python_declared is True assert any("requests" in d for d in packages) assert not any("python" in d for d in packages) def test_parse_deps_no_python_key() -> None: deps = {"click": ">=8.0"} - packages, requires_python = _parse_poetry_deps(deps) + packages, requires_python, python_declared = _parse_poetry_deps(deps) assert requires_python is None + assert python_declared is False assert any("click" in d for d in packages) def test_parse_deps_empty() -> None: - packages, requires_python = _parse_poetry_deps({}) + packages, requires_python, python_declared = _parse_poetry_deps({}) assert not packages assert requires_python is None + assert python_declared is False def test_parse_deps_not_a_dict() -> None: - packages, requires_python = _parse_poetry_deps("invalid") + packages, requires_python, python_declared = _parse_poetry_deps("invalid") assert not packages assert requires_python is None + assert python_declared is False + + +def test_parse_deps_wildcard_python_declared_but_no_constraint() -> None: + """`python = "*"` resolves requires_python to None, but python_declared + must still be True -- the caller needs to distinguish "declared, no + constraint" from "not declared at all" to gate provenance correctly.""" + deps = {"python": "*"} + packages, requires_python, python_declared = _parse_poetry_deps(deps) + assert requires_python is None + assert python_declared is True + assert not packages def test_parse_deps_skips_unrepresentable_git_dependency() -> None: @@ -226,8 +242,9 @@ def test_parse_deps_skips_unrepresentable_git_dependency() -> None: "dev-pkg": {"git": "https://github.com/x/y"}, "requests": "^2.28", } - packages, requires_python = _parse_poetry_deps(deps) + packages, requires_python, python_declared = _parse_poetry_deps(deps) assert requires_python is None + assert python_declared is False assert not any("dev-pkg" in d for d in packages) assert any("requests" in d for d in packages) @@ -358,13 +375,13 @@ def test_extract_provenance_empty_declared_dependencies() -> None: assert "requires_python" in metadata.provenance -def test_extract_provenance_wildcard_python_leaves_requires_python_unset() -> None: +def test_extract_provenance_wildcard_python_records_requires_python() -> None: """`python = "*"` (no real constraint) resolves requires_python to - None -- unlike the container fields, provenance must follow that - resolved value, not the raw `python` key's mere presence, or a - misattributed provenance tag could survive a later - merge_project_metadata() call that fills requires_python from a - different, real source.""" + None, but that's a deliberate, explicitly-declared answer, not an + absent field -- provenance must record it so merge_project_metadata() + can protect it against a lower-priority source's real (possibly + wrong) constraint, the same presence-based rule every container field + already follows.""" data = { "tool": { "poetry": { @@ -377,7 +394,58 @@ def test_extract_provenance_wildcard_python_leaves_requires_python_unset() -> No with tempfile.TemporaryDirectory() as d: metadata = extract_poetry_metadata(data, Path(d)) assert metadata.requires_python is None - assert "requires_python" not in metadata.provenance + assert "requires_python" in metadata.provenance + + +def test_extract_provenance_capitalized_python_key_records_requires_python() -> None: + """A capitalized `Python` key (unusual, but _parse_poetry_deps() + matches it case-insensitively for the *value*) must get the same + provenance treatment -- a case-sensitive presence check would + silently miss it, reopening a narrower version of the misattribution + bug the presence-based check exists to close.""" + data = { + "tool": { + "poetry": { + "name": "my-pkg", + "version": "1.0.0", + "dependencies": {"Python": "^3.9"}, + } + } + } + with tempfile.TemporaryDirectory() as d: + metadata = extract_poetry_metadata(data, Path(d)) + assert metadata.requires_python is not None + assert "requires_python" in metadata.provenance + + +def test_poetry_wildcard_python_survives_merge_as_primary() -> None: + """Poetry-derived metadata with an explicit `python = "*"` must keep + requires_python as None when merged as *primary* against a secondary + with a real constraint -- the end-to-end proof (real producer output + fed through the real merge function) that the presence-based + provenance fix actually changes merge_project_metadata()'s decision, + not just a synthetic ProjectMetadata literal.""" + data = { + "tool": { + "poetry": { + "name": "my-pkg", + "version": "1.0.0", + "dependencies": {"python": "*"}, + } + } + } + with tempfile.TemporaryDirectory() as d: + poetry_metadata = extract_poetry_metadata(data, Path(d)) + secondary = ProjectMetadata( + name="my-pkg", + version="1.0.0", + requires_python=">=3.8", + provenance={ + "requires_python": "Source: setup.py | Field: setup(python_requires=...)" + }, + ) + merged = merge_project_metadata(poetry_metadata, secondary) + assert merged.requires_python is None def test_convert_caret_and_tilde_edge_cases() -> None: diff --git a/tests/extract/test_pyproject.py b/tests/extract/test_pyproject.py index 9cfc810b..8e5e94c2 100644 --- a/tests/extract/test_pyproject.py +++ b/tests/extract/test_pyproject.py @@ -95,6 +95,65 @@ def test_read_pyproject_no_license_files_declared() -> None: assert "license_files" not in metadata.provenance +def test_read_pyproject_explicit_empty_requires_python_gets_provenance() -> None: + """An explicit `requires-python = ""` (PEP 621's equivalent of Poetry's + `python = "*"`) resolves to None but must still record provenance -- + merge_project_metadata() relies on that presence to treat the None as + an authoritative "no constraint", not absent.""" + with tempfile.TemporaryDirectory() as d: + tmp_path = Path(d) + (tmp_path / "pyproject.toml").write_text( + '[project]\nname = "pkg"\nversion = "1.0.0"\nrequires-python = ""\n', + encoding="utf-8", + ) + metadata, _config = read_pyproject(tmp_path / "pyproject.toml") + assert metadata.requires_python is None + assert metadata.provenance["requires_python"] == ( + "Source: pyproject.toml | Field: project.requires-python" + ) + + +def test_read_pyproject_explicit_empty_requires_python_survives_poetry_gap_fill() -> ( + None +): + """The [project] table's explicit `requires-python = ""` must win over + [tool.poetry]'s real constraint through the actual read_pyproject() + merge -- the concrete, reachable regression this presence check + exists to prevent, not just a synthetic unit-level scenario.""" + with tempfile.TemporaryDirectory() as d: + tmp_path = Path(d) + (tmp_path / "pyproject.toml").write_text( + "[project]\n" + 'name = "pkg"\n' + 'version = "1.0.0"\n' + 'requires-python = ""\n' + "\n" + "[tool.poetry]\n" + 'name = "pkg"\n' + 'version = "1.0.0"\n' + "\n" + "[tool.poetry.dependencies]\n" + 'python = ">=3.8"\n', + encoding="utf-8", + ) + metadata, _config = read_pyproject(tmp_path / "pyproject.toml") + assert metadata.requires_python is None + + +def test_read_pyproject_no_requires_python_declared() -> None: + """No `[project.requires-python]` key: resolves to None, and no + provenance is recorded -- distinct from an explicit empty string.""" + with tempfile.TemporaryDirectory() as d: + tmp_path = Path(d) + (tmp_path / "pyproject.toml").write_text( + '[project]\nname = "pkg"\nversion = "1.0.0"\n', + encoding="utf-8", + ) + metadata, _config = read_pyproject(tmp_path / "pyproject.toml") + assert metadata.requires_python is None + assert "requires_python" not in metadata.provenance + + def test_read_pyproject_no_project_no_poetry_no_license_found() -> None: """No ``[project]``, no ``[tool.poetry]``, and nothing in the directory that looks like a license: ``license_prov`` stays falsy.""" diff --git a/tests/extract/test_setuptools_cfg.py b/tests/extract/test_setuptools_cfg.py index 2c63e83c..66d5d097 100644 --- a/tests/extract/test_setuptools_cfg.py +++ b/tests/extract/test_setuptools_cfg.py @@ -319,6 +319,18 @@ def test_read_setup_cfg_empty_install_requires_gets_provenance() -> None: assert "dependencies" in metadata.provenance +def test_read_setup_cfg_empty_python_requires_gets_provenance() -> None: + """An explicitly declared but empty python_requires must still record + provenance -- merge_project_metadata() relies on that presence to + treat the resulting None as authoritative, not absent.""" + content = "[metadata]\nname = pkg\nversion = 1.0\n[options]\npython_requires =\n" + with tempfile.TemporaryDirectory() as d: + (Path(d) / "setup.cfg").write_text(content) + metadata, _ = read_setup_cfg(Path(d)) + assert metadata.requires_python is None + assert "requires_python" in metadata.provenance + + def test_resolve_cfg_version_edge_cases(tmp_path: Path) -> None: """_resolve_cfg_version handles empty strings, invalid attrs, and directives.""" from pitloom.extract._setuptools_cfg import _resolve_cfg_version diff --git a/tests/extract/test_setuptools_integration.py b/tests/extract/test_setuptools_integration.py index 2e6f026b..0ee3d12a 100644 --- a/tests/extract/test_setuptools_integration.py +++ b/tests/extract/test_setuptools_integration.py @@ -38,6 +38,24 @@ def test_read_setuptools_cfg_only() -> None: assert metadata.name == "pkg" +def test_read_setuptools_cfg_empty_python_requires_protected_from_py() -> None: + """setup.cfg's deliberately-empty python_requires (explicit "no + constraint") is primary and must win over setup.py's real constraint + through the actual read_setuptools() merge -- merge_project_metadata() + must treat cfg's explicitly-declared None as authoritative, not as an + absent value to fill from setup.py.""" + cfg = "[metadata]\nname = cfg-pkg\nversion = 1.0\n[options]\npython_requires =\n" + py = ( + "from setuptools import setup\n" + "setup(name='py-pkg', version='9.9', python_requires='>=3.8')\n" + ) + with tempfile.TemporaryDirectory() as d: + (Path(d) / "setup.cfg").write_text(cfg) + (Path(d) / "setup.py").write_text(py) + metadata, _ = read_setuptools(Path(d)) + assert metadata.requires_python is None + + def test_read_setuptools_py_only() -> None: """read_setuptools() succeeds with setup.py alone.""" content = "from setuptools import setup\nsetup(name='pkg2', version='2.0')\n" diff --git a/tests/extract/test_setuptools_py.py b/tests/extract/test_setuptools_py.py index 112f375f..bd1b97c7 100644 --- a/tests/extract/test_setuptools_py.py +++ b/tests/extract/test_setuptools_py.py @@ -164,13 +164,30 @@ def test_read_setup_py_empty_install_requires_gets_provenance() -> None: assert "dependencies" in metadata.provenance -def test_read_setup_py_unresolvable_install_requires_still_gets_provenance() -> None: - """A declared but statically-unresolvable install_requires (a module- - level variable, not a literal) must still record provenance -- the - kwarg was genuinely written, even though its value can't be resolved - by AST parsing alone. Dropping the key entirely (as an earlier bug - did) would make this indistinguishable from install_requires never - being mentioned at all.""" +def test_read_setup_py_empty_python_requires_gets_provenance() -> None: + """An explicitly declared but empty python_requires='' must still + record provenance -- merge_project_metadata() relies on that presence + to treat the resulting None as authoritative, not absent.""" + content = ( + "from setuptools import setup\n" + "setup(name='pkg', version='1.0', python_requires='')\n" + ) + with tempfile.TemporaryDirectory() as d: + (Path(d) / "setup.py").write_text(content) + metadata, _ = read_setup_py(Path(d)) + assert metadata.requires_python is None + assert "requires_python" in metadata.provenance + + +def test_read_setup_py_unresolvable_install_requires_is_not_declared( + caplog: pytest.LogCaptureFixture, +) -> None: + """A statically-unresolvable install_requires (a module-level variable, + not a literal) must be treated as undeclared, not as an authoritative + empty list -- Pitloom has no real value for it, and asserting + 'declared, zero dependencies' would block merge_project_metadata()'s + fallback to a lower-priority source that might actually have the + real dependency list. A WARNING names the dropped kwarg instead.""" content = ( "from setuptools import setup\n" "DEPS = ['requests']\n" @@ -178,9 +195,11 @@ def test_read_setup_py_unresolvable_install_requires_still_gets_provenance() -> ) with tempfile.TemporaryDirectory() as d: (Path(d) / "setup.py").write_text(content) - metadata, _ = read_setup_py(Path(d)) + with caplog.at_level("WARNING"): + metadata, _ = read_setup_py(Path(d)) assert metadata.dependencies == [] - assert "dependencies" in metadata.provenance + assert "dependencies" not in metadata.provenance + assert "install_requires" in caplog.text def test_read_setup_py_returns_default_pitloom_config() -> None: From e5014f32190542404e380df0f400395a0445068f Mon Sep 17 00:00:00 2001 From: Arthit Suriyawongkul Date: Tue, 8 Sep 2026 22:45:01 +0700 Subject: [PATCH 31/35] Update docs Signed-off-by: Arthit Suriyawongkul --- CHANGELOG.md | 2 +- README.md | 14 +++++++++----- docs/agent-skills.md | 2 +- docs/ai-model-formats.md | 2 +- docs/api.md | 2 +- docs/claude-code-plugin.md | 4 ++-- docs/cli.md | 10 ++++++---- docs/configuration.md | 18 ++++++++++-------- docs/creation-metadata.md | 8 ++++---- docs/dependency-sources.md | 10 +++++----- docs/github-action.md | 6 +++--- docs/hatchling-build-hook.md | 4 +++- docs/index.md | 2 +- docs/metadata-provenance.md | 14 ++++++++------ docs/python-api.md | 12 ++++++++++-- docs/resources.md | 8 ++++---- skills/sbom-enrich/SKILL.md | 6 +++--- skills/sbom-enrich/references/examples.md | 4 ++-- .../sbom-enrich/references/minimum-elements.md | 12 ++++++------ skills/sbom-generate/SKILL.md | 8 ++++---- skills/sbom-generate/references/examples.md | 2 +- skills/sbom-validate/SKILL.md | 2 +- skills/sbom-validate/references/examples.md | 2 +- 23 files changed, 87 insertions(+), 67 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index d276bada..e6271a00 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,5 +1,5 @@ --- -Last-Modified: 2026-09-05 +Last-Modified: 2026-09-08 SPDX-FileCopyrightText: 2026-present Arthit Suriyawongkul SPDX-FileType: DOCUMENTATION SPDX-License-Identifier: CC0-1.0 diff --git a/README.md b/README.md index a91983a9..0edde06e 100644 --- a/README.md +++ b/README.md @@ -11,7 +11,7 @@ **Pitloom** automates the generation of [SPDX 3]-compliant SBOMs for AI models and Python projects. It reads metadata directly from Python packages and AI models (GGUF, ONNX, PyTorch, Safetensors), producing -standardized SPDX 3 JSON artifacts -- as a CLI, a library, or a native +standardised SPDX 3 JSON artifacts -- as a CLI, a library, or a native Hatchling build hook. When used with Hatchling, Pitloom automatically embeds the generated @@ -91,7 +91,10 @@ Merkle root) is backend-aware for Hatchling, setuptools, Poetry, PDM-backend, and Flit-core, and falls back to a Hatchling-based heuristic with a warning for other backends -- see [Command line](docs/cli.md#generate-an-sbom) for the full limitation -note. +note. If a lock file (`pylock.toml`, `uv.lock`, `poetry.lock`, `pdm.lock`, +`Pipfile.lock`, or pinned `requirements.txt`) is present, Pitloom includes +its exact resolved dependencies automatically -- see +[Dependency sources](docs/dependency-sources.md). Generate an **Analyzed SBOM** from a pre-built wheel (extracting bundled binaries as phantom dependencies): @@ -160,8 +163,9 @@ loom enrich path/to/model.safetensors --project-dir . -o model.enrich.spdx3.json Register the fragment under `[tool.pitloom.fragment]` and re-run `loom project`/`loom generate` to merge it in. See -[`sbom-enrichment.md`](working-docs/design/sbom-enrichment.md) for the -full surface list (Python API, Hatchling hook, GitHub Action, Skill). +[Command line](docs/cli.md#enrich-an-sbom) and +[Agent Skills](docs/agent-skills.md) for the full surface list +(Python API, Hatchling hook, GitHub Action, Skill). ### Hatchling build hook @@ -461,7 +465,7 @@ and a worked example. - [SPDX 3.0 Specification](https://spdx.dev/wp-content/uploads/sites/31/2024/12/SPDX-3.0.1-1.pdf) - [PEP 770 – SBOM metadata in Python packages](https://peps.python.org/pep-0770/) -- [Design document](working-docs/design/architecture-overview.md) +- [Resources and standards list](docs/resources.md) - Bennet et al., [“Implementing AI Bill of Materials with SPDX 3.0”](https://www.linuxfoundation.org/research/ai-bom), The Linux Foundation, 2024. diff --git a/docs/agent-skills.md b/docs/agent-skills.md index 5fb3ab16..88c34e20 100644 --- a/docs/agent-skills.md +++ b/docs/agent-skills.md @@ -1,6 +1,6 @@ --- Created: 2026-08-11 -Last-Modified: 2026-08-14 +Last-Modified: 2026-09-08 SPDX-FileCopyrightText: 2026-present Arthit Suriyawongkul SPDX-FileType: DOCUMENTATION SPDX-License-Identifier: CC0-1.0 diff --git a/docs/ai-model-formats.md b/docs/ai-model-formats.md index 417d625f..92183b22 100644 --- a/docs/ai-model-formats.md +++ b/docs/ai-model-formats.md @@ -1,6 +1,6 @@ --- Created: 2026-08-14 -Last-Modified: 2026-08-14 +Last-Modified: 2026-09-08 SPDX-FileCopyrightText: 2026-present Arthit Suriyawongkul SPDX-FileType: DOCUMENTATION SPDX-License-Identifier: CC0-1.0 diff --git a/docs/api.md b/docs/api.md index 4ca2f914..2b063ae9 100644 --- a/docs/api.md +++ b/docs/api.md @@ -1,6 +1,6 @@ --- Created: 2026-08-11 -Last-Modified: 2026-08-29 +Last-Modified: 2026-09-08 SPDX-FileCopyrightText: 2026-present Arthit Suriyawongkul SPDX-FileType: DOCUMENTATION SPDX-License-Identifier: CC0-1.0 diff --git a/docs/claude-code-plugin.md b/docs/claude-code-plugin.md index 66cca8a4..87a870b7 100644 --- a/docs/claude-code-plugin.md +++ b/docs/claude-code-plugin.md @@ -1,6 +1,6 @@ --- Created: 2026-08-11 -Last-Modified: 2026-08-11 +Last-Modified: 2026-09-08 SPDX-FileCopyrightText: 2026-present Arthit Suriyawongkul SPDX-FileType: DOCUMENTATION SPDX-License-Identifier: CC0-1.0 @@ -76,7 +76,7 @@ Either trigger path works, same as standalone Skills: All arguments are optional. See the [Agent Skills](agent-skills.md) page for what each of the three Skills actually does and worked-example -recipes -- the behavior is identical to the standalone install, only the +recipes -- the behaviour is identical to the standalone install, only the invocation prefix changes. ## Verifying it works diff --git a/docs/cli.md b/docs/cli.md index 98a29d1f..c25d16c9 100644 --- a/docs/cli.md +++ b/docs/cli.md @@ -1,6 +1,6 @@ --- Created: 2026-08-11 -Last-Modified: 2026-09-04 +Last-Modified: 2026-09-08 SPDX-FileCopyrightText: 2026-present Arthit Suriyawongkul SPDX-FileType: DOCUMENTATION SPDX-License-Identifier: CC0-1.0 @@ -106,7 +106,7 @@ Or inject an existing pre-generated SBOM into built wheels: loom embed-wheel dist/*.whl --sbom sbom.spdx3.json ``` -`sbom.spdx3.json`'s declared subject name/version (PEP 503/440-normalized) +`sbom.spdx3.json`'s declared subject name/version (PEP 503/440-normalised) is cross-checked against the target wheel's own `.dist-info/METADATA` *before* anything is written: a mismatch is an `ERROR:` that aborts the embed (exit 1, nothing written); pass `--allow-mismatch` to downgrade it @@ -122,7 +122,7 @@ without a per-wheel naming scheme; omit it to modify each wheel in place. Check a wheel's embedded SBOM is at the correct PEP 770 location (`.dist-info/sboms/`), uses its format's recommended extension, and its -declared subject name/version (PEP 503/440-normalized) match the wheel's +declared subject name/version (PEP 503/440-normalised) match the wheel's own `.dist-info/METADATA`: ```bash @@ -149,7 +149,7 @@ needs `pip install "pitloom[validate]"`): loom validate-wheel dist/*.whl ``` -An embedded file in an unrecognized format prints a `WARNING:` and skips +An embedded file in an unrecognised format prints a `WARNING:` and skips validation (exit 0) rather than failing -- unsupported isn't the same as invalid. `embed-wheel` itself takes `--verify`/`--validate` as convenience flags that run these same checks against the wheel just embedded: @@ -422,6 +422,8 @@ does and worked examples. ## See also +- [Dependency sources and precedence](dependency-sources.md) -- how + resolved lock files feed into Source SBOM dependencies. - [Python API](python-api.md) -- calling Pitloom from Python code instead of the shell. - [Hatchling build hook](hatchling-build-hook.md) -- generate the SBOM diff --git a/docs/configuration.md b/docs/configuration.md index 7c804e08..8f3e1aea 100644 --- a/docs/configuration.md +++ b/docs/configuration.md @@ -1,6 +1,6 @@ --- Created: 2026-08-12 -Last-Modified: 2026-08-26 +Last-Modified: 2026-09-08 SPDX-FileCopyrightText: 2026-present Arthit Suriyawongkul SPDX-FileType: DOCUMENTATION SPDX-License-Identifier: CC0-1.0 @@ -33,7 +33,7 @@ the same way. | `ids-file` | string | `null` (auto-discovers `loom-ids.json` by walking up from the project directory) | -- | -- | -- (see `registry` param) | Path to the Loom ID registry file. | | `update-registry` | bool | `true` (`project` command only -- `wheel`/`env` aren't pyproject-cascaded, same as `ids-file`) | `--update-registry` / `--no-update-registry` | -- | `update_registry` | After generating, harvest newly-minted ids back into the resolved registry and save it. Only consulted by `project`/`wheel`/`env`/`generate`; accepted but has no effect on `model`/`enrich`/`embed-wheel`. No effect when no registry is resolved -- see [Loom IDs across fragments](https://github.com/bact/pitloom/blob/main/README.md#loom-ids-across-fragments-pitloom-ids). | -**Invalid values / fallback behavior:** every boolean above raises +**Invalid values / fallback behaviour:** every boolean above raises `ValueError` at config-read time if set to a non-boolean (e.g. the TOML string `"true"` instead of the bare value `true`) -- no silent coercion. `sbom-basename`/`ids-file` raise `ValueError` if set to a @@ -53,7 +53,7 @@ to every file, text or binary). | `enabled` | bool | `false` | `--content-type` / `--no-content-type` | `content-type` | `content_type` | Detect each file's real IANA media type. Off by default -- `magika` inference is a real per-file cost (~5ms/file). | | `method` | `"auto"` \| `"magika"` \| `"extension"` | `"auto"` | `--content-type-method` | `content-type-method` | `content_type_method` | Which detector resolves a value: `"auto"` tries `magika`, falling back to a filename-extension guess when `magika` isn't installed or its result is inconclusive; `"magika"` behaves identically per-file but raises immediately if the package isn't installed at all; `"extension"` skips `magika` entirely. | -**Invalid values / fallback behavior:** `enabled` non-boolean raises +**Invalid values / fallback behaviour:** `enabled` non-boolean raises `ValueError` at config-read time. `method` not one of the three listed values raises `ValueError` at config-read time. `method = "magika"` with the `magika` package not installed raises `RuntimeError` at @@ -93,7 +93,7 @@ pattern = "vendor/*" content-type = "application/octet-stream" ``` -**Invalid values / fallback behavior:** `override` present but not an +**Invalid values / fallback behaviour:** `override` present but not an array of tables, an entry not a table, a missing/empty `pattern`, or a `content-type` not shaped like `type/subtype` -- each raises `ValueError` at config-read time with a message naming the exact @@ -158,7 +158,7 @@ what these fields record in the generated SBOM. | | `type` | `"person"` \| `"organization"` \| `"software-agent"` \| `"agent"` | Defaults to `"person"`. | | `[[tool.pitloom.creation-tool]]` | `name` | string (required) | Tool name recorded as having produced the SBOM. | -**Invalid values / fallback behavior:** a missing/empty `name` on +**Invalid values / fallback behaviour:** a missing/empty `name` on either table, or a non-string `type`/`email`, raises `ValueError` at config-read time. `--creator-name`/`--creation-tool` on the CLI replace the whole configured list for that run rather than merging with it. @@ -175,9 +175,9 @@ changes in the generated SBOM's Annotations. | `schema` | string | `"pitloom/1"` | -- | -- | -- | Which statement schema encodes provenance Annotations. | | `detail` | `"minimal"` \| `"full"` | `"minimal"` | -- | -- | -- | `"minimal"` emits a field-source Annotation only when the source adds signal the native value can't convey; `"full"` emits the per-field source map for every field. | | `preserve-source-metadata` | `"auto"` \| `"always"` \| `"never"` | `"auto"` | -- | -- | -- | Whether to embed an artifact's verbatim original metadata blob. `"auto"` does so only when the artifact isn't shipped with the distribution (and so can't be re-extracted later). | -| `max-source-metadata-bytes` | non-negative integer | `0` | `--max-source-metadata-bytes` | `max-source-metadata-bytes` | -- | Byte budget for the serialized artifact-metadata `Annotation.statement`. `0` means unlimited (today's behavior). When exceeded, the largest metadata entries are dropped first and the result is marked `truncated`/`truncatedKeys`/`truncatedKeyCount`/`maxMetadataBytes` -- see [Metadata provenance](metadata-provenance.md#size-bounded-preservation). Unlike its siblings above, this one has a CLI flag / Action input (no dedicated API param -- set it via the same `ProvenanceConfig` object the others use): a byte cap is an operational knob someone may want to override per-run without editing `pyproject.toml`. | +| `max-source-metadata-bytes` | non-negative integer | `0` | `--max-source-metadata-bytes` | `max-source-metadata-bytes` | -- | Byte budget for the serialised artifact-metadata `Annotation.statement`. `0` means unlimited (today's behaviour). When exceeded, the largest metadata entries are dropped first and the result is marked `truncated`/`truncatedKeys`/`truncatedKeyCount`/`maxMetadataBytes` -- see [Metadata provenance](metadata-provenance.md#size-bounded-preservation). Unlike its siblings above, this one has a CLI flag / Action input (no dedicated API param -- set it via the same `ProvenanceConfig` object the others use): a byte cap is an operational knob someone may want to override per-run without editing `pyproject.toml`. | -**Invalid values / fallback behavior:** a non-string value, or a +**Invalid values / fallback behaviour:** a non-string value, or a `format`/`detail`/`preserve-source-metadata` outside its listed set, raises `ValueError` at config-read time. An unknown `schema` id is not caught here (`core` doesn't import the assembly layer's encoder @@ -185,7 +185,7 @@ registry) -- it's caught with a clear error the first time an SBOM is actually generated. `max-source-metadata-bytes`: a non-integer or `bool` value raises `ValueError` at config-read time; a negative value, or a positive value below the smallest possible JSON object it -could ever hold (8 bytes), is normalized to `0` (unlimited) with a +could ever hold (8 bytes), is normalised to `0` (unlimited) with a logged `WARNING`, not rejected. ## See also @@ -193,6 +193,8 @@ logged `WARNING`, not rejected. - [Command line](cli.md) -- flag-by-flag usage with worked examples. - [GitHub Action](github-action.md) -- input reference for CI. - [Python API](python-api.md) -- calling Pitloom from Python code. +- [Dependency sources and precedence](dependency-sources.md) -- how + resolved lock files feed into Source SBOM dependencies. - [Hatchling build hook](hatchling-build-hook.md) -- inherits the project's `[tool.pitloom]` automatically, no separate hook-level config surface (only `[tool.hatch.build.hooks.pitloom] enabled` diff --git a/docs/creation-metadata.md b/docs/creation-metadata.md index 169f8b88..fd69e80b 100644 --- a/docs/creation-metadata.md +++ b/docs/creation-metadata.md @@ -1,6 +1,6 @@ --- Created: 2026-07-08 -Last-Modified: 2026-08-14 +Last-Modified: 2026-09-08 SPDX-FileCopyrightText: 2026-present Arthit Suriyawongkul SPDX-FileType: DOCUMENTATION SPDX-License-Identifier: CC0-1.0 @@ -52,7 +52,7 @@ explicit `creation_metadata=CreationMetadata(...)` to that call. | Field (SPDX 3 name) | Meaning | What Pitloom puts there | | :--- | :--- | :--- | -| `createdBy` (**≥1**) | *Who* created it | One or more **creators**: a person, organization, software agent, or generic agent for each one you name (`--creator-name`, repeatable); otherwise Pitloom itself, acting unattended (see below). | +| `createdBy` (**≥1**) | *Who* created it | One or more **creators**: a person, organisation, software agent, or generic agent for each one you name (`--creator-name`, repeatable); otherwise Pitloom itself, acting unattended (see below). | | `createdUsing` (0+) | *What* tool produced it | **Pitloom** by default, with a version summary; repeat `--creation-tool` for more than one. Suppress with `--no-creation-tool`. | | `created` (1) | *When* | `--creation-datetime` if set, else [`SOURCE_DATE_EPOCH`](https://reproducible-builds.org/specs/source-date-epoch/) if set, else the current UTC time. | | `comment` (0-1) | *How* it was invoked | A short static note per channel (`Generated via Pitloom CLI`, `... Hatchling build hook`, `... loom SDK`), or your `--creation-comment`. | @@ -68,7 +68,7 @@ the `SoftwareAgent` creator too (see below) -- but never pretending a human did the work. - **You name one or more creators** (repeatable `--creator-name`, or - `[[tool.pitloom.creator]]`): each becomes a person (default), organization, + `[[tool.pitloom.creator]]`): each becomes a person (default), organisation, software agent, or generic agent (via `--creator-type`/`type`, bound to the most recently named creator). The software-agent/agent types are for naming an automated creator that isn't Pitloom itself -- e.g. a CI bot @@ -77,7 +77,7 @@ human did the work. or more are given. - **You name no creator** (zero-config): rather than invent a fake person, Pitloom records itself as the creator too, but as a software agent, not a - person or organization -- honestly "an unattended Pitloom run made this" -- + person or organisation -- honestly "an unattended Pitloom run made this" -- and omits a supplier for the main package. Pitloom is also recorded as the tool by default (unless suppressed with `--no-creation-tool`), so the same Pitloom can show up twice in this case: once as the (software diff --git a/docs/dependency-sources.md b/docs/dependency-sources.md index 5146aed5..b59a0af9 100644 --- a/docs/dependency-sources.md +++ b/docs/dependency-sources.md @@ -1,6 +1,6 @@ --- Created: 2026-09-04 -Last-Modified: 2026-09-07 +Last-Modified: 2026-09-08 SPDX-FileCopyrightText: 2026-present Arthit Suriyawongkul SPDX-FileType: DOCUMENTATION SPDX-License-Identifier: CC0-1.0 @@ -57,7 +57,7 @@ lines that happen to already be fully pinned. pin, even when the URL looks like it points at a tagged release** (e.g. `name @ https://github.com/org/repo/archive/refs/tags/v2.31.0.zip`). A git tag or release filename is an arbitrary string with no guaranteed -relationship to the package's real, normalized version -- Pitloom +relationship to the package's real, normalised version -- Pitloom doesn't fetch the URL to check, so a line like that disqualifies the whole file the same as an unpinned or ranged one would. @@ -88,7 +88,7 @@ missing. ## Version comparison: PEP 440, not SemVer **Pitloom compares dependency versions using [PEP 440][pep-440] equality, -not SemVer.** "Same version" means the two version strings normalize to +not SemVer.** "Same version" means the two version strings normalise to the identical release under PEP 440 -- trailing-zero components are padded and compared, so `1.0`, `1.0.0`, and `1.0.0.0` are all the same version. It does **not** mean "the latest release compatible with 1.0" @@ -103,7 +103,7 @@ two places: - **A lock file's own duplicate entries.** If one lock file records the same package name more than once (e.g. a platform-specific variant), - entries that normalize to the same PEP 440 release are silently + entries that normalise to the same PEP 440 release are silently collapsed into one; entries that don't get a `WARNING:` naming both versions, and that package is left out of the transitive list entirely rather than guessed at. @@ -111,7 +111,7 @@ two places: direct dependency is unpinned or declared as a range, the lock file's resolved version is used (see above). When it's already pinned exactly (e.g. `requests==2.31.0`) and the lock file separately - resolved it to a version that doesn't normalize the same way (e.g. + resolved it to a version that doesn't normalise the same way (e.g. `2.31.1`), Pitloom logs a `WARNING:` but keeps the *declared* pin -- the lock's differing value never silently overrides an exact pin the project itself declared. diff --git a/docs/github-action.md b/docs/github-action.md index 784f3e36..4635c831 100644 --- a/docs/github-action.md +++ b/docs/github-action.md @@ -1,6 +1,6 @@ --- Created: 2026-08-11 -Last-Modified: 2026-09-02 +Last-Modified: 2026-09-08 SPDX-FileCopyrightText: 2026-present Arthit Suriyawongkul SPDX-FileType: DOCUMENTATION SPDX-License-Identifier: CC0-1.0 @@ -88,7 +88,7 @@ GitHub Actions annotations -- `::notice::`/`::warning::`/`::error::` -- so they show up in the PR "Checks" tab and job summary, not just buried in the raw log. A failing `loom` invocation still fails the step/job (its `ERROR:` line is annotated first); this doesn't change the action's -exit behavior. +exit behaviour. ## Persisting the Loom ID registry in CI @@ -198,7 +198,7 @@ Inputs (all optional): | `extract-file-header` | *(empty)* | `true`/`false` to force per-file SPDX header scanning on or off; empty defers to `[tool.pitloom] extract-file-header` (on by default). | | `content-type` | *(empty)* | `true`/`false` to force per-file content-type detection on or off; empty defers to `[tool.pitloom.content-type] enabled` (off by default). | | `content-type-method` | *(empty)* | `auto`/`magika`/`extension` -- which detector resolves content-type values; empty defers to `[tool.pitloom.content-type] method` (`auto` by default). | -| `max-source-metadata-bytes` | *(empty)* | Cap the artifact-metadata preservation Annotation's serialized size to this many UTF-8 bytes, truncating the largest entries first when exceeded; empty defers to `[tool.pitloom.provenance] max-source-metadata-bytes` (unbounded by default). | +| `max-source-metadata-bytes` | *(empty)* | Cap the artifact-metadata preservation Annotation's serialised size to this many UTF-8 bytes, truncating the largest entries first when exceeded; empty defers to `[tool.pitloom.provenance] max-source-metadata-bytes` (unbounded by default). | | `args` | *(empty)* | Extra raw flags passed through to the `loom` command, e.g. `--verify --validate` when `embed-wheel` is set. | | `pitloom-version` | *(empty)* | Pitloom version/specifier to install, e.g. `0.17.0` or `>=0.13,<1.0`. Empty installs the latest release. | | `python-version` | `3.x` | Python version passed to `actions/setup-python`. | diff --git a/docs/hatchling-build-hook.md b/docs/hatchling-build-hook.md index 860fd2c3..c6c14165 100644 --- a/docs/hatchling-build-hook.md +++ b/docs/hatchling-build-hook.md @@ -1,6 +1,6 @@ --- Created: 2026-08-11 -Last-Modified: 2026-08-29 +Last-Modified: 2026-09-08 SPDX-FileCopyrightText: 2026-present Arthit Suriyawongkul SPDX-FileType: DOCUMENTATION SPDX-License-Identifier: CC0-1.0 @@ -89,6 +89,8 @@ provenance](metadata-provenance.md). - [Command line](cli.md) -- generate an SBOM manually or post-process built wheels with `loom embed-wheel`. +- [Dependency sources and precedence](dependency-sources.md) -- why wheel + embedding scopes dependencies to the build stage. - [GitHub Action](github-action.md) -- embed PEP 770 SBOMs in CI for any build backend. - [Python API](python-api.md) -- the tracking decorator that produces the diff --git a/docs/index.md b/docs/index.md index 4b67885f..d0da4871 100644 --- a/docs/index.md +++ b/docs/index.md @@ -1,6 +1,6 @@ --- Created: 2026-07-08 -Last-Modified: 2026-09-04 +Last-Modified: 2026-09-08 SPDX-FileCopyrightText: 2026-present Arthit Suriyawongkul SPDX-FileType: DOCUMENTATION SPDX-License-Identifier: CC0-1.0 diff --git a/docs/metadata-provenance.md b/docs/metadata-provenance.md index 59a3ebcd..f97e9a7d 100644 --- a/docs/metadata-provenance.md +++ b/docs/metadata-provenance.md @@ -1,6 +1,6 @@ --- Created: 2026-07-08 -Last-Modified: 2026-08-26 +Last-Modified: 2026-09-08 SPDX-FileCopyrightText: 2026-present Arthit Suriyawongkul SPDX-FileType: DOCUMENTATION SPDX-License-Identifier: CC0-1.0 @@ -85,7 +85,7 @@ vocabulary) into a single `Annotation.statement`. For a real model this can be large -- a 32K-128K-entry vocab array easily reaches multi-megabyte territory. `max-source-metadata-bytes` (also `--max-source-metadata-bytes` on the CLI, or the Action's `max-source-metadata-bytes` input) caps the -serialized `Annotation.statement`'s size in UTF-8 bytes; `0` (the default) +serialised `Annotation.statement`'s size in UTF-8 bytes; `0` (the default) means unlimited. When the budget is exceeded, whole metadata entries are dropped -- @@ -116,7 +116,7 @@ claims a budget its own overhead violates would be worse than omitting it. A budget that forces every key to be dropped, but still fits the marker overhead, is emitted with `metadata: {}` and a `WARNING`. -The `Annotation.statement` value is itself serialized via RFC 8785 (JSON +The `Annotation.statement` value is itself serialised via RFC 8785 (JSON Canonicalization Scheme, JCS) -- the same canonicalization the whole SBOM document uses -- so it has no insignificant whitespace and a deterministic key order; byte-for-byte comparing or hashing this blob @@ -160,11 +160,11 @@ declared value was already found. A `CITATION.cff`/`codemeta.json` value that's already a bare SPDX id is used as-is; anything else (typically a `LICENSE` file's full text) is matched against known SPDX licenses via `licenseid` (`method: licenseid_detection`). Either way counts as -Pitloom's own independent-detection procedure. Both sides are normalized +Pitloom's own independent-detection procedure. Both sides are normalised before comparison -- not just casing (a declared `"mit"` and a detected -`"MIT"` are recognized as the same license), but also equivalent compound +`"MIT"` are recognised as the same license), but also equivalent compound expressions written differently (`"MIT AND MIT"` and plain `"MIT"`; -`"MIT OR Apache-2.0"` and `"Apache-2.0 OR MIT"` all normalize to the same +`"MIT OR Apache-2.0"` and `"Apache-2.0 OR MIT"` all normalise to the same value) -- so none of these are misreported as a conflict. - If only one of the two exists, only that one is recorded, as @@ -207,3 +207,5 @@ value) -- so none of these are misreported as a conflict. point -- see [Command line](cli.md#configuration), [Hatchling build hook](hatchling-build-hook.md), and [Python API](python-api.md) for where to set it. +- [Dependency sources and precedence](dependency-sources.md) -- how + resolved lock files feed into Source SBOM dependencies and provenance. diff --git a/docs/python-api.md b/docs/python-api.md index b3009f05..9ac88ef2 100644 --- a/docs/python-api.md +++ b/docs/python-api.md @@ -1,6 +1,6 @@ --- Created: 2026-08-11 -Last-Modified: 2026-08-29 +Last-Modified: 2026-09-08 SPDX-FileCopyrightText: 2026-present Arthit Suriyawongkul SPDX-FileType: DOCUMENTATION SPDX-License-Identifier: CC0-1.0 @@ -82,6 +82,12 @@ generate_project_sbom( ) ``` +When a supported lock file (`pylock.toml`, `uv.lock`, `poetry.lock`, +`pdm.lock`, `Pipfile.lock`, or pinned `requirements.txt`) is present +next to `pyproject.toml`, project generation automatically resolves and +includes its exact transitive dependencies -- see +[Dependency sources and precedence](dependency-sources.md). + `pitloom.assemble` also exposes `generate_wheel_sbom()`, `generate_model_sbom()`, and `generate_env_sbom()` -- the same target kinds the [CLI](cli.md)'s `loom wheel` / `loom model` / `loom env` @@ -123,7 +129,7 @@ of the embed; `floored` is `True` when the wheel's ZIP entry timestamp had to be floored to 1980-01-01 (see [Configuration](configuration.md#toolpitloomcreation)). With `sbom_path=` (form 2, the equivalent of the CLI's `embed-wheel --sbom`), -the SBOM's declared subject name/version (PEP 503/440-normalized) is +the SBOM's declared subject name/version (PEP 503/440-normalised) is cross-checked against the wheel's own `.dist-info/METADATA` *before* anything is written: a mismatch raises `ValueError` and nothing is written, unless `allow_mismatch=True` downgrades it to a `WARNING:` log @@ -270,6 +276,8 @@ merging again. See [API reference](api.md#fragment-merging). ## See also - [Command line](cli.md) -- the same generation targets, from a shell. +- [Dependency sources and precedence](dependency-sources.md) -- how + resolved lock files feed into Source SBOM dependencies. - [Hatchling build hook](hatchling-build-hook.md) -- how registered fragments get merged automatically at build time. - [Creation metadata](creation-metadata.md) and [Metadata diff --git a/docs/resources.md b/docs/resources.md index 23c7411e..1c08634f 100644 --- a/docs/resources.md +++ b/docs/resources.md @@ -1,6 +1,6 @@ --- Created: 2026-03-26 -Last-Modified: 2026-09-05 +Last-Modified: 2026-09-08 SPDX-FileCopyrightText: 2026-present Arthit Suriyawongkul SPDX-FileType: DOCUMENTATION SPDX-License-Identifier: CC0-1.0 @@ -10,7 +10,7 @@ SPDX-License-Identifier: CC0-1.0 ## Python Enhancement Proposals (PEPs) -> Note: PEPs are historical document. +> Note: PEPs are historical documents. > The up-to-date, canonical spec for Python packaging, > is maintained on the > [PyPA specs page](https://packaging.python.org/en/latest/specifications/). @@ -25,7 +25,7 @@ each with a short note on what SBOM metadata it feeds: `.dist-info/` layout Pitloom reads/writes package files against. **Stale on one point** the PEP text itself doesn't reflect: the name/version escaping rule for `.dist-info` directory naming (PEP 503 - normalization, then `-` → `_`) was *revised in 2021* to match real + normalisation, then `-` → `_`) was *revised in 2021* to match real tooling — see the canonical [Binary Distribution Format spec][pep-427-spec] instead of this PEP for that rule specifically. @@ -33,7 +33,7 @@ each with a short note on what SBOM metadata it feeds: Specification: version syntax used for dependency-constraint conversion (e.g. Poetry's `^`/`~`) and wheel-vs-SBOM version checks. - [PEP 503][pep-503] – Simple Repository API: package-name - normalization, used generically wherever two package names must + normalisation, used generically wherever two package names must compare equal regardless of case/`-`/`_`/`.` -- PyPI purl construction, dependency dedup, wheel-vs-SBOM name checks, and wheel `.dist-info` path escaping. diff --git a/skills/sbom-enrich/SKILL.md b/skills/sbom-enrich/SKILL.md index df1c9661..e129094e 100644 --- a/skills/sbom-enrich/SKILL.md +++ b/skills/sbom-enrich/SKILL.md @@ -1,6 +1,6 @@ --- # Created: 2026-07-05 -# Last-Modified: 2026-08-26 +# Last-Modified: 2026-09-08 # SPDX-FileCopyrightText: 2026-present Arthit Suriyawongkul # SPDX-FileType: SOURCE # SPDX-License-Identifier: Apache-2.0 @@ -214,7 +214,7 @@ Steps: ``` For a stronger check, run the fragment through the same SPDX 3 - JSON-LD deserializer `merge_fragments()` itself uses -- this catches + JSON-LD deserialiser `merge_fragments()` itself uses -- this catches the same broken-JSON-LD cases `merge_fragments()` swallows as a warning, plus SPDX-shape problems (e.g. an unknown property or type) that plain JSON-syntax validity would miss: @@ -303,7 +303,7 @@ e. **Contradiction check.** Before drafting the fragment, compare each new answe against the base SBOM's existing value for that field and against other answers already collected this session -- if they conflict, surface both and ask the user to confirm which stands, the same way step 6 above handles a prose-vs-frontmatter - conflict, generalized to interactively-collected answers too. Never silently pick + conflict, generalised to interactively-collected answers too. Never silently pick one. f. **Draft, validate, register, merge, validate** -- reuse steps 6-10 above verbatim. No new mechanism: this workflow only changes *what* gets proposed and *how it's diff --git a/skills/sbom-enrich/references/examples.md b/skills/sbom-enrich/references/examples.md index e1a2ce49..bfad586c 100644 --- a/skills/sbom-enrich/references/examples.md +++ b/skills/sbom-enrich/references/examples.md @@ -1,6 +1,6 @@ --- Created: 2026-07-05 -Last-Modified: 2026-08-25 +Last-Modified: 2026-09-08 SPDX-FileCopyrightText: 2026-present Arthit Suriyawongkul SPDX-FileType: DOCUMENTATION SPDX-License-Identifier: CC0-1.0 @@ -153,7 +153,7 @@ python3 -c "import json,sys; json.load(open(sys.argv[1]))" \ ``` For a stronger check, run the fragment through the same SPDX 3 JSON-LD -deserializer `merge_fragments()` itself uses. This catches the same +deserialiser `merge_fragments()` itself uses. This catches the same broken-JSON-LD cases `merge_fragments()` swallows as a warning, plus SPDX-shape problems (e.g. an unknown property or type) that plain JSON-syntax validity would miss: diff --git a/skills/sbom-enrich/references/minimum-elements.md b/skills/sbom-enrich/references/minimum-elements.md index 174d6903..ef166c60 100644 --- a/skills/sbom-enrich/references/minimum-elements.md +++ b/skills/sbom-enrich/references/minimum-elements.md @@ -1,6 +1,6 @@ --- Created: 2026-08-12 -Last-Modified: 2026-08-12 +Last-Modified: 2026-09-08 SPDX-FileCopyrightText: 2026-present Arthit Suriyawongkul SPDX-FileType: DOCUMENTATION SPDX-License-Identifier: CC0-1.0 @@ -151,17 +151,17 @@ in the project. - **SBOM Author** (when `[[tool.pitloom.creator]]` isn't set): "Pitloom's own `CreationInfo` currently only names Pitloom itself as the generating tool, not the - person or organization that ran it. Who should be recorded as the SBOM author -- - you, or an organization? This can also be set permanently via + person or organisation that ran it. Who should be recorded as the SBOM author -- + you, or an organisation? This can also be set permanently via `[[tool.pitloom.creator]]` in `pyproject.toml` (note the double brackets -- it's an array of tables) so future runs don't need to ask -- and it also fills in Component Producer for the main package at the same time." - **Component/Model Producer**: "Is this dependency/model something your - organization built, or a third-party component? If third-party, do you know the - maintaining organization or project (check the package's PyPI page, GitHub org, or + organisation built, or a third-party component? If third-party, do you know the + maintaining organisation or project (check the package's PyPI page, GitHub org, or model card)?" - **SBOM Author Signature**: "This requires a detached digital signature over the - SBOM using your organization's own signing infrastructure (see NIST SP 800-57 + SBOM using your organisation's own signing infrastructure (see NIST SP 800-57 Pt. 1 for key-management guidance). Pitloom doesn't generate signatures -- do you already have a signing process, or is this out of scope for now?" - **Model license**: "Does the model have its own license, separate from the diff --git a/skills/sbom-generate/SKILL.md b/skills/sbom-generate/SKILL.md index 09b441a8..6cd72375 100644 --- a/skills/sbom-generate/SKILL.md +++ b/skills/sbom-generate/SKILL.md @@ -1,6 +1,6 @@ --- # Created: 2026-07-05 -# Last-Modified: 2026-09-06 +# Last-Modified: 2026-09-08 # SPDX-FileCopyrightText: 2026-present Arthit Suriyawongkul # SPDX-FileType: SOURCE # SPDX-License-Identifier: Apache-2.0 @@ -255,7 +255,7 @@ Pitloom logs to stderr with a grep-able `INFO:`/`WARNING:`/`ERROR:` prefix -- exactly one of the three, always at the start of the line (see `AGENTS.md`'s "CLI output" section for the full convention). `WARNING:` examples: "a config value was too small to be useful and got -normalized instead", "a requested detector isn't installed". `INFO:` +normalised instead", "a requested detector isn't installed". `INFO:` covers normal status the command wants a human to see, most importantly **generation being skipped or scoped down** -- e.g. a Hatchling build hook run that produced no SBOM because it's disabled or the target @@ -301,8 +301,8 @@ back a JSON file that looks complete but isn't: - **AI model formats**: broad but not universal coverage (GGUF, ONNX, PyTorch, PyTorch PT2/ExecuTorch, Safetensors, Keras, HDF5, NumPy, fastText, plus Hugging Face Hub models). A model in some other - serialization format isn't - recognized at all -- same "say so" rule applies rather than silently + serialisation format isn't + recognised at all -- same "say so" rule applies rather than silently skipping it. - **Unsupported build backend** for `loom project`/`loom generate` against a project directory -- check `pyproject.toml`'s diff --git a/skills/sbom-generate/references/examples.md b/skills/sbom-generate/references/examples.md index 2f0cd676..5d2264ed 100644 --- a/skills/sbom-generate/references/examples.md +++ b/skills/sbom-generate/references/examples.md @@ -1,6 +1,6 @@ --- Created: 2026-07-05 -Last-Modified: 2026-09-06 +Last-Modified: 2026-09-08 SPDX-FileCopyrightText: 2026-present Arthit Suriyawongkul SPDX-FileType: DOCUMENTATION SPDX-License-Identifier: CC0-1.0 diff --git a/skills/sbom-validate/SKILL.md b/skills/sbom-validate/SKILL.md index d4688524..ad63d889 100644 --- a/skills/sbom-validate/SKILL.md +++ b/skills/sbom-validate/SKILL.md @@ -1,6 +1,6 @@ --- # Created: 2026-08-10 -# Last-Modified: 2026-09-02 +# Last-Modified: 2026-09-08 # SPDX-FileCopyrightText: 2026-present Arthit Suriyawongkul # SPDX-FileType: SOURCE # SPDX-License-Identifier: Apache-2.0 diff --git a/skills/sbom-validate/references/examples.md b/skills/sbom-validate/references/examples.md index 6649db54..f84260c2 100644 --- a/skills/sbom-validate/references/examples.md +++ b/skills/sbom-validate/references/examples.md @@ -1,6 +1,6 @@ --- Created: 2026-08-10 -Last-Modified: 2026-08-30 +Last-Modified: 2026-09-08 SPDX-FileCopyrightText: 2026-present Arthit Suriyawongkul SPDX-FileType: DOCUMENTATION SPDX-License-Identifier: CC0-1.0 From a8d21dcdea68cf05bc3c6e3391a567252f78cbb4 Mon Sep 17 00:00:00 2001 From: Arthit Suriyawongkul Date: Wed, 9 Sep 2026 05:37:33 +0700 Subject: [PATCH 32/35] Fix license noassertion bug Signed-off-by: Arthit Suriyawongkul --- .../assemble/spdx3/_document_locked_deps.py | 10 +- src/pitloom/assemble/spdx3/deps_license.py | 38 ++- .../test_deps_enrichment_pypi_fallback.py | 46 +--- tests/assemble/test_deps_license.py | 145 +++++++++++ .../core/generator/test_generator_project.py | 57 ++++- tests/extract/conftest.py | 39 ++- tests/extract/test_hatch_hook_metadata.py | 26 ++ tests/extract/test_poetry_parsing.py | 38 +++ tests/extract/test_setuptools_cfg.py | 237 ++---------------- tests/extract/test_setuptools_cfg_backend.py | 231 +++++++++++++++++ tests/extract/test_setuptools_py.py | 15 ++ tests/extract/test_utils.py | 26 +- .../implementation/setuptools-support.md | 2 +- 13 files changed, 603 insertions(+), 307 deletions(-) create mode 100644 tests/assemble/test_deps_license.py create mode 100644 tests/extract/test_setuptools_cfg_backend.py diff --git a/src/pitloom/assemble/spdx3/_document_locked_deps.py b/src/pitloom/assemble/spdx3/_document_locked_deps.py index f2b785af..597e7321 100644 --- a/src/pitloom/assemble/spdx3/_document_locked_deps.py +++ b/src/pitloom/assemble/spdx3/_document_locked_deps.py @@ -8,10 +8,12 @@ detection, the exact-locked-version map, and the combined PyPI release-info prefetch. Split out of :mod:`pitloom.assemble.spdx3.document` to keep that module under this repo's file-size soft limit; every name that was already -public from that module before the split is re-exported from there, so -existing imports of those names from ``pitloom.assemble.spdx3.document`` -keep working. ``_dedup_and_locked_versions`` and ``_canon_names_and_pins`` -are new, module-internal to this split and not re-exported. +public from that module before the split is listed in its ``__all__`` and +re-exported from there, so existing imports of those names from +``pitloom.assemble.spdx3.document`` keep working. ``_dedup_and_locked_versions`` +and ``_canon_names_and_pins`` are new, module-internal to this split -- +``document.py`` imports the former for its own use in :func:`build` but +omits it from ``__all__``, and never imports the latter at all. See also: :mod:`pitloom.extract._lock_common` for the shared canonical-name-grouping and version-equality helpers this module builds on. diff --git a/src/pitloom/assemble/spdx3/deps_license.py b/src/pitloom/assemble/spdx3/deps_license.py index 5179ace0..7bcc9f79 100644 --- a/src/pitloom/assemble/spdx3/deps_license.py +++ b/src/pitloom/assemble/spdx3/deps_license.py @@ -419,7 +419,15 @@ def attach_main_package_license( encoder: ProvenanceEncoder | None = None, ) -> None: """Attach declared and/or concluded license elements and relationships for - the main Python project package.""" + the main Python project package. + + ``metadata.license_name`` truthy does not guarantee two-candidate mode: + when ``metadata.license_concluded`` is unset, :func:`build_license_elements` + still runs single-candidate on ``license_name``'s own provenance, which + can classify it as concluded (``rel_declared is None``) -- see the + comment on the ``elif`` branch below for why that branch, unlike this + one, needs its own NOASSERTION fallback for the symmetric case. + """ if metadata.license_name: spdx_doc.profileConformance.append(spdx3.ProfileIdentifierType.simpleLicensing) rel_declared, rel_concluded = build_license_elements( @@ -443,16 +451,7 @@ def attach_main_package_license( exporter.add_relationship(rel_concluded) elif metadata.license_concluded: spdx_doc.profileConformance.append(spdx3.ProfileIdentifierType.simpleLicensing) - _add_license_noassertion( - main_package, - spdx_ci, - metadata.name, - doc_uuid, - exporter, - provenance_config=provenance_config, - encoder=encoder, - ) - _rel_dec, rel_concluded = build_license_elements( + rel_declared, rel_concluded = build_license_elements( license_id=metadata.license_concluded, package_spdx_id=require_spdx_id(main_package), license_provenance=metadata.provenance.get( @@ -466,6 +465,23 @@ def attach_main_package_license( provenance_config=provenance_config, encoder=encoder, ) + # Unlike the `if` branch above, this branch has no second candidate + # at all -- when its one candidate (metadata.license_concluded) + # classifies as declared rather than concluded, that's the ONLY + # relationship this package can get, so it must be emitted for + # real, not silently dropped in favour of a NOASSERTION filler. + if rel_declared: + exporter.add_relationship(rel_declared) + else: + _add_license_noassertion( + main_package, + spdx_ci, + metadata.name, + doc_uuid, + exporter, + provenance_config=provenance_config, + encoder=encoder, + ) if rel_concluded: exporter.add_relationship(rel_concluded) else: diff --git a/tests/assemble/test_deps_enrichment_pypi_fallback.py b/tests/assemble/test_deps_enrichment_pypi_fallback.py index 769d43ed..deaccd12 100644 --- a/tests/assemble/test_deps_enrichment_pypi_fallback.py +++ b/tests/assemble/test_deps_enrichment_pypi_fallback.py @@ -9,6 +9,8 @@ See also: test_deps_enrichment_names_versions.py, test_deps_enrichment_originator_license.py, test_deps_enrichment_prefetch.py -- this module's siblings, split from the original test_deps_enrichment.py. +test_deps_license.py holds the deps_license.py unit tests split out of +this file. Covers the PyPI JSON API fallback (used when installed metadata doesn't cover a field) and the NOASSERTION policy for whatever neither source can @@ -28,11 +30,7 @@ from pitloom.assemble.spdx3 import deps_installed as deps_mod from pitloom.assemble.spdx3 import deps_pypi from pitloom.assemble.spdx3.deps import _enrich_from_pypi, add_dependencies -from pitloom.assemble.spdx3.deps_license import ( - _add_license_noassertion, - _build_license_relationship, - _get_or_create_license_element, -) +from pitloom.assemble.spdx3.deps_license import _add_license_noassertion from pitloom.assemble.spdx3.deps_originator import _resolve_metadata_url from pitloom.core.models import _clear_doc_counters, compute_doc_uuid, generate_spdx_id from pitloom.export.spdx3_json import Spdx3JsonExporter, require_spdx_id @@ -424,41 +422,3 @@ def test_enrich_from_pypi_unknown_version_skips_hash_extraction() -> None: ) assert "hash" not in filled assert not dep_pkg.verifiedUsing - - -# --------------------------------------------------------------------------- -# deps_license -- long-name truncation and the defensive raise -# --------------------------------------------------------------------------- - - -def test_get_or_create_license_element_truncates_long_name() -> None: - doc_uuid = compute_doc_uuid("longlicense", "1.0", []) - _clear_doc_counters(doc_uuid) - exporter = Spdx3JsonExporter() - ci = _make_ci() - long_id = "X" * 80 - - spdx_id = _get_or_create_license_element( - long_id, "Source: test", ci, "longlicense", doc_uuid, exporter - ) - - license_text = exporter.object_set.obj_by_id[spdx_id] - assert isinstance(license_text, spdx3.simplelicensing_SimpleLicensingText) - assert license_text.name == "X" * 57 + "..." - assert len(license_text.name) == 60 - - -def test_build_license_relationship_raises_when_relationship_build_fails() -> None: - """``build_relationship`` returns ``None`` when ``from_id`` is ``None``; - ``_build_license_relationship`` must fail loudly rather than silently - swallow it.""" - ci = _make_ci() - with pytest.raises(ValueError, match="Failed to build relationship"): - _build_license_relationship( - None, # type: ignore[arg-type] - "http://spdx.org/spdxdocs/license-1", - spdx3.RelationshipType.hasDeclaredLicense, - ci, - "doc", - "uuid", - ) diff --git a/tests/assemble/test_deps_license.py b/tests/assemble/test_deps_license.py new file mode 100644 index 00000000..e8abef99 --- /dev/null +++ b/tests/assemble/test_deps_license.py @@ -0,0 +1,145 @@ +# SPDX-FileContributor: Arthit Suriyawongkul +# SPDX-FileCopyrightText: 2026-present Arthit Suriyawongkul +# SPDX-FileType: SOURCE +# SPDX-License-Identifier: Apache-2.0 + +"""Unit tests for ``pitloom.assemble.spdx3.deps_license`` -- license-element +creation/truncation, declared-vs-concluded classification, and the +defensive relationship-build raise. Split out of +test_deps_enrichment_pypi_fallback.py to keep that file under this repo's +file-size soft limit. + +See also: test_deps_enrichment_originator_license.py for the +higher-level, pipeline-integration license tests. +""" + +# pylint: disable=protected-access +# pylint: disable=missing-function-docstring + +from __future__ import annotations + +import pytest +from spdx_python_model.bindings import v3_0_1 as spdx3 + +from pitloom.assemble.spdx3.deps_license import ( + _build_license_relationship, + _get_or_create_license_element, + _is_license_concluded, + build_license_elements, +) +from pitloom.core.models import _clear_doc_counters, compute_doc_uuid +from pitloom.export.spdx3_json import Spdx3JsonExporter + +from .conftest import _make_ci + + +def test_get_or_create_license_element_truncates_long_name() -> None: + doc_uuid = compute_doc_uuid("longlicense", "1.0", []) + _clear_doc_counters(doc_uuid) + exporter = Spdx3JsonExporter() + ci = _make_ci() + long_id = "X" * 80 + + spdx_id = _get_or_create_license_element( + long_id, "Source: test", ci, "longlicense", doc_uuid, exporter + ) + + license_text = exporter.object_set.obj_by_id[spdx_id] + assert isinstance(license_text, spdx3.simplelicensing_SimpleLicensingText) + assert license_text.name == "X" * 57 + "..." + assert len(license_text.name) == 60 + + +def test_get_or_create_license_element_truncates_at_first_newline() -> None: + """A multi-line license_id (e.g. full custom license text used as its + own LicenseRef identifier) must use only its first line as the + element's display name.""" + doc_uuid = compute_doc_uuid("multilinelicense", "1.0", []) + _clear_doc_counters(doc_uuid) + exporter = Spdx3JsonExporter() + ci = _make_ci() + multiline_id = "Custom License\nAll rights reserved.\nSee LICENSE for details." + + spdx_id = _get_or_create_license_element( + multiline_id, "Source: test", ci, "multilinelicense", doc_uuid, exporter + ) + + license_text = exporter.object_set.obj_by_id[spdx_id] + assert isinstance(license_text, spdx3.simplelicensing_SimpleLicensingText) + assert license_text.name == "Custom License" + assert license_text.simplelicensing_licenseText == multiline_id + + +def test_is_license_concluded_transparent_no_method_classified_as_declared() -> None: + """A transparent, method-less source (e.g. read straight out of + pyproject.toml, not detected/inferred) is classified as declared, not + concluded -- every shipped extractor's real license_concluded + provenance is non-transparent (a LICENSE/CITATION.cff/codemeta.json + scan), so this only fires for a value a future/library-API caller + supplies directly, but build_license_elements()'s single-candidate + mode must still classify it correctly rather than assuming "concluded" + just because the caller happened to populate license_concluded.""" + assert ( + _is_license_concluded({"source": "pyproject.toml", "field": "license"}) is False + ) + + +def test_is_license_concluded_non_transparent_source_classified_as_concluded() -> None: + """A non-transparent source (e.g. an independent LICENSE-file scan) is + classified as concluded even with no explicit Method: tag.""" + assert _is_license_concluded({"source": "LICENSE"}) is True + + +def test_is_license_concluded_method_tag_always_wins() -> None: + """An explicit Method: tag (a detection heuristic ran) always means + concluded, regardless of source transparency.""" + assert ( + _is_license_concluded( + {"source": "pyproject.toml", "method": "licenseid_detection"} + ) + is True + ) + + +def test_build_license_elements_single_candidate_transparent_source_is_declared() -> ( + None +): + """build_license_elements() in single-candidate mode, given a + transparent/method-less provenance, must return (rel_declared, + None) -- not silently return None for both halves, which would leave + the caller with no relationship to add at all for a value it does + have.""" + doc_uuid = compute_doc_uuid("single-candidate", "1.0", []) + _clear_doc_counters(doc_uuid) + exporter = Spdx3JsonExporter() + ci = _make_ci() + + rel_declared, rel_concluded = build_license_elements( + license_id="MIT", + package_spdx_id="https://example.com/Package-1", + license_provenance="Source: pyproject.toml | Field: project.license_concluded", + creation_info=ci, + doc_name="single-candidate", + doc_uuid=doc_uuid, + exporter=exporter, + ) + + assert rel_concluded is None + assert rel_declared is not None + assert rel_declared.relationshipType == spdx3.RelationshipType.hasDeclaredLicense + + +def test_build_license_relationship_raises_when_relationship_build_fails() -> None: + """``build_relationship`` returns ``None`` when ``from_id`` is ``None``; + ``_build_license_relationship`` must fail loudly rather than silently + swallow it.""" + ci = _make_ci() + with pytest.raises(ValueError, match="Failed to build relationship"): + _build_license_relationship( + None, # type: ignore[arg-type] + "http://spdx.org/spdxdocs/license-1", + spdx3.RelationshipType.hasDeclaredLicense, + ci, + "doc", + "uuid", + ) diff --git a/tests/core/generator/test_generator_project.py b/tests/core/generator/test_generator_project.py index 2b7e8ba0..29402720 100644 --- a/tests/core/generator/test_generator_project.py +++ b/tests/core/generator/test_generator_project.py @@ -353,7 +353,12 @@ def test_build_document_ai_model_license_adds_simple_licensing_profile() -> None def test_build_concluded_license_without_declared_license() -> None: """When license_name is None but license_concluded is present, - concluded license relationship must be emitted and simpleLicensing added.""" + concluded license relationship must be emitted and simpleLicensing added + -- and since there's no declared value at all, the declared side must + still get an explicit NOASSERTION relationship, not be silently absent + (the elif branch's NOASSERTION fallback exists specifically for this + concluded-classified sub-case, distinct from the declared-classified + one covered by test_build_transparent_concluded_license_classified_as_declared).""" project = ProjectMetadata( name="concluded-only", version="1.0.0", @@ -379,6 +384,56 @@ def test_build_concluded_license_without_declared_license() -> None: } assert licenses[concluded_rels[0]["to"][0]] == "MIT" + declared_rels = [ + r for r in rels if r.get("relationshipType") == "hasDeclaredLicense" + ] + assert len(declared_rels) == 1 + assert licenses[declared_rels[0]["to"][0]] == "NOASSERTION" + + +def test_build_transparent_concluded_license_classified_as_declared() -> None: + """When license_name is None but license_concluded is present with a + transparent, method-less provenance (e.g. read directly from + pyproject.toml, not detected/inferred), _is_license_concluded() + classifies it as declared rather than concluded -- build_license_elements() + then returns no concluded relationship at all, and attach_main_package_license() + must skip adding one rather than erroring on the None.""" + project = ProjectMetadata( + name="transparent-concluded", + version="1.0.0", + license_name=None, + license_concluded="MIT", + provenance={ + "license_concluded": ( + "Source: pyproject.toml | Field: project.license_concluded" + ) + }, + ) + doc = DocumentModel(project=project, creation_metadata=CreationMetadata()) + exporter = build(doc, offline=True) + graph = json.loads(exporter.to_json())["@graph"] + + rels = [e for e in graph if e.get("type") == "Relationship"] + concluded_rels = [ + r for r in rels if r.get("relationshipType") == "hasConcludedLicense" + ] + assert concluded_rels == [] + declared_rels = [ + r for r in rels if r.get("relationshipType") == "hasDeclaredLicense" + ] + assert len(declared_rels) == 1 + licenses = { + e["spdxId"]: e.get("simplelicensing_licenseText") + for e in graph + if e.get("type") == "simplelicensing_SimpleLicensingText" + } + # Must be the real MIT license, not a NOASSERTION filler -- reverting + # attach_main_package_license()'s use of the declared relationship + # build_license_elements() actually returns here would make this + # NOASSERTION again while MIT sat orphaned in the graph with no + # relationship pointing to it. + assert licenses[declared_rels[0]["to"][0]] == "MIT" + def test_generate_project_sbom_does_not_mutate_caller_files( tmp_path: Path, diff --git a/tests/extract/conftest.py b/tests/extract/conftest.py index 1163e2f4..39261014 100644 --- a/tests/extract/conftest.py +++ b/tests/extract/conftest.py @@ -9,6 +9,7 @@ import pytest from hatchling.plugin.manager import PluginManager # noqa: E402 +from pitloom.core.project import ProjectMetadata from pitloom.plugins.hatch import ( # noqa: E402 PitloomBuildHook, ) @@ -17,6 +18,19 @@ pytest.importorskip("hatchling", reason="hatchling is required for hook tests") + +def assert_declared_empty_authors_no_copyright_text(metadata: ProjectMetadata) -> None: + """Assert the shared contract every ``ProjectMetadata`` producer applies + to an explicitly declared but empty ``authors`` field: provenance is + still recorded (presence, not truthiness, gates it -- see AGENTS.md's + "tri-state signal" bullet), but with no author to derive a name from, + no ``copyright_text`` is inferred. + """ + assert metadata.authors == [] + assert "authors" in metadata.provenance + assert "copyright_text" not in metadata.provenance + + MINIMAL_PYPROJECT = """\ [build-system] requires = ["hatchling"] @@ -231,30 +245,7 @@ def _fake_hatch_metadata( "_FAKE_CORE_DEFAULTS", "_fake_hatch_metadata", "annotations", - "hatchling_metadata_core", - "make_hook", - "pytest", - "types", - "write_pyproject", - "write_pyproject_with_pitloom_config", -] - -__all__ = [ - "Any", - "CONFLICT_PYPROJECT", - "MINIMAL_PYPROJECT", - "MISSING_LICENSE_FILE_PYPROJECT", - "MISSING_README_PYPROJECT", - "POETRY_GAP_FILL_PYPROJECT", - "PYPROJECT_WITH_PRETTY", - "Path", - "PitloomBuildHook", - "PluginManager", - "SYNTHETIC_NONCANONICAL_PYPROJECT", - "SimpleNamespace", - "_FAKE_CORE_DEFAULTS", - "_fake_hatch_metadata", - "annotations", + "assert_declared_empty_authors_no_copyright_text", "hatchling_metadata_core", "make_hook", "pytest", diff --git a/tests/extract/test_hatch_hook_metadata.py b/tests/extract/test_hatch_hook_metadata.py index df51b818..9d3456b4 100644 --- a/tests/extract/test_hatch_hook_metadata.py +++ b/tests/extract/test_hatch_hook_metadata.py @@ -13,6 +13,7 @@ from pitloom.core.models import compute_doc_uuid # noqa: E402 from pitloom.extract._pyproject import read_pyproject # noqa: E402 from pitloom.extract.hatchling import ( # noqa: E402 + _hatchling_field_declared, _resolve_hatchling_license_files, metadata_from_hatchling, ) @@ -28,6 +29,7 @@ POETRY_GAP_FILL_PYPROJECT, SYNTHETIC_NONCANONICAL_PYPROJECT, _fake_hatch_metadata, + assert_declared_empty_authors_no_copyright_text, write_pyproject, ) @@ -65,6 +67,16 @@ def test_metadata_from_hatchling_maps_license_files() -> None: ) +def test_metadata_from_hatchling_declared_empty_authors_no_copyright_text() -> None: + """An explicitly declared but empty ``authors`` (``authors_data`` with + no names or emails) must still record provenance for ``authors``, but + with no authors to derive a name from, no ``copyright_text`` is + inferred.""" + hatch_meta = _fake_hatch_metadata(core={"authors_data": {"name": [], "email": []}}) + metadata = metadata_from_hatchling(hatch_meta, Path(".")) + assert_declared_empty_authors_no_copyright_text(metadata) + + def test_metadata_from_hatchling_empty_declared_dependencies_gets_provenance() -> None: """An explicitly declared but empty ``[project.dependencies]`` must still record provenance -- merge_project_metadata() relies on that @@ -174,6 +186,20 @@ def license_files(self) -> list[str]: assert _resolve_hatchling_license_files(_RaisingCore()) == [] +def test_hatchling_field_declared_tolerates_oserror_on_config_access() -> None: + """A ``core.config`` property access that raises ``OSError`` must + resolve to "not declared" (``False``), not propagate -- mirroring the + same class of lazily-evaluated Hatchling property failure every other + ``core.X`` read in this module tolerates.""" + + class _RaisingCore: + @property + def config(self) -> dict[str, object]: + raise OSError("simulated filesystem error") + + assert _hatchling_field_declared(_RaisingCore(), "dependencies") is False + + def test_metadata_from_hatchling_canonicalises_dependency_markers() -> None: """Dependency specifiers are normalised to ``packaging`` canonical form. diff --git a/tests/extract/test_poetry_parsing.py b/tests/extract/test_poetry_parsing.py index afbb3855..29fcb87f 100644 --- a/tests/extract/test_poetry_parsing.py +++ b/tests/extract/test_poetry_parsing.py @@ -24,6 +24,8 @@ extract_poetry_metadata, ) +from .conftest import assert_declared_empty_authors_no_copyright_text + # --------------------------------------------------------------------------- # _parse_poetry_authors # --------------------------------------------------------------------------- @@ -375,6 +377,42 @@ def test_extract_provenance_empty_declared_dependencies() -> None: assert "requires_python" in metadata.provenance +def test_extract_non_list_keywords_treated_as_empty() -> None: + """A malformed `keywords` value that isn't a list (e.g. a bare string) + must resolve to an empty list, not raise or pass the raw value + through.""" + data = { + "tool": { + "poetry": { + "name": "my-pkg", + "version": "1.0.0", + "keywords": "not-a-list", + } + } + } + with tempfile.TemporaryDirectory() as d: + metadata = extract_poetry_metadata(data, Path(d)) + assert metadata.keywords == [] + + +def test_extract_provenance_declared_empty_authors_no_copyright_text() -> None: + """An explicitly declared but empty `authors = []` must still record + provenance for `authors`, but with no authors to derive a name from, + no `copyright_text` is inferred.""" + data = { + "tool": { + "poetry": { + "name": "my-pkg", + "version": "1.0.0", + "authors": [], + } + } + } + with tempfile.TemporaryDirectory() as d: + metadata = extract_poetry_metadata(data, Path(d)) + assert_declared_empty_authors_no_copyright_text(metadata) + + def test_extract_provenance_wildcard_python_records_requires_python() -> None: """`python = "*"` (no real constraint) resolves requires_python to None, but that's a deliberate, explicitly-declared answer, not an diff --git a/tests/extract/test_setuptools_cfg.py b/tests/extract/test_setuptools_cfg.py index 66d5d097..d345b899 100644 --- a/tests/extract/test_setuptools_cfg.py +++ b/tests/extract/test_setuptools_cfg.py @@ -6,6 +6,8 @@ """Tests for metadata extraction from setup.cfg. See also: +- :mod:`tests.extract.test_setuptools_cfg_backend` for detect_build_backend() + tests, split out to keep this file under this repo's file-size soft limit. - :mod:`tests.extract.test_setuptools_cfg_config` for [tool:pitloom] config in setup.cfg. - :mod:`tests.extract.test_setuptools_py` for setup.py and merge/fixture tests. @@ -13,105 +15,15 @@ from __future__ import annotations -import logging import tempfile from pathlib import Path from unittest.mock import patch import pytest -from pitloom.extract._setuptools import detect_build_backend, read_setup_cfg +from pitloom.extract._setuptools import read_setup_cfg - -def test_detect_backend_hatchling() -> None: - """Detects hatchling backend from pyproject.toml build-backend key.""" - content = """ -[build-system] -requires = ["hatchling"] -build-backend = "hatchling.build" -""" - with tempfile.TemporaryDirectory() as d: - (Path(d) / "pyproject.toml").write_text(content) - assert detect_build_backend(Path(d)) == "hatchling" - - -def test_detect_backend_setuptools_in_pyproject() -> None: - """Detects setuptools backend when pyproject.toml declares setuptools.build_meta.""" - content = """ -[build-system] -requires = ["setuptools>=68"] -build-backend = "setuptools.build_meta" - -[project] -name = "mypackage" -version = "1.0.0" -""" - with tempfile.TemporaryDirectory() as d: - (Path(d) / "pyproject.toml").write_text(content) - assert detect_build_backend(Path(d)) == "setuptools" - - -def test_detect_backend_no_pyproject_with_setup_cfg() -> None: - """Infers setuptools backend when only setup.cfg exists.""" - with tempfile.TemporaryDirectory() as d: - (Path(d) / "setup.cfg").write_text("[metadata]\nname = pkg\n") - assert detect_build_backend(Path(d)) == "setuptools" - - -def test_detect_backend_no_pyproject_with_setup_py() -> None: - """Infers setuptools backend when only setup.py exists.""" - with tempfile.TemporaryDirectory() as d: - (Path(d) / "setup.py").write_text( - 'from setuptools import setup\nsetup(name="pkg")\n' - ) - assert detect_build_backend(Path(d)) == "setuptools" - - -def test_detect_backend_no_config_files() -> None: - """Returns None when no build configuration files are present.""" - with tempfile.TemporaryDirectory() as d: - assert detect_build_backend(Path(d)) is None - - -def test_detect_backend_malformed_pyproject_logs_and_returns_none( - caplog: pytest.LogCaptureFixture, -) -> None: - """A pyproject.toml that fails to parse is caught, logged, and returns None.""" - content = "[build-system\nbroken toml" - with tempfile.TemporaryDirectory() as d: - (Path(d) / "pyproject.toml").write_text(content) - with caplog.at_level(logging.DEBUG, logger="pitloom.extract._setuptools"): - result = detect_build_backend(Path(d)) - assert result is None - assert any("pyproject.toml" in r.message for r in caplog.records) - - -def test_detect_backend_explicit_none_pyproject_data_skips_reread() -> None: - """Regression: a caller that already parsed ``pyproject.toml`` itself - and got ``None`` (missing/unparseable) should be able to pass that - ``None`` straight through -- ``detect_build_backend`` must not - re-open and re-parse the same file a second time for the same - answer, distinguishing this from simply omitting the argument - (which does mean "please read it for me").""" - content = "[build-system\nbroken toml" - with tempfile.TemporaryDirectory() as d: - (Path(d) / "pyproject.toml").write_text(content) - with patch("pitloom.extract._setuptools.read_pyproject_toml") as mock_read: - result = detect_build_backend(Path(d), pyproject_data=None) - mock_read.assert_not_called() - assert result is None - - -def test_detect_backend_unknown_backend() -> None: - """Returns the raw backend string for unrecognised build backends.""" - content = """ -[build-system] -requires = ["meson-python"] -build-backend = "mesonpy" -""" - with tempfile.TemporaryDirectory() as d: - (Path(d) / "pyproject.toml").write_text(content) - assert detect_build_backend(Path(d)) == "mesonpy" +from .conftest import assert_declared_empty_authors_no_copyright_text def test_read_setup_cfg_basic() -> None: @@ -307,6 +219,17 @@ def test_read_setup_cfg_provenance() -> None: assert "inferred_from_authors" in metadata.provenance.get("copyright_text", "") +def test_read_setup_cfg_declared_empty_author_no_copyright_text() -> None: + """An explicitly declared but empty `author =` must still record + provenance for `authors`, but with no author to derive a name from, + no `copyright_text` is inferred.""" + content = "[metadata]\nname = pkg\nversion = 1.0\nauthor =\n" + with tempfile.TemporaryDirectory() as d: + (Path(d) / "setup.cfg").write_text(content) + metadata, _ = read_setup_cfg(Path(d)) + assert_declared_empty_authors_no_copyright_text(metadata) + + def test_read_setup_cfg_empty_install_requires_gets_provenance() -> None: """An explicitly declared but empty install_requires must still record provenance -- merge_project_metadata() relies on that presence to @@ -384,136 +307,6 @@ def test_read_setup_cfg_pitloom_config_sections() -> None: assert config.provenance.detail == "full" -def test_detect_build_backend_custom_backend() -> None: - """detect_build_backend returns prefix for unknown custom build backend.""" - from pitloom.extract._setuptools import detect_build_backend - - content = ( - "[build-system]\n" - 'requires = ["custom-build"]\n' - 'build-backend = "my_builder.api"\n' - ) - with tempfile.TemporaryDirectory() as d: - (Path(d) / "pyproject.toml").write_text(content) - assert detect_build_backend(Path(d)) == "my_builder" - - -def test_detect_build_backend_empty_string() -> None: - """detect_build_backend returns None when build-backend is empty string.""" - from pitloom.extract._setuptools import detect_build_backend - - content = '[build-system]\nrequires = ["custom-build"]\nbuild-backend = ""\n' - with tempfile.TemporaryDirectory() as d: - (Path(d) / "pyproject.toml").write_text(content) - assert detect_build_backend(Path(d)) is None - - -def test_detect_build_backend_pep518_only_falls_back_to_setuptools() -> None: - """A pyproject.toml with [build-system] but no build-backend key (a - legacy PEP 518-only declaration) is still detected as setuptools - when setup.cfg/setup.py back it up -- same fallback as when - pyproject.toml is absent entirely, not a silent None.""" - content = '[build-system]\nrequires = ["setuptools"]\n' - with tempfile.TemporaryDirectory() as d: - (Path(d) / "pyproject.toml").write_text(content) - (Path(d) / "setup.cfg").write_text("[metadata]\nname = pkg\n") - assert detect_build_backend(Path(d)) == "setuptools" - - -def test_detect_build_backend_unparseable_pyproject_falls_back_to_setuptools() -> None: - """Regression: a ``pyproject.toml`` that exists but is unparseable - (malformed TOML) must fall back to the same setup.cfg/setup.py check - as the file-absent and no-build-backend-key branches -- not return - ``None`` outright just because the file happens to exist.""" - from pitloom.extract._setuptools import detect_build_backend - - with tempfile.TemporaryDirectory() as d: - (Path(d) / "pyproject.toml").write_text("this is not [valid toml\n") - (Path(d) / "setup.cfg").write_text("[metadata]\nname = pkg\n") - assert detect_build_backend(Path(d)) == "setuptools" - - -def test_detect_build_backend_unparseable_pyproject_no_fallback_is_none() -> None: - """Same malformed-TOML case, but with no setup.cfg/setup.py to back - it up -- there is genuinely nothing to detect a backend from.""" - from pitloom.extract._setuptools import detect_build_backend - - with tempfile.TemporaryDirectory() as d: - (Path(d) / "pyproject.toml").write_text("this is not [valid toml\n") - assert detect_build_backend(Path(d)) is None - - -def test_detect_build_backend_non_dict_build_system_falls_back() -> None: - """Regression: a ``build-system`` key that isn't a table (e.g. a - stray top-level ``build-system = "..."`` scalar instead of a - ``[build-system]`` section -- valid TOML, just the wrong shape) must - not crash ``.get()`` on it -- treated the same as no build-backend - resolvable, falling back to the setup.cfg/setup.py check.""" - from pitloom.extract._setuptools import detect_build_backend - - content = 'build-system = "not-a-table"\n' - with tempfile.TemporaryDirectory() as d: - (Path(d) / "pyproject.toml").write_text(content) - (Path(d) / "setup.cfg").write_text("[metadata]\nname = pkg\n") - assert detect_build_backend(Path(d)) == "setuptools" - - -def test_detect_build_backend_non_string_build_backend_falls_back() -> None: - """Regression: a ``build-backend`` value that isn't a string (e.g. a - stray integer -- valid TOML, just the wrong type) must not crash on - string operations -- treated the same as no build-backend - resolvable, falling back to the setup.cfg/setup.py check.""" - from pitloom.extract._setuptools import detect_build_backend - - content = '[build-system]\nrequires = ["setuptools"]\nbuild-backend = 123\n' - with tempfile.TemporaryDirectory() as d: - (Path(d) / "pyproject.toml").write_text(content) - (Path(d) / "setup.cfg").write_text("[metadata]\nname = pkg\n") - assert detect_build_backend(Path(d)) == "setuptools" - - -def test_detect_build_backend_rejects_substring_lookalike() -> None: - """Regression: a build-backend whose top-level module merely - *contains* "setuptools" as a substring (but isn't setuptools) must - not be misdetected -- matching is on the top-level module name, not - substring containment.""" - content = ( - "[build-system]\n" - 'requires = ["my-setuptools-shim"]\n' - 'build-backend = "my_setuptools_shim.api"\n' - ) - with tempfile.TemporaryDirectory() as d: - (Path(d) / "pyproject.toml").write_text(content) - assert detect_build_backend(Path(d)) == "my_setuptools_shim" - - -def test_detect_build_backend_flit_core_alias() -> None: - """flit's actual top-level module is flit_core, not flit -- still - detected as the canonical "flit" identifier pitloom uses elsewhere.""" - content = ( - "[build-system]\n" - 'requires = ["flit_core"]\n' - 'build-backend = "flit_core.buildapi"\n' - ) - with tempfile.TemporaryDirectory() as d: - (Path(d) / "pyproject.toml").write_text(content) - assert detect_build_backend(Path(d)) == "flit" - - -def test_detect_build_backend_legacy_colon_suffix() -> None: - """A build-backend with a PEP 517 object-reference suffix - (``module:obj``) is still matched on its top-level module, ignoring - everything from the colon onward.""" - content = ( - "[build-system]\n" - 'requires = ["setuptools"]\n' - 'build-backend = "setuptools.build_meta:__legacy__"\n' - ) - with tempfile.TemporaryDirectory() as d: - (Path(d) / "pyproject.toml").write_text(content) - assert detect_build_backend(Path(d)) == "setuptools" - - def test_resolve_setuptools_license_without_provenance(tmp_path: Path) -> None: """_resolve_setuptools_license handles detected license with None provenance.""" from pitloom.core.project import ProjectMetadata diff --git a/tests/extract/test_setuptools_cfg_backend.py b/tests/extract/test_setuptools_cfg_backend.py new file mode 100644 index 00000000..04a255af --- /dev/null +++ b/tests/extract/test_setuptools_cfg_backend.py @@ -0,0 +1,231 @@ +# SPDX-FileContributor: Arthit Suriyawongkul +# SPDX-FileCopyrightText: 2026-present Arthit Suriyawongkul +# SPDX-FileType: SOURCE +# SPDX-License-Identifier: Apache-2.0 + +"""Tests for detect_build_backend() -- split out of test_setuptools_cfg.py +to keep that file under this repo's file-size soft limit. + +See also: :mod:`tests.extract.test_setuptools_cfg` for setup.cfg field +parsing tests. +""" + +from __future__ import annotations + +import logging +import tempfile +from pathlib import Path +from unittest.mock import patch + +import pytest + +from pitloom.extract._setuptools import detect_build_backend + + +def test_detect_backend_hatchling() -> None: + """Detects hatchling backend from pyproject.toml build-backend key.""" + content = """ +[build-system] +requires = ["hatchling"] +build-backend = "hatchling.build" +""" + with tempfile.TemporaryDirectory() as d: + (Path(d) / "pyproject.toml").write_text(content) + assert detect_build_backend(Path(d)) == "hatchling" + + +def test_detect_backend_setuptools_in_pyproject() -> None: + """Detects setuptools backend when pyproject.toml declares setuptools.build_meta.""" + content = """ +[build-system] +requires = ["setuptools>=68"] +build-backend = "setuptools.build_meta" + +[project] +name = "mypackage" +version = "1.0.0" +""" + with tempfile.TemporaryDirectory() as d: + (Path(d) / "pyproject.toml").write_text(content) + assert detect_build_backend(Path(d)) == "setuptools" + + +def test_detect_backend_no_pyproject_with_setup_cfg() -> None: + """Infers setuptools backend when only setup.cfg exists.""" + with tempfile.TemporaryDirectory() as d: + (Path(d) / "setup.cfg").write_text("[metadata]\nname = pkg\n") + assert detect_build_backend(Path(d)) == "setuptools" + + +def test_detect_backend_no_pyproject_with_setup_py() -> None: + """Infers setuptools backend when only setup.py exists.""" + with tempfile.TemporaryDirectory() as d: + (Path(d) / "setup.py").write_text( + 'from setuptools import setup\nsetup(name="pkg")\n' + ) + assert detect_build_backend(Path(d)) == "setuptools" + + +def test_detect_backend_no_config_files() -> None: + """Returns None when no build configuration files are present.""" + with tempfile.TemporaryDirectory() as d: + assert detect_build_backend(Path(d)) is None + + +def test_detect_backend_malformed_pyproject_logs_and_returns_none( + caplog: pytest.LogCaptureFixture, +) -> None: + """A pyproject.toml that fails to parse is caught, logged, and returns None.""" + content = "[build-system\nbroken toml" + with tempfile.TemporaryDirectory() as d: + (Path(d) / "pyproject.toml").write_text(content) + with caplog.at_level(logging.DEBUG, logger="pitloom.extract._setuptools"): + result = detect_build_backend(Path(d)) + assert result is None + assert any("pyproject.toml" in r.message for r in caplog.records) + + +def test_detect_backend_explicit_none_pyproject_data_skips_reread() -> None: + """Regression: a caller that already parsed ``pyproject.toml`` itself + and got ``None`` (missing/unparseable) should be able to pass that + ``None`` straight through -- ``detect_build_backend`` must not + re-open and re-parse the same file a second time for the same + answer, distinguishing this from simply omitting the argument + (which does mean "please read it for me").""" + content = "[build-system\nbroken toml" + with tempfile.TemporaryDirectory() as d: + (Path(d) / "pyproject.toml").write_text(content) + with patch("pitloom.extract._setuptools.read_pyproject_toml") as mock_read: + result = detect_build_backend(Path(d), pyproject_data=None) + mock_read.assert_not_called() + assert result is None + + +def test_detect_backend_unknown_backend() -> None: + """Returns the raw backend string for unrecognised build backends.""" + content = """ +[build-system] +requires = ["meson-python"] +build-backend = "mesonpy" +""" + with tempfile.TemporaryDirectory() as d: + (Path(d) / "pyproject.toml").write_text(content) + assert detect_build_backend(Path(d)) == "mesonpy" + + +def test_detect_build_backend_custom_backend() -> None: + """detect_build_backend returns prefix for unknown custom build backend.""" + content = ( + "[build-system]\n" + 'requires = ["custom-build"]\n' + 'build-backend = "my_builder.api"\n' + ) + with tempfile.TemporaryDirectory() as d: + (Path(d) / "pyproject.toml").write_text(content) + assert detect_build_backend(Path(d)) == "my_builder" + + +def test_detect_build_backend_empty_string() -> None: + """detect_build_backend returns None when build-backend is empty string.""" + content = '[build-system]\nrequires = ["custom-build"]\nbuild-backend = ""\n' + with tempfile.TemporaryDirectory() as d: + (Path(d) / "pyproject.toml").write_text(content) + assert detect_build_backend(Path(d)) is None + + +def test_detect_build_backend_pep518_only_falls_back_to_setuptools() -> None: + """A pyproject.toml with [build-system] but no build-backend key (a + legacy PEP 518-only declaration) is still detected as setuptools + when setup.cfg/setup.py back it up -- same fallback as when + pyproject.toml is absent entirely, not a silent None.""" + content = '[build-system]\nrequires = ["setuptools"]\n' + with tempfile.TemporaryDirectory() as d: + (Path(d) / "pyproject.toml").write_text(content) + (Path(d) / "setup.cfg").write_text("[metadata]\nname = pkg\n") + assert detect_build_backend(Path(d)) == "setuptools" + + +def test_detect_build_backend_unparseable_pyproject_falls_back_to_setuptools() -> None: + """Regression: a ``pyproject.toml`` that exists but is unparseable + (malformed TOML) must fall back to the same setup.cfg/setup.py check + as the file-absent and no-build-backend-key branches -- not return + ``None`` outright just because the file happens to exist.""" + with tempfile.TemporaryDirectory() as d: + (Path(d) / "pyproject.toml").write_text("this is not [valid toml\n") + (Path(d) / "setup.cfg").write_text("[metadata]\nname = pkg\n") + assert detect_build_backend(Path(d)) == "setuptools" + + +def test_detect_build_backend_unparseable_pyproject_no_fallback_is_none() -> None: + """Same malformed-TOML case, but with no setup.cfg/setup.py to back + it up -- there is genuinely nothing to detect a backend from.""" + with tempfile.TemporaryDirectory() as d: + (Path(d) / "pyproject.toml").write_text("this is not [valid toml\n") + assert detect_build_backend(Path(d)) is None + + +def test_detect_build_backend_non_dict_build_system_falls_back() -> None: + """Regression: a ``build-system`` key that isn't a table (e.g. a + stray top-level ``build-system = "..."`` scalar instead of a + ``[build-system]`` section -- valid TOML, just the wrong shape) must + not crash ``.get()`` on it -- treated the same as no build-backend + resolvable, falling back to the setup.cfg/setup.py check.""" + content = 'build-system = "not-a-table"\n' + with tempfile.TemporaryDirectory() as d: + (Path(d) / "pyproject.toml").write_text(content) + (Path(d) / "setup.cfg").write_text("[metadata]\nname = pkg\n") + assert detect_build_backend(Path(d)) == "setuptools" + + +def test_detect_build_backend_non_string_build_backend_falls_back() -> None: + """Regression: a ``build-backend`` value that isn't a string (e.g. a + stray integer -- valid TOML, just the wrong type) must not crash on + string operations -- treated the same as no build-backend + resolvable, falling back to the setup.cfg/setup.py check.""" + content = '[build-system]\nrequires = ["setuptools"]\nbuild-backend = 123\n' + with tempfile.TemporaryDirectory() as d: + (Path(d) / "pyproject.toml").write_text(content) + (Path(d) / "setup.cfg").write_text("[metadata]\nname = pkg\n") + assert detect_build_backend(Path(d)) == "setuptools" + + +def test_detect_build_backend_rejects_substring_lookalike() -> None: + """Regression: a build-backend whose top-level module merely + *contains* "setuptools" as a substring (but isn't setuptools) must + not be misdetected -- matching is on the top-level module name, not + substring containment.""" + content = ( + "[build-system]\n" + 'requires = ["my-setuptools-shim"]\n' + 'build-backend = "my_setuptools_shim.api"\n' + ) + with tempfile.TemporaryDirectory() as d: + (Path(d) / "pyproject.toml").write_text(content) + assert detect_build_backend(Path(d)) == "my_setuptools_shim" + + +def test_detect_build_backend_flit_core_alias() -> None: + """flit's actual top-level module is flit_core, not flit -- still + detected as the canonical "flit" identifier pitloom uses elsewhere.""" + content = ( + "[build-system]\n" + 'requires = ["flit_core"]\n' + 'build-backend = "flit_core.buildapi"\n' + ) + with tempfile.TemporaryDirectory() as d: + (Path(d) / "pyproject.toml").write_text(content) + assert detect_build_backend(Path(d)) == "flit" + + +def test_detect_build_backend_legacy_colon_suffix() -> None: + """A build-backend with a PEP 517 object-reference suffix + (``module:obj``) is still matched on its top-level module, ignoring + everything from the colon onward.""" + content = ( + "[build-system]\n" + 'requires = ["setuptools"]\n' + 'build-backend = "setuptools.build_meta:__legacy__"\n' + ) + with tempfile.TemporaryDirectory() as d: + (Path(d) / "pyproject.toml").write_text(content) + assert detect_build_backend(Path(d)) == "setuptools" diff --git a/tests/extract/test_setuptools_py.py b/tests/extract/test_setuptools_py.py index bd1b97c7..c4b4b6bb 100644 --- a/tests/extract/test_setuptools_py.py +++ b/tests/extract/test_setuptools_py.py @@ -20,6 +20,8 @@ from pitloom.extract._setuptools import read_setup_py +from .conftest import assert_declared_empty_authors_no_copyright_text + FIXTURE_DIR = Path(__file__).parent.parent / "fixtures" SETUPTOOLS_FIXTURE = FIXTURE_DIR / "projects" / "sampleproject-setuptools" @@ -164,6 +166,19 @@ def test_read_setup_py_empty_install_requires_gets_provenance() -> None: assert "dependencies" in metadata.provenance +def test_read_setup_py_declared_empty_author_no_copyright_text() -> None: + """An explicitly declared but empty author='' must still record + provenance for `authors`, but with no author to derive a name from, + no `copyright_text` is inferred.""" + content = ( + "from setuptools import setup\nsetup(name='pkg', version='1.0', author='')\n" + ) + with tempfile.TemporaryDirectory() as d: + (Path(d) / "setup.py").write_text(content) + metadata, _ = read_setup_py(Path(d)) + assert_declared_empty_authors_no_copyright_text(metadata) + + def test_read_setup_py_empty_python_requires_gets_provenance() -> None: """An explicitly declared but empty python_requires='' must still record provenance -- merge_project_metadata() relies on that presence diff --git a/tests/extract/test_utils.py b/tests/extract/test_utils.py index aa0eefdb..e2713d54 100644 --- a/tests/extract/test_utils.py +++ b/tests/extract/test_utils.py @@ -9,7 +9,31 @@ from __future__ import annotations -from pitloom.extract._extract_utils import record_dict_field_provenance +from pitloom.extract._extract_utils import field_declared, record_dict_field_provenance + + +class _RaisesOSErrorOnContains: + """A container whose ``__contains__`` raises OSError, mirroring a + Hatchling ``core.config``-style property accessor that can fail the + same way its other property accessors do.""" + + def __contains__(self, key: object) -> bool: + raise OSError("simulated backend failure") + + +def test_field_declared_true_for_present_key() -> None: + assert field_declared({"keywords": []}, "keywords") is True + + +def test_field_declared_false_for_absent_key() -> None: + assert field_declared({}, "keywords") is False + + +def test_field_declared_false_on_oserror() -> None: + """An OSError from the container's own ``__contains__`` is treated as + "not declared", not propagated -- the same defensive contract + documented in the function's own docstring.""" + assert field_declared(_RaisesOSErrorOnContains(), "keywords") is False def test_record_dict_field_provenance_per_key() -> None: diff --git a/working-docs/implementation/setuptools-support.md b/working-docs/implementation/setuptools-support.md index b0744df6..6f128f67 100644 --- a/working-docs/implementation/setuptools-support.md +++ b/working-docs/implementation/setuptools-support.md @@ -33,7 +33,7 @@ initial setuptools support added in the `setuptools-support` branch. | `src/pitloom/extract/_setuptools_py.py` | `setup.py` AST metadata parser (split from `_setuptools.py`) | | `src/pitloom/extract/project.py` | Shared resolver (`read_project()`) used by both the CLI and `generate_project_sbom()` | | `src/pitloom/cli/` | CLI updated to accept projects without `pyproject.toml` (originally in `__main__.py`, since split into `cli/` -- see `cli-test-coverage-roadmap.md`) | -| `tests/extract/test_setuptools_cfg.py`, `test_setuptools_cfg_config.py`, `test_setuptools_py.py`, `test_setuptools_integration.py` | Unit and integration tests (originally `tests/test_setuptools.py`, later split into these modular suites -- see `cli-test-coverage-roadmap.md`) | +| `tests/extract/test_setuptools_cfg.py`, `test_setuptools_cfg_backend.py`, `test_setuptools_cfg_config.py`, `test_setuptools_py.py`, `test_setuptools_integration.py` | Unit and integration tests (originally `tests/test_setuptools.py`, later split into these modular suites -- see `cli-test-coverage-roadmap.md`) | | `tests/fixtures/projects/sampleproject-setuptools/` | Transitional-layout fixture project | | `src/pitloom/core/_models_wheel.py` | Backend-dispatch facade for wheel file discovery (`get_wheel_files()`), shared per-file hashing/header loop | | `src/pitloom/core/_models_wheel_setuptools.py` | Setuptools wheel file discovery -- static config only, see below | From 43416bb7461c9ec8ba445f43a242f8eaaf6e84f4 Mon Sep 17 00:00:00 2001 From: Arthit Suriyawongkul Date: Wed, 9 Sep 2026 05:54:21 +0700 Subject: [PATCH 33/35] Fix build_license_elements bug Signed-off-by: Arthit Suriyawongkul --- src/pitloom/assemble/spdx3/deps_license.py | 10 +- tests/assemble/test_deps_license.py | 27 ++ .../core/generator/test_generator_project.py | 11 +- tests/extract/test_hatch_hook_metadata.py | 132 +------ .../test_hatch_hook_metadata_parity.py | 155 ++++++++ tests/extract/test_poetry_extract.py | 285 +++++++++++++++ tests/extract/test_poetry_parsing.py | 274 +------------- tests/extract/test_pyproject.py | 329 +---------------- tests/extract/test_pyproject_license.py | 345 ++++++++++++++++++ .../complexity-and-file-size-roadmap.md | 10 +- 10 files changed, 854 insertions(+), 724 deletions(-) create mode 100644 tests/extract/test_hatch_hook_metadata_parity.py create mode 100644 tests/extract/test_poetry_extract.py create mode 100644 tests/extract/test_pyproject_license.py diff --git a/src/pitloom/assemble/spdx3/deps_license.py b/src/pitloom/assemble/spdx3/deps_license.py index 7bcc9f79..02e8e10a 100644 --- a/src/pitloom/assemble/spdx3/deps_license.py +++ b/src/pitloom/assemble/spdx3/deps_license.py @@ -143,7 +143,7 @@ def build_license_elements( """Get or create SimpleLicensingText element(s) and build declared/concluded license relationships. - Single-candidate mode (*concluded_license_id* omitted, the default): + Single-candidate mode (*concluded_license_id* falsy, the default): unchanged behavior -- one element, classified as declared XOR concluded via :func:`_is_license_concluded` on *license_provenance*. @@ -156,8 +156,12 @@ def build_license_elements( license element. When they disagree, an additional G2 conflict Annotation is emitted on *package_spdx_id* recording both candidates; see :func:`~pitloom.assemble.spdx3.provenance.build_conflict_annotation`. + + Dispatches on truthiness, not just ``is None`` -- unlike ``requires_python``, + a license id is never meaningfully ``""``, so that must not route into + two-candidate mode with a spurious empty second candidate. """ - if concluded_license_id is None: + if not concluded_license_id: license_spdx_id = _get_or_create_license_element( license_id, license_provenance, @@ -422,7 +426,7 @@ def attach_main_package_license( the main Python project package. ``metadata.license_name`` truthy does not guarantee two-candidate mode: - when ``metadata.license_concluded`` is unset, :func:`build_license_elements` + when ``metadata.license_concluded`` is falsy, :func:`build_license_elements` still runs single-candidate on ``license_name``'s own provenance, which can classify it as concluded (``rel_declared is None``) -- see the comment on the ``elif`` branch below for why that branch, unlike this diff --git a/tests/assemble/test_deps_license.py b/tests/assemble/test_deps_license.py index e8abef99..24b4b7db 100644 --- a/tests/assemble/test_deps_license.py +++ b/tests/assemble/test_deps_license.py @@ -129,6 +129,33 @@ def test_build_license_elements_single_candidate_transparent_source_is_declared( assert rel_declared.relationshipType == spdx3.RelationshipType.hasDeclaredLicense +def test_build_license_elements_empty_string_concluded_id_is_single_candidate() -> None: + """An empty-string concluded_license_id (never a meaningful license id, + unlike a field like requires_python where "" can mean "explicitly no + constraint") must dispatch to single-candidate mode exactly like None + -- not silently enter two-candidate mode with a spurious empty second + candidate.""" + doc_uuid = compute_doc_uuid("empty-concluded", "1.0", []) + _clear_doc_counters(doc_uuid) + exporter = Spdx3JsonExporter() + ci = _make_ci() + + rel_declared, rel_concluded = build_license_elements( + license_id="MIT", + package_spdx_id="https://example.com/Package-1", + license_provenance="Source: pyproject.toml | Field: project.license", + creation_info=ci, + doc_name="empty-concluded", + doc_uuid=doc_uuid, + exporter=exporter, + concluded_license_id="", + ) + + assert rel_concluded is None + assert rel_declared is not None + assert rel_declared.relationshipType == spdx3.RelationshipType.hasDeclaredLicense + + def test_build_license_relationship_raises_when_relationship_build_fails() -> None: """``build_relationship`` returns ``None`` when ``from_id`` is ``None``; ``_build_license_relationship`` must fail loudly rather than silently diff --git a/tests/core/generator/test_generator_project.py b/tests/core/generator/test_generator_project.py index 29402720..71fb7a6a 100644 --- a/tests/core/generator/test_generator_project.py +++ b/tests/core/generator/test_generator_project.py @@ -372,7 +372,16 @@ def test_build_concluded_license_without_declared_license() -> None: spdx_doc = next(e for e in graph if e.get("type") == "SpdxDocument") assert "simpleLicensing" in spdx_doc["profileConformance"] - rels = [e for e in graph if e.get("type") == "Relationship"] + main_package_ids = { + e["spdxId"] + for e in graph + if e.get("type") == "software_Package" and e.get("name") == "concluded-only" + } + rels = [ + e + for e in graph + if e.get("type") == "Relationship" and e.get("from") in main_package_ids + ] concluded_rels = [ r for r in rels if r.get("relationshipType") == "hasConcludedLicense" ] diff --git a/tests/extract/test_hatch_hook_metadata.py b/tests/extract/test_hatch_hook_metadata.py index 9d3456b4..4cd31d87 100644 --- a/tests/extract/test_hatch_hook_metadata.py +++ b/tests/extract/test_hatch_hook_metadata.py @@ -1,4 +1,12 @@ # ruff: noqa: F403, F405 +"""Tests for metadata_from_hatchling()'s field-mapping and edge-case +behavior. + +See also: test_hatch_hook_metadata_parity.py for the CLI-vs-hook +metadata-parity tests, split out to keep this file under this repo's +file-size soft limit. +""" + from __future__ import annotations import tempfile @@ -10,8 +18,6 @@ import pytest from hatchling.plugin.manager import PluginManager # noqa: E402 -from pitloom.core.models import compute_doc_uuid # noqa: E402 -from pitloom.extract._pyproject import read_pyproject # noqa: E402 from pitloom.extract.hatchling import ( # noqa: E402 _hatchling_field_declared, _resolve_hatchling_license_files, @@ -22,12 +28,10 @@ ) from .conftest import ( - CONFLICT_PYPROJECT, MINIMAL_PYPROJECT, MISSING_LICENSE_FILE_PYPROJECT, MISSING_README_PYPROJECT, POETRY_GAP_FILL_PYPROJECT, - SYNTHETIC_NONCANONICAL_PYPROJECT, _fake_hatch_metadata, assert_declared_empty_authors_no_copyright_text, write_pyproject, @@ -216,126 +220,6 @@ def test_metadata_from_hatchling_canonicalises_dependency_markers() -> None: assert metadata.dependencies == ['tomli>=2.0.0; python_version < "3.11"'] -def test_metadata_from_hatchling_matches_read_pyproject_for_uuid() -> None: - """Hook and CLI paths must yield the same doc UUID for a static project. - - Regression guard: switching the hook to Hatchling's resolved metadata - must not change the document identity of a project whose metadata is - fully static (as Pitloom's own is). - """ - root = Path(__file__).resolve().parent.parent.parent - cli_meta, _ = read_pyproject(root / "pyproject.toml") - hatch_pm = hatchling_metadata_core.ProjectMetadata(str(root), PluginManager()) - hook_meta = metadata_from_hatchling(hatch_pm, root) - - assert hook_meta.name == cli_meta.name - assert hook_meta.version == cli_meta.version - assert hook_meta.dependencies == cli_meta.dependencies - assert compute_doc_uuid( - hook_meta.name, hook_meta.version or "x", hook_meta.dependencies - ) == compute_doc_uuid(cli_meta.name, cli_meta.version or "x", cli_meta.dependencies) - - -def test_metadata_from_hatchling_matches_read_pyproject_for_noncanonical_name() -> None: - """CLI and hook paths must agree even when the name/deps are non-canonical. - - Regression guard for the gap the earlier, name-only ``raw_name`` fix and - the marker-only ``_normalize_dependencies`` helper both missed: a project - name with an uppercase letter, underscore, and dot (``My_Package.Extra``) - and dependency names with an underscore (``typing_extensions``) and a dot - (``zope.interface``). Before this fix, Hatchling's own PEP 503 - normalisation made the hook report ``name == "my-package-extra"`` and - canonicalised dependency names, while the CLI path left both untouched, - giving the same project two different deterministic document UUIDs - depending on which path generated the SBOM. - """ - with tempfile.TemporaryDirectory() as tmp: - tmp_path = Path(tmp) - write_pyproject(tmp_path, SYNTHETIC_NONCANONICAL_PYPROJECT) - - cli_meta, _ = read_pyproject(tmp_path / "pyproject.toml") - hatch_pm = hatchling_metadata_core.ProjectMetadata( - str(tmp_path), PluginManager() - ) - hook_meta = metadata_from_hatchling(hatch_pm, tmp_path) - - assert cli_meta.name == "My_Package.Extra" - assert hook_meta.name == "My_Package.Extra" - assert hook_meta.name == cli_meta.name - - expected_deps = ["typing-extensions>=4.0", "zope-interface>=5.0"] - assert cli_meta.dependencies == expected_deps - assert hook_meta.dependencies == expected_deps - - assert compute_doc_uuid( - hook_meta.name, hook_meta.version or "x", hook_meta.dependencies - ) == compute_doc_uuid( - cli_meta.name, cli_meta.version or "x", cli_meta.dependencies - ) - - -def test_metadata_from_hatchling_matches_read_pyproject_for_license_conflict() -> None: - """CLI and hook paths must agree on G2 when the declared license and an - independently-detected LICENSE file disagree. - - Regression guard for the systemic gap ``resolve_license_concluded()`` - exists to close: the Hatchling build-hook path - (:func:`~pitloom.extract.hatchling.metadata_from_hatchling`) originally - called :func:`~pitloom.extract._license.detect_license_for_project` - directly and never ran the independent directory scan at all, so G2 - only ever fired via the CLI's - :func:`~pitloom.extract._pyproject.read_pyproject`. Both paths must now - resolve the same ``license_concluded`` value for the same project. - """ - with tempfile.TemporaryDirectory() as tmp: - tmp_path = Path(tmp) - write_pyproject(tmp_path, CONFLICT_PYPROJECT) - (tmp_path / "LICENSE").write_text( - "Apache License\nVersion 2.0" + "x" * 200, encoding="utf-8" - ) - - with patch( - "pitloom.extract._license.detect_license_from_text", - return_value="Apache-2.0", - ): - cli_meta, _ = read_pyproject(tmp_path / "pyproject.toml") - hatch_pm = hatchling_metadata_core.ProjectMetadata( - str(tmp_path), PluginManager() - ) - hook_meta = metadata_from_hatchling(hatch_pm, tmp_path) - - assert cli_meta.license_name == "MIT" - assert hook_meta.license_name == "MIT" - assert cli_meta.license_concluded == "Apache-2.0" - assert hook_meta.license_concluded == cli_meta.license_concluded - - -def test_metadata_from_hatchling_matches_read_pyproject_for_license_agreement() -> None: - """Same as above, but declared and detected agree: both paths must - still populate ``license_concluded`` (equal to the declared value), - not just leave it unset -- G2 records both sides regardless of - agreement.""" - with tempfile.TemporaryDirectory() as tmp: - tmp_path = Path(tmp) - write_pyproject(tmp_path, CONFLICT_PYPROJECT) - (tmp_path / "LICENSE").write_text( - "MIT License\n\nPermission" + "x" * 200, encoding="utf-8" - ) - - with patch( - "pitloom.extract._license.detect_license_from_text", - return_value="MIT", - ): - cli_meta, _ = read_pyproject(tmp_path / "pyproject.toml") - hatch_pm = hatchling_metadata_core.ProjectMetadata( - str(tmp_path), PluginManager() - ) - hook_meta = metadata_from_hatchling(hatch_pm, tmp_path) - - assert cli_meta.license_concluded == "MIT" - assert hook_meta.license_concluded == "MIT" - - def test_metadata_from_hatchling_maps_urls() -> None: """Resolved project URLs must be carried over verbatim.""" hatch_meta = _fake_hatch_metadata( diff --git a/tests/extract/test_hatch_hook_metadata_parity.py b/tests/extract/test_hatch_hook_metadata_parity.py new file mode 100644 index 00000000..25599e52 --- /dev/null +++ b/tests/extract/test_hatch_hook_metadata_parity.py @@ -0,0 +1,155 @@ +# SPDX-FileContributor: Arthit Suriyawongkul +# SPDX-FileCopyrightText: 2026-present Arthit Suriyawongkul +# SPDX-FileType: SOURCE +# SPDX-License-Identifier: Apache-2.0 + +# ruff: noqa: F403, F405 +"""CLI-vs-Hatchling-build-hook metadata-parity tests -- both entry points +(``read_pyproject()`` and ``metadata_from_hatchling()``) must resolve the +same metadata for the same project, since a mismatch changes the +deterministic document UUID depending on which path generated the SBOM. +Split out of test_hatch_hook_metadata.py to keep that file under this +repo's file-size soft limit. + +See also: test_hatch_hook_metadata.py for the rest of +metadata_from_hatchling()'s field-mapping and edge-case tests. +""" + +from __future__ import annotations + +import tempfile +from pathlib import Path +from unittest.mock import patch + +import hatchling.metadata.core as hatchling_metadata_core # noqa: E402 +from hatchling.plugin.manager import PluginManager # noqa: E402 + +from pitloom.core.models import compute_doc_uuid # noqa: E402 +from pitloom.extract._pyproject import read_pyproject # noqa: E402 +from pitloom.extract.hatchling import metadata_from_hatchling # noqa: E402 + +from .conftest import ( + CONFLICT_PYPROJECT, + SYNTHETIC_NONCANONICAL_PYPROJECT, + write_pyproject, +) + + +def test_metadata_from_hatchling_matches_read_pyproject_for_uuid() -> None: + """Hook and CLI paths must yield the same doc UUID for a static project. + + Regression guard: switching the hook to Hatchling's resolved metadata + must not change the document identity of a project whose metadata is + fully static (as Pitloom's own is). + """ + root = Path(__file__).resolve().parent.parent.parent + cli_meta, _ = read_pyproject(root / "pyproject.toml") + hatch_pm = hatchling_metadata_core.ProjectMetadata(str(root), PluginManager()) + hook_meta = metadata_from_hatchling(hatch_pm, root) + + assert hook_meta.name == cli_meta.name + assert hook_meta.version == cli_meta.version + assert hook_meta.dependencies == cli_meta.dependencies + assert compute_doc_uuid( + hook_meta.name, hook_meta.version or "x", hook_meta.dependencies + ) == compute_doc_uuid(cli_meta.name, cli_meta.version or "x", cli_meta.dependencies) + + +def test_metadata_from_hatchling_matches_read_pyproject_for_noncanonical_name() -> None: + """CLI and hook paths must agree even when the name/deps are non-canonical. + + Regression guard for the gap the earlier, name-only ``raw_name`` fix and + the marker-only ``_normalize_dependencies`` helper both missed: a project + name with an uppercase letter, underscore, and dot (``My_Package.Extra``) + and dependency names with an underscore (``typing_extensions``) and a dot + (``zope.interface``). Before this fix, Hatchling's own PEP 503 + normalisation made the hook report ``name == "my-package-extra"`` and + canonicalised dependency names, while the CLI path left both untouched, + giving the same project two different deterministic document UUIDs + depending on which path generated the SBOM. + """ + with tempfile.TemporaryDirectory() as tmp: + tmp_path = Path(tmp) + write_pyproject(tmp_path, SYNTHETIC_NONCANONICAL_PYPROJECT) + + cli_meta, _ = read_pyproject(tmp_path / "pyproject.toml") + hatch_pm = hatchling_metadata_core.ProjectMetadata( + str(tmp_path), PluginManager() + ) + hook_meta = metadata_from_hatchling(hatch_pm, tmp_path) + + assert cli_meta.name == "My_Package.Extra" + assert hook_meta.name == "My_Package.Extra" + assert hook_meta.name == cli_meta.name + + expected_deps = ["typing-extensions>=4.0", "zope-interface>=5.0"] + assert cli_meta.dependencies == expected_deps + assert hook_meta.dependencies == expected_deps + + assert compute_doc_uuid( + hook_meta.name, hook_meta.version or "x", hook_meta.dependencies + ) == compute_doc_uuid( + cli_meta.name, cli_meta.version or "x", cli_meta.dependencies + ) + + +def test_metadata_from_hatchling_matches_read_pyproject_for_license_conflict() -> None: + """CLI and hook paths must agree on G2 when the declared license and an + independently-detected LICENSE file disagree. + + Regression guard for the systemic gap ``resolve_license_concluded()`` + exists to close: the Hatchling build-hook path + (:func:`~pitloom.extract.hatchling.metadata_from_hatchling`) originally + called :func:`~pitloom.extract._license.detect_license_for_project` + directly and never ran the independent directory scan at all, so G2 + only ever fired via the CLI's + :func:`~pitloom.extract._pyproject.read_pyproject`. Both paths must now + resolve the same ``license_concluded`` value for the same project. + """ + with tempfile.TemporaryDirectory() as tmp: + tmp_path = Path(tmp) + write_pyproject(tmp_path, CONFLICT_PYPROJECT) + (tmp_path / "LICENSE").write_text( + "Apache License\nVersion 2.0" + "x" * 200, encoding="utf-8" + ) + + with patch( + "pitloom.extract._license.detect_license_from_text", + return_value="Apache-2.0", + ): + cli_meta, _ = read_pyproject(tmp_path / "pyproject.toml") + hatch_pm = hatchling_metadata_core.ProjectMetadata( + str(tmp_path), PluginManager() + ) + hook_meta = metadata_from_hatchling(hatch_pm, tmp_path) + + assert cli_meta.license_name == "MIT" + assert hook_meta.license_name == "MIT" + assert cli_meta.license_concluded == "Apache-2.0" + assert hook_meta.license_concluded == cli_meta.license_concluded + + +def test_metadata_from_hatchling_matches_read_pyproject_for_license_agreement() -> None: + """Same as above, but declared and detected agree: both paths must + still populate ``license_concluded`` (equal to the declared value), + not just leave it unset -- G2 records both sides regardless of + agreement.""" + with tempfile.TemporaryDirectory() as tmp: + tmp_path = Path(tmp) + write_pyproject(tmp_path, CONFLICT_PYPROJECT) + (tmp_path / "LICENSE").write_text( + "MIT License\n\nPermission" + "x" * 200, encoding="utf-8" + ) + + with patch( + "pitloom.extract._license.detect_license_from_text", + return_value="MIT", + ): + cli_meta, _ = read_pyproject(tmp_path / "pyproject.toml") + hatch_pm = hatchling_metadata_core.ProjectMetadata( + str(tmp_path), PluginManager() + ) + hook_meta = metadata_from_hatchling(hatch_pm, tmp_path) + + assert cli_meta.license_concluded == "MIT" + assert hook_meta.license_concluded == "MIT" diff --git a/tests/extract/test_poetry_extract.py b/tests/extract/test_poetry_extract.py new file mode 100644 index 00000000..21f2f204 --- /dev/null +++ b/tests/extract/test_poetry_extract.py @@ -0,0 +1,285 @@ +# SPDX-FileContributor: Arthit Suriyawongkul +# SPDX-FileCopyrightText: 2026-present Arthit Suriyawongkul +# SPDX-FileType: SOURCE +# SPDX-License-Identifier: Apache-2.0 + +"""Tests for extract_poetry_metadata() -- the higher-level [tool.poetry] +entry point built on the low-level parsing helpers. Split out of +test_poetry_parsing.py to keep that file under this repo's file-size +soft limit. + +See also: test_poetry_parsing.py for the low-level [tool.poetry] parsing +helper tests; test_poetry_pyproject.py for read_pyproject() poetry-fallback, +fixture integration, and license-conflict tests. +""" + +import tempfile +from pathlib import Path + +import pytest + +from pitloom.core.project import ProjectMetadata, merge_project_metadata +from pitloom.extract._poetry import extract_poetry_metadata + +from .conftest import assert_declared_empty_authors_no_copyright_text + +# --------------------------------------------------------------------------- +# extract_poetry_metadata -- from dict +# --------------------------------------------------------------------------- + + +def test_extract_basic_fields() -> None: + data = { + "tool": { + "poetry": { + "name": "my-pkg", + "version": "1.2.3", + "description": "A test package", + "license": "MIT", + "keywords": ["foo", "bar"], + "authors": ["Alice "], + "homepage": "https://example.com", + "repository": "https://github.com/example/my-pkg", + "documentation": "https://docs.example.com", + } + } + } + with tempfile.TemporaryDirectory() as d: + metadata = extract_poetry_metadata(data, Path(d)) + assert metadata.name == "my-pkg" + assert metadata.version == "1.2.3" + assert metadata.description == "A test package" + assert metadata.license_name == "MIT" + assert metadata.keywords == ["foo", "bar"] + assert metadata.authors == [{"name": "Alice", "email": "alice@example.com"}] + assert metadata.urls["Homepage"] == "https://example.com" + assert metadata.urls["Repository"] == "https://github.com/example/my-pkg" + assert metadata.urls["Documentation"] == "https://docs.example.com" + + +def test_extract_dependencies() -> None: + data = { + "tool": { + "poetry": { + "name": "my-pkg", + "dependencies": { + "python": "^3.10", + "requests": "^2.28", + "numpy": ">=1.23", + }, + } + } + } + with tempfile.TemporaryDirectory() as d: + metadata = extract_poetry_metadata(data, Path(d)) + assert metadata.requires_python == ">=3.10,<4.0.0" + assert any("requests" in d for d in metadata.dependencies) + assert any("numpy" in d for d in metadata.dependencies) + assert not any("python" in d for d in metadata.dependencies) + + +def test_extract_readme_string() -> None: + data = {"tool": {"poetry": {"name": "pkg", "readme": "README.md"}}} + with tempfile.TemporaryDirectory() as d: + metadata = extract_poetry_metadata(data, Path(d)) + assert metadata.readme == "README.md" + + +def test_extract_readme_list() -> None: + data = { + "tool": {"poetry": {"name": "pkg", "readme": ["README.md", "CHANGELOG.md"]}} + } + with tempfile.TemporaryDirectory() as d: + metadata = extract_poetry_metadata(data, Path(d)) + assert metadata.readme == "README.md" + + +def test_extract_missing_section_raises() -> None: + with pytest.raises(ValueError, match=r"\[tool\.poetry\]"): + extract_poetry_metadata({}, Path(".")) + + +def test_extract_missing_name_raises() -> None: + data = {"tool": {"poetry": {"version": "1.0"}}} + with pytest.raises(ValueError, match="name is required"): + extract_poetry_metadata(data, Path(".")) + + +def test_extract_provenance_sources() -> None: + data = { + "tool": { + "poetry": { + "name": "my-pkg", + "version": "1.0.0", + "description": "desc", + "authors": ["Alice "], + } + } + } + with tempfile.TemporaryDirectory() as d: + metadata = extract_poetry_metadata(data, Path(d)) + assert "tool.poetry.name" in metadata.provenance.get("name", "") + assert "tool.poetry.version" in metadata.provenance.get("version", "") + assert "tool.poetry.description" in metadata.provenance.get("description", "") + assert "tool.poetry.authors" in metadata.provenance.get("authors", "") + assert "inferred_from_authors" in metadata.provenance.get("copyright_text", "") + + +def test_extract_provenance_empty_declared_dependencies() -> None: + """An explicitly declared but empty [tool.poetry.dependencies] (besides + the always-present `python` key) must still record provenance for + `dependencies` -- merge_project_metadata() relies on that presence to + treat the empty list as authoritative, not absent.""" + data = { + "tool": { + "poetry": { + "name": "my-pkg", + "version": "1.0.0", + "dependencies": {"python": "^3.10"}, + "keywords": [], + } + } + } + with tempfile.TemporaryDirectory() as d: + metadata = extract_poetry_metadata(data, Path(d)) + assert metadata.dependencies == [] + assert metadata.keywords == [] + assert "dependencies" in metadata.provenance + assert "keywords" in metadata.provenance + assert "requires_python" in metadata.provenance + + +def test_extract_non_list_keywords_treated_as_empty() -> None: + """A malformed `keywords` value that isn't a list (e.g. a bare string) + must resolve to an empty list, not raise or pass the raw value + through.""" + data = { + "tool": { + "poetry": { + "name": "my-pkg", + "version": "1.0.0", + "keywords": "not-a-list", + } + } + } + with tempfile.TemporaryDirectory() as d: + metadata = extract_poetry_metadata(data, Path(d)) + assert metadata.keywords == [] + + +def test_extract_provenance_declared_empty_authors_no_copyright_text() -> None: + """An explicitly declared but empty `authors = []` must still record + provenance for `authors`, but with no authors to derive a name from, + no `copyright_text` is inferred.""" + data = { + "tool": { + "poetry": { + "name": "my-pkg", + "version": "1.0.0", + "authors": [], + } + } + } + with tempfile.TemporaryDirectory() as d: + metadata = extract_poetry_metadata(data, Path(d)) + assert_declared_empty_authors_no_copyright_text(metadata) + + +def test_extract_provenance_wildcard_python_records_requires_python() -> None: + """`python = "*"` (no real constraint) resolves requires_python to + None, but that's a deliberate, explicitly-declared answer, not an + absent field -- provenance must record it so merge_project_metadata() + can protect it against a lower-priority source's real (possibly + wrong) constraint, the same presence-based rule every container field + already follows.""" + data = { + "tool": { + "poetry": { + "name": "my-pkg", + "version": "1.0.0", + "dependencies": {"python": "*"}, + } + } + } + with tempfile.TemporaryDirectory() as d: + metadata = extract_poetry_metadata(data, Path(d)) + assert metadata.requires_python is None + assert "requires_python" in metadata.provenance + + +def test_extract_provenance_capitalized_python_key_records_requires_python() -> None: + """A capitalized `Python` key (unusual, but _parse_poetry_deps() + matches it case-insensitively for the *value*) must get the same + provenance treatment -- a case-sensitive presence check would + silently miss it, reopening a narrower version of the misattribution + bug the presence-based check exists to close.""" + data = { + "tool": { + "poetry": { + "name": "my-pkg", + "version": "1.0.0", + "dependencies": {"Python": "^3.9"}, + } + } + } + with tempfile.TemporaryDirectory() as d: + metadata = extract_poetry_metadata(data, Path(d)) + assert metadata.requires_python is not None + assert "requires_python" in metadata.provenance + + +def test_poetry_wildcard_python_survives_merge_as_primary() -> None: + """Poetry-derived metadata with an explicit `python = "*"` must keep + requires_python as None when merged as *primary* against a secondary + with a real constraint -- the end-to-end proof (real producer output + fed through the real merge function) that the presence-based + provenance fix actually changes merge_project_metadata()'s decision, + not just a synthetic ProjectMetadata literal.""" + data = { + "tool": { + "poetry": { + "name": "my-pkg", + "version": "1.0.0", + "dependencies": {"python": "*"}, + } + } + } + with tempfile.TemporaryDirectory() as d: + poetry_metadata = extract_poetry_metadata(data, Path(d)) + secondary = ProjectMetadata( + name="my-pkg", + version="1.0.0", + requires_python=">=3.8", + provenance={ + "requires_python": "Source: setup.py | Field: setup(python_requires=...)" + }, + ) + merged = merge_project_metadata(poetry_metadata, secondary) + assert merged.requires_python is None + + +def test_convert_caret_and_tilde_edge_cases() -> None: + """_convert_caret and _convert_tilde handle zero/short/invalid versions.""" + from pitloom.extract._poetry import ( + _convert_caret, + _convert_tilde, + _parse_poetry_authors, + _poetry_constraint_to_pep440, + ) + + # Caret edge cases + assert _convert_caret("0") == ">=0" + assert _convert_caret("0.0") == ">=0.0,<0.1.0" + assert _convert_caret("invalid") == ">=invalid" + + # Tilde edge cases + assert _convert_tilde("1") == ">=1" + assert _convert_tilde("abc") == ">=abc" + assert _convert_tilde("abc.def") == ">=abc.def" + + # Non-string / invalid constraint + assert _poetry_constraint_to_pep440(12345) is None + assert _poetry_constraint_to_pep440(None) is None + + # Authors with invalid string formats + assert _parse_poetry_authors([123, "", " "]) == [] diff --git a/tests/extract/test_poetry_parsing.py b/tests/extract/test_poetry_parsing.py index 29fcb87f..9a6865f2 100644 --- a/tests/extract/test_poetry_parsing.py +++ b/tests/extract/test_poetry_parsing.py @@ -5,27 +5,23 @@ """Tests for low-level [tool.poetry] parsing helpers. -See also: test_poetry_pyproject.py for read_pyproject() poetry-fallback, -fixture integration, and license-conflict tests. +See also: test_poetry_extract.py for extract_poetry_metadata() tests, +split out to keep this file under this repo's file-size soft limit; +test_poetry_pyproject.py for read_pyproject() poetry-fallback, fixture +integration, and license-conflict tests. """ import logging -import tempfile -from pathlib import Path import pytest -from pitloom.core.project import ProjectMetadata, merge_project_metadata from pitloom.extract._poetry import ( _parse_poetry_authors, _parse_poetry_deps, _poetry_constraint_to_pep440, _poetry_dep_to_pep508, - extract_poetry_metadata, ) -from .conftest import assert_declared_empty_authors_no_copyright_text - # --------------------------------------------------------------------------- # _parse_poetry_authors # --------------------------------------------------------------------------- @@ -249,265 +245,3 @@ def test_parse_deps_skips_unrepresentable_git_dependency() -> None: assert python_declared is False assert not any("dev-pkg" in d for d in packages) assert any("requests" in d for d in packages) - - -# --------------------------------------------------------------------------- -# extract_poetry_metadata -- from dict -# --------------------------------------------------------------------------- - - -def test_extract_basic_fields() -> None: - data = { - "tool": { - "poetry": { - "name": "my-pkg", - "version": "1.2.3", - "description": "A test package", - "license": "MIT", - "keywords": ["foo", "bar"], - "authors": ["Alice "], - "homepage": "https://example.com", - "repository": "https://github.com/example/my-pkg", - "documentation": "https://docs.example.com", - } - } - } - with tempfile.TemporaryDirectory() as d: - metadata = extract_poetry_metadata(data, Path(d)) - assert metadata.name == "my-pkg" - assert metadata.version == "1.2.3" - assert metadata.description == "A test package" - assert metadata.license_name == "MIT" - assert metadata.keywords == ["foo", "bar"] - assert metadata.authors == [{"name": "Alice", "email": "alice@example.com"}] - assert metadata.urls["Homepage"] == "https://example.com" - assert metadata.urls["Repository"] == "https://github.com/example/my-pkg" - assert metadata.urls["Documentation"] == "https://docs.example.com" - - -def test_extract_dependencies() -> None: - data = { - "tool": { - "poetry": { - "name": "my-pkg", - "dependencies": { - "python": "^3.10", - "requests": "^2.28", - "numpy": ">=1.23", - }, - } - } - } - with tempfile.TemporaryDirectory() as d: - metadata = extract_poetry_metadata(data, Path(d)) - assert metadata.requires_python == ">=3.10,<4.0.0" - assert any("requests" in d for d in metadata.dependencies) - assert any("numpy" in d for d in metadata.dependencies) - assert not any("python" in d for d in metadata.dependencies) - - -def test_extract_readme_string() -> None: - data = {"tool": {"poetry": {"name": "pkg", "readme": "README.md"}}} - with tempfile.TemporaryDirectory() as d: - metadata = extract_poetry_metadata(data, Path(d)) - assert metadata.readme == "README.md" - - -def test_extract_readme_list() -> None: - data = { - "tool": {"poetry": {"name": "pkg", "readme": ["README.md", "CHANGELOG.md"]}} - } - with tempfile.TemporaryDirectory() as d: - metadata = extract_poetry_metadata(data, Path(d)) - assert metadata.readme == "README.md" - - -def test_extract_missing_section_raises() -> None: - with pytest.raises(ValueError, match=r"\[tool\.poetry\]"): - extract_poetry_metadata({}, Path(".")) - - -def test_extract_missing_name_raises() -> None: - data = {"tool": {"poetry": {"version": "1.0"}}} - with pytest.raises(ValueError, match="name is required"): - extract_poetry_metadata(data, Path(".")) - - -def test_extract_provenance_sources() -> None: - data = { - "tool": { - "poetry": { - "name": "my-pkg", - "version": "1.0.0", - "description": "desc", - "authors": ["Alice "], - } - } - } - with tempfile.TemporaryDirectory() as d: - metadata = extract_poetry_metadata(data, Path(d)) - assert "tool.poetry.name" in metadata.provenance.get("name", "") - assert "tool.poetry.version" in metadata.provenance.get("version", "") - assert "tool.poetry.description" in metadata.provenance.get("description", "") - assert "tool.poetry.authors" in metadata.provenance.get("authors", "") - assert "inferred_from_authors" in metadata.provenance.get("copyright_text", "") - - -def test_extract_provenance_empty_declared_dependencies() -> None: - """An explicitly declared but empty [tool.poetry.dependencies] (besides - the always-present `python` key) must still record provenance for - `dependencies` -- merge_project_metadata() relies on that presence to - treat the empty list as authoritative, not absent.""" - data = { - "tool": { - "poetry": { - "name": "my-pkg", - "version": "1.0.0", - "dependencies": {"python": "^3.10"}, - "keywords": [], - } - } - } - with tempfile.TemporaryDirectory() as d: - metadata = extract_poetry_metadata(data, Path(d)) - assert metadata.dependencies == [] - assert metadata.keywords == [] - assert "dependencies" in metadata.provenance - assert "keywords" in metadata.provenance - assert "requires_python" in metadata.provenance - - -def test_extract_non_list_keywords_treated_as_empty() -> None: - """A malformed `keywords` value that isn't a list (e.g. a bare string) - must resolve to an empty list, not raise or pass the raw value - through.""" - data = { - "tool": { - "poetry": { - "name": "my-pkg", - "version": "1.0.0", - "keywords": "not-a-list", - } - } - } - with tempfile.TemporaryDirectory() as d: - metadata = extract_poetry_metadata(data, Path(d)) - assert metadata.keywords == [] - - -def test_extract_provenance_declared_empty_authors_no_copyright_text() -> None: - """An explicitly declared but empty `authors = []` must still record - provenance for `authors`, but with no authors to derive a name from, - no `copyright_text` is inferred.""" - data = { - "tool": { - "poetry": { - "name": "my-pkg", - "version": "1.0.0", - "authors": [], - } - } - } - with tempfile.TemporaryDirectory() as d: - metadata = extract_poetry_metadata(data, Path(d)) - assert_declared_empty_authors_no_copyright_text(metadata) - - -def test_extract_provenance_wildcard_python_records_requires_python() -> None: - """`python = "*"` (no real constraint) resolves requires_python to - None, but that's a deliberate, explicitly-declared answer, not an - absent field -- provenance must record it so merge_project_metadata() - can protect it against a lower-priority source's real (possibly - wrong) constraint, the same presence-based rule every container field - already follows.""" - data = { - "tool": { - "poetry": { - "name": "my-pkg", - "version": "1.0.0", - "dependencies": {"python": "*"}, - } - } - } - with tempfile.TemporaryDirectory() as d: - metadata = extract_poetry_metadata(data, Path(d)) - assert metadata.requires_python is None - assert "requires_python" in metadata.provenance - - -def test_extract_provenance_capitalized_python_key_records_requires_python() -> None: - """A capitalized `Python` key (unusual, but _parse_poetry_deps() - matches it case-insensitively for the *value*) must get the same - provenance treatment -- a case-sensitive presence check would - silently miss it, reopening a narrower version of the misattribution - bug the presence-based check exists to close.""" - data = { - "tool": { - "poetry": { - "name": "my-pkg", - "version": "1.0.0", - "dependencies": {"Python": "^3.9"}, - } - } - } - with tempfile.TemporaryDirectory() as d: - metadata = extract_poetry_metadata(data, Path(d)) - assert metadata.requires_python is not None - assert "requires_python" in metadata.provenance - - -def test_poetry_wildcard_python_survives_merge_as_primary() -> None: - """Poetry-derived metadata with an explicit `python = "*"` must keep - requires_python as None when merged as *primary* against a secondary - with a real constraint -- the end-to-end proof (real producer output - fed through the real merge function) that the presence-based - provenance fix actually changes merge_project_metadata()'s decision, - not just a synthetic ProjectMetadata literal.""" - data = { - "tool": { - "poetry": { - "name": "my-pkg", - "version": "1.0.0", - "dependencies": {"python": "*"}, - } - } - } - with tempfile.TemporaryDirectory() as d: - poetry_metadata = extract_poetry_metadata(data, Path(d)) - secondary = ProjectMetadata( - name="my-pkg", - version="1.0.0", - requires_python=">=3.8", - provenance={ - "requires_python": "Source: setup.py | Field: setup(python_requires=...)" - }, - ) - merged = merge_project_metadata(poetry_metadata, secondary) - assert merged.requires_python is None - - -def test_convert_caret_and_tilde_edge_cases() -> None: - """_convert_caret and _convert_tilde handle zero/short/invalid versions.""" - from pitloom.extract._poetry import ( - _convert_caret, - _convert_tilde, - _parse_poetry_authors, - _poetry_constraint_to_pep440, - ) - - # Caret edge cases - assert _convert_caret("0") == ">=0" - assert _convert_caret("0.0") == ">=0.0,<0.1.0" - assert _convert_caret("invalid") == ">=invalid" - - # Tilde edge cases - assert _convert_tilde("1") == ">=1" - assert _convert_tilde("abc") == ">=abc" - assert _convert_tilde("abc.def") == ">=abc.def" - - # Non-string / invalid constraint - assert _poetry_constraint_to_pep440(12345) is None - assert _poetry_constraint_to_pep440(None) is None - - # Authors with invalid string formats - assert _parse_poetry_authors([123, "", " "]) == [] diff --git a/tests/extract/test_pyproject.py b/tests/extract/test_pyproject.py index 8e5e94c2..fc8bb8e3 100644 --- a/tests/extract/test_pyproject.py +++ b/tests/extract/test_pyproject.py @@ -4,18 +4,19 @@ # SPDX-License-Identifier: Apache-2.0 """Tests for read_pyproject()'s [project]-focused parsing paths and its -private helpers (license hint resolution, readme/author extraction, -provenance building). +private helpers (readme/author extraction, provenance building). -See also: test_pyproject_dynamic.py for PEP 621 ``dynamic`` field -resolution (:mod:`pitloom.extract._pyproject_dynamic`); test_poetry_pyproject.py +See also: test_pyproject_license.py for license-classifier-conflict +recovery and license-hint-resolution tests, split out to keep this file +under this repo's file-size soft limit; test_pyproject_dynamic.py for +PEP 621 ``dynamic`` field resolution +(:mod:`pitloom.extract._pyproject_dynamic`); test_poetry_pyproject.py for the [tool.poetry] fallback/override behaviour; test_hatch_hook_metadata.py for the Hatchling build-hook path. """ from __future__ import annotations -import logging import tempfile from pathlib import Path from types import SimpleNamespace @@ -23,18 +24,14 @@ from unittest.mock import patch import pytest -from pyproject_metadata import ConfigurationError, StandardMetadata +from pyproject_metadata import StandardMetadata from pitloom.core._config_types import PitloomConfig from pitloom.extract._pyproject import ( _build_provenance, - _drop_redundant_license_classifiers, - _extract_and_detect_license, _extract_authors, _extract_readme, - _is_license_classifier_conflict, _read_pyproject_fallback, - _resolve_license_hint, _try_read_poetry, read_pyproject, ) @@ -228,158 +225,6 @@ def test_read_pyproject_invalid_metadata_raises_value_error() -> None: read_pyproject(tmp_path / "pyproject.toml") -# --------------------------------------------------------------------------- -# read_pyproject -- PEP 639 SPDX-license/classifier transitional conflict -# --------------------------------------------------------------------------- - - -def test_read_pyproject_spdx_license_with_redundant_classifier_recovers( - caplog: pytest.LogCaptureFixture, -) -> None: - """Regression, found validating against real PyPI packages (colorama, - redis-py, httpx, platformdirs, virtualenv all currently ship both): a - modern SPDX ``license`` string alongside a legacy ``License ::`` - classifier is a hard error for ``pyproject-metadata``, but a common, - benign transitional state real projects are in mid-PEP-639-migration. - ``read_pyproject()`` must recover by dropping the redundant - classifier and keeping the SPDX expression -- with a ``WARNING:``, - not silently -- rather than failing the whole SBOM. - - Also the regression guard for `_is_license_classifier_conflict`'s - own documented fragility (it matches pyproject-metadata's exception - message verbatim, with no more stable discriminator available): this - exercises the *real*, installed pyproject-metadata, not a mocked - exception, so a future dependency bump that rewords the message - fails this test loudly (`ValueError` instead of a clean recovery) - rather than silently degrading in production.""" - content = """ -[project] -name = "transitional-license-pkg" -version = "1.0.0" -license = "MIT" -classifiers = [ - "License :: OSI Approved :: MIT License", - "Programming Language :: Python :: 3", -] -""" - with tempfile.TemporaryDirectory() as d: - tmp_path = Path(d) - (tmp_path / "pyproject.toml").write_text(content) - with caplog.at_level(logging.WARNING): - metadata, _config = read_pyproject(tmp_path / "pyproject.toml") - - assert metadata.name == "transitional-license-pkg" - assert metadata.license_name == "MIT" - assert "License ::" in caplog.text - assert "PEP 639 transitional state" in caplog.text - - -def test_read_pyproject_retry_still_failing_raises_value_error() -> None: - """Regression: if the PEP 639 recovery retry itself still fails (a - genuinely unrecoverable error surfacing only on the second parse - attempt), it must still surface as ``ValueError`` -- not propagate - the raw underlying exception, and not be silently swallowed. Forces - this by patching ``_drop_redundant_license_classifiers`` to be a - no-op, so the retry hits the exact same classifier conflict again.""" - content = """ -[project] -name = "transitional-license-pkg" -version = "1.0.0" -license = "MIT" -classifiers = [ - "License :: OSI Approved :: MIT License", -] -""" - with tempfile.TemporaryDirectory() as d: - tmp_path = Path(d) - (tmp_path / "pyproject.toml").write_text(content) - with patch( - "pitloom.extract._pyproject._drop_redundant_license_classifiers", - side_effect=lambda data: data, - ): - with pytest.raises(ValueError, match="Failed to parse project metadata"): - read_pyproject(tmp_path / "pyproject.toml") - - -def test_drop_redundant_license_classifiers_ignores_non_list_classifiers() -> None: - """Defensive branch, not reachable through ``read_pyproject()`` itself - (pyproject-metadata's own ``validate()`` only raises the classifier- - conflict error this function recovers from when ``self.classifiers`` - is already iterable-of-strings, so a genuinely non-list value never - gets this far in the real pipeline) -- exercised directly: a - malformed ``classifiers`` value is left untouched rather than - crashing the recovery path.""" - data = {"project": {"name": "pkg", "classifiers": "not-a-list"}} - assert _drop_redundant_license_classifiers(data) == data - - -def test_read_pyproject_non_configuration_error_still_raises() -> None: - """Regression: not every ``StandardMetadata.from_pyproject()`` - failure is a ``ConfigurationError`` -- a malformed ``dynamic`` field - (a bare string instead of an array, a plausible real-world typo) - raises a raw ``AttributeError`` from inside pyproject-metadata. This - must still surface as ``ValueError``, via the sibling - ``except Exception`` clause, not propagate the raw exception.""" - content = """ -[project] -name = "bad-dynamic-pkg" -version = "1.0.0" -dynamic = "version" -""" - with tempfile.TemporaryDirectory() as d: - tmp_path = Path(d) - (tmp_path / "pyproject.toml").write_text(content) - with pytest.raises(ValueError, match="Failed to parse project metadata"): - read_pyproject(tmp_path / "pyproject.toml") - - -def test_is_license_classifier_conflict_rejects_pre_spdx_metadata_version_error() -> ( - None -): - """Regression for a fragility documented but not directly exercised - elsewhere: pyproject-metadata raises ``ConfigurationError`` with - ``key == "project.license"`` for *two* distinct failures -- the - classifier conflict this module recovers from, and a separate error - for an SPDX license string paired with an explicit pre-2.4 - ``metadata_version``. ``.key`` alone can't tell them apart, so - ``_is_license_classifier_conflict`` must also check the message. - This second case isn't reachable through ``read_pyproject()`` itself - (it never pins an explicit ``metadata_version``, and - ``auto_metadata_version`` always resolves to >= 2.4 when ``license`` - is a string) -- exercised directly against ``StandardMetadata`` here - instead.""" - with pytest.raises(ConfigurationError) as excinfo: - StandardMetadata.from_pyproject( - {"project": {"name": "pkg", "version": "1.0.0", "license": "MIT"}}, - metadata_version="2.1", - ) - exc = excinfo.value - assert exc.key == "project.license" - assert not _is_license_classifier_conflict(exc) - - -def test_read_pyproject_genuine_license_error_still_raises() -> None: - """A ``project.license``-adjacent failure unrelated to the - classifier-conflict case (here: ``license-files`` combined with the - legacy dict-style ``license = {text = ...}``, a different - ``pyproject-metadata`` validation error with a different key) must - still surface as ``ValueError`` -- the transitional-state recovery - must not swallow every license-related error, only the one specific, - narrow case it's scoped to.""" - content = """ -[project] -name = "bad-license-pkg" -version = "1.0.0" -license = {text = "MIT"} -license-files = ["LICENSE"] -""" - with tempfile.TemporaryDirectory() as d: - tmp_path = Path(d) - (tmp_path / "pyproject.toml").write_text(content) - with pytest.raises(ValueError, match="Failed to parse project metadata"): - read_pyproject(tmp_path / "pyproject.toml") - - # --------------------------------------------------------------------------- # _build_provenance -- direct unit tests for branches unreachable/awkward # to reach purely through read_pyproject() @@ -429,166 +274,6 @@ def test_extract_readme_no_file_no_text() -> None: assert result is None -# --------------------------------------------------------------------------- -# _resolve_license_hint -- direct unit tests for each license object shape -# --------------------------------------------------------------------------- - - -def test_read_pyproject_license_toml_dotted_key_matches_inline_table( - tmp_path: Path, -) -> None: - """PEP 621's TOML dotted-key license form (``license.text = "..."``, - as seen in apple/tree-sitter-pkl's real ``pyproject.toml`` -- see - ``tests/fixtures/projects/sampleproject-setuptools-license-dotted/``) - parses identically to the more common inline-table form (``license = - {text = "..."}``) -- both produce the same nested dict once read by - any TOML library, so ``read_pyproject()`` needs no special handling - for either. Regression: constructs both forms and asserts they - resolve to the same ``license_name``.""" - dotted_fixture = ( - Path(__file__).parent.parent - / "fixtures" - / "projects" - / "sampleproject-setuptools-license-dotted" - / "pyproject.toml" - ) - dotted_metadata, _ = read_pyproject(dotted_fixture) - - inline_dir = tmp_path / "inline" - inline_dir.mkdir() - inline_pyproject = inline_dir / "pyproject.toml" - inline_pyproject.write_text( - '[build-system]\nrequires = ["setuptools>=42"]\n' - 'build-backend = "setuptools.build_meta"\n\n' - "[project]\n" - 'name = "sampleproject-setuptools-license-dotted"\n' - 'version = "0.1.0"\n' - 'license = {text = "Apache-2.0"}\n', - encoding="utf-8", - ) - inline_metadata, _ = read_pyproject(inline_pyproject) - - assert dotted_metadata.license_name == inline_metadata.license_name - - -def test_resolve_license_hint_text_object() -> None: - """A License object exposing ``.text`` (PEP 639 inline license text).""" - license_obj = SimpleNamespace(text="Some custom license text.") - with tempfile.TemporaryDirectory() as d: - hint, base_prov, fallback = _resolve_license_hint(license_obj, Path(d)) - assert hint == "Some custom license text." - assert base_prov.endswith(".text") - assert fallback == ("Some custom license text.", None) - - -def test_resolve_license_hint_file_object_reads_content() -> None: - """A License object exposing ``.file`` reads the referenced file's - content as the detection hint.""" - with tempfile.TemporaryDirectory() as d: - tmp_path = Path(d) - (tmp_path / "LICENSE.txt").write_text("MIT License text", encoding="utf-8") - license_obj = SimpleNamespace(file="LICENSE.txt") - hint, base_prov, fallback = _resolve_license_hint(license_obj, tmp_path) - assert hint == "MIT License text" - assert base_prov == "Source: LICENSE.txt" - assert fallback == ("LICENSE.txt", None) - - -def test_resolve_license_hint_file_object_missing_file() -> None: - """A License object whose ``.file`` does not exist on disk: the ``OSError`` - is caught, ``hint`` is ``None``, and the filename is preserved as - fallback.""" - license_obj = SimpleNamespace(file="does-not-exist.txt") - with tempfile.TemporaryDirectory() as d: - hint, base_prov, fallback = _resolve_license_hint(license_obj, Path(d)) - assert hint is None - assert base_prov == "Source: pyproject.toml | Field: project.license" - assert fallback == ("does-not-exist.txt", None) - - -def test_resolve_license_hint_unrecognised_object() -> None: - """A license object with neither ``.text`` nor ``.file`` falls through to - the catch-all branch, stringifying the object as the fallback id.""" - license_obj = object() - with tempfile.TemporaryDirectory() as d: - hint, base_prov, fallback = _resolve_license_hint(license_obj, Path(d)) - assert hint is None - assert base_prov == "Source: pyproject.toml | Field: project.license" - assert fallback == (str(license_obj), None) - - -# --------------------------------------------------------------------------- -# _extract_and_detect_license -- branches around _resolve_license_hint / -# detect_license_for_project interplay -# --------------------------------------------------------------------------- - - -def test_extract_and_detect_license_hint_none_returns_fallback() -> None: - """When ``_resolve_license_hint`` cannot produce a hint (missing license - file), ``_extract_and_detect_license`` returns its fallback tuple - directly (line 296).""" - std = SimpleNamespace(license=SimpleNamespace(file="missing-license.txt")) - with tempfile.TemporaryDirectory() as d: - license_id, prov = _extract_and_detect_license( - cast(StandardMetadata, std), Path(d) - ) - assert license_id == "missing-license.txt" - assert prov is None - - -def test_extract_and_detect_license_detected_differs_from_hint() -> None: - """Free-text license hint that ``licenseid`` detection resolves to a - *different* SPDX id: reports the detected id with a - ``licenseid_detection`` provenance tag (lines 301-303).""" - std = SimpleNamespace(license="Some custom license text that isn't SPDX") - with tempfile.TemporaryDirectory() as d: - with patch( - "pitloom.extract._pyproject.detect_license_for_project", - return_value=("Apache-2.0", "Source: detected"), - ): - license_id, prov = _extract_and_detect_license( - cast(StandardMetadata, std), Path(d) - ) - assert license_id == "Apache-2.0" - assert prov == ( - "Source: pyproject.toml | Field: project.license | Method: licenseid_detection" - ) - - -def test_extract_and_detect_license_detected_equals_hint_uses_fallback_id() -> None: - """Detection agrees with the hint (no new information): when the hint - came from license *text* (so a non-``None`` ``fallback_id`` exists), - the fallback id/provenance pair is returned instead (lines 305-307).""" - std = SimpleNamespace(license=SimpleNamespace(text="MIT-like text")) - with tempfile.TemporaryDirectory() as d: - with patch( - "pitloom.extract._pyproject.detect_license_for_project", - return_value=("MIT-like text", "Source: irrelevant"), - ): - license_id, prov = _extract_and_detect_license( - cast(StandardMetadata, std), Path(d) - ) - assert license_id == "MIT-like text" - assert prov is None - - -def test_extract_and_detect_license_detected_equals_hint_no_fallback_id() -> None: - """Detection agrees with the hint and there is no ``fallback_id`` (plain - string license hint): falls through to the final - ``return detected, prov`` (line 308).""" - std = SimpleNamespace(license="Some Custom License Text") - with tempfile.TemporaryDirectory() as d: - with patch( - "pitloom.extract._pyproject.detect_license_for_project", - return_value=("Some Custom License Text", "Source: detected-prov"), - ): - license_id, prov = _extract_and_detect_license( - cast(StandardMetadata, std), Path(d) - ) - assert license_id == "Some Custom License Text" - assert prov == "Source: detected-prov" - - # --------------------------------------------------------------------------- # _extract_authors -- entries that resolve to nothing get skipped # --------------------------------------------------------------------------- diff --git a/tests/extract/test_pyproject_license.py b/tests/extract/test_pyproject_license.py new file mode 100644 index 00000000..dbc84997 --- /dev/null +++ b/tests/extract/test_pyproject_license.py @@ -0,0 +1,345 @@ +# SPDX-FileContributor: Arthit Suriyawongkul +# SPDX-FileCopyrightText: 2026-present Arthit Suriyawongkul +# SPDX-FileType: SOURCE +# SPDX-License-Identifier: Apache-2.0 + +"""Tests for read_pyproject()'s license-classifier-conflict recovery and +its license-hint-resolution private helpers. Split out of test_pyproject.py +to keep that file under this repo's file-size soft limit. + +See also: test_pyproject.py for the rest of read_pyproject()'s +[project]-focused parsing paths; test_pyproject_dynamic.py for PEP 621 +``dynamic`` field resolution; test_poetry_pyproject.py for the +[tool.poetry] fallback/override behaviour. +""" + +from __future__ import annotations + +import logging +import tempfile +from pathlib import Path +from types import SimpleNamespace +from typing import cast +from unittest.mock import patch + +import pytest +from pyproject_metadata import ConfigurationError, StandardMetadata + +from pitloom.extract._pyproject import ( + _drop_redundant_license_classifiers, + _extract_and_detect_license, + _is_license_classifier_conflict, + _resolve_license_hint, + read_pyproject, +) + +# --------------------------------------------------------------------------- +# read_pyproject -- PEP 639 SPDX-license/classifier transitional conflict +# --------------------------------------------------------------------------- + + +def test_read_pyproject_spdx_license_with_redundant_classifier_recovers( + caplog: pytest.LogCaptureFixture, +) -> None: + """Regression, found validating against real PyPI packages (colorama, + redis-py, httpx, platformdirs, virtualenv all currently ship both): a + modern SPDX ``license`` string alongside a legacy ``License ::`` + classifier is a hard error for ``pyproject-metadata``, but a common, + benign transitional state real projects are in mid-PEP-639-migration. + ``read_pyproject()`` must recover by dropping the redundant + classifier and keeping the SPDX expression -- with a ``WARNING:``, + not silently -- rather than failing the whole SBOM. + + Also the regression guard for `_is_license_classifier_conflict`'s + own documented fragility (it matches pyproject-metadata's exception + message verbatim, with no more stable discriminator available): this + exercises the *real*, installed pyproject-metadata, not a mocked + exception, so a future dependency bump that rewords the message + fails this test loudly (`ValueError` instead of a clean recovery) + rather than silently degrading in production.""" + content = """ +[project] +name = "transitional-license-pkg" +version = "1.0.0" +license = "MIT" +classifiers = [ + "License :: OSI Approved :: MIT License", + "Programming Language :: Python :: 3", +] +""" + with tempfile.TemporaryDirectory() as d: + tmp_path = Path(d) + (tmp_path / "pyproject.toml").write_text(content) + with caplog.at_level(logging.WARNING): + metadata, _config = read_pyproject(tmp_path / "pyproject.toml") + + assert metadata.name == "transitional-license-pkg" + assert metadata.license_name == "MIT" + assert "License ::" in caplog.text + assert "PEP 639 transitional state" in caplog.text + + +def test_read_pyproject_retry_still_failing_raises_value_error() -> None: + """Regression: if the PEP 639 recovery retry itself still fails (a + genuinely unrecoverable error surfacing only on the second parse + attempt), it must still surface as ``ValueError`` -- not propagate + the raw underlying exception, and not be silently swallowed. Forces + this by patching ``_drop_redundant_license_classifiers`` to be a + no-op, so the retry hits the exact same classifier conflict again.""" + content = """ +[project] +name = "transitional-license-pkg" +version = "1.0.0" +license = "MIT" +classifiers = [ + "License :: OSI Approved :: MIT License", +] +""" + with tempfile.TemporaryDirectory() as d: + tmp_path = Path(d) + (tmp_path / "pyproject.toml").write_text(content) + with patch( + "pitloom.extract._pyproject._drop_redundant_license_classifiers", + side_effect=lambda data: data, + ): + with pytest.raises(ValueError, match="Failed to parse project metadata"): + read_pyproject(tmp_path / "pyproject.toml") + + +def test_drop_redundant_license_classifiers_ignores_non_list_classifiers() -> None: + """Defensive branch, not reachable through ``read_pyproject()`` itself + (pyproject-metadata's own ``validate()`` only raises the classifier- + conflict error this function recovers from when ``self.classifiers`` + is already iterable-of-strings, so a genuinely non-list value never + gets this far in the real pipeline) -- exercised directly: a + malformed ``classifiers`` value is left untouched rather than + crashing the recovery path.""" + data = {"project": {"name": "pkg", "classifiers": "not-a-list"}} + assert _drop_redundant_license_classifiers(data) == data + + +def test_read_pyproject_non_configuration_error_still_raises() -> None: + """Regression: not every ``StandardMetadata.from_pyproject()`` + failure is a ``ConfigurationError`` -- a malformed ``dynamic`` field + (a bare string instead of an array, a plausible real-world typo) + raises a raw ``AttributeError`` from inside pyproject-metadata. This + must still surface as ``ValueError``, via the sibling + ``except Exception`` clause, not propagate the raw exception.""" + content = """ +[project] +name = "bad-dynamic-pkg" +version = "1.0.0" +dynamic = "version" +""" + with tempfile.TemporaryDirectory() as d: + tmp_path = Path(d) + (tmp_path / "pyproject.toml").write_text(content) + with pytest.raises(ValueError, match="Failed to parse project metadata"): + read_pyproject(tmp_path / "pyproject.toml") + + +def test_is_license_classifier_conflict_rejects_pre_spdx_metadata_version_error() -> ( + None +): + """Regression for a fragility documented but not directly exercised + elsewhere: pyproject-metadata raises ``ConfigurationError`` with + ``key == "project.license"`` for *two* distinct failures -- the + classifier conflict this module recovers from, and a separate error + for an SPDX license string paired with an explicit pre-2.4 + ``metadata_version``. ``.key`` alone can't tell them apart, so + ``_is_license_classifier_conflict`` must also check the message. + This second case isn't reachable through ``read_pyproject()`` itself + (it never pins an explicit ``metadata_version``, and + ``auto_metadata_version`` always resolves to >= 2.4 when ``license`` + is a string) -- exercised directly against ``StandardMetadata`` here + instead.""" + with pytest.raises(ConfigurationError) as excinfo: + StandardMetadata.from_pyproject( + {"project": {"name": "pkg", "version": "1.0.0", "license": "MIT"}}, + metadata_version="2.1", + ) + exc = excinfo.value + assert exc.key == "project.license" + assert not _is_license_classifier_conflict(exc) + + +def test_read_pyproject_genuine_license_error_still_raises() -> None: + """A ``project.license``-adjacent failure unrelated to the + classifier-conflict case (here: ``license-files`` combined with the + legacy dict-style ``license = {text = ...}``, a different + ``pyproject-metadata`` validation error with a different key) must + still surface as ``ValueError`` -- the transitional-state recovery + must not swallow every license-related error, only the one specific, + narrow case it's scoped to.""" + content = """ +[project] +name = "bad-license-pkg" +version = "1.0.0" +license = {text = "MIT"} +license-files = ["LICENSE"] +""" + with tempfile.TemporaryDirectory() as d: + tmp_path = Path(d) + (tmp_path / "pyproject.toml").write_text(content) + with pytest.raises(ValueError, match="Failed to parse project metadata"): + read_pyproject(tmp_path / "pyproject.toml") + + +# --------------------------------------------------------------------------- +# _resolve_license_hint -- direct unit tests for each license object shape +# --------------------------------------------------------------------------- + + +def test_read_pyproject_license_toml_dotted_key_matches_inline_table( + tmp_path: Path, +) -> None: + """PEP 621's TOML dotted-key license form (``license.text = "..."``, + as seen in apple/tree-sitter-pkl's real ``pyproject.toml`` -- see + ``tests/fixtures/projects/sampleproject-setuptools-license-dotted/``) + parses identically to the more common inline-table form (``license = + {text = "..."}``) -- both produce the same nested dict once read by + any TOML library, so ``read_pyproject()`` needs no special handling + for either. Regression: constructs both forms and asserts they + resolve to the same ``license_name``.""" + dotted_fixture = ( + Path(__file__).parent.parent + / "fixtures" + / "projects" + / "sampleproject-setuptools-license-dotted" + / "pyproject.toml" + ) + dotted_metadata, _ = read_pyproject(dotted_fixture) + + inline_dir = tmp_path / "inline" + inline_dir.mkdir() + inline_pyproject = inline_dir / "pyproject.toml" + inline_pyproject.write_text( + '[build-system]\nrequires = ["setuptools>=42"]\n' + 'build-backend = "setuptools.build_meta"\n\n' + "[project]\n" + 'name = "sampleproject-setuptools-license-dotted"\n' + 'version = "0.1.0"\n' + 'license = {text = "Apache-2.0"}\n', + encoding="utf-8", + ) + inline_metadata, _ = read_pyproject(inline_pyproject) + + assert dotted_metadata.license_name == inline_metadata.license_name + + +def test_resolve_license_hint_text_object() -> None: + """A License object exposing ``.text`` (PEP 639 inline license text).""" + license_obj = SimpleNamespace(text="Some custom license text.") + with tempfile.TemporaryDirectory() as d: + hint, base_prov, fallback = _resolve_license_hint(license_obj, Path(d)) + assert hint == "Some custom license text." + assert base_prov.endswith(".text") + assert fallback == ("Some custom license text.", None) + + +def test_resolve_license_hint_file_object_reads_content() -> None: + """A License object exposing ``.file`` reads the referenced file's + content as the detection hint.""" + with tempfile.TemporaryDirectory() as d: + tmp_path = Path(d) + (tmp_path / "LICENSE.txt").write_text("MIT License text", encoding="utf-8") + license_obj = SimpleNamespace(file="LICENSE.txt") + hint, base_prov, fallback = _resolve_license_hint(license_obj, tmp_path) + assert hint == "MIT License text" + assert base_prov == "Source: LICENSE.txt" + assert fallback == ("LICENSE.txt", None) + + +def test_resolve_license_hint_file_object_missing_file() -> None: + """A License object whose ``.file`` does not exist on disk: the ``OSError`` + is caught, ``hint`` is ``None``, and the filename is preserved as + fallback.""" + license_obj = SimpleNamespace(file="does-not-exist.txt") + with tempfile.TemporaryDirectory() as d: + hint, base_prov, fallback = _resolve_license_hint(license_obj, Path(d)) + assert hint is None + assert base_prov == "Source: pyproject.toml | Field: project.license" + assert fallback == ("does-not-exist.txt", None) + + +def test_resolve_license_hint_unrecognised_object() -> None: + """A license object with neither ``.text`` nor ``.file`` falls through to + the catch-all branch, stringifying the object as the fallback id.""" + license_obj = object() + with tempfile.TemporaryDirectory() as d: + hint, base_prov, fallback = _resolve_license_hint(license_obj, Path(d)) + assert hint is None + assert base_prov == "Source: pyproject.toml | Field: project.license" + assert fallback == (str(license_obj), None) + + +# --------------------------------------------------------------------------- +# _extract_and_detect_license -- branches around _resolve_license_hint / +# detect_license_for_project interplay +# --------------------------------------------------------------------------- + + +def test_extract_and_detect_license_hint_none_returns_fallback() -> None: + """When ``_resolve_license_hint`` cannot produce a hint (missing license + file), ``_extract_and_detect_license`` returns its fallback tuple + directly (line 296).""" + std = SimpleNamespace(license=SimpleNamespace(file="missing-license.txt")) + with tempfile.TemporaryDirectory() as d: + license_id, prov = _extract_and_detect_license( + cast(StandardMetadata, std), Path(d) + ) + assert license_id == "missing-license.txt" + assert prov is None + + +def test_extract_and_detect_license_detected_differs_from_hint() -> None: + """Free-text license hint that ``licenseid`` detection resolves to a + *different* SPDX id: reports the detected id with a + ``licenseid_detection`` provenance tag (lines 301-303).""" + std = SimpleNamespace(license="Some custom license text that isn't SPDX") + with tempfile.TemporaryDirectory() as d: + with patch( + "pitloom.extract._pyproject.detect_license_for_project", + return_value=("Apache-2.0", "Source: detected"), + ): + license_id, prov = _extract_and_detect_license( + cast(StandardMetadata, std), Path(d) + ) + assert license_id == "Apache-2.0" + assert prov == ( + "Source: pyproject.toml | Field: project.license | Method: licenseid_detection" + ) + + +def test_extract_and_detect_license_detected_equals_hint_uses_fallback_id() -> None: + """Detection agrees with the hint (no new information): when the hint + came from license *text* (so a non-``None`` ``fallback_id`` exists), + the fallback id/provenance pair is returned instead (lines 305-307).""" + std = SimpleNamespace(license=SimpleNamespace(text="MIT-like text")) + with tempfile.TemporaryDirectory() as d: + with patch( + "pitloom.extract._pyproject.detect_license_for_project", + return_value=("MIT-like text", "Source: irrelevant"), + ): + license_id, prov = _extract_and_detect_license( + cast(StandardMetadata, std), Path(d) + ) + assert license_id == "MIT-like text" + assert prov is None + + +def test_extract_and_detect_license_detected_equals_hint_no_fallback_id() -> None: + """Detection agrees with the hint and there is no ``fallback_id`` (plain + string license hint): falls through to the final + ``return detected, prov`` (line 308).""" + std = SimpleNamespace(license="Some Custom License Text") + with tempfile.TemporaryDirectory() as d: + with patch( + "pitloom.extract._pyproject.detect_license_for_project", + return_value=("Some Custom License Text", "Source: detected-prov"), + ): + license_id, prov = _extract_and_detect_license( + cast(StandardMetadata, std), Path(d) + ) + assert license_id == "Some Custom License Text" + assert prov == "Source: detected-prov" diff --git a/working-docs/design/complexity-and-file-size-roadmap.md b/working-docs/design/complexity-and-file-size-roadmap.md index 90b411ff..0fbdbd66 100644 --- a/working-docs/design/complexity-and-file-size-roadmap.md +++ b/working-docs/design/complexity-and-file-size-roadmap.md @@ -46,10 +46,12 @@ trip review attention: 411, `export/spdx3_json.py` 401). - **`tests/`** (excluding `tests/extract/huggingface/` mock fixture catalogs): 9 files exceed 415 lines, worst is `test_hdf5.py` at 552 - (`test_deps_enrichment_pypi_fallback.py` 460, `test_pytorch_pt2.py` 459, - `test_assembly_edge_cases.py` 456, `test_annotation_provenance_emit.py` - 449, `test_pyproject.py` 436, `test_cli_options.py` 429, - `test_setuptools_cfg.py` 426, `test_gguf.py` 423). + (`test_pytorch_pt2.py` 459, `test_assembly_edge_cases.py` 456, + `test_annotation_provenance_emit.py` 449, `test_pyproject.py` 436, + `test_cli_options.py` 429, `test_gguf.py` 423 -- + `test_deps_enrichment_pypi_fallback.py` and `test_setuptools_cfg.py` have + since been split under this limit, see `test_deps_license.py` and + `test_setuptools_cfg_backend.py`). None have crossed the 800-line hard cap, so nothing is currently broken -- but per AGENTS.md, a file should be split *before* crossing the soft From 8486b6df6f52061b3db4e554343f33cc38b7823f Mon Sep 17 00:00:00 2001 From: Arthit Suriyawongkul Date: Wed, 9 Sep 2026 06:14:45 +0700 Subject: [PATCH 34/35] Fix uv.lock bug Signed-off-by: Arthit Suriyawongkul --- src/pitloom/assemble/spdx3/document.py | 2 +- src/pitloom/extract/_uv_lock.py | 57 ++++++++++++++++--- tests/extract/test_pdm_lock.py | 25 ++++++++ tests/extract/test_poetry_lock.py | 25 ++++++++ tests/extract/test_uv_lock.py | 47 +++++++++++++++ tests/extract/test_uv_lock_transitive.py | 27 +++++++++ .../complexity-and-file-size-roadmap.md | 12 ++-- .../backend-file-discovery-validation.md | 2 +- .../implementation/license-pipeline.md | 5 ++ working-docs/implementation/poetry-support.md | 2 +- .../provenance/multi-source-conflict.md | 2 +- 11 files changed, 189 insertions(+), 17 deletions(-) diff --git a/src/pitloom/assemble/spdx3/document.py b/src/pitloom/assemble/spdx3/document.py index cfc196a1..be7db646 100644 --- a/src/pitloom/assemble/spdx3/document.py +++ b/src/pitloom/assemble/spdx3/document.py @@ -295,7 +295,7 @@ def build( add_dependencies( dependencies=transitive_only, dep_provenance=metadata.provenance.get( - "locked_dependencies", "Source: lock file" + "locked_dependencies", "Source: lock file | Method: resolved_lockfile" ), main_package_spdx_id=require_spdx_id(main_package), creation_info=spdx_ci, diff --git a/src/pitloom/extract/_uv_lock.py b/src/pitloom/extract/_uv_lock.py index 6422faf0..acfdc10e 100644 --- a/src/pitloom/extract/_uv_lock.py +++ b/src/pitloom/extract/_uv_lock.py @@ -58,6 +58,7 @@ from pitloom.extract._lock_common import ( find_first_present_key, + is_same_version, is_usable_version, load_lock_toml, warn_malformed_entry_not_table, @@ -206,13 +207,26 @@ def _resolved_package_for_dependency( ) return None if len(candidates) > 1: - log.warning( - "Skipping uv.lock dependency %r: %d resolved versions present " - "(marker-conditional) -- no marker evaluation", - name, - len(candidates), - ) - return None + versions = [c.get("version") for c in candidates] + first_version = versions[0] + if not all( + isinstance(v, str) + and isinstance(first_version, str) + and is_same_version(v, first_version) + for v in versions + ): + log.warning( + "Skipping uv.lock dependency %r: %d resolved versions present " + "(marker-conditional) -- no marker evaluation", + name, + len(candidates), + ) + return None + # Every candidate agrees on version (PEP 440) -- the marker + # branches they come from don't change the resolved output, so + # there's nothing ambiguous to guess about; same "agreeing + # duplicates collapse" rule every sibling lock format already + # applies (see e.g. `_poetry_lock.py`'s `is_same_version` check). return candidates[0] @@ -315,8 +329,22 @@ def _enqueue_requested_extras( for k, v in opt_deps_map.items() if canonicalize_name(k) == extra_canon ), - [], + None, ) + if extra_deps is None: + # Requested but not declared by the package's own + # `optional-dependencies` table (stale/hand-edited lock, or + # an extra renamed/removed since resolution) -- a deviation + # worth a WARNING, same as every other unresolvable + # reference in this module, not a silent no-op. + log.warning( + "uv.lock: %r requested extra %r not found in %r's " + "optional-dependencies -- skipping", + dep_ref.get("name", canonical_name), + extra_name, + pkg.get("name", canonical_name), + ) + continue if isinstance(extra_deps, list): queue.extend(extra_deps) @@ -380,6 +408,19 @@ def extract_uv_lock_dependencies( data = load_lock_toml(lock_path) if data is None: return None + # uv.lock's own format marker is a flat top-level `version` (int), + # not nested in a sub-table like poetry.lock's `metadata.lock-version` + # or pdm.lock's `metadata.lock_version` -- same genuineness check as + # those two siblings, adapted to this format's actual shape, so an + # unrelated/hand-edited TOML file can't silently win the cascade via + # a spurious authoritative-empty `[[package]]` list. + if not isinstance(data.get("version"), int): + log.warning( + "%s: no top-level 'version' key (int) -- doesn't look like a " + "genuine uv.lock, ignoring", + lock_path, + ) + return None packages = data.get("package", []) if not isinstance(packages, list): diff --git a/tests/extract/test_pdm_lock.py b/tests/extract/test_pdm_lock.py index 6b1010c2..21a65a1c 100644 --- a/tests/extract/test_pdm_lock.py +++ b/tests/extract/test_pdm_lock.py @@ -155,6 +155,31 @@ def test_non_default_group_package_excluded() -> None: assert not extract_pdm_lock_dependencies(tmp_path) +def test_non_default_group_non_registry_sourced_package_is_silent( + caplog: pytest.LogCaptureFixture, +) -> None: + """A package that is both non-default-group and non-registry-sourced + must be dropped with no warning at all -- it's excluded either way, + and warning about a source type that's being discarded regardless is + noise. Regression for the exact filter-ordering bug class found and + fixed in `_pylock.py`'s equivalent + (`test_marker_excluded_non_registry_sourced_package_is_silent`), + checked here for `_default_group_package_or_none`'s own ordering.""" + with tempfile.TemporaryDirectory() as tmp: + tmp_path = Path(tmp) + _write_lock( + tmp_path, + '[[package]]\nname = "dev-only-vcs"\nversion = "0.1.0"\n' + 'groups = ["test"]\ngit = "some-value"\n', + ) + + with caplog.at_level(logging.WARNING): + result = extract_pdm_lock_dependencies(tmp_path) + + assert not result + assert caplog.text == "" + + def test_package_in_default_and_other_group_included() -> None: """A package listed under both `default` and another group still counts -- only *exclusively* non-default packages are dropped.""" diff --git a/tests/extract/test_poetry_lock.py b/tests/extract/test_poetry_lock.py index 57c0393a..8b823e61 100644 --- a/tests/extract/test_poetry_lock.py +++ b/tests/extract/test_poetry_lock.py @@ -157,6 +157,31 @@ def test_dev_only_group_package_excluded() -> None: assert not extract_poetry_lock_dependencies(tmp_path) +def test_dev_only_group_non_registry_sourced_package_is_silent( + caplog: pytest.LogCaptureFixture, +) -> None: + """A package that is both dev-only-group and non-registry-sourced + must be dropped with no warning at all -- it's excluded either way, + and warning about a source type that's being discarded regardless + is noise. Regression for the exact filter-ordering bug class found + and fixed in `_pylock.py`'s equivalent + (`test_marker_excluded_non_registry_sourced_package_is_silent`), + checked here for `_main_group_package_or_none`'s own ordering.""" + with tempfile.TemporaryDirectory() as tmp: + tmp_path = Path(tmp) + _write_lock( + tmp_path, + '[[package]]\nname = "dev-only-vcs"\nversion = "0.1.0"\n' + 'groups = ["dev"]\n[package.source]\ntype = "git"\n', + ) + + with caplog.at_level(logging.WARNING): + result = extract_poetry_lock_dependencies(tmp_path) + + assert not result + assert caplog.text == "" + + def test_package_in_main_and_dev_groups_included() -> None: """A package listed under both `main` and another group still counts -- only *exclusively* non-main packages are dropped.""" diff --git a/tests/extract/test_uv_lock.py b/tests/extract/test_uv_lock.py index da007939..2ce9583d 100644 --- a/tests/extract/test_uv_lock.py +++ b/tests/extract/test_uv_lock.py @@ -61,6 +61,31 @@ def test_malformed_toml_returns_none_and_warns( assert "Failed to parse" in caplog.text +def test_missing_top_level_version_key_returns_none_and_warns( + caplog: pytest.LogCaptureFixture, +) -> None: + """A TOML file with no top-level `version` int is missing uv.lock's + own format marker -- doesn't look genuine, matching poetry.lock's/ + pdm.lock's own genuineness check (`has_required_top_level_table`), + adapted to uv.lock's flat (non-nested) marker key. Without this + check, an unrelated/hand-edited TOML file could silently win the + cascade over a genuinely usable lower-priority lock format via a + spurious authoritative-empty `[[package]]` list.""" + with tempfile.TemporaryDirectory() as tmp: + tmp_path = Path(tmp) + (tmp_path / "uv.lock").write_text( + '[[package]]\nname = "demo"\nversion = "1.0.0"\n' + 'source = { editable = "." }\n', + encoding="utf-8", + ) + + with caplog.at_level(logging.WARNING): + result = extract_uv_lock_dependencies(tmp_path) + + assert result is None + assert "doesn't look like a genuine uv.lock" in caplog.text + + def test_package_key_not_a_list_returns_none_and_warns( caplog: pytest.LogCaptureFixture, ) -> None: @@ -362,6 +387,28 @@ def test_ambiguous_multi_version_dependency_skipped_and_warns( assert "2 resolved versions" in caplog.text +def test_same_name_agreeing_version_candidates_collapsed() -> None: + """Two `[[package]]` entries for the same name from different + `resolution-markers` branches that happen to pin the *same* PEP 440 + version aren't ambiguous -- the resolved output is identical + regardless of which marker branch applies, so this collapses to one + entry instead of being skipped like a genuine version conflict.""" + with tempfile.TemporaryDirectory() as tmp: + tmp_path = Path(tmp) + _write_lock( + tmp_path, + _ROOT_HEADER + 'dependencies = [{ name = "click" }]\n\n' + '[[package]]\nname = "click"\nversion = "8.1.8"\n' + 'source = { registry = "https://pypi.org/simple" }\n\n' + '[[package]]\nname = "click"\nversion = "8.1.8.0"\n' + 'source = { registry = "https://pypi.org/simple" }\n', + ) + + result = extract_uv_lock_dependencies(tmp_path) + + assert result == ["click==8.1.8"] + + @pytest.mark.parametrize( "source_key", ["git", "url", "path", "directory", "editable", "virtual"] ) diff --git a/tests/extract/test_uv_lock_transitive.py b/tests/extract/test_uv_lock_transitive.py index 4c07e897..bafda929 100644 --- a/tests/extract/test_uv_lock_transitive.py +++ b/tests/extract/test_uv_lock_transitive.py @@ -230,6 +230,33 @@ def test_transitive_walk_extra_name_canonicalization() -> None: assert set(result) == {"pkg==1.0.0", "dep==1.0.0"} +def test_transitive_walk_requested_extra_not_declared_warns_and_skips( + caplog: pytest.LogCaptureFixture, +) -> None: + """A requested extra that the referenced package's own + `optional-dependencies` table doesn't declare at all (stale/hand- + edited lock, or an extra renamed/removed since resolution) must warn + -- silently yielding nothing is a deviation like any other + unresolvable reference in this module.""" + with tempfile.TemporaryDirectory() as tmp: + tmp_path = Path(tmp) + _write_lock( + tmp_path, + _ROOT_HEADER + 'dependencies = [{ name = "requests", extra = "socks" }]\n\n' + '[[package]]\nname = "requests"\nversion = "2.31.0"\n' + 'source = { registry = "https://pypi.org/simple" }\n' + "[package.optional-dependencies]\n" + 'security = [{ name = "PySocks" }]\n', + ) + + with caplog.at_level(logging.WARNING): + result = extract_uv_lock_dependencies(tmp_path) + + assert result == ["requests==2.31.0"] + assert "not found" in caplog.text + assert "socks" in caplog.text + + def test_transitive_walk_extra_deps_non_list() -> None: """When an entry in optional-dependencies is not a list, it is not queued.""" with tempfile.TemporaryDirectory() as tmp: diff --git a/working-docs/design/complexity-and-file-size-roadmap.md b/working-docs/design/complexity-and-file-size-roadmap.md index 0fbdbd66..2ce9630c 100644 --- a/working-docs/design/complexity-and-file-size-roadmap.md +++ b/working-docs/design/complexity-and-file-size-roadmap.md @@ -47,11 +47,13 @@ trip review attention: - **`tests/`** (excluding `tests/extract/huggingface/` mock fixture catalogs): 9 files exceed 415 lines, worst is `test_hdf5.py` at 552 (`test_pytorch_pt2.py` 459, `test_assembly_edge_cases.py` 456, - `test_annotation_provenance_emit.py` 449, `test_pyproject.py` 436, - `test_cli_options.py` 429, `test_gguf.py` 423 -- - `test_deps_enrichment_pypi_fallback.py` and `test_setuptools_cfg.py` have - since been split under this limit, see `test_deps_license.py` and - `test_setuptools_cfg_backend.py`). + `test_annotation_provenance_emit.py` 449, `test_cli_options.py` 429, + `test_gguf.py` 423 -- `test_deps_enrichment_pypi_fallback.py`, + `test_setuptools_cfg.py`, `test_pyproject.py`, `test_hatch_hook_metadata.py`, + and `test_poetry_parsing.py` have since been split under this limit, see + `test_deps_license.py`, `test_setuptools_cfg_backend.py`, + `test_pyproject_license.py`, `test_hatch_hook_metadata_parity.py`, and + `test_poetry_extract.py` respectively). None have crossed the 800-line hard cap, so nothing is currently broken -- but per AGENTS.md, a file should be split *before* crossing the soft diff --git a/working-docs/implementation/backend-file-discovery-validation.md b/working-docs/implementation/backend-file-discovery-validation.md index 98e52b39..c2b580b3 100644 --- a/working-docs/implementation/backend-file-discovery-validation.md +++ b/working-docs/implementation/backend-file-discovery-validation.md @@ -437,7 +437,7 @@ PyPI) covering PEP 621's TOML dotted-key license form (`license.text = the inline-table form (`license = {text = "..."}`) -- no code-level handling needed either way, just a regression test (`test_read_pyproject_license_toml_dotted_key_matches_inline_table` in -`tests/extract/test_pyproject.py`) documenting the equivalence. +`tests/extract/test_pyproject_license.py`) documenting the equivalence. ### Findings diff --git a/working-docs/implementation/license-pipeline.md b/working-docs/implementation/license-pipeline.md index 77364ec4..f81df62e 100644 --- a/working-docs/implementation/license-pipeline.md +++ b/working-docs/implementation/license-pipeline.md @@ -481,3 +481,8 @@ path. | `tests/assemble/test_license_files_bundling.py` | End-to-end `[project.license-files]` bundling tests against the vendored real-world fixtures | +| `tests/assemble/test_deps_license.py` | Unit tests for + `build_license_elements()`/`_is_license_concluded()`/ + `_get_or_create_license_element()`, split out of + `test_deps_enrichment_pypi_fallback.py` to stay under the file-size + soft limit | diff --git a/working-docs/implementation/poetry-support.md b/working-docs/implementation/poetry-support.md index aa28184b..4546d8ae 100644 --- a/working-docs/implementation/poetry-support.md +++ b/working-docs/implementation/poetry-support.md @@ -39,7 +39,7 @@ under `[tool.poetry]`. Issue [#64]. | `src/pitloom/extract/_pyproject.py` | Falls back to / merges Poetry metadata; wires in `poetry.lock` reading | | `src/pitloom/core/_models_wheel_poetry.py` | Wheel file discovery, delegating to poetry-core's own `WheelBuilder` | | `src/pitloom/assemble/spdx3/deps.py`, `document.py` | Additive locked-transitive-dependency `dependsOn` edges, `completeness` tagging | -| `tests/extract/test_poetry_parsing.py`, `tests/extract/test_poetry_pyproject.py` | Unit and integration tests for metadata extraction (originally `tests/test_poetry.py`, later split into these two files -- see `working-docs/design/cli-test-coverage-roadmap.md`) | +| `tests/extract/test_poetry_parsing.py`, `tests/extract/test_poetry_extract.py`, `tests/extract/test_poetry_pyproject.py` | Unit and integration tests for metadata extraction (originally `tests/test_poetry.py`, later split into these files -- see `working-docs/design/cli-test-coverage-roadmap.md`; `test_poetry_extract.py` holds `extract_poetry_metadata()` tests, split out of `test_poetry_parsing.py` to stay under the file-size soft limit) | | `tests/extract/test_poetry_lock.py` | `poetry.lock` parsing unit and integration tests | | `tests/core/models_wheel/test_models_wheel_poetry.py` | Wheel file discovery unit tests | | `tests/assemble/test_deps_locked_dependencies.py` | Assemble-layer additive-edge/`completeness` tests | diff --git a/working-docs/implementation/provenance/multi-source-conflict.md b/working-docs/implementation/provenance/multi-source-conflict.md index dad79604..cdcb328a 100644 --- a/working-docs/implementation/provenance/multi-source-conflict.md +++ b/working-docs/implementation/provenance/multi-source-conflict.md @@ -98,7 +98,7 @@ paths, the same directory-detection fallback when nothing is declared) so a future fifth extraction path can't reintroduce the same gap by omission. Cross-path regression tests (`test_metadata_from_hatchling_matches_read_pyproject_for_license_conflict` -in `tests/extract/test_hatch_hook_metadata.py`, +in `tests/extract/test_hatch_hook_metadata_parity.py`, `test_read_poetry_matches_read_pyproject_fallback_for_license_conflict` in `tests/extract/test_poetry_pyproject.py` -- paths since renamed and moved, see `cli-test-coverage-roadmap.md`) assert the paths agree on the same From b2cf21f85e2ea8ee999818b28c4da7f83fd7a79a Mon Sep 17 00:00:00 2001 From: Arthit Suriyawongkul Date: Wed, 9 Sep 2026 06:25:13 +0700 Subject: [PATCH 35/35] Update AGENTS.md Signed-off-by: Arthit Suriyawongkul --- AGENTS.md | 43 +++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 43 insertions(+) diff --git a/AGENTS.md b/AGENTS.md index fc2fffff..aaa60149 100644 --- a/AGENTS.md +++ b/AGENTS.md @@ -151,6 +151,17 @@ shape described, not just the module where each was first found. compatibility terms. See "Version comparison: PEP 440, not SemVer" in `docs/dependency-sources.md` for the user-facing version of this same distinction. + - **The same PEP 440-not-raw-string rule applies to detecting version + *conflicts*, not just equality checks in prose.** A duplicate-name + entry across a lock file's own `[[package]]` list (the same + canonical name appearing more than once, e.g. once per marker + branch) needs `is_same_version()` before being treated as a real + conflict -- `"1.0"` and `"1.0.0"` from two different branches are the + same release, not a conflict to warn about and drop. A sibling that + skips this check (comparing the raw version strings, or dropping + every duplicate name outright regardless of whether the versions + agree) both over-warns on non-conflicts and under-reports a real, + agreeing dependency that every other sibling format would have kept. - **A private third-party API (`obj._attr`) does not owe you any structural guarantee beyond what it happens to return today.** E.g. `packaging.markers.Marker()._markers` does not pre-group same- @@ -223,6 +234,38 @@ shape described, not just the module where each was first found. is correct. When broadening a check across several fields, broaden the fixture that backs its tests across the same fields in the same change, or the new branches go untested despite "the tests pass." +- **A source that can legitimately resolve to zero entries needs its own + "is this genuinely a file of this format" check, or an empty result + becomes indistinguishable from a wrong file.** In a priority cascade + (e.g. `_locked_dependencies.py` picking among `poetry.lock`/`pdm.lock`/ + `pylock.toml`/`uv.lock`/`Pipfile.lock`/`requirements.txt`), a resolver + that genuinely produces zero packages must still look different from an + unrelated/truncated/hand-edited file that merely happens to be found + under that format's filename -- otherwise the latter silently wins the + cascade over a real, lower-priority lock file via a spurious + authoritative-empty result. Check for the format's own identifying + top-level marker (`poetry.lock`'s string `metadata.lock-version`, + `pdm.lock`'s string `metadata.lock_version`, `Pipfile.lock`'s int + `_meta.pipfile-spec`, `uv.lock`'s flat int `version`) before trusting an + empty package list as real, not just when it's non-empty. This was + missed for `uv.lock` well after the identical check had already been + added to three sibling formats -- when a new source joins an existing + cascade/fallback family, check whether it needs the same class of guard + every existing sibling already has, not just the guards relevant to + the bug that prompted adding the new source. +- **A presence-only check (`find_first_present_key()`-style: "is any of + these keys present at all") silently misfires the moment one key in + the set is genuinely boolean-valued instead of presence-implies-true.** + `Pipfile.lock`'s non-registry-source keys are almost all presence-only + (a `"git"`/`"path"`/`"url"` string means "non-registry, full stop"), + but `"editable"` is schema-legal as an explicit `false` -- a naive + presence check would misread `"editable": false` as "editable source, + exclude" instead of "not editable, no exemption needed here." Before + reusing a presence-only helper across a whole key set, check each key's + real schema: a key that can legitimately carry a meaningful `false` (or + any other falsy-but-real value) needs its own value check, not just a + presence check, even when every other key in the same set is fine with + presence alone. ## CLI output