From 8652e0f9effc27f04d6fd29de80519ad3cae2e5d Mon Sep 17 00:00:00 2001 From: Patrick Ogenstad Date: Tue, 25 Aug 2026 11:36:22 +0200 Subject: [PATCH 1/6] fix(packaging): correct declared dependencies and publish the tests extra The declared dependency surface had drifted from what the SDK actually needs. pydantic>=2.0.0 admitted 2.0 and 2.0.2, on which `import infrahub_sdk` raises SchemaError because their regex engine rejects the `\_` escape in the generated schema model patterns. anyio, typing-extensions and packaging were imported by the shipped package but only ever arrived as transitive dependencies of httpx, pydantic and the test tooling, so a constrained resolution could install the SDK unusable. The `tests` extra is described in the installation guide but was never published, so `pip install 'infrahub-sdk[tests]'` warned and installed nothing beyond the base package. It now exists and carries what `infrahub_sdk.testing` and the bundled pytest plugin import. `all` becomes self-referential so it cannot drift from the extras it aggregates, which is how it had already lost mdxify. numpy and mdxify were declared but never imported. pyarrow declares numpy itself on the releases that need it, and mdxify only builds the docs, so it moves to a docs dependency group. Each floor was established by installing the candidate version and exercising the import rather than read off release notes: anyio.Path appears in 3.3.0 rather than 3.0, and typing-extensions needs 4.4.0 for PEP 696 TypeVar defaults. New unit tests over the packaging metadata keep the declared and imported dependency sets in agreement. --- changelog/+dependency-lower-bounds.changed.md | 5 + .../+packaging-metadata-tests.housekeeping.md | 1 + changelog/+tests-extra.added.md | 5 + .../+unused-extra-dependencies.removed.md | 5 + pyproject.toml | 36 ++-- tests/constants.py | 4 + tests/unit/test_packaging_metadata.py | 178 +++++++++++++++++ uv.lock | 189 +++--------------- 8 files changed, 244 insertions(+), 179 deletions(-) create mode 100644 changelog/+dependency-lower-bounds.changed.md create mode 100644 changelog/+packaging-metadata-tests.housekeeping.md create mode 100644 changelog/+tests-extra.added.md create mode 100644 changelog/+unused-extra-dependencies.removed.md create mode 100644 tests/unit/test_packaging_metadata.py diff --git a/changelog/+dependency-lower-bounds.changed.md b/changelog/+dependency-lower-bounds.changed.md new file mode 100644 index 000000000..8542f3326 --- /dev/null +++ b/changelog/+dependency-lower-bounds.changed.md @@ -0,0 +1,5 @@ +`infrahub-sdk` now requires `pydantic>=2.0.3`, up from `>=2.0.0`. + +If you pin `pydantic` to 2.0, 2.0.1 or 2.0.2, installing the SDK now fails while resolving dependencies. Those versions never actually worked: `import infrahub_sdk` raised a `SchemaError` on them, because their regex engine rejects a pattern used by the schema models. Pin `pydantic>=2.0.3` to resolve it. + +`anyio`, `typing-extensions` and `packaging` are also now installed as direct requirements. The SDK has always imported them but relied on other packages to pull them in, so a minimal or heavily constrained environment could end up with the SDK installed and unusable. No action is needed, installs simply become reliable. diff --git a/changelog/+packaging-metadata-tests.housekeeping.md b/changelog/+packaging-metadata-tests.housekeeping.md new file mode 100644 index 000000000..8804dc074 --- /dev/null +++ b/changelog/+packaging-metadata-tests.housekeeping.md @@ -0,0 +1 @@ +Added unit tests over the packaging metadata so the declared dependencies stay honest. They check that every requirement has a lower bound, that the `all` extra keeps covering the other extras, and that the packages the SDK imports and the packages it declares match each other. diff --git a/changelog/+tests-extra.added.md b/changelog/+tests-extra.added.md new file mode 100644 index 000000000..6caae804e --- /dev/null +++ b/changelog/+tests-extra.added.md @@ -0,0 +1,5 @@ +`pip install 'infrahub-sdk[tests]'` now works. The `tests` extra is described in the installation guide but was never actually published, so the command warned that no such extra existed and installed nothing beyond the base package. + +Install it if you use the `pytest-infrahub` plugin to test Transforms, Queries and Checks, or the `infrahub_sdk.testing` helpers. Previously you had to work out the missing requirements and declare them yourself. + +Be aware that it is a large install, adding around 66 packages on top of the base SDK, among them Docker, FastAPI and Prefect client libraries. These come from the `infrahub_sdk.testing` helpers, which run Infrahub in containers. If you only need the `infrahubctl` CLI, install `infrahub-sdk[ctl]` instead. `infrahub-sdk[all]` now covers both `ctl` and `tests`, so it pulls in considerably more than it used to. diff --git a/changelog/+unused-extra-dependencies.removed.md b/changelog/+unused-extra-dependencies.removed.md new file mode 100644 index 000000000..18046a268 --- /dev/null +++ b/changelog/+unused-extra-dependencies.removed.md @@ -0,0 +1,5 @@ +`numpy` and `mdxify` are no longer installed by the `ctl` and `all` extras. + +Neither is used by the SDK. `numpy` was only ever needed because older `pyarrow` releases required it, and `pyarrow` still installs it itself on the versions that do. `mdxify` is a documentation tool used when building these docs, not at runtime. + +If your project imports either package directly, add it to your own dependencies rather than relying on `infrahub-sdk[ctl]` to supply it. diff --git a/pyproject.toml b/pyproject.toml index f6fc39879..64911cb25 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -18,10 +18,14 @@ classifiers = [ "Programming Language :: Python :: 3.14", ] dependencies = [ - "pydantic>=2.0.0,!=2.0.1,!=2.1.0,<3.0.0", + # 2.0 and 2.0.2 cannot build the generated schema models: their regex engine rejects the + # `\_` escape in the model field patterns, so `import infrahub_sdk` fails outright. + "pydantic>=2.0.3,!=2.1.0,<3.0.0", "pydantic-settings>=2.0", "graphql-core>=3.1,<3.3", "httpx>=0.20", + "anyio>=3.3.0", # 3.3.0 introduced `anyio.Path`, used by the async file handler + "typing-extensions>=4.4.0", # 4.4.0 introduced PEP 696 `TypeVar(default=...)` "ujson>=5", "dulwich>=0.24.7", "whenever>=0.9.3,<0.10.0", @@ -43,30 +47,28 @@ infrahubctl = "infrahub_sdk.ctl.cli:app" [project.optional-dependencies] ctl = [ "Jinja2>=3", - "numpy>=1.24.2; python_version<'3.12'", - "numpy>=1.26.2; python_version>='3.12'", "pyarrow>=14", "pyyaml>=6", "ruamel.yaml>=0.18", "rich>=12,<14", "typer>=0.15.0", + # Not imported directly; constrains the `click` that `typer` resolves to. "click>=8.3,<9", "ariadne-codegen==0.18.0", - "mdxify>=0.2.23; python_version>='3.10'", ] +# The user-facing testing surface: `infrahub_sdk.testing` and the bundled `pytest-infrahub` +# plugin. Both ship in the wheel but import packages that no other extra installs. Distinct from +# the `tests` dependency group below, which is the tooling used to test the SDK itself. +tests = [ + "infrahub-testcontainers>=1.7.3", + "packaging>=21.0", + "pytest>=7.0", +] + +# Self-referential so it cannot drift out of sync with the extras it aggregates. all = [ - "Jinja2>=3", - "numpy>=1.24.2; python_version<'3.12'", - "numpy>=1.26.2; python_version>='3.12'", - "pyarrow>=14", - "pytest", - "pyyaml>=6", - "ruamel.yaml>=0.18", - "rich>=12,<14", - "typer>=0.15.0", - "click>=8.3,<9", - "ariadne-codegen==0.18.0", + "infrahub-sdk[ctl,tests]", ] [dependency-groups] @@ -94,10 +96,14 @@ types = [ "types-pyyaml", "types-python-slugify>=8.0.0.3", ] +docs = [ + "mdxify>=0.2.23", +] dev = [ {include-group = "tests"}, {include-group = "lint"}, {include-group = "types"}, + {include-group = "docs"}, "ipython", "requests>=2.33.0", "prek>=0.3.0", diff --git a/tests/constants.py b/tests/constants.py index c41bd318a..74d17b00d 100644 --- a/tests/constants.py +++ b/tests/constants.py @@ -7,3 +7,7 @@ TEST_DIR = Path(__file__).parent FIXTURES_DIR = TEST_DIR / "fixtures" FIXTURE_REPOS_DIR = FIXTURES_DIR / "repos" + +REPO_ROOT = TEST_DIR.parent +PACKAGE_DIR = REPO_ROOT / "infrahub_sdk" +PYPROJECT_FILE = REPO_ROOT / "pyproject.toml" diff --git a/tests/unit/test_packaging_metadata.py b/tests/unit/test_packaging_metadata.py new file mode 100644 index 000000000..34de15cce --- /dev/null +++ b/tests/unit/test_packaging_metadata.py @@ -0,0 +1,178 @@ +"""Checks that keep the declared dependency surface in `pyproject.toml` honest. + +The version matrix in CI installs whatever `uv.lock` pins, so it never exercises the ranges +declared in `pyproject.toml`. These checks cover the part of that gap that costs no extra CI +time: that every requirement is bounded, that the extras aggregate correctly, and that the +declared dependencies and the imports in the shipped package agree with each other. +""" + +from __future__ import annotations + +import ast +import sys +from dataclasses import dataclass +from pathlib import Path +from typing import Any + +import pytest +from packaging.requirements import Requirement +from packaging.specifiers import SpecifierSet +from packaging.version import Version + +from tests.constants import PACKAGE_DIR, PYPROJECT_FILE, REPO_ROOT + +if sys.version_info >= (3, 11): + import tomllib +else: + # Drop this branch, and the suppression, once Python 3.10 support ends. + import tomli as tomllib # type: ignore[import-not-found] + +PROJECT_NAME = "infrahub-sdk" + +# Operators that establish a floor. A requirement without one of these lets a resolver reach back +# to the first release ever published, which is never a version we have tested against. +LOWER_BOUND_OPERATORS = frozenset({">=", ">", "==", "~="}) + +# Only modules whose import name differs from their distribution name in a way PEP 503 +# normalisation cannot recover. Everything else maps by normalising the module name. +IMPORT_NAME_TO_DISTRIBUTION = { + "graphql": "graphql-core", + "ruamel": "ruamel.yaml", + "yaml": "pyyaml", +} + +# Distributions declared on purpose without being imported, with the reason why. +DECLARED_WITHOUT_IMPORT = { + "click": "Constrains the `click` version that `typer` resolves to; never imported directly.", +} + + +def _normalize(name: str) -> str: + """Normalise a distribution name per PEP 503 so `ruamel.yaml` and `Jinja2` compare cleanly.""" + return "".join("-" if character in "-_." else character for character in name.lower()) + + +def _pyproject() -> dict[str, Any]: + with PYPROJECT_FILE.open("rb") as handle: + return tomllib.load(handle)["project"] + + +def _optional_dependencies() -> dict[str, list[str]]: + return _pyproject()["optional-dependencies"] + + +def _is_self_reference(requirement: Requirement) -> bool: + return _normalize(requirement.name) == _normalize(PROJECT_NAME) + + +@dataclass +class RequirementCase: + name: str + requirement: str + + +def _requirement_cases() -> list[RequirementCase]: + """Every requirement a user can install, labelled with the section that declares it.""" + project = _pyproject() + sections = {"project.dependencies": project["dependencies"], **project["optional-dependencies"]} + return [ + RequirementCase(name=f"{section}-{Requirement(requirement).name}", requirement=requirement) + for section, requirements in sections.items() + for requirement in requirements + ] + + +def _declared_distributions() -> set[str]: + """Every distribution a user can install, across the core dependencies and all extras.""" + parsed = (Requirement(case.requirement) for case in _requirement_cases()) + return {_normalize(requirement.name) for requirement in parsed if not _is_self_reference(requirement)} + + +def _imported_distributions() -> dict[str, Path]: + """Map each third-party distribution imported under the package to a file that imports it.""" + imported: dict[str, Path] = {} + for path in sorted(PACKAGE_DIR.rglob("*.py")): + for node in ast.walk(ast.parse(path.read_text(encoding="utf-8"))): + if isinstance(node, ast.Import): + modules = [alias.name for alias in node.names] + elif isinstance(node, ast.ImportFrom) and node.level == 0 and node.module: + modules = [node.module] + else: + continue + for module in modules: + top_level = module.split(".")[0] + if top_level in sys.stdlib_module_names or top_level == PACKAGE_DIR.name: + continue + distribution = _normalize(IMPORT_NAME_TO_DISTRIBUTION.get(top_level, top_level)) + imported.setdefault(distribution, path.relative_to(REPO_ROOT)) + return imported + + +@pytest.mark.parametrize("case", [pytest.param(tc, id=tc.name) for tc in _requirement_cases()]) +def test_requirement_declares_a_lower_bound(case: RequirementCase) -> None: + requirement = Requirement(case.requirement) + if _is_self_reference(requirement): + pytest.skip("self-referential extra, versioned by the project itself") + + operators = {specifier.operator for specifier in requirement.specifier} + assert operators & LOWER_BOUND_OPERATORS, ( + f"{case.requirement!r} has no lower bound, so a resolver may select an ancient release we " + f"have never tested against. Add a floor matching the oldest version known to work." + ) + + +def test_all_extra_aggregates_every_other_extra() -> None: + """`all` must be defined by self-reference so it cannot drift out of sync with the other extras.""" + extras = _optional_dependencies() + aggregated: set[str] = set() + for raw in extras["all"]: + requirement = Requirement(raw) + assert _is_self_reference(requirement), ( + f"the 'all' extra must aggregate this project's own extras by self-reference, found {raw!r}" + ) + aggregated |= {_normalize(extra) for extra in requirement.extras} + + assert aggregated == {_normalize(name) for name in extras if name != "all"} + + +def test_imports_in_shipped_package_are_declared() -> None: + """Anything the wheel imports must be installable, not left to arrive as a transitive dependency.""" + declared = _declared_distributions() + undeclared = { + distribution: source + for distribution, source in _imported_distributions().items() + if distribution not in declared + } + assert not undeclared, "\n".join( + f"{distribution} is imported by {source} but is declared nowhere in pyproject.toml" + for distribution, source in sorted(undeclared.items()) + ) + + +def test_declared_dependencies_are_imported() -> None: + """A declared dependency nobody imports is either dead weight or belongs in a dependency group.""" + imported = _imported_distributions() + unused = sorted( + distribution + for distribution in _declared_distributions() + if distribution not in imported and distribution not in DECLARED_WITHOUT_IMPORT + ) + assert not unused, ( + f"declared but never imported under {PACKAGE_DIR.name}/: {', '.join(unused)}. Move it to a " + f"dependency group if it is only needed for development, or record the reason it is declared " + f"in DECLARED_WITHOUT_IMPORT." + ) + + +def test_requires_python_matches_classifiers() -> None: + project = _pyproject() + supported = SpecifierSet(project["requires-python"]) + prefix = "Programming Language :: Python :: 3." + + from_classifiers = {c.removeprefix(prefix) for c in project["classifiers"] if c.startswith(prefix)} + from_specifier = {str(minor) for minor in range(50) if Version(f"3.{minor}") in supported} + + assert from_classifiers == from_specifier, ( + f"requires-python allows 3.x for x in {sorted(from_specifier)} but the classifiers " + f"advertise {sorted(from_classifiers)}" + ) diff --git a/uv.lock b/uv.lock index d5ba79e90..3ea77902f 100644 --- a/uv.lock +++ b/uv.lock @@ -678,6 +678,7 @@ wheels = [ name = "infrahub-sdk" source = { editable = "." } dependencies = [ + { name = "anyio" }, { name = "dulwich" }, { name = "graphql-core" }, { name = "httpx" }, @@ -685,6 +686,7 @@ dependencies = [ { name = "pydantic" }, { name = "pydantic-settings" }, { name = "tomli", marker = "python_full_version < '3.11'" }, + { name = "typing-extensions" }, { name = "ujson" }, { name = "whenever" }, ] @@ -693,9 +695,9 @@ dependencies = [ all = [ { name = "ariadne-codegen" }, { name = "click" }, + { name = "infrahub-testcontainers" }, { name = "jinja2" }, - { name = "numpy", version = "2.2.6", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version < '3.11'" }, - { name = "numpy", version = "2.3.4", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version >= '3.11'" }, + { name = "packaging" }, { name = "pyarrow" }, { name = "pytest" }, { name = "pyyaml" }, @@ -707,15 +709,17 @@ ctl = [ { name = "ariadne-codegen" }, { name = "click" }, { name = "jinja2" }, - { name = "mdxify" }, - { name = "numpy", version = "2.2.6", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version < '3.11'" }, - { name = "numpy", version = "2.3.4", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version >= '3.11'" }, { name = "pyarrow" }, { name = "pyyaml" }, { name = "rich" }, { name = "ruamel-yaml" }, { name = "typer" }, ] +tests = [ + { name = "infrahub-testcontainers" }, + { name = "packaging" }, + { name = "pytest" }, +] [package.dev-dependencies] dev = [ @@ -726,6 +730,7 @@ dev = [ { name = "ipython", version = "8.37.0", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version < '3.11'" }, { name = "ipython", version = "9.6.0", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version >= '3.11'" }, { name = "jsonschema" }, + { name = "mdxify" }, { name = "mypy" }, { name = "prek" }, { name = "pytest" }, @@ -744,6 +749,9 @@ dev = [ { name = "types-ujson" }, { name = "yamllint" }, ] +docs = [ + { name = "mdxify" }, +] lint = [ { name = "astroid" }, { name = "mypy" }, @@ -770,6 +778,7 @@ types = [ [package.metadata] requires-dist = [ + { name = "anyio", specifier = ">=3.3.0" }, { name = "ariadne-codegen", marker = "extra == 'all'", specifier = "==0.18.0" }, { name = "ariadne-codegen", marker = "extra == 'ctl'", specifier = "==0.18.0" }, { name = "click", marker = "extra == 'all'", specifier = ">=8.3,<9" }, @@ -777,19 +786,19 @@ requires-dist = [ { name = "dulwich", specifier = ">=0.24.7" }, { name = "graphql-core", specifier = ">=3.1,<3.3" }, { name = "httpx", specifier = ">=0.20" }, + { name = "infrahub-testcontainers", marker = "extra == 'all'", specifier = ">=1.7.3" }, + { name = "infrahub-testcontainers", marker = "extra == 'tests'", specifier = ">=1.7.3" }, { name = "jinja2", marker = "extra == 'all'", specifier = ">=3" }, { name = "jinja2", marker = "extra == 'ctl'", specifier = ">=3" }, - { name = "mdxify", marker = "python_full_version >= '3.10' and extra == 'ctl'", specifier = ">=0.2.23" }, { name = "netutils", specifier = ">=1.0.0" }, - { name = "numpy", marker = "python_full_version >= '3.12' and extra == 'all'", specifier = ">=1.26.2" }, - { name = "numpy", marker = "python_full_version >= '3.12' and extra == 'ctl'", specifier = ">=1.26.2" }, - { name = "numpy", marker = "python_full_version < '3.12' and extra == 'all'", specifier = ">=1.24.2" }, - { name = "numpy", marker = "python_full_version < '3.12' and extra == 'ctl'", specifier = ">=1.24.2" }, + { name = "packaging", marker = "extra == 'all'", specifier = ">=21.0" }, + { name = "packaging", marker = "extra == 'tests'", specifier = ">=21.0" }, { name = "pyarrow", marker = "extra == 'all'", specifier = ">=14" }, { name = "pyarrow", marker = "extra == 'ctl'", specifier = ">=14" }, - { name = "pydantic", specifier = ">=2.0.0,!=2.0.1,!=2.1.0,<3.0.0" }, + { name = "pydantic", specifier = ">=2.0.3,!=2.1.0,<3.0.0" }, { name = "pydantic-settings", specifier = ">=2.0" }, - { name = "pytest", marker = "extra == 'all'" }, + { name = "pytest", marker = "extra == 'all'", specifier = ">=7.0" }, + { name = "pytest", marker = "extra == 'tests'", specifier = ">=7.0" }, { name = "pyyaml", marker = "extra == 'all'", specifier = ">=6" }, { name = "pyyaml", marker = "extra == 'ctl'", specifier = ">=6" }, { name = "rich", marker = "extra == 'all'", specifier = ">=12,<14" }, @@ -799,10 +808,11 @@ requires-dist = [ { name = "tomli", marker = "python_full_version < '3.11'", specifier = ">=1.1.0" }, { name = "typer", marker = "extra == 'all'", specifier = ">=0.15.0" }, { name = "typer", marker = "extra == 'ctl'", specifier = ">=0.15.0" }, + { name = "typing-extensions", specifier = ">=4.4.0" }, { name = "ujson", specifier = ">=5" }, { name = "whenever", specifier = ">=0.9.3,<0.10.0" }, ] -provides-extras = ["all", "ctl"] +provides-extras = ["all", "ctl", "tests"] [package.metadata.requires-dev] dev = [ @@ -812,6 +822,7 @@ dev = [ { name = "invoke", specifier = ">=2.2.1" }, { name = "ipython" }, { name = "jsonschema", specifier = ">=4.25.1" }, + { name = "mdxify", specifier = ">=0.2.23" }, { name = "mypy", specifier = "==1.11.2" }, { name = "prek", specifier = ">=0.3.0" }, { name = "pytest", specifier = ">=9.0,<9.1" }, @@ -830,6 +841,7 @@ dev = [ { name = "types-ujson" }, { name = "yamllint" }, ] +docs = [{ name = "mdxify", specifier = ">=0.2.23" }] lint = [ { name = "astroid", specifier = ">=3.1,<4.0" }, { name = "mypy", specifier = "==1.11.2" }, @@ -1202,157 +1214,6 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/da/a2/20e3cc569b3a41cc36181212c99f3e3c0aa9201174b2bf99313328824a2b/netutils-1.15.1-py3-none-any.whl", hash = "sha256:c42886d456f9b21bee395628b100dc2cd4b68fcc223f33c669672c3468d6b4dc", size = 532245, upload-time = "2025-10-21T00:41:06.141Z" }, ] -[[package]] -name = "numpy" -version = "2.2.6" -source = { registry = "https://pypi.org/simple" } -resolution-markers = [ - "python_full_version < '3.11'", -] -sdist = { url = "https://files.pythonhosted.org/packages/76/21/7d2a95e4bba9dc13d043ee156a356c0a8f0c6309dff6b21b4d71a073b8a8/numpy-2.2.6.tar.gz", hash = "sha256:e29554e2bef54a90aa5cc07da6ce955accb83f21ab5de01a62c8478897b264fd", size = 20276440, upload-time = "2025-05-17T22:38:04.611Z" } -wheels = [ - { url = "https://files.pythonhosted.org/packages/9a/3e/ed6db5be21ce87955c0cbd3009f2803f59fa08df21b5df06862e2d8e2bdd/numpy-2.2.6-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:b412caa66f72040e6d268491a59f2c43bf03eb6c96dd8f0307829feb7fa2b6fb", size = 21165245, upload-time = "2025-05-17T21:27:58.555Z" }, - { url = "https://files.pythonhosted.org/packages/22/c2/4b9221495b2a132cc9d2eb862e21d42a009f5a60e45fc44b00118c174bff/numpy-2.2.6-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:8e41fd67c52b86603a91c1a505ebaef50b3314de0213461c7a6e99c9a3beff90", size = 14360048, upload-time = "2025-05-17T21:28:21.406Z" }, - { url = "https://files.pythonhosted.org/packages/fd/77/dc2fcfc66943c6410e2bf598062f5959372735ffda175b39906d54f02349/numpy-2.2.6-cp310-cp310-macosx_14_0_arm64.whl", hash = "sha256:37e990a01ae6ec7fe7fa1c26c55ecb672dd98b19c3d0e1d1f326fa13cb38d163", size = 5340542, upload-time = "2025-05-17T21:28:30.931Z" }, - { url = "https://files.pythonhosted.org/packages/7a/4f/1cb5fdc353a5f5cc7feb692db9b8ec2c3d6405453f982435efc52561df58/numpy-2.2.6-cp310-cp310-macosx_14_0_x86_64.whl", hash = "sha256:5a6429d4be8ca66d889b7cf70f536a397dc45ba6faeb5f8c5427935d9592e9cf", size = 6878301, upload-time = "2025-05-17T21:28:41.613Z" }, - { url = "https://files.pythonhosted.org/packages/eb/17/96a3acd228cec142fcb8723bd3cc39c2a474f7dcf0a5d16731980bcafa95/numpy-2.2.6-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:efd28d4e9cd7d7a8d39074a4d44c63eda73401580c5c76acda2ce969e0a38e83", size = 14297320, upload-time = "2025-05-17T21:29:02.78Z" }, - { url = "https://files.pythonhosted.org/packages/b4/63/3de6a34ad7ad6646ac7d2f55ebc6ad439dbbf9c4370017c50cf403fb19b5/numpy-2.2.6-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:fc7b73d02efb0e18c000e9ad8b83480dfcd5dfd11065997ed4c6747470ae8915", size = 16801050, upload-time = "2025-05-17T21:29:27.675Z" }, - { url = "https://files.pythonhosted.org/packages/07/b6/89d837eddef52b3d0cec5c6ba0456c1bf1b9ef6a6672fc2b7873c3ec4e2e/numpy-2.2.6-cp310-cp310-musllinux_1_2_aarch64.whl", hash = "sha256:74d4531beb257d2c3f4b261bfb0fc09e0f9ebb8842d82a7b4209415896adc680", size = 15807034, upload-time = "2025-05-17T21:29:51.102Z" }, - { url = "https://files.pythonhosted.org/packages/01/c8/dc6ae86e3c61cfec1f178e5c9f7858584049b6093f843bca541f94120920/numpy-2.2.6-cp310-cp310-musllinux_1_2_x86_64.whl", hash = "sha256:8fc377d995680230e83241d8a96def29f204b5782f371c532579b4f20607a289", size = 18614185, upload-time = "2025-05-17T21:30:18.703Z" }, - { url = "https://files.pythonhosted.org/packages/5b/c5/0064b1b7e7c89137b471ccec1fd2282fceaae0ab3a9550f2568782d80357/numpy-2.2.6-cp310-cp310-win32.whl", hash = "sha256:b093dd74e50a8cba3e873868d9e93a85b78e0daf2e98c6797566ad8044e8363d", size = 6527149, upload-time = "2025-05-17T21:30:29.788Z" }, - { url = "https://files.pythonhosted.org/packages/a3/dd/4b822569d6b96c39d1215dbae0582fd99954dcbcf0c1a13c61783feaca3f/numpy-2.2.6-cp310-cp310-win_amd64.whl", hash = "sha256:f0fd6321b839904e15c46e0d257fdd101dd7f530fe03fd6359c1ea63738703f3", size = 12904620, upload-time = "2025-05-17T21:30:48.994Z" }, - { url = "https://files.pythonhosted.org/packages/da/a8/4f83e2aa666a9fbf56d6118faaaf5f1974d456b1823fda0a176eff722839/numpy-2.2.6-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:f9f1adb22318e121c5c69a09142811a201ef17ab257a1e66ca3025065b7f53ae", size = 21176963, upload-time = "2025-05-17T21:31:19.36Z" }, - { url = "https://files.pythonhosted.org/packages/b3/2b/64e1affc7972decb74c9e29e5649fac940514910960ba25cd9af4488b66c/numpy-2.2.6-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:c820a93b0255bc360f53eca31a0e676fd1101f673dda8da93454a12e23fc5f7a", size = 14406743, upload-time = "2025-05-17T21:31:41.087Z" }, - { url = "https://files.pythonhosted.org/packages/4a/9f/0121e375000b5e50ffdd8b25bf78d8e1a5aa4cca3f185d41265198c7b834/numpy-2.2.6-cp311-cp311-macosx_14_0_arm64.whl", hash = "sha256:3d70692235e759f260c3d837193090014aebdf026dfd167834bcba43e30c2a42", size = 5352616, upload-time = "2025-05-17T21:31:50.072Z" }, - { url = "https://files.pythonhosted.org/packages/31/0d/b48c405c91693635fbe2dcd7bc84a33a602add5f63286e024d3b6741411c/numpy-2.2.6-cp311-cp311-macosx_14_0_x86_64.whl", hash = "sha256:481b49095335f8eed42e39e8041327c05b0f6f4780488f61286ed3c01368d491", size = 6889579, upload-time = "2025-05-17T21:32:01.712Z" }, - { url = "https://files.pythonhosted.org/packages/52/b8/7f0554d49b565d0171eab6e99001846882000883998e7b7d9f0d98b1f934/numpy-2.2.6-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:b64d8d4d17135e00c8e346e0a738deb17e754230d7e0810ac5012750bbd85a5a", size = 14312005, upload-time = "2025-05-17T21:32:23.332Z" }, - { url = "https://files.pythonhosted.org/packages/b3/dd/2238b898e51bd6d389b7389ffb20d7f4c10066d80351187ec8e303a5a475/numpy-2.2.6-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:ba10f8411898fc418a521833e014a77d3ca01c15b0c6cdcce6a0d2897e6dbbdf", size = 16821570, upload-time = "2025-05-17T21:32:47.991Z" }, - { url = "https://files.pythonhosted.org/packages/83/6c/44d0325722cf644f191042bf47eedad61c1e6df2432ed65cbe28509d404e/numpy-2.2.6-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:bd48227a919f1bafbdda0583705e547892342c26fb127219d60a5c36882609d1", size = 15818548, upload-time = "2025-05-17T21:33:11.728Z" }, - { url = "https://files.pythonhosted.org/packages/ae/9d/81e8216030ce66be25279098789b665d49ff19eef08bfa8cb96d4957f422/numpy-2.2.6-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:9551a499bf125c1d4f9e250377c1ee2eddd02e01eac6644c080162c0c51778ab", size = 18620521, upload-time = "2025-05-17T21:33:39.139Z" }, - { url = "https://files.pythonhosted.org/packages/6a/fd/e19617b9530b031db51b0926eed5345ce8ddc669bb3bc0044b23e275ebe8/numpy-2.2.6-cp311-cp311-win32.whl", hash = "sha256:0678000bb9ac1475cd454c6b8c799206af8107e310843532b04d49649c717a47", size = 6525866, upload-time = "2025-05-17T21:33:50.273Z" }, - { url = "https://files.pythonhosted.org/packages/31/0a/f354fb7176b81747d870f7991dc763e157a934c717b67b58456bc63da3df/numpy-2.2.6-cp311-cp311-win_amd64.whl", hash = "sha256:e8213002e427c69c45a52bbd94163084025f533a55a59d6f9c5b820774ef3303", size = 12907455, upload-time = "2025-05-17T21:34:09.135Z" }, - { url = "https://files.pythonhosted.org/packages/82/5d/c00588b6cf18e1da539b45d3598d3557084990dcc4331960c15ee776ee41/numpy-2.2.6-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:41c5a21f4a04fa86436124d388f6ed60a9343a6f767fced1a8a71c3fbca038ff", size = 20875348, upload-time = "2025-05-17T21:34:39.648Z" }, - { url = "https://files.pythonhosted.org/packages/66/ee/560deadcdde6c2f90200450d5938f63a34b37e27ebff162810f716f6a230/numpy-2.2.6-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:de749064336d37e340f640b05f24e9e3dd678c57318c7289d222a8a2f543e90c", size = 14119362, upload-time = "2025-05-17T21:35:01.241Z" }, - { url = "https://files.pythonhosted.org/packages/3c/65/4baa99f1c53b30adf0acd9a5519078871ddde8d2339dc5a7fde80d9d87da/numpy-2.2.6-cp312-cp312-macosx_14_0_arm64.whl", hash = "sha256:894b3a42502226a1cac872f840030665f33326fc3dac8e57c607905773cdcde3", size = 5084103, upload-time = "2025-05-17T21:35:10.622Z" }, - { url = "https://files.pythonhosted.org/packages/cc/89/e5a34c071a0570cc40c9a54eb472d113eea6d002e9ae12bb3a8407fb912e/numpy-2.2.6-cp312-cp312-macosx_14_0_x86_64.whl", hash = "sha256:71594f7c51a18e728451bb50cc60a3ce4e6538822731b2933209a1f3614e9282", size = 6625382, upload-time = "2025-05-17T21:35:21.414Z" }, - { url = "https://files.pythonhosted.org/packages/f8/35/8c80729f1ff76b3921d5c9487c7ac3de9b2a103b1cd05e905b3090513510/numpy-2.2.6-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:f2618db89be1b4e05f7a1a847a9c1c0abd63e63a1607d892dd54668dd92faf87", size = 14018462, upload-time = "2025-05-17T21:35:42.174Z" }, - { url = "https://files.pythonhosted.org/packages/8c/3d/1e1db36cfd41f895d266b103df00ca5b3cbe965184df824dec5c08c6b803/numpy-2.2.6-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:fd83c01228a688733f1ded5201c678f0c53ecc1006ffbc404db9f7a899ac6249", size = 16527618, upload-time = "2025-05-17T21:36:06.711Z" }, - { url = "https://files.pythonhosted.org/packages/61/c6/03ed30992602c85aa3cd95b9070a514f8b3c33e31124694438d88809ae36/numpy-2.2.6-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:37c0ca431f82cd5fa716eca9506aefcabc247fb27ba69c5062a6d3ade8cf8f49", size = 15505511, upload-time = "2025-05-17T21:36:29.965Z" }, - { url = "https://files.pythonhosted.org/packages/b7/25/5761d832a81df431e260719ec45de696414266613c9ee268394dd5ad8236/numpy-2.2.6-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:fe27749d33bb772c80dcd84ae7e8df2adc920ae8297400dabec45f0dedb3f6de", size = 18313783, upload-time = "2025-05-17T21:36:56.883Z" }, - { url = "https://files.pythonhosted.org/packages/57/0a/72d5a3527c5ebffcd47bde9162c39fae1f90138c961e5296491ce778e682/numpy-2.2.6-cp312-cp312-win32.whl", hash = "sha256:4eeaae00d789f66c7a25ac5f34b71a7035bb474e679f410e5e1a94deb24cf2d4", size = 6246506, upload-time = "2025-05-17T21:37:07.368Z" }, - { url = "https://files.pythonhosted.org/packages/36/fa/8c9210162ca1b88529ab76b41ba02d433fd54fecaf6feb70ef9f124683f1/numpy-2.2.6-cp312-cp312-win_amd64.whl", hash = "sha256:c1f9540be57940698ed329904db803cf7a402f3fc200bfe599334c9bd84a40b2", size = 12614190, upload-time = "2025-05-17T21:37:26.213Z" }, - { url = "https://files.pythonhosted.org/packages/f9/5c/6657823f4f594f72b5471f1db1ab12e26e890bb2e41897522d134d2a3e81/numpy-2.2.6-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:0811bb762109d9708cca4d0b13c4f67146e3c3b7cf8d34018c722adb2d957c84", size = 20867828, upload-time = "2025-05-17T21:37:56.699Z" }, - { url = "https://files.pythonhosted.org/packages/dc/9e/14520dc3dadf3c803473bd07e9b2bd1b69bc583cb2497b47000fed2fa92f/numpy-2.2.6-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:287cc3162b6f01463ccd86be154f284d0893d2b3ed7292439ea97eafa8170e0b", size = 14143006, upload-time = "2025-05-17T21:38:18.291Z" }, - { url = "https://files.pythonhosted.org/packages/4f/06/7e96c57d90bebdce9918412087fc22ca9851cceaf5567a45c1f404480e9e/numpy-2.2.6-cp313-cp313-macosx_14_0_arm64.whl", hash = "sha256:f1372f041402e37e5e633e586f62aa53de2eac8d98cbfb822806ce4bbefcb74d", size = 5076765, upload-time = "2025-05-17T21:38:27.319Z" }, - { url = "https://files.pythonhosted.org/packages/73/ed/63d920c23b4289fdac96ddbdd6132e9427790977d5457cd132f18e76eae0/numpy-2.2.6-cp313-cp313-macosx_14_0_x86_64.whl", hash = "sha256:55a4d33fa519660d69614a9fad433be87e5252f4b03850642f88993f7b2ca566", size = 6617736, upload-time = "2025-05-17T21:38:38.141Z" }, - { url = "https://files.pythonhosted.org/packages/85/c5/e19c8f99d83fd377ec8c7e0cf627a8049746da54afc24ef0a0cb73d5dfb5/numpy-2.2.6-cp313-cp313-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:f92729c95468a2f4f15e9bb94c432a9229d0d50de67304399627a943201baa2f", size = 14010719, upload-time = "2025-05-17T21:38:58.433Z" }, - { url = "https://files.pythonhosted.org/packages/19/49/4df9123aafa7b539317bf6d342cb6d227e49f7a35b99c287a6109b13dd93/numpy-2.2.6-cp313-cp313-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:1bc23a79bfabc5d056d106f9befb8d50c31ced2fbc70eedb8155aec74a45798f", size = 16526072, upload-time = "2025-05-17T21:39:22.638Z" }, - { url = "https://files.pythonhosted.org/packages/b2/6c/04b5f47f4f32f7c2b0e7260442a8cbcf8168b0e1a41ff1495da42f42a14f/numpy-2.2.6-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:e3143e4451880bed956e706a3220b4e5cf6172ef05fcc397f6f36a550b1dd868", size = 15503213, upload-time = "2025-05-17T21:39:45.865Z" }, - { url = "https://files.pythonhosted.org/packages/17/0a/5cd92e352c1307640d5b6fec1b2ffb06cd0dabe7d7b8227f97933d378422/numpy-2.2.6-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:b4f13750ce79751586ae2eb824ba7e1e8dba64784086c98cdbbcc6a42112ce0d", size = 18316632, upload-time = "2025-05-17T21:40:13.331Z" }, - { url = "https://files.pythonhosted.org/packages/f0/3b/5cba2b1d88760ef86596ad0f3d484b1cbff7c115ae2429678465057c5155/numpy-2.2.6-cp313-cp313-win32.whl", hash = "sha256:5beb72339d9d4fa36522fc63802f469b13cdbe4fdab4a288f0c441b74272ebfd", size = 6244532, upload-time = "2025-05-17T21:43:46.099Z" }, - { url = "https://files.pythonhosted.org/packages/cb/3b/d58c12eafcb298d4e6d0d40216866ab15f59e55d148a5658bb3132311fcf/numpy-2.2.6-cp313-cp313-win_amd64.whl", hash = "sha256:b0544343a702fa80c95ad5d3d608ea3599dd54d4632df855e4c8d24eb6ecfa1c", size = 12610885, upload-time = "2025-05-17T21:44:05.145Z" }, - { url = "https://files.pythonhosted.org/packages/6b/9e/4bf918b818e516322db999ac25d00c75788ddfd2d2ade4fa66f1f38097e1/numpy-2.2.6-cp313-cp313t-macosx_10_13_x86_64.whl", hash = "sha256:0bca768cd85ae743b2affdc762d617eddf3bcf8724435498a1e80132d04879e6", size = 20963467, upload-time = "2025-05-17T21:40:44Z" }, - { url = "https://files.pythonhosted.org/packages/61/66/d2de6b291507517ff2e438e13ff7b1e2cdbdb7cb40b3ed475377aece69f9/numpy-2.2.6-cp313-cp313t-macosx_11_0_arm64.whl", hash = "sha256:fc0c5673685c508a142ca65209b4e79ed6740a4ed6b2267dbba90f34b0b3cfda", size = 14225144, upload-time = "2025-05-17T21:41:05.695Z" }, - { url = "https://files.pythonhosted.org/packages/e4/25/480387655407ead912e28ba3a820bc69af9adf13bcbe40b299d454ec011f/numpy-2.2.6-cp313-cp313t-macosx_14_0_arm64.whl", hash = "sha256:5bd4fc3ac8926b3819797a7c0e2631eb889b4118a9898c84f585a54d475b7e40", size = 5200217, upload-time = "2025-05-17T21:41:15.903Z" }, - { url = "https://files.pythonhosted.org/packages/aa/4a/6e313b5108f53dcbf3aca0c0f3e9c92f4c10ce57a0a721851f9785872895/numpy-2.2.6-cp313-cp313t-macosx_14_0_x86_64.whl", hash = "sha256:fee4236c876c4e8369388054d02d0e9bb84821feb1a64dd59e137e6511a551f8", size = 6712014, upload-time = "2025-05-17T21:41:27.321Z" }, - { url = "https://files.pythonhosted.org/packages/b7/30/172c2d5c4be71fdf476e9de553443cf8e25feddbe185e0bd88b096915bcc/numpy-2.2.6-cp313-cp313t-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:e1dda9c7e08dc141e0247a5b8f49cf05984955246a327d4c48bda16821947b2f", size = 14077935, upload-time = "2025-05-17T21:41:49.738Z" }, - { url = "https://files.pythonhosted.org/packages/12/fb/9e743f8d4e4d3c710902cf87af3512082ae3d43b945d5d16563f26ec251d/numpy-2.2.6-cp313-cp313t-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:f447e6acb680fd307f40d3da4852208af94afdfab89cf850986c3ca00562f4fa", size = 16600122, upload-time = "2025-05-17T21:42:14.046Z" }, - { url = "https://files.pythonhosted.org/packages/12/75/ee20da0e58d3a66f204f38916757e01e33a9737d0b22373b3eb5a27358f9/numpy-2.2.6-cp313-cp313t-musllinux_1_2_aarch64.whl", hash = "sha256:389d771b1623ec92636b0786bc4ae56abafad4a4c513d36a55dce14bd9ce8571", size = 15586143, upload-time = "2025-05-17T21:42:37.464Z" }, - { url = "https://files.pythonhosted.org/packages/76/95/bef5b37f29fc5e739947e9ce5179ad402875633308504a52d188302319c8/numpy-2.2.6-cp313-cp313t-musllinux_1_2_x86_64.whl", hash = "sha256:8e9ace4a37db23421249ed236fdcdd457d671e25146786dfc96835cd951aa7c1", size = 18385260, upload-time = "2025-05-17T21:43:05.189Z" }, - { url = "https://files.pythonhosted.org/packages/09/04/f2f83279d287407cf36a7a8053a5abe7be3622a4363337338f2585e4afda/numpy-2.2.6-cp313-cp313t-win32.whl", hash = "sha256:038613e9fb8c72b0a41f025a7e4c3f0b7a1b5d768ece4796b674c8f3fe13efff", size = 6377225, upload-time = "2025-05-17T21:43:16.254Z" }, - { url = "https://files.pythonhosted.org/packages/67/0e/35082d13c09c02c011cf21570543d202ad929d961c02a147493cb0c2bdf5/numpy-2.2.6-cp313-cp313t-win_amd64.whl", hash = "sha256:6031dd6dfecc0cf9f668681a37648373bddd6421fff6c66ec1624eed0180ee06", size = 12771374, upload-time = "2025-05-17T21:43:35.479Z" }, - { url = "https://files.pythonhosted.org/packages/9e/3b/d94a75f4dbf1ef5d321523ecac21ef23a3cd2ac8b78ae2aac40873590229/numpy-2.2.6-pp310-pypy310_pp73-macosx_10_15_x86_64.whl", hash = "sha256:0b605b275d7bd0c640cad4e5d30fa701a8d59302e127e5f79138ad62762c3e3d", size = 21040391, upload-time = "2025-05-17T21:44:35.948Z" }, - { url = "https://files.pythonhosted.org/packages/17/f4/09b2fa1b58f0fb4f7c7963a1649c64c4d315752240377ed74d9cd878f7b5/numpy-2.2.6-pp310-pypy310_pp73-macosx_14_0_x86_64.whl", hash = "sha256:7befc596a7dc9da8a337f79802ee8adb30a552a94f792b9c9d18c840055907db", size = 6786754, upload-time = "2025-05-17T21:44:47.446Z" }, - { url = "https://files.pythonhosted.org/packages/af/30/feba75f143bdc868a1cc3f44ccfa6c4b9ec522b36458e738cd00f67b573f/numpy-2.2.6-pp310-pypy310_pp73-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:ce47521a4754c8f4593837384bd3424880629f718d87c5d44f8ed763edd63543", size = 16643476, upload-time = "2025-05-17T21:45:11.871Z" }, - { url = "https://files.pythonhosted.org/packages/37/48/ac2a9584402fb6c0cd5b5d1a91dcf176b15760130dd386bbafdbfe3640bf/numpy-2.2.6-pp310-pypy310_pp73-win_amd64.whl", hash = "sha256:d042d24c90c41b54fd506da306759e06e568864df8ec17ccc17e9e884634fd00", size = 12812666, upload-time = "2025-05-17T21:45:31.426Z" }, -] - -[[package]] -name = "numpy" -version = "2.3.4" -source = { registry = "https://pypi.org/simple" } -resolution-markers = [ - "python_full_version >= '3.13'", - "python_full_version == '3.12.*'", - "python_full_version == '3.11.*'", -] -sdist = { url = "https://files.pythonhosted.org/packages/b5/f4/098d2270d52b41f1bd7db9fc288aaa0400cb48c2a3e2af6fa365d9720947/numpy-2.3.4.tar.gz", hash = "sha256:a7d018bfedb375a8d979ac758b120ba846a7fe764911a64465fd87b8729f4a6a", size = 20582187, upload-time = "2025-10-15T16:18:11.77Z" } -wheels = [ - { url = "https://files.pythonhosted.org/packages/60/e7/0e07379944aa8afb49a556a2b54587b828eb41dc9adc56fb7615b678ca53/numpy-2.3.4-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:e78aecd2800b32e8347ce49316d3eaf04aed849cd5b38e0af39f829a4e59f5eb", size = 21259519, upload-time = "2025-10-15T16:15:19.012Z" }, - { url = "https://files.pythonhosted.org/packages/d0/cb/5a69293561e8819b09e34ed9e873b9a82b5f2ade23dce4c51dc507f6cfe1/numpy-2.3.4-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:7fd09cc5d65bda1e79432859c40978010622112e9194e581e3415a3eccc7f43f", size = 14452796, upload-time = "2025-10-15T16:15:23.094Z" }, - { url = "https://files.pythonhosted.org/packages/e4/04/ff11611200acd602a1e5129e36cfd25bf01ad8e5cf927baf2e90236eb02e/numpy-2.3.4-cp311-cp311-macosx_14_0_arm64.whl", hash = "sha256:1b219560ae2c1de48ead517d085bc2d05b9433f8e49d0955c82e8cd37bd7bf36", size = 5381639, upload-time = "2025-10-15T16:15:25.572Z" }, - { url = "https://files.pythonhosted.org/packages/ea/77/e95c757a6fe7a48d28a009267408e8aa382630cc1ad1db7451b3bc21dbb4/numpy-2.3.4-cp311-cp311-macosx_14_0_x86_64.whl", hash = "sha256:bafa7d87d4c99752d07815ed7a2c0964f8ab311eb8168f41b910bd01d15b6032", size = 6914296, upload-time = "2025-10-15T16:15:27.079Z" }, - { url = "https://files.pythonhosted.org/packages/a3/d2/137c7b6841c942124eae921279e5c41b1c34bab0e6fc60c7348e69afd165/numpy-2.3.4-cp311-cp311-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:36dc13af226aeab72b7abad501d370d606326a0029b9f435eacb3b8c94b8a8b7", size = 14591904, upload-time = "2025-10-15T16:15:29.044Z" }, - { url = "https://files.pythonhosted.org/packages/bb/32/67e3b0f07b0aba57a078c4ab777a9e8e6bc62f24fb53a2337f75f9691699/numpy-2.3.4-cp311-cp311-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:a7b2f9a18b5ff9824a6af80de4f37f4ec3c2aab05ef08f51c77a093f5b89adda", size = 16939602, upload-time = "2025-10-15T16:15:31.106Z" }, - { url = "https://files.pythonhosted.org/packages/95/22/9639c30e32c93c4cee3ccdb4b09c2d0fbff4dcd06d36b357da06146530fb/numpy-2.3.4-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:9984bd645a8db6ca15d850ff996856d8762c51a2239225288f08f9050ca240a0", size = 16372661, upload-time = "2025-10-15T16:15:33.546Z" }, - { url = "https://files.pythonhosted.org/packages/12/e9/a685079529be2b0156ae0c11b13d6be647743095bb51d46589e95be88086/numpy-2.3.4-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:64c5825affc76942973a70acf438a8ab618dbd692b84cd5ec40a0a0509edc09a", size = 18884682, upload-time = "2025-10-15T16:15:36.105Z" }, - { url = "https://files.pythonhosted.org/packages/cf/85/f6f00d019b0cc741e64b4e00ce865a57b6bed945d1bbeb1ccadbc647959b/numpy-2.3.4-cp311-cp311-win32.whl", hash = "sha256:ed759bf7a70342f7817d88376eb7142fab9fef8320d6019ef87fae05a99874e1", size = 6570076, upload-time = "2025-10-15T16:15:38.225Z" }, - { url = "https://files.pythonhosted.org/packages/7d/10/f8850982021cb90e2ec31990291f9e830ce7d94eef432b15066e7cbe0bec/numpy-2.3.4-cp311-cp311-win_amd64.whl", hash = "sha256:faba246fb30ea2a526c2e9645f61612341de1a83fb1e0c5edf4ddda5a9c10996", size = 13089358, upload-time = "2025-10-15T16:15:40.404Z" }, - { url = "https://files.pythonhosted.org/packages/d1/ad/afdd8351385edf0b3445f9e24210a9c3971ef4de8fd85155462fc4321d79/numpy-2.3.4-cp311-cp311-win_arm64.whl", hash = "sha256:4c01835e718bcebe80394fd0ac66c07cbb90147ebbdad3dcecd3f25de2ae7e2c", size = 10462292, upload-time = "2025-10-15T16:15:42.896Z" }, - { url = "https://files.pythonhosted.org/packages/96/7a/02420400b736f84317e759291b8edaeee9dc921f72b045475a9cbdb26b17/numpy-2.3.4-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:ef1b5a3e808bc40827b5fa2c8196151a4c5abe110e1726949d7abddfe5c7ae11", size = 20957727, upload-time = "2025-10-15T16:15:44.9Z" }, - { url = "https://files.pythonhosted.org/packages/18/90/a014805d627aa5750f6f0e878172afb6454552da929144b3c07fcae1bb13/numpy-2.3.4-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:c2f91f496a87235c6aaf6d3f3d89b17dba64996abadccb289f48456cff931ca9", size = 14187262, upload-time = "2025-10-15T16:15:47.761Z" }, - { url = "https://files.pythonhosted.org/packages/c7/e4/0a94b09abe89e500dc748e7515f21a13e30c5c3fe3396e6d4ac108c25fca/numpy-2.3.4-cp312-cp312-macosx_14_0_arm64.whl", hash = "sha256:f77e5b3d3da652b474cc80a14084927a5e86a5eccf54ca8ca5cbd697bf7f2667", size = 5115992, upload-time = "2025-10-15T16:15:50.144Z" }, - { url = "https://files.pythonhosted.org/packages/88/dd/db77c75b055c6157cbd4f9c92c4458daef0dd9cbe6d8d2fe7f803cb64c37/numpy-2.3.4-cp312-cp312-macosx_14_0_x86_64.whl", hash = "sha256:8ab1c5f5ee40d6e01cbe96de5863e39b215a4d24e7d007cad56c7184fdf4aeef", size = 6648672, upload-time = "2025-10-15T16:15:52.442Z" }, - { url = "https://files.pythonhosted.org/packages/e1/e6/e31b0d713719610e406c0ea3ae0d90760465b086da8783e2fd835ad59027/numpy-2.3.4-cp312-cp312-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:77b84453f3adcb994ddbd0d1c5d11db2d6bda1a2b7fd5ac5bd4649d6f5dc682e", size = 14284156, upload-time = "2025-10-15T16:15:54.351Z" }, - { url = "https://files.pythonhosted.org/packages/f9/58/30a85127bfee6f108282107caf8e06a1f0cc997cb6b52cdee699276fcce4/numpy-2.3.4-cp312-cp312-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:4121c5beb58a7f9e6dfdee612cb24f4df5cd4db6e8261d7f4d7450a997a65d6a", size = 16641271, upload-time = "2025-10-15T16:15:56.67Z" }, - { url = "https://files.pythonhosted.org/packages/06/f2/2e06a0f2adf23e3ae29283ad96959267938d0efd20a2e25353b70065bfec/numpy-2.3.4-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:65611ecbb00ac9846efe04db15cbe6186f562f6bb7e5e05f077e53a599225d16", size = 16059531, upload-time = "2025-10-15T16:15:59.412Z" }, - { url = "https://files.pythonhosted.org/packages/b0/e7/b106253c7c0d5dc352b9c8fab91afd76a93950998167fa3e5afe4ef3a18f/numpy-2.3.4-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:dabc42f9c6577bcc13001b8810d300fe814b4cfbe8a92c873f269484594f9786", size = 18578983, upload-time = "2025-10-15T16:16:01.804Z" }, - { url = "https://files.pythonhosted.org/packages/73/e3/04ecc41e71462276ee867ccbef26a4448638eadecf1bc56772c9ed6d0255/numpy-2.3.4-cp312-cp312-win32.whl", hash = "sha256:a49d797192a8d950ca59ee2d0337a4d804f713bb5c3c50e8db26d49666e351dc", size = 6291380, upload-time = "2025-10-15T16:16:03.938Z" }, - { url = "https://files.pythonhosted.org/packages/3d/a8/566578b10d8d0e9955b1b6cd5db4e9d4592dd0026a941ff7994cedda030a/numpy-2.3.4-cp312-cp312-win_amd64.whl", hash = "sha256:985f1e46358f06c2a09921e8921e2c98168ed4ae12ccd6e5e87a4f1857923f32", size = 12787999, upload-time = "2025-10-15T16:16:05.801Z" }, - { url = "https://files.pythonhosted.org/packages/58/22/9c903a957d0a8071b607f5b1bff0761d6e608b9a965945411f867d515db1/numpy-2.3.4-cp312-cp312-win_arm64.whl", hash = "sha256:4635239814149e06e2cb9db3dd584b2fa64316c96f10656983b8026a82e6e4db", size = 10197412, upload-time = "2025-10-15T16:16:07.854Z" }, - { url = "https://files.pythonhosted.org/packages/57/7e/b72610cc91edf138bc588df5150957a4937221ca6058b825b4725c27be62/numpy-2.3.4-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:c090d4860032b857d94144d1a9976b8e36709e40386db289aaf6672de2a81966", size = 20950335, upload-time = "2025-10-15T16:16:10.304Z" }, - { url = "https://files.pythonhosted.org/packages/3e/46/bdd3370dcea2f95ef14af79dbf81e6927102ddf1cc54adc0024d61252fd9/numpy-2.3.4-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:a13fc473b6db0be619e45f11f9e81260f7302f8d180c49a22b6e6120022596b3", size = 14179878, upload-time = "2025-10-15T16:16:12.595Z" }, - { url = "https://files.pythonhosted.org/packages/ac/01/5a67cb785bda60f45415d09c2bc245433f1c68dd82eef9c9002c508b5a65/numpy-2.3.4-cp313-cp313-macosx_14_0_arm64.whl", hash = "sha256:3634093d0b428e6c32c3a69b78e554f0cd20ee420dcad5a9f3b2a63762ce4197", size = 5108673, upload-time = "2025-10-15T16:16:14.877Z" }, - { url = "https://files.pythonhosted.org/packages/c2/cd/8428e23a9fcebd33988f4cb61208fda832800ca03781f471f3727a820704/numpy-2.3.4-cp313-cp313-macosx_14_0_x86_64.whl", hash = "sha256:043885b4f7e6e232d7df4f51ffdef8c36320ee9d5f227b380ea636722c7ed12e", size = 6641438, upload-time = "2025-10-15T16:16:16.805Z" }, - { url = "https://files.pythonhosted.org/packages/3e/d1/913fe563820f3c6b079f992458f7331278dcd7ba8427e8e745af37ddb44f/numpy-2.3.4-cp313-cp313-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:4ee6a571d1e4f0ea6d5f22d6e5fbd6ed1dc2b18542848e1e7301bd190500c9d7", size = 14281290, upload-time = "2025-10-15T16:16:18.764Z" }, - { url = "https://files.pythonhosted.org/packages/9e/7e/7d306ff7cb143e6d975cfa7eb98a93e73495c4deabb7d1b5ecf09ea0fd69/numpy-2.3.4-cp313-cp313-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:fc8a63918b04b8571789688b2780ab2b4a33ab44bfe8ccea36d3eba51228c953", size = 16636543, upload-time = "2025-10-15T16:16:21.072Z" }, - { url = "https://files.pythonhosted.org/packages/47/6a/8cfc486237e56ccfb0db234945552a557ca266f022d281a2f577b98e955c/numpy-2.3.4-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:40cc556d5abbc54aabe2b1ae287042d7bdb80c08edede19f0c0afb36ae586f37", size = 16056117, upload-time = "2025-10-15T16:16:23.369Z" }, - { url = "https://files.pythonhosted.org/packages/b1/0e/42cb5e69ea901e06ce24bfcc4b5664a56f950a70efdcf221f30d9615f3f3/numpy-2.3.4-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:ecb63014bb7f4ce653f8be7f1df8cbc6093a5a2811211770f6606cc92b5a78fd", size = 18577788, upload-time = "2025-10-15T16:16:27.496Z" }, - { url = "https://files.pythonhosted.org/packages/86/92/41c3d5157d3177559ef0a35da50f0cda7fa071f4ba2306dd36818591a5bc/numpy-2.3.4-cp313-cp313-win32.whl", hash = "sha256:e8370eb6925bb8c1c4264fec52b0384b44f675f191df91cbe0140ec9f0955646", size = 6282620, upload-time = "2025-10-15T16:16:29.811Z" }, - { url = "https://files.pythonhosted.org/packages/09/97/fd421e8bc50766665ad35536c2bb4ef916533ba1fdd053a62d96cc7c8b95/numpy-2.3.4-cp313-cp313-win_amd64.whl", hash = "sha256:56209416e81a7893036eea03abcb91c130643eb14233b2515c90dcac963fe99d", size = 12784672, upload-time = "2025-10-15T16:16:31.589Z" }, - { url = "https://files.pythonhosted.org/packages/ad/df/5474fb2f74970ca8eb978093969b125a84cc3d30e47f82191f981f13a8a0/numpy-2.3.4-cp313-cp313-win_arm64.whl", hash = "sha256:a700a4031bc0fd6936e78a752eefb79092cecad2599ea9c8039c548bc097f9bc", size = 10196702, upload-time = "2025-10-15T16:16:33.902Z" }, - { url = "https://files.pythonhosted.org/packages/11/83/66ac031464ec1767ea3ed48ce40f615eb441072945e98693bec0bcd056cc/numpy-2.3.4-cp313-cp313t-macosx_10_13_x86_64.whl", hash = "sha256:86966db35c4040fdca64f0816a1c1dd8dbd027d90fca5a57e00e1ca4cd41b879", size = 21049003, upload-time = "2025-10-15T16:16:36.101Z" }, - { url = "https://files.pythonhosted.org/packages/5f/99/5b14e0e686e61371659a1d5bebd04596b1d72227ce36eed121bb0aeab798/numpy-2.3.4-cp313-cp313t-macosx_11_0_arm64.whl", hash = "sha256:838f045478638b26c375ee96ea89464d38428c69170360b23a1a50fa4baa3562", size = 14302980, upload-time = "2025-10-15T16:16:39.124Z" }, - { url = "https://files.pythonhosted.org/packages/2c/44/e9486649cd087d9fc6920e3fc3ac2aba10838d10804b1e179fb7cbc4e634/numpy-2.3.4-cp313-cp313t-macosx_14_0_arm64.whl", hash = "sha256:d7315ed1dab0286adca467377c8381cd748f3dc92235f22a7dfc42745644a96a", size = 5231472, upload-time = "2025-10-15T16:16:41.168Z" }, - { url = "https://files.pythonhosted.org/packages/3e/51/902b24fa8887e5fe2063fd61b1895a476d0bbf46811ab0c7fdf4bd127345/numpy-2.3.4-cp313-cp313t-macosx_14_0_x86_64.whl", hash = "sha256:84f01a4d18b2cc4ade1814a08e5f3c907b079c847051d720fad15ce37aa930b6", size = 6739342, upload-time = "2025-10-15T16:16:43.777Z" }, - { url = "https://files.pythonhosted.org/packages/34/f1/4de9586d05b1962acdcdb1dc4af6646361a643f8c864cef7c852bf509740/numpy-2.3.4-cp313-cp313t-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:817e719a868f0dacde4abdfc5c1910b301877970195db9ab6a5e2c4bd5b121f7", size = 14354338, upload-time = "2025-10-15T16:16:46.081Z" }, - { url = "https://files.pythonhosted.org/packages/1f/06/1c16103b425de7969d5a76bdf5ada0804b476fed05d5f9e17b777f1cbefd/numpy-2.3.4-cp313-cp313t-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:85e071da78d92a214212cacea81c6da557cab307f2c34b5f85b628e94803f9c0", size = 16702392, upload-time = "2025-10-15T16:16:48.455Z" }, - { url = "https://files.pythonhosted.org/packages/34/b2/65f4dc1b89b5322093572b6e55161bb42e3e0487067af73627f795cc9d47/numpy-2.3.4-cp313-cp313t-musllinux_1_2_aarch64.whl", hash = "sha256:2ec646892819370cf3558f518797f16597b4e4669894a2ba712caccc9da53f1f", size = 16134998, upload-time = "2025-10-15T16:16:51.114Z" }, - { url = "https://files.pythonhosted.org/packages/d4/11/94ec578896cdb973aaf56425d6c7f2aff4186a5c00fac15ff2ec46998b46/numpy-2.3.4-cp313-cp313t-musllinux_1_2_x86_64.whl", hash = "sha256:035796aaaddfe2f9664b9a9372f089cfc88bd795a67bd1bfe15e6e770934cf64", size = 18651574, upload-time = "2025-10-15T16:16:53.429Z" }, - { url = "https://files.pythonhosted.org/packages/62/b7/7efa763ab33dbccf56dade36938a77345ce8e8192d6b39e470ca25ff3cd0/numpy-2.3.4-cp313-cp313t-win32.whl", hash = "sha256:fea80f4f4cf83b54c3a051f2f727870ee51e22f0248d3114b8e755d160b38cfb", size = 6413135, upload-time = "2025-10-15T16:16:55.992Z" }, - { url = "https://files.pythonhosted.org/packages/43/70/aba4c38e8400abcc2f345e13d972fb36c26409b3e644366db7649015f291/numpy-2.3.4-cp313-cp313t-win_amd64.whl", hash = "sha256:15eea9f306b98e0be91eb344a94c0e630689ef302e10c2ce5f7e11905c704f9c", size = 12928582, upload-time = "2025-10-15T16:16:57.943Z" }, - { url = "https://files.pythonhosted.org/packages/67/63/871fad5f0073fc00fbbdd7232962ea1ac40eeaae2bba66c76214f7954236/numpy-2.3.4-cp313-cp313t-win_arm64.whl", hash = "sha256:b6c231c9c2fadbae4011ca5e7e83e12dc4a5072f1a1d85a0a7b3ed754d145a40", size = 10266691, upload-time = "2025-10-15T16:17:00.048Z" }, - { url = "https://files.pythonhosted.org/packages/72/71/ae6170143c115732470ae3a2d01512870dd16e0953f8a6dc89525696069b/numpy-2.3.4-cp314-cp314-macosx_10_15_x86_64.whl", hash = "sha256:81c3e6d8c97295a7360d367f9f8553973651b76907988bb6066376bc2252f24e", size = 20955580, upload-time = "2025-10-15T16:17:02.509Z" }, - { url = "https://files.pythonhosted.org/packages/af/39/4be9222ffd6ca8a30eda033d5f753276a9c3426c397bb137d8e19dedd200/numpy-2.3.4-cp314-cp314-macosx_11_0_arm64.whl", hash = "sha256:7c26b0b2bf58009ed1f38a641f3db4be8d960a417ca96d14e5b06df1506d41ff", size = 14188056, upload-time = "2025-10-15T16:17:04.873Z" }, - { url = "https://files.pythonhosted.org/packages/6c/3d/d85f6700d0a4aa4f9491030e1021c2b2b7421b2b38d01acd16734a2bfdc7/numpy-2.3.4-cp314-cp314-macosx_14_0_arm64.whl", hash = "sha256:62b2198c438058a20b6704351b35a1d7db881812d8512d67a69c9de1f18ca05f", size = 5116555, upload-time = "2025-10-15T16:17:07.499Z" }, - { url = "https://files.pythonhosted.org/packages/bf/04/82c1467d86f47eee8a19a464c92f90a9bb68ccf14a54c5224d7031241ffb/numpy-2.3.4-cp314-cp314-macosx_14_0_x86_64.whl", hash = "sha256:9d729d60f8d53a7361707f4b68a9663c968882dd4f09e0d58c044c8bf5faee7b", size = 6643581, upload-time = "2025-10-15T16:17:09.774Z" }, - { url = "https://files.pythonhosted.org/packages/0c/d3/c79841741b837e293f48bd7db89d0ac7a4f2503b382b78a790ef1dc778a5/numpy-2.3.4-cp314-cp314-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:bd0c630cf256b0a7fd9d0a11c9413b42fef5101219ce6ed5a09624f5a65392c7", size = 14299186, upload-time = "2025-10-15T16:17:11.937Z" }, - { url = "https://files.pythonhosted.org/packages/e8/7e/4a14a769741fbf237eec5a12a2cbc7a4c4e061852b6533bcb9e9a796c908/numpy-2.3.4-cp314-cp314-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:d5e081bc082825f8b139f9e9fe42942cb4054524598aaeb177ff476cc76d09d2", size = 16638601, upload-time = "2025-10-15T16:17:14.391Z" }, - { url = "https://files.pythonhosted.org/packages/93/87/1c1de269f002ff0a41173fe01dcc925f4ecff59264cd8f96cf3b60d12c9b/numpy-2.3.4-cp314-cp314-musllinux_1_2_aarch64.whl", hash = "sha256:15fb27364ed84114438fff8aaf998c9e19adbeba08c0b75409f8c452a8692c52", size = 16074219, upload-time = "2025-10-15T16:17:17.058Z" }, - { url = "https://files.pythonhosted.org/packages/cd/28/18f72ee77408e40a76d691001ae599e712ca2a47ddd2c4f695b16c65f077/numpy-2.3.4-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:85d9fb2d8cd998c84d13a79a09cc0c1091648e848e4e6249b0ccd7f6b487fa26", size = 18576702, upload-time = "2025-10-15T16:17:19.379Z" }, - { url = "https://files.pythonhosted.org/packages/c3/76/95650169b465ececa8cf4b2e8f6df255d4bf662775e797ade2025cc51ae6/numpy-2.3.4-cp314-cp314-win32.whl", hash = "sha256:e73d63fd04e3a9d6bc187f5455d81abfad05660b212c8804bf3b407e984cd2bc", size = 6337136, upload-time = "2025-10-15T16:17:22.886Z" }, - { url = "https://files.pythonhosted.org/packages/dc/89/a231a5c43ede5d6f77ba4a91e915a87dea4aeea76560ba4d2bf185c683f0/numpy-2.3.4-cp314-cp314-win_amd64.whl", hash = "sha256:3da3491cee49cf16157e70f607c03a217ea6647b1cea4819c4f48e53d49139b9", size = 12920542, upload-time = "2025-10-15T16:17:24.783Z" }, - { url = "https://files.pythonhosted.org/packages/0d/0c/ae9434a888f717c5ed2ff2393b3f344f0ff6f1c793519fa0c540461dc530/numpy-2.3.4-cp314-cp314-win_arm64.whl", hash = "sha256:6d9cd732068e8288dbe2717177320723ccec4fb064123f0caf9bbd90ab5be868", size = 10480213, upload-time = "2025-10-15T16:17:26.935Z" }, - { url = "https://files.pythonhosted.org/packages/83/4b/c4a5f0841f92536f6b9592694a5b5f68c9ab37b775ff342649eadf9055d3/numpy-2.3.4-cp314-cp314t-macosx_10_15_x86_64.whl", hash = "sha256:22758999b256b595cf0b1d102b133bb61866ba5ceecf15f759623b64c020c9ec", size = 21052280, upload-time = "2025-10-15T16:17:29.638Z" }, - { url = "https://files.pythonhosted.org/packages/3e/80/90308845fc93b984d2cc96d83e2324ce8ad1fd6efea81b324cba4b673854/numpy-2.3.4-cp314-cp314t-macosx_11_0_arm64.whl", hash = "sha256:9cb177bc55b010b19798dc5497d540dea67fd13a8d9e882b2dae71de0cf09eb3", size = 14302930, upload-time = "2025-10-15T16:17:32.384Z" }, - { url = "https://files.pythonhosted.org/packages/3d/4e/07439f22f2a3b247cec4d63a713faae55e1141a36e77fb212881f7cda3fb/numpy-2.3.4-cp314-cp314t-macosx_14_0_arm64.whl", hash = "sha256:0f2bcc76f1e05e5ab58893407c63d90b2029908fa41f9f1cc51eecce936c3365", size = 5231504, upload-time = "2025-10-15T16:17:34.515Z" }, - { url = "https://files.pythonhosted.org/packages/ab/de/1e11f2547e2fe3d00482b19721855348b94ada8359aef5d40dd57bfae9df/numpy-2.3.4-cp314-cp314t-macosx_14_0_x86_64.whl", hash = "sha256:8dc20bde86802df2ed8397a08d793da0ad7a5fd4ea3ac85d757bf5dd4ad7c252", size = 6739405, upload-time = "2025-10-15T16:17:36.128Z" }, - { url = "https://files.pythonhosted.org/packages/3b/40/8cd57393a26cebe2e923005db5134a946c62fa56a1087dc7c478f3e30837/numpy-2.3.4-cp314-cp314t-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:5e199c087e2aa71c8f9ce1cb7a8e10677dc12457e7cc1be4798632da37c3e86e", size = 14354866, upload-time = "2025-10-15T16:17:38.884Z" }, - { url = "https://files.pythonhosted.org/packages/93/39/5b3510f023f96874ee6fea2e40dfa99313a00bf3ab779f3c92978f34aace/numpy-2.3.4-cp314-cp314t-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:85597b2d25ddf655495e2363fe044b0ae999b75bc4d630dc0d886484b03a5eb0", size = 16703296, upload-time = "2025-10-15T16:17:41.564Z" }, - { url = "https://files.pythonhosted.org/packages/41/0d/19bb163617c8045209c1996c4e427bccbc4bbff1e2c711f39203c8ddbb4a/numpy-2.3.4-cp314-cp314t-musllinux_1_2_aarch64.whl", hash = "sha256:04a69abe45b49c5955923cf2c407843d1c85013b424ae8a560bba16c92fe44a0", size = 16136046, upload-time = "2025-10-15T16:17:43.901Z" }, - { url = "https://files.pythonhosted.org/packages/e2/c1/6dba12fdf68b02a21ac411c9df19afa66bed2540f467150ca64d246b463d/numpy-2.3.4-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:e1708fac43ef8b419c975926ce1eaf793b0c13b7356cfab6ab0dc34c0a02ac0f", size = 18652691, upload-time = "2025-10-15T16:17:46.247Z" }, - { url = "https://files.pythonhosted.org/packages/f8/73/f85056701dbbbb910c51d846c58d29fd46b30eecd2b6ba760fc8b8a1641b/numpy-2.3.4-cp314-cp314t-win32.whl", hash = "sha256:863e3b5f4d9915aaf1b8ec79ae560ad21f0b8d5e3adc31e73126491bb86dee1d", size = 6485782, upload-time = "2025-10-15T16:17:48.872Z" }, - { url = "https://files.pythonhosted.org/packages/17/90/28fa6f9865181cb817c2471ee65678afa8a7e2a1fb16141473d5fa6bacc3/numpy-2.3.4-cp314-cp314t-win_amd64.whl", hash = "sha256:962064de37b9aef801d33bc579690f8bfe6c5e70e29b61783f60bcba838a14d6", size = 13113301, upload-time = "2025-10-15T16:17:50.938Z" }, - { url = "https://files.pythonhosted.org/packages/54/23/08c002201a8e7e1f9afba93b97deceb813252d9cfd0d3351caed123dcf97/numpy-2.3.4-cp314-cp314t-win_arm64.whl", hash = "sha256:8b5a9a39c45d852b62693d9b3f3e0fe052541f804296ff401a72a1b60edafb29", size = 10547532, upload-time = "2025-10-15T16:17:53.48Z" }, - { url = "https://files.pythonhosted.org/packages/b1/b6/64898f51a86ec88ca1257a59c1d7fd077b60082a119affefcdf1dd0df8ca/numpy-2.3.4-pp311-pypy311_pp73-macosx_10_15_x86_64.whl", hash = "sha256:6e274603039f924c0fe5cb73438fa9246699c78a6df1bd3decef9ae592ae1c05", size = 21131552, upload-time = "2025-10-15T16:17:55.845Z" }, - { url = "https://files.pythonhosted.org/packages/ce/4c/f135dc6ebe2b6a3c77f4e4838fa63d350f85c99462012306ada1bd4bc460/numpy-2.3.4-pp311-pypy311_pp73-macosx_11_0_arm64.whl", hash = "sha256:d149aee5c72176d9ddbc6803aef9c0f6d2ceeea7626574fc68518da5476fa346", size = 14377796, upload-time = "2025-10-15T16:17:58.308Z" }, - { url = "https://files.pythonhosted.org/packages/d0/a4/f33f9c23fcc13dd8412fc8614559b5b797e0aba9d8e01dfa8bae10c84004/numpy-2.3.4-pp311-pypy311_pp73-macosx_14_0_arm64.whl", hash = "sha256:6d34ed9db9e6395bb6cd33286035f73a59b058169733a9db9f85e650b88df37e", size = 5306904, upload-time = "2025-10-15T16:18:00.596Z" }, - { url = "https://files.pythonhosted.org/packages/28/af/c44097f25f834360f9fb960fa082863e0bad14a42f36527b2a121abdec56/numpy-2.3.4-pp311-pypy311_pp73-macosx_14_0_x86_64.whl", hash = "sha256:fdebe771ca06bb8d6abce84e51dca9f7921fe6ad34a0c914541b063e9a68928b", size = 6819682, upload-time = "2025-10-15T16:18:02.32Z" }, - { url = "https://files.pythonhosted.org/packages/c5/8c/cd283b54c3c2b77e188f63e23039844f56b23bba1712318288c13fe86baf/numpy-2.3.4-pp311-pypy311_pp73-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:957e92defe6c08211eb77902253b14fe5b480ebc5112bc741fd5e9cd0608f847", size = 14422300, upload-time = "2025-10-15T16:18:04.271Z" }, - { url = "https://files.pythonhosted.org/packages/b0/f0/8404db5098d92446b3e3695cf41c6f0ecb703d701cb0b7566ee2177f2eee/numpy-2.3.4-pp311-pypy311_pp73-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:13b9062e4f5c7ee5c7e5be96f29ba71bc5a37fed3d1d77c37390ae00724d296d", size = 16760806, upload-time = "2025-10-15T16:18:06.668Z" }, - { url = "https://files.pythonhosted.org/packages/95/8e/2844c3959ce9a63acc7c8e50881133d86666f0420bcde695e115ced0920f/numpy-2.3.4-pp311-pypy311_pp73-win_amd64.whl", hash = "sha256:81b3a59793523e552c4a96109dde028aa4448ae06ccac5a76ff6532a85558a7f", size = 12973130, upload-time = "2025-10-15T16:18:09.397Z" }, -] - [[package]] name = "opentelemetry-api" version = "1.38.0" From 04d1fb44b72d6ae9196a42d0b765ccd6ca2b1a68 Mon Sep 17 00:00:00 2001 From: Patrick Ogenstad Date: Tue, 25 Aug 2026 13:51:06 +0200 Subject: [PATCH 2/6] docs: note the install size of the tests extra The extra is newly installable, so readers will reach it for the first time. It pulls in the container tooling behind `infrahub_sdk.testing`, which is a lot more than the `ctl` extra beside it. --- docs/docs/python-sdk/guides/installation.mdx | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/docs/docs/python-sdk/guides/installation.mdx b/docs/docs/python-sdk/guides/installation.mdx index 5478a956a..22be88511 100644 --- a/docs/docs/python-sdk/guides/installation.mdx +++ b/docs/docs/python-sdk/guides/installation.mdx @@ -39,6 +39,12 @@ The `tests` extra provides all the components for the testing framework of Trans pip install 'infrahub-sdk[tests]' ``` +:::note + +This extra is much larger than the others. The `infrahub_sdk.testing` helpers run Infrahub in containers, which adds roughly 66 packages on top of the base SDK. Install the `ctl` extra on its own if you only need the `infrahubctl` command. + +::: + ### all From 1700088e8e24c7cec185f9723a1e2e922f4a05c7 Mon Sep 17 00:00:00 2001 From: Patrick Ogenstad Date: Tue, 25 Aug 2026 15:25:15 +0200 Subject: [PATCH 3/6] test: treat tomllib as stdlib on the older interpreters in the matrix The import scan classified modules with the running interpreter's `sys.stdlib_module_names`. `tomllib` only joined the standard library in 3.11, so on 3.10 the guarded `import tomllib` in ctl/config.py looked like an undeclared third-party package and failed the check. Exempt it by name rather than skipping imports nested under a `sys.version_info` guard: the `tomli` backport sits in the same else branch and is a real declared dependency that must stay verified. --- tests/unit/test_packaging_metadata.py | 12 +++++++++++- 1 file changed, 11 insertions(+), 1 deletion(-) diff --git a/tests/unit/test_packaging_metadata.py b/tests/unit/test_packaging_metadata.py index 34de15cce..7d92d470a 100644 --- a/tests/unit/test_packaging_metadata.py +++ b/tests/unit/test_packaging_metadata.py @@ -41,6 +41,12 @@ "yaml": "pyyaml", } +# Standard library on a Python newer than the interpreter running these checks, so absent from +# this interpreter's `sys.stdlib_module_names`. They are imported behind a `sys.version_info` +# guard alongside a declared backport, and must not be mistaken for undeclared packages on the +# older interpreters in the test matrix. +STDLIB_ON_NEWER_PYTHON = frozenset({"tomllib"}) + # Distributions declared on purpose without being imported, with the reason why. DECLARED_WITHOUT_IMPORT = { "click": "Constrains the `click` version that `typer` resolves to; never imported directly.", @@ -101,7 +107,11 @@ def _imported_distributions() -> dict[str, Path]: continue for module in modules: top_level = module.split(".")[0] - if top_level in sys.stdlib_module_names or top_level == PACKAGE_DIR.name: + if ( + top_level in sys.stdlib_module_names + or top_level in STDLIB_ON_NEWER_PYTHON + or top_level == PACKAGE_DIR.name + ): continue distribution = _normalize(IMPORT_NAME_TO_DISTRIBUTION.get(top_level, top_level)) imported.setdefault(distribution, path.relative_to(REPO_ROOT)) From 421fbaaa6df2735b51444646197568c4eb0ba424 Mon Sep 17 00:00:00 2001 From: Patrick Ogenstad Date: Wed, 26 Aug 2026 13:03:24 +0200 Subject: [PATCH 4/6] fix(packaging): install the template dependencies with the SDK itself Jinja2, PyYAML and rich sat in the `ctl` extra, but `template/`, `spec/`, `transfer/` and `protocols_generator/` import them at module level. On a plain `pip install infrahub-sdk` those modules raised ModuleNotFoundError: 11 of them, including `infrahub_sdk.template`, which renders Transforms and is nothing to do with the CLI. rich is not merely presentation there either, its Traceback/Frame/Syntax types are carried in the Jinja error model. Moving the three to the core dependencies takes the non-ctl modules that fail to import on a base install from 20 to 9, and the remaining 9 legitimately need an extra: seven want pytest, and async_typer and graphql/plugin.py want the CLI dependencies. It also means the `tests` extra provides a working pytest environment, which it did not: the bundled plugin imports yaml and jinja2, so pytest could not start at all. A base install grows from 19 to 26 packages. ruamel.yaml stays in `ctl`, where its round-trip mode is only used to preserve comments in `schema format`. --- changelog/+core-template-dependencies.fixed.md | 5 +++++ changelog/+dependency-lower-bounds.changed.md | 4 +++- changelog/+tests-extra.added.md | 2 +- docs/docs/python-sdk/guides/installation.mdx | 2 +- pyproject.toml | 8 ++++---- uv.lock | 18 ++++++------------ 6 files changed, 20 insertions(+), 19 deletions(-) create mode 100644 changelog/+core-template-dependencies.fixed.md diff --git a/changelog/+core-template-dependencies.fixed.md b/changelog/+core-template-dependencies.fixed.md new file mode 100644 index 000000000..db0f344db --- /dev/null +++ b/changelog/+core-template-dependencies.fixed.md @@ -0,0 +1,5 @@ +Parts of the SDK no longer fail to import on a plain `pip install infrahub-sdk`. + +`infrahub_sdk.template`, `infrahub_sdk.yaml`, `infrahub_sdk.spec`, `infrahub_sdk.transfer` and `infrahub_sdk.protocols_generator` import Jinja2, PyYAML and rich, but those three were only installed by the `ctl` extra. Using any of them without that extra raised `ModuleNotFoundError`, even though none of it is CLI-specific: `template` renders your Transforms, and rich supplies the traceback types carried in Jinja error reporting. + +All three are now installed with the SDK itself, so a plain install grows from 19 to 26 packages. The `ctl` extra is correspondingly smaller and now covers only what `infrahubctl` genuinely needs. diff --git a/changelog/+dependency-lower-bounds.changed.md b/changelog/+dependency-lower-bounds.changed.md index 8542f3326..7105a61d7 100644 --- a/changelog/+dependency-lower-bounds.changed.md +++ b/changelog/+dependency-lower-bounds.changed.md @@ -2,4 +2,6 @@ If you pin `pydantic` to 2.0, 2.0.1 or 2.0.2, installing the SDK now fails while resolving dependencies. Those versions never actually worked: `import infrahub_sdk` raised a `SchemaError` on them, because their regex engine rejects a pattern used by the schema models. Pin `pydantic>=2.0.3` to resolve it. -`anyio`, `typing-extensions` and `packaging` are also now installed as direct requirements. The SDK has always imported them but relied on other packages to pull them in, so a minimal or heavily constrained environment could end up with the SDK installed and unusable. No action is needed, installs simply become reliable. +`anyio` and `typing-extensions` are now installed as direct requirements of the SDK itself. It has always imported them but relied on other packages to pull them in, so a minimal or heavily constrained environment could end up with the SDK installed and unusable. No action is needed, installs simply become reliable. + +`packaging` is now a direct requirement of the `tests` extra, which is where the `infrahub_sdk.testing` helpers that import it live. A plain `pip install infrahub-sdk` does not install it. diff --git a/changelog/+tests-extra.added.md b/changelog/+tests-extra.added.md index 6caae804e..576f1d53d 100644 --- a/changelog/+tests-extra.added.md +++ b/changelog/+tests-extra.added.md @@ -2,4 +2,4 @@ Install it if you use the `pytest-infrahub` plugin to test Transforms, Queries and Checks, or the `infrahub_sdk.testing` helpers. Previously you had to work out the missing requirements and declare them yourself. -Be aware that it is a large install, adding around 66 packages on top of the base SDK, among them Docker, FastAPI and Prefect client libraries. These come from the `infrahub_sdk.testing` helpers, which run Infrahub in containers. If you only need the `infrahubctl` CLI, install `infrahub-sdk[ctl]` instead. `infrahub-sdk[all]` now covers both `ctl` and `tests`, so it pulls in considerably more than it used to. +Be aware that it is a large install, adding around 61 packages on top of the base SDK, among them Docker, FastAPI and Prefect client libraries. These come from the `infrahub_sdk.testing` helpers, which run Infrahub in containers. If you only need the `infrahubctl` CLI, install `infrahub-sdk[ctl]` instead. `infrahub-sdk[all]` now covers both `ctl` and `tests`, so it pulls in considerably more than it used to. diff --git a/docs/docs/python-sdk/guides/installation.mdx b/docs/docs/python-sdk/guides/installation.mdx index 22be88511..e1bda11e4 100644 --- a/docs/docs/python-sdk/guides/installation.mdx +++ b/docs/docs/python-sdk/guides/installation.mdx @@ -41,7 +41,7 @@ pip install 'infrahub-sdk[tests]' :::note -This extra is much larger than the others. The `infrahub_sdk.testing` helpers run Infrahub in containers, which adds roughly 66 packages on top of the base SDK. Install the `ctl` extra on its own if you only need the `infrahubctl` command. +This extra is much larger than the others. The `infrahub_sdk.testing` helpers run Infrahub in containers, which adds roughly 61 packages on top of the base SDK. Install the `ctl` extra on its own if you only need the `infrahubctl` command. ::: diff --git a/pyproject.toml b/pyproject.toml index 64911cb25..dcec7f472 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -26,6 +26,9 @@ dependencies = [ "httpx>=0.20", "anyio>=3.3.0", # 3.3.0 introduced `anyio.Path`, used by the async file handler "typing-extensions>=4.4.0", # 4.4.0 introduced PEP 696 `TypeVar(default=...)` + "Jinja2>=3", # template rendering in `template/` and `protocols_generator/` + "pyyaml>=6", # YAML parsing in `yaml.py`, `spec/`, `template/infrahub_filters.py` and the pytest plugin + "rich>=12,<14", # `Traceback`/`Frame`/`Syntax` in the Jinja error model, and transfer progress bars "ujson>=5", "dulwich>=0.24.7", "whenever>=0.9.3,<0.10.0", @@ -46,11 +49,8 @@ infrahubctl = "infrahub_sdk.ctl.cli:app" [project.optional-dependencies] ctl = [ - "Jinja2>=3", "pyarrow>=14", - "pyyaml>=6", - "ruamel.yaml>=0.18", - "rich>=12,<14", + "ruamel.yaml>=0.18", # round-trip mode preserves comments when `schema format` rewrites a file "typer>=0.15.0", # Not imported directly; constrains the `click` that `typer` resolves to. "click>=8.3,<9", diff --git a/uv.lock b/uv.lock index 3ea77902f..5e1ac9384 100644 --- a/uv.lock +++ b/uv.lock @@ -682,9 +682,12 @@ dependencies = [ { name = "dulwich" }, { name = "graphql-core" }, { name = "httpx" }, + { name = "jinja2" }, { name = "netutils" }, { name = "pydantic" }, { name = "pydantic-settings" }, + { name = "pyyaml" }, + { name = "rich" }, { name = "tomli", marker = "python_full_version < '3.11'" }, { name = "typing-extensions" }, { name = "ujson" }, @@ -696,22 +699,16 @@ all = [ { name = "ariadne-codegen" }, { name = "click" }, { name = "infrahub-testcontainers" }, - { name = "jinja2" }, { name = "packaging" }, { name = "pyarrow" }, { name = "pytest" }, - { name = "pyyaml" }, - { name = "rich" }, { name = "ruamel-yaml" }, { name = "typer" }, ] ctl = [ { name = "ariadne-codegen" }, { name = "click" }, - { name = "jinja2" }, { name = "pyarrow" }, - { name = "pyyaml" }, - { name = "rich" }, { name = "ruamel-yaml" }, { name = "typer" }, ] @@ -788,8 +785,7 @@ requires-dist = [ { name = "httpx", specifier = ">=0.20" }, { name = "infrahub-testcontainers", marker = "extra == 'all'", specifier = ">=1.7.3" }, { name = "infrahub-testcontainers", marker = "extra == 'tests'", specifier = ">=1.7.3" }, - { name = "jinja2", marker = "extra == 'all'", specifier = ">=3" }, - { name = "jinja2", marker = "extra == 'ctl'", specifier = ">=3" }, + { name = "jinja2", specifier = ">=3" }, { name = "netutils", specifier = ">=1.0.0" }, { name = "packaging", marker = "extra == 'all'", specifier = ">=21.0" }, { name = "packaging", marker = "extra == 'tests'", specifier = ">=21.0" }, @@ -799,10 +795,8 @@ requires-dist = [ { name = "pydantic-settings", specifier = ">=2.0" }, { name = "pytest", marker = "extra == 'all'", specifier = ">=7.0" }, { name = "pytest", marker = "extra == 'tests'", specifier = ">=7.0" }, - { name = "pyyaml", marker = "extra == 'all'", specifier = ">=6" }, - { name = "pyyaml", marker = "extra == 'ctl'", specifier = ">=6" }, - { name = "rich", marker = "extra == 'all'", specifier = ">=12,<14" }, - { name = "rich", marker = "extra == 'ctl'", specifier = ">=12,<14" }, + { name = "pyyaml", specifier = ">=6" }, + { name = "rich", specifier = ">=12,<14" }, { name = "ruamel-yaml", marker = "extra == 'all'", specifier = ">=0.18" }, { name = "ruamel-yaml", marker = "extra == 'ctl'", specifier = ">=0.18" }, { name = "tomli", marker = "python_full_version < '3.11'", specifier = ">=1.1.0" }, From 78faca97ec9eba98c688cfa49b8a5d896bfb1ac4 Mon Sep 17 00:00:00 2001 From: Patrick Ogenstad Date: Wed, 26 Aug 2026 13:44:32 +0200 Subject: [PATCH 5/6] test: check imports against the install surface that ships them The import check compared against the union of every extra, so a module in the base wheel could import a package only `ctl` installed and still pass. That is exactly how `infrahub_sdk.template` came to be broken on a plain install. Each shipped module is now checked against the requirements its own surface implies: base modules against the core dependencies, `pytest_plugin/` and `testing/` against the `tests` extra, and `ctl/` plus the two modules only reachable from it against `ctl`. Against the previous metadata this reports 36 violations. Only imports that run at load time count. An import inside a function is the sanctioned way to reach for an extra, as the JSON importer does for pyarrow, and flagging it would punish the correct pattern. Also collapse separator runs in the name normaliser so it matches PEP 503 as its docstring claims. --- tests/unit/test_packaging_metadata.py | 123 ++++++++++++++++++++------ 1 file changed, 97 insertions(+), 26 deletions(-) diff --git a/tests/unit/test_packaging_metadata.py b/tests/unit/test_packaging_metadata.py index 7d92d470a..b921ea68e 100644 --- a/tests/unit/test_packaging_metadata.py +++ b/tests/unit/test_packaging_metadata.py @@ -9,7 +9,9 @@ from __future__ import annotations import ast +import re import sys +from collections.abc import Container, Iterator from dataclasses import dataclass from pathlib import Path from typing import Any @@ -28,6 +30,15 @@ import tomli as tomllib # type: ignore[import-not-found] PROJECT_NAME = "infrahub-sdk" +BASE_SECTION = "project.dependencies" + +# Modules that ship in the wheel but are allowed to need an extra, mapped to the extra that +# supplies them. `ctl/` is the CLI; `async_typer` and `graphql/plugin.py` sit outside it but are +# only reachable from it. Everything else must import on a plain install. +EXTRA_ONLY_MODULES = { + "ctl": ("ctl/", "async_typer.py", "graphql/plugin.py"), + "tests": ("pytest_plugin/", "testing/"), +} # Operators that establish a floor. A requirement without one of these lets a resolver reach back # to the first release ever published, which is never a version we have tested against. @@ -55,7 +66,7 @@ def _normalize(name: str) -> str: """Normalise a distribution name per PEP 503 so `ruamel.yaml` and `Jinja2` compare cleanly.""" - return "".join("-" if character in "-_." else character for character in name.lower()) + return re.sub(r"[-_.]+", "-", name).lower() def _pyproject() -> dict[str, Any]: @@ -74,47 +85,86 @@ def _is_self_reference(requirement: Requirement) -> bool: @dataclass class RequirementCase: name: str + section: str requirement: str def _requirement_cases() -> list[RequirementCase]: """Every requirement a user can install, labelled with the section that declares it.""" project = _pyproject() - sections = {"project.dependencies": project["dependencies"], **project["optional-dependencies"]} + sections = {BASE_SECTION: project["dependencies"], **project["optional-dependencies"]} return [ - RequirementCase(name=f"{section}-{Requirement(requirement).name}", requirement=requirement) + RequirementCase(name=f"{section}-{Requirement(requirement).name}", section=section, requirement=requirement) for section, requirements in sections.items() for requirement in requirements ] -def _declared_distributions() -> set[str]: - """Every distribution a user can install, across the core dependencies and all extras.""" - parsed = (Requirement(case.requirement) for case in _requirement_cases()) - return {_normalize(requirement.name) for requirement in parsed if not _is_self_reference(requirement)} +def _declared_in(sections: Container[str]) -> set[str]: + """The distributions installed by the given sections of `pyproject.toml`.""" + return { + _normalize(requirement.name) + for requirement in (Requirement(case.requirement) for case in _requirement_cases() if case.section in sections) + if not _is_self_reference(requirement) + } + + +def _surface_of(relative_path: Path) -> str | None: + """The extra a shipped module may rely on, or None when it has to work on a base install.""" + posix = relative_path.as_posix() + for extra, prefixes in EXTRA_ONLY_MODULES.items(): + if any(posix.startswith(prefix) for prefix in prefixes): + return extra + return None + + +def _import_time_nodes(tree: ast.Module) -> Iterator[ast.AST]: + """Walk the nodes that execute when the module is first loaded, so excluding function bodies. + + An import inside a function is the sanctioned way to reach for an extra, as the JSON importer + does for pyarrow, and must not count against the install its module ships in. + + Yields: + Each node reachable at import time, including those nested in module-level `if` and `try` + blocks and in class bodies. + """ + stack: list[ast.AST] = list(tree.body) + while stack: + node = stack.pop() + yield node + if not isinstance(node, (ast.FunctionDef, ast.AsyncFunctionDef)): + stack.extend(ast.iter_child_nodes(node)) + + +def _imports_of(path: Path, *, include_deferred: bool) -> set[str]: + """The third-party distributions a single module imports.""" + tree = ast.parse(path.read_text(encoding="utf-8")) + distributions = set() + for node in ast.walk(tree) if include_deferred else _import_time_nodes(tree): + if isinstance(node, ast.Import): + modules = [alias.name for alias in node.names] + elif isinstance(node, ast.ImportFrom) and node.level == 0 and node.module: + modules = [node.module] + else: + continue + for module in modules: + top_level = module.split(".")[0] + if ( + top_level in sys.stdlib_module_names + or top_level in STDLIB_ON_NEWER_PYTHON + or top_level == PACKAGE_DIR.name + ): + continue + distributions.add(_normalize(IMPORT_NAME_TO_DISTRIBUTION.get(top_level, top_level))) + return distributions def _imported_distributions() -> dict[str, Path]: """Map each third-party distribution imported under the package to a file that imports it.""" imported: dict[str, Path] = {} for path in sorted(PACKAGE_DIR.rglob("*.py")): - for node in ast.walk(ast.parse(path.read_text(encoding="utf-8"))): - if isinstance(node, ast.Import): - modules = [alias.name for alias in node.names] - elif isinstance(node, ast.ImportFrom) and node.level == 0 and node.module: - modules = [node.module] - else: - continue - for module in modules: - top_level = module.split(".")[0] - if ( - top_level in sys.stdlib_module_names - or top_level in STDLIB_ON_NEWER_PYTHON - or top_level == PACKAGE_DIR.name - ): - continue - distribution = _normalize(IMPORT_NAME_TO_DISTRIBUTION.get(top_level, top_level)) - imported.setdefault(distribution, path.relative_to(REPO_ROOT)) + for distribution in sorted(_imports_of(path, include_deferred=True)): + imported.setdefault(distribution, path.relative_to(REPO_ROOT)) return imported @@ -145,9 +195,30 @@ def test_all_extra_aggregates_every_other_extra() -> None: assert aggregated == {_normalize(name) for name in extras if name != "all"} +def test_shipped_modules_only_import_what_their_install_provides() -> None: + """Every shipped module must be importable with the install its own surface implies. + + Checking against the union of every extra would let a module that ships in the base wheel + import a package only the `ctl` extra installs, which is how `infrahub_sdk.template` came to + raise `ModuleNotFoundError` on a plain install. + """ + base = _declared_in({BASE_SECTION}) + failures = [] + for path in sorted(PACKAGE_DIR.rglob("*.py")): + extra = _surface_of(path.relative_to(PACKAGE_DIR)) + available = base if extra is None else base | _declared_in({extra}) + install = "a base install" if extra is None else f"the '{extra}' extra" + failures += [ + f"{path.relative_to(REPO_ROOT)} imports {distribution}, which is not installed by {install}" + for distribution in sorted(_imports_of(path, include_deferred=False) - available) + ] + + assert not failures, "\n".join(failures) + + def test_imports_in_shipped_package_are_declared() -> None: """Anything the wheel imports must be installable, not left to arrive as a transitive dependency.""" - declared = _declared_distributions() + declared = _declared_in({BASE_SECTION, *_optional_dependencies()}) undeclared = { distribution: source for distribution, source in _imported_distributions().items() @@ -164,7 +235,7 @@ def test_declared_dependencies_are_imported() -> None: imported = _imported_distributions() unused = sorted( distribution - for distribution in _declared_distributions() + for distribution in _declared_in({BASE_SECTION, *_optional_dependencies()}) if distribution not in imported and distribution not in DECLARED_WITHOUT_IMPORT ) assert not unused, ( From ffb9351fddcc5a00d620aaf739592024e52f1c97 Mon Sep 17 00:00:00 2001 From: Patrick Ogenstad Date: Thu, 27 Aug 2026 14:17:37 +0200 Subject: [PATCH 6/6] fix(packaging): drop the rich upper bound and ignore type-checking imports Moving rich into the core dependencies also promoted its `<14` cap from CLI users to everyone, which would conflict with any project already on a newer rich. The cap has no recorded cause: it arrived with the UV conversion, a mechanical commit that postdates rich 14.0, and nothing needs it. The unit suite passes against rich 13.9.4, 14.0.0, 14.2.0 and 15.0.0, and `Traceback._guess_lexer`, the one private API in use, behaves the same on all four. The lock stays on 13.9.4; only the accepted range widens. The import scan also counted `if TYPE_CHECKING:` blocks as running at import time, because it unwound every module-level `if` without reading the guard. Nothing triggered it yet, but annotating a base module against a ctl-only package is precisely a type-checking import, so the check would have rejected the correct pattern. The `else` branch of such a guard is a runtime fallback and still counts. Import classification now has direct tests, since the distinction between import-time, deferred and type-checking-only is subtle enough to regress quietly. Reverting the guard fix fails two of them. --- changelog/+rich-upper-bound.changed.md | 3 + pyproject.toml | 2 +- tests/unit/test_packaging_metadata.py | 76 ++++++++++++++++++++++++-- uv.lock | 2 +- 4 files changed, 75 insertions(+), 8 deletions(-) create mode 100644 changelog/+rich-upper-bound.changed.md diff --git a/changelog/+rich-upper-bound.changed.md b/changelog/+rich-upper-bound.changed.md new file mode 100644 index 000000000..789aedb2e --- /dev/null +++ b/changelog/+rich-upper-bound.changed.md @@ -0,0 +1,3 @@ +The `rich` requirement is now `>=12` with no upper bound, so the SDK can be installed alongside rich 14 and later. + +The previous `<14` cap would otherwise have applied to every install rather than only those using the `infrahubctl` CLI, and would have conflicted with any project already on a newer rich. The SDK's unit tests pass against rich 13, 14 and 15. diff --git a/pyproject.toml b/pyproject.toml index dcec7f472..e4c1a8e96 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -28,7 +28,7 @@ dependencies = [ "typing-extensions>=4.4.0", # 4.4.0 introduced PEP 696 `TypeVar(default=...)` "Jinja2>=3", # template rendering in `template/` and `protocols_generator/` "pyyaml>=6", # YAML parsing in `yaml.py`, `spec/`, `template/infrahub_filters.py` and the pytest plugin - "rich>=12,<14", # `Traceback`/`Frame`/`Syntax` in the Jinja error model, and transfer progress bars + "rich>=12", # `Traceback`/`Frame`/`Syntax` in the Jinja error model, and transfer progress bars "ujson>=5", "dulwich>=0.24.7", "whenever>=0.9.3,<0.10.0", diff --git a/tests/unit/test_packaging_metadata.py b/tests/unit/test_packaging_metadata.py index b921ea68e..c05d9a7d1 100644 --- a/tests/unit/test_packaging_metadata.py +++ b/tests/unit/test_packaging_metadata.py @@ -14,7 +14,7 @@ from collections.abc import Container, Iterator from dataclasses import dataclass from pathlib import Path -from typing import Any +from typing import Any, TypeGuard import pytest from packaging.requirements import Requirement @@ -89,6 +89,43 @@ class RequirementCase: requirement: str +@dataclass +class ImportCase: + name: str + source: str + runs_on_import: bool + + +IMPORT_CLASSIFICATION_CASES = [ + ImportCase(name="module-level", source="import pyarrow\n", runs_on_import=True), + ImportCase( + name="module-level-try", + source="try:\n import pyarrow\nexcept ImportError:\n pyarrow = None\n", + runs_on_import=True, + ), + ImportCase( + name="inside-function", + source="def load():\n import pyarrow\n return pyarrow\n", + runs_on_import=False, + ), + ImportCase( + name="type-checking-guard", + source="from typing import TYPE_CHECKING\nif TYPE_CHECKING:\n import pyarrow\n", + runs_on_import=False, + ), + ImportCase( + name="qualified-type-checking-guard", + source="import typing\nif typing.TYPE_CHECKING:\n import pyarrow\n", + runs_on_import=False, + ), + ImportCase( + name="runtime-fallback-of-type-checking-guard", + source="from typing import TYPE_CHECKING\nif TYPE_CHECKING:\n pass\nelse:\n import pyarrow\n", + runs_on_import=True, + ), +] + + def _requirement_cases() -> list[RequirementCase]: """Every requirement a user can install, labelled with the section that declares it.""" project = _pyproject() @@ -118,11 +155,21 @@ def _surface_of(relative_path: Path) -> str | None: return None +def _is_type_checking_guard(node: ast.AST) -> TypeGuard[ast.If]: + """Whether *node* is an `if TYPE_CHECKING:` (or `if typing.TYPE_CHECKING:`) statement.""" + if not isinstance(node, ast.If): + return False + test = node.test + name = test.id if isinstance(test, ast.Name) else test.attr if isinstance(test, ast.Attribute) else None + return name == "TYPE_CHECKING" + + def _import_time_nodes(tree: ast.Module) -> Iterator[ast.AST]: - """Walk the nodes that execute when the module is first loaded, so excluding function bodies. + """Walk the nodes that execute when the module is first loaded. - An import inside a function is the sanctioned way to reach for an extra, as the JSON importer - does for pyarrow, and must not count against the install its module ships in. + Function bodies and `if TYPE_CHECKING:` blocks are both skipped, because neither runs on + import: a deferred import is the sanctioned way to reach for an extra, as the JSON importer + does for pyarrow, and a type-checking import never executes at all. Yields: Each node reachable at import time, including those nested in module-level `if` and `try` @@ -132,8 +179,13 @@ def _import_time_nodes(tree: ast.Module) -> Iterator[ast.AST]: while stack: node = stack.pop() yield node - if not isinstance(node, (ast.FunctionDef, ast.AsyncFunctionDef)): - stack.extend(ast.iter_child_nodes(node)) + if isinstance(node, (ast.FunctionDef, ast.AsyncFunctionDef)): + continue + if _is_type_checking_guard(node): + # The else branch of a type-checking guard is the runtime fallback, so it still counts. + stack.extend(node.orelse) + continue + stack.extend(ast.iter_child_nodes(node)) def _imports_of(path: Path, *, include_deferred: bool) -> set[str]: @@ -168,6 +220,18 @@ def _imported_distributions() -> dict[str, Path]: return imported +@pytest.mark.parametrize("case", [pytest.param(tc, id=tc.name) for tc in IMPORT_CLASSIFICATION_CASES]) +def test_import_classification(case: ImportCase, tmp_path: Path) -> None: + """Whether an import counts at import time decides what the surface check enforces.""" + module = tmp_path / "probe.py" + module.write_text(case.source, encoding="utf-8") + + assert ("pyarrow" in _imports_of(module, include_deferred=False)) is case.runs_on_import + assert "pyarrow" in _imports_of(module, include_deferred=True), ( + "every import should be found when deferred ones are included" + ) + + @pytest.mark.parametrize("case", [pytest.param(tc, id=tc.name) for tc in _requirement_cases()]) def test_requirement_declares_a_lower_bound(case: RequirementCase) -> None: requirement = Requirement(case.requirement) diff --git a/uv.lock b/uv.lock index 5e1ac9384..fc937cd9e 100644 --- a/uv.lock +++ b/uv.lock @@ -796,7 +796,7 @@ requires-dist = [ { name = "pytest", marker = "extra == 'all'", specifier = ">=7.0" }, { name = "pytest", marker = "extra == 'tests'", specifier = ">=7.0" }, { name = "pyyaml", specifier = ">=6" }, - { name = "rich", specifier = ">=12,<14" }, + { name = "rich", specifier = ">=12" }, { name = "ruamel-yaml", marker = "extra == 'all'", specifier = ">=0.18" }, { name = "ruamel-yaml", marker = "extra == 'ctl'", specifier = ">=0.18" }, { name = "tomli", marker = "python_full_version < '3.11'", specifier = ">=1.1.0" },