From 79d620a8e44ed936621b893d295a949eaa824160 Mon Sep 17 00:00:00 2001 From: Max Ghenis Date: Fri, 11 Sep 2026 12:26:27 -0400 Subject: [PATCH 01/46] Record the file-set comparison and local check results Co-Authored-By: Claude Fable 5.1 --- PROGRESS.md | 47 +++++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 47 insertions(+) create mode 100644 PROGRESS.md diff --git a/PROGRESS.md b/PROGRESS.md new file mode 100644 index 00000000..e7a13479 --- /dev/null +++ b/PROGRESS.md @@ -0,0 +1,47 @@ +# Canonical SPM wrapper release PR + +## State + +Turning the canonical SPM wrapper worktree +(`max/spm-canonical-wrapper-release-20260910`, base `6a56ced4` = `origin/main`) +into a reviewable draft PR against `main` with hosted CI. + +Verified at start: `git rev-parse origin/main` = `6a56ced4f959ce9a1f3b794389a4b42699de8abc` += local HEAD. `git ls-remote origin refs/heads/max/spm-canonical-wrapper-release-20260910` +is empty, so the branch does not yet exist on origin. + +## Done + +- Read background: production checkout handoff/output/progress/native-loader + reports and `rollout/wrapper-loader-candidate/FINAL-REPORT.md`. +- Step 1 file-set comparison. release-final carries 72 changed/untracked paths; + production carries 70. Release-final adds exactly six: the TRO schema fix + (`src/policyengine/data/schemas/trace_tro.schema.json`, regenerated + `uk`/`us.trace.tro.jsonld`, `tests/test_generate_trace_tros.py`, + `changelog.d/repository-tro-schema.fixed.md`) plus a regenerated `uv.lock`. + Production holds only four extra paths, all working reports that must not be + committed. 55 of the 66 common paths are byte-identical; 11 differ. +- Verified the TRO sidecars rebind to the working-tree manifest hash + `d430b8e561d76e5836a17bf506b06b9cd16560eb77f15718dd46f4e78513de85`. +- Step 2 safety audit: all 36 untracked paths are text, 195 KB total. No wheels, + venv, `__pycache__`, `.h5` or binaries; no secret-shaped strings; no + `/Users/...` paths. Every `/tmp/...` occurrence is a runbook example or a + rejection sentinel in a test, never a default in committed logic. +- Step 4 local checks (see the report for exit codes). Green: ruff format, ruff + check, `uv pip check`, `scripts/bundle.py check`, `scripts/release_lock.py`, + `policyengine bundle verify`, the Python-Compat smoke import. +- `make test` FAILS: 39 failed, 1005 passed, 14 skipped, exit 1. All 39 share + one cause: `ModuleNotFoundError: No module named + 'spm_calculator.policyengine_adapter'`, from the new lazy validation imports + in `us/model.py` and `us/household.py`. +- Established why no pin bump fixes that. `spm-calculator 1.0.0` reached PyPI + today (2026-09-11T15:45:10Z), but `policyengine-us 2.0.0` has not (latest + 1.825.0). Probed in a throwaway venv: with calculator 1.0.0, country model + 1.764.6 cannot even load its variables (`spm_unit_geographic_adjustment` + imports `spm_calculator.geoadj`, removed in 1.0.0). So the canonical tuple + needs policyengine-us 2.0.0, which is unpublished. + +## Next + +- Step 3: logical commits with the Fable 5.1 co-author trailer. +- Step 5: push branch. Step 6: draft PR. Step 7: watch CI and report. From d9ca5ebbdebcdf77f246f6c6cce927e9a94c41a3 Mon Sep 17 00:00:00 2001 From: Max Ghenis Date: Fri, 11 Sep 2026 12:30:23 -0400 Subject: [PATCH 02/46] Require Python 3.11 through 3.14 and regenerate the lock The canonical US model bundle only supports Python 3.11 to 3.14, so the wrapper's floor moves from 3.9 to that same range and the classifiers, ruff target and mypy python_version follow. Dropping 3.9/3.10 lets the pyupgrade ignore list shrink to a deliberate style choice (Optional/List kept for the Pydantic models, plus UP042 to preserve public Enum string representations) rather than a compatibility workaround, and the two datetime call sites that were still using datetime.timezone.utc now import UTC directly. uv.lock is regenerated against the narrower requires-python, which collapses the resolution markers and drops every dependency and wheel that existed only to serve 3.9/3.10. Co-Authored-By: Claude Fable 5.1 --- changelog.d/canonical-spm-python.breaking.md | 1 + pyproject.toml | 21 +- .../core/tax_benefit_model_version.py | 6 +- src/policyengine/provenance/zenodo.py | 4 +- uv.lock | 1225 +---------------- 5 files changed, 73 insertions(+), 1184 deletions(-) create mode 100644 changelog.d/canonical-spm-python.breaking.md diff --git a/changelog.d/canonical-spm-python.breaking.md b/changelog.d/canonical-spm-python.breaking.md new file mode 100644 index 00000000..478aa73b --- /dev/null +++ b/changelog.d/canonical-spm-python.breaking.md @@ -0,0 +1 @@ +Require Python 3.11–3.14, matching the supported runtime range of the canonical US model bundle. Install checks now cover that complete range. diff --git a/pyproject.toml b/pyproject.toml index 5d4086e7..22c29404 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -11,10 +11,8 @@ authors = [ {name = "PolicyEngine", email = "hello@policyengine.org"}, ] license = {file = "LICENSE"} -requires-python = ">=3.9" +requires-python = ">=3.11,<3.15" classifiers = [ - "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", @@ -100,7 +98,7 @@ filterwarnings = [ [tool.ruff] line-length = 88 -target-version = "py39" +target-version = "py311" extend-exclude = ["*.ipynb"] [tool.ruff.lint] @@ -113,13 +111,12 @@ select = [ ] ignore = [ "E501", # Ignore line length errors - # The following pyupgrade rules would require Python 3.10+, but we - # support 3.9+. Re-enable these once the 3.9 floor is dropped. - "UP006", # prefer `list` over `List` — OK on 3.9 at runtime, but for - # pydantic models we use typing.List for consistency - "UP007", # prefer `X | Y` over `Union[X, Y]` — needs 3.10+ - "UP035", # `typing.List` is deprecated — same as UP006 - "UP045", # prefer `X | None` over `Optional[X]` — needs 3.10+ + # Preserve the existing annotation style across Pydantic models. + "UP006", + "UP007", + "UP035", + "UP045", + "UP042", # Preserve public Enum string representations. ] [tool.ruff.format] @@ -129,7 +126,7 @@ skip-magic-trailing-comma = false line-ending = "auto" [tool.mypy] -python_version = "3.9" +python_version = "3.11" warn_return_any = true warn_unused_configs = true ignore_missing_imports = true diff --git a/src/policyengine/core/tax_benefit_model_version.py b/src/policyengine/core/tax_benefit_model_version.py index 530d75a8..d6aead49 100644 --- a/src/policyengine/core/tax_benefit_model_version.py +++ b/src/policyengine/core/tax_benefit_model_version.py @@ -1,4 +1,4 @@ -from datetime import datetime, timezone +from datetime import UTC, datetime from typing import TYPE_CHECKING, Optional from uuid import uuid4 @@ -31,9 +31,7 @@ class TaxBenefitModelVersion(BaseModel): model: TaxBenefitModel version: str description: Optional[str] = None - created_at: Optional[datetime] = Field( - default_factory=lambda: datetime.now(timezone.utc) - ) + created_at: Optional[datetime] = Field(default_factory=lambda: datetime.now(UTC)) variables: list["Variable"] = Field(default_factory=list) parameters: list["Parameter"] = Field(default_factory=list) diff --git a/src/policyengine/provenance/zenodo.py b/src/policyengine/provenance/zenodo.py index 97d7801f..8fe4c41b 100644 --- a/src/policyengine/provenance/zenodo.py +++ b/src/policyengine/provenance/zenodo.py @@ -27,7 +27,7 @@ import hashlib import os from dataclasses import dataclass, field -from datetime import datetime, timezone +from datetime import UTC, datetime from typing import Any, Callable, Optional from .manifest import ( @@ -477,7 +477,7 @@ def mirror_release_to_zenodo( "resume it manually before retrying, to avoid orphaned drafts." ) from exc - deposited_at = datetime.now(timezone.utc).isoformat() + deposited_at = datetime.now(UTC).isoformat() mirrors = [ PreservationMirror( kind="zenodo", diff --git a/uv.lock b/uv.lock index 4a50b731..ac5c8db2 100644 --- a/uv.lock +++ b/uv.lock @@ -1,13 +1,9 @@ version = 1 revision = 3 -requires-python = ">=3.9" +requires-python = ">=3.11, <3.15" resolution-markers = [ - "python_full_version >= '3.13'", - "python_full_version == '3.12.*'", - "python_full_version == '3.11.*'", - "python_full_version == '3.10.*'", - "python_full_version > '3.9' and python_full_version < '3.10'", - "python_full_version <= '3.9'", + "python_full_version >= '3.12'", + "python_full_version < '3.12'", ] [[package]] @@ -98,15 +94,6 @@ 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 = "backports-asyncio-runner" -version = "1.2.0" -source = { registry = "https://pypi.org/simple" } -sdist = { url = "https://files.pythonhosted.org/packages/8e/ff/70dca7d7cb1cbc0edb2c6cc0c38b65cba36cccc491eca64cabd5fe7f8670/backports_asyncio_runner-1.2.0.tar.gz", hash = "sha256:a5aa7b2b7d8f8bfcaa2b57313f70792df84e32a2a746f585213373f900b42162", size = 69893, upload-time = "2025-07-02T02:27:15.685Z" } -wheels = [ - { url = "https://files.pythonhosted.org/packages/a0/59/76ab57e3fe74484f48a53f8e337171b4a2349e506eabe136d7e01d059086/backports_asyncio_runner-1.2.0-py3-none-any.whl", hash = "sha256:0da0a936a8aeb554eccb426dc55af3ba63bcdc69fa1a600b5bb305413a4477b5", size = 12313, upload-time = "2025-07-02T02:27:14.263Z" }, -] - [[package]] name = "beautifulsoup4" version = "4.13.5" @@ -120,78 +107,21 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/04/eb/f4151e0c7377a6e08a38108609ba5cede57986802757848688aeedd1b9e8/beautifulsoup4-4.13.5-py3-none-any.whl", hash = "sha256:642085eaa22233aceadff9c69651bc51e8bf3f874fb6d7104ece2beb24b47c4a", size = 105113, upload-time = "2025-08-24T14:06:14.884Z" }, ] -[[package]] -name = "blosc2" -version = "2.5.1" -source = { registry = "https://pypi.org/simple" } -resolution-markers = [ - "python_full_version > '3.9' and python_full_version < '3.10'", - "python_full_version <= '3.9'", -] -dependencies = [ - { name = "msgpack", marker = "python_full_version < '3.10'" }, - { name = "ndindex", marker = "python_full_version < '3.10'" }, - { name = "numpy", version = "2.0.2", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version < '3.10'" }, - { name = "py-cpuinfo", marker = "python_full_version < '3.10'" }, -] -sdist = { url = "https://files.pythonhosted.org/packages/f7/60/5bc8601f8ffcd5d8787b346898de8a0b454d031c3e158e3bbc312003984e/blosc2-2.5.1.tar.gz", hash = "sha256:47d5df50e7286edf81e629ece35f87f13f55c13c5e8545832188c420c75d1659", size = 4676483, upload-time = "2024-01-25T12:31:31.168Z" } -wheels = [ - { url = "https://files.pythonhosted.org/packages/b2/75/4511f1f9cea0aad8c5464736720a9de1e762e60e359cde6b7d69186e97b0/blosc2-2.5.1-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:c861262b7fe317c1614a9b59b6c9edf409532b4a6aaf5b2f4ad0d79c6f800b57", size = 4417584, upload-time = "2024-01-25T12:30:31.487Z" }, - { url = "https://files.pythonhosted.org/packages/c0/8a/c7ce322f90750eb7fdde0948e5c6b9c47ed03e276f2481b701f9d2360d3a/blosc2-2.5.1-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:8f35b5d69a7a41e9d5054297d2540c25f8af5ea3c62e4a80ca7359292d783c04", size = 3471945, upload-time = "2024-01-25T12:30:34.745Z" }, - { url = "https://files.pythonhosted.org/packages/32/72/32bbaf583eea51b27c91ddb145de017d970ec0a835ec70e966925b00c9cf/blosc2-2.5.1-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:546fa39f397dd54b13d7c42a4f890afaf16c70fe478712070942d464c440ce03", size = 4210024, upload-time = "2024-01-25T12:30:36.556Z" }, - { url = "https://files.pythonhosted.org/packages/a5/8c/d4ab68a40004f93c1fd20dceb1899e54477425542bffa1f5ebab2647956f/blosc2-2.5.1-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:5455af77e7e94159bb4966cae554f232ca2d52bb80cd3f878ecef39cf569da2a", size = 4374211, upload-time = "2024-01-25T12:30:38.355Z" }, - { url = "https://files.pythonhosted.org/packages/16/7d/c81b1caa0aded9f3e63c4dbab7a334681ded12ad80a18de34e7199939df5/blosc2-2.5.1-cp310-cp310-musllinux_1_1_x86_64.whl", hash = "sha256:b4dc4f595bf95c350c50bb77a8749cdd08a5dc2bdf3bdb18983d49a52d60b595", size = 4467025, upload-time = "2024-01-25T12:30:40.682Z" }, - { url = "https://files.pythonhosted.org/packages/9b/cd/56150f9367516f039b62923a5cb243771e2a1f440de0232b195f6e79f1ab/blosc2-2.5.1-cp310-cp310-win32.whl", hash = "sha256:873483bd5c6afb8d139039180ee57b74373232e87b032cb80389fd8bb883ea8e", size = 1948573, upload-time = "2024-01-25T12:30:43.019Z" }, - { url = "https://files.pythonhosted.org/packages/67/ff/0cf994e434b5ae860546cfe6941c58bdcdc27ed10009caffac9a29b66651/blosc2-2.5.1-cp310-cp310-win_amd64.whl", hash = "sha256:d5a7ef00b82fbca069e949335f9c92ce7cbe2039a9fa2e2bd4f5f418043d6262", size = 2343858, upload-time = "2024-01-25T12:30:44.621Z" }, - { url = "https://files.pythonhosted.org/packages/c3/f4/25a82b7191109beb0fc20c580f35667a2e86087ebac2f6a9e50469f618d7/blosc2-2.5.1-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:da826d42d616f8a939f27e1501b40e764fded66bc80177eeaefcebdbf3b3afb8", size = 4419509, upload-time = "2024-01-25T12:30:46.347Z" }, - { url = "https://files.pythonhosted.org/packages/03/50/b1d2bad6b9f77c3540b511b5dee77f213c8b179e942bbc34f5c410b92ede/blosc2-2.5.1-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:ae2e0c5dc8561a6b17842ee4320b49621434c20e622c9e9f5c67c9c6eb3b06a3", size = 3472870, upload-time = "2024-01-25T12:30:48.698Z" }, - { url = "https://files.pythonhosted.org/packages/17/26/d34d521fc60b164738d986144642c58675bcdc344cc9fbb8c08e58bb11ab/blosc2-2.5.1-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:af3cab9c12a4364c643266ee7d9583b526c0f484a291d72ec6efb09ea7ffbbf9", size = 4208463, upload-time = "2024-01-25T12:30:51.019Z" }, - { url = "https://files.pythonhosted.org/packages/2a/00/35169a0765044be69a3bfb73441ff8b9ea05d92d4660be622bbf90c46913/blosc2-2.5.1-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:22f03a723130cf07e4309fe34b1360c868f4376e862f8ff664eb40d019fdd3f6", size = 4371942, upload-time = "2024-01-25T12:30:53.304Z" }, - { url = "https://files.pythonhosted.org/packages/8f/9b/923e29af25f4c34b5b331829a87bc8397b18e8a361551230b70e6b0a785a/blosc2-2.5.1-cp311-cp311-musllinux_1_1_x86_64.whl", hash = "sha256:0fd109eef815ea1e50fde4f676388aa2f3bb5543502d125fb63f16ec7a014464", size = 4468495, upload-time = "2024-01-25T12:30:55.165Z" }, - { url = "https://files.pythonhosted.org/packages/9f/39/21d6d1dc00d765f5c1671f8c0b323096f0dde360e8e1d8005160679b4831/blosc2-2.5.1-cp311-cp311-win32.whl", hash = "sha256:1a3edc3256bad04d3db30c9de7eac3a820f96e741fc754cdabb6a9991e5c37e8", size = 1948151, upload-time = "2024-01-25T12:30:57.515Z" }, - { url = "https://files.pythonhosted.org/packages/d1/48/7d4be4e57342ec60c05d11478257b235cf33dc96c31049fb47e6ae98b652/blosc2-2.5.1-cp311-cp311-win_amd64.whl", hash = "sha256:e7499e277c13334d54f84e74f429f32341f99f7b978deaf9a7c2e963904cb48c", size = 2344506, upload-time = "2024-01-25T12:30:59.641Z" }, - { url = "https://files.pythonhosted.org/packages/5b/4d/00824754a3b5b5c6ffb92a043b5122be6202039fc76a25fa913fe0d4e235/blosc2-2.5.1-cp312-cp312-macosx_10_9_x86_64.whl", hash = "sha256:ab849d3adaeb035f2f16cf495cff1792b28d58dfb3de21b9459ee355c6bb8df3", size = 4414447, upload-time = "2024-01-25T12:31:01.39Z" }, - { url = "https://files.pythonhosted.org/packages/9b/21/9a887c9fdc46a5cfe4aa250eac4ed4f18213bf1996e113c1d1c662e31678/blosc2-2.5.1-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:dd66e60dafcc93d4c1f815d726d76f9fb067ecc9106a6c661010e709135c79ce", size = 3470989, upload-time = "2024-01-25T12:31:03.626Z" }, - { url = "https://files.pythonhosted.org/packages/92/8d/b13ea33ea4d5da344d1170638b2d7b3cc63a921b97f9a8184128fd78aacd/blosc2-2.5.1-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:eb5fcd1775b3884d9825aa51fb45253f45cfa21c77f4135fad5dc5db710c2a34", size = 4190080, upload-time = "2024-01-25T12:31:06.036Z" }, - { url = "https://files.pythonhosted.org/packages/33/bf/52c8385aa71ed8c42296016d48c80dcc41dd005e951a83970bde4e6d4ff2/blosc2-2.5.1-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:19f79071a336fcf1eda01cd0171291a4ab82b16cf9a15d2b4d26c010146f13b5", size = 4355536, upload-time = "2024-01-25T12:31:07.839Z" }, - { url = "https://files.pythonhosted.org/packages/78/21/f1815f711f98c04eba5116d55041b2f64e17e8a9437d7c832d1168ed2fa8/blosc2-2.5.1-cp312-cp312-musllinux_1_1_x86_64.whl", hash = "sha256:956a63231f1b448803e9b4bc3e704ea424c89fc14418d99093472c74f19c19e1", size = 4448554, upload-time = "2024-01-25T12:31:09.514Z" }, - { url = "https://files.pythonhosted.org/packages/a1/e6/e05e987db27986ca8bdaaf442b94212e116a1eb937a8cd35250548704aa6/blosc2-2.5.1-cp312-cp312-win32.whl", hash = "sha256:5856e57e0e81f9018f1a12e803b9f768fa5533175092d72d165ac60069c7d2ab", size = 1940131, upload-time = "2024-01-25T12:31:12.02Z" }, - { url = "https://files.pythonhosted.org/packages/29/80/871cf959e5e0d3d2a177caf7b9f4b714850a50c33bb5cedd86a95c6e05b2/blosc2-2.5.1-cp312-cp312-win_amd64.whl", hash = "sha256:585d780c5e85f251dec72b75a47666e4a261dbfe1d228769bca545e9fe07f480", size = 2338243, upload-time = "2024-01-25T12:31:13.967Z" }, - { url = "https://files.pythonhosted.org/packages/c9/4b/799dcec670a1d511d11a40e919241c26a273cfd2305a4f3fe7444c39758a/blosc2-2.5.1-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:0cb9a6ac1abc466c12bdc90052f17545512de8f854e672a1ea4d2b40292323f5", size = 4417609, upload-time = "2024-01-25T12:31:15.635Z" }, - { url = "https://files.pythonhosted.org/packages/07/cb/e97de5b2c40b6c2106d5b68c1d823707a16386fc08781a6c7c609000866e/blosc2-2.5.1-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:3def4650faa1db43143d821228ef58797108cc95d6698c4b1581909cc2b149ca", size = 3471971, upload-time = "2024-01-25T12:31:18.231Z" }, - { url = "https://files.pythonhosted.org/packages/65/c4/0c9740c4f5efb7adde87ed8b5af88c171a3ec007df912cc2fbfb2736963d/blosc2-2.5.1-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:cf6efecc1a22da26c73ff5c60d0dc086db1e7edcceb6b360dd193cda893bef28", size = 4209984, upload-time = "2024-01-25T12:31:20.855Z" }, - { url = "https://files.pythonhosted.org/packages/a6/e2/a1367ad174006e5625a1c8ef1b526fc135adc6f546bfc46264ac8e508ebe/blosc2-2.5.1-cp39-cp39-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:b473472b977b770aab3bf20d0feeee84ecd5bb8b15a675287e090ce818c1cd40", size = 4374484, upload-time = "2024-01-25T12:31:22.587Z" }, - { url = "https://files.pythonhosted.org/packages/62/88/a185ab2a50b930011396b601ae8e82e83dfa24597e5f1fd91f483f178a94/blosc2-2.5.1-cp39-cp39-musllinux_1_1_x86_64.whl", hash = "sha256:7afe59d35d93bf8da7db8de43f4d8aef277514de43953c1e5e416ca839b9023a", size = 4466917, upload-time = "2024-01-25T12:31:24.387Z" }, - { url = "https://files.pythonhosted.org/packages/b5/7c/6a7d46a6094a100e1bdfba7e1df584971efea00732006fdddcf0320d4846/blosc2-2.5.1-cp39-cp39-win32.whl", hash = "sha256:4315ae8d467fe91efa0dbe22004e967008f5fe021ebb3945518f5213d7c4511f", size = 1948282, upload-time = "2024-01-25T12:31:26.074Z" }, - { url = "https://files.pythonhosted.org/packages/5c/c1/f80115c66a181e2ff18027818d3e1f37a3d5133b350b3e25189e53e28cd8/blosc2-2.5.1-cp39-cp39-win_amd64.whl", hash = "sha256:73eb5e569a91fbe67f7dd78efe6a1ca9a54afff2c847db5dfa675bfd6a424f60", size = 2343778, upload-time = "2024-01-25T12:31:27.669Z" }, -] - [[package]] name = "blosc2" version = "3.7.2" source = { registry = "https://pypi.org/simple" } -resolution-markers = [ - "python_full_version >= '3.13'", - "python_full_version == '3.12.*'", - "python_full_version == '3.11.*'", - "python_full_version == '3.10.*'", -] dependencies = [ - { name = "msgpack", marker = "python_full_version >= '3.10'" }, - { name = "ndindex", marker = "python_full_version >= '3.10'" }, - { name = "numexpr", version = "2.11.0", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version >= '3.10' and platform_machine != 'wasm32'" }, - { name = "numpy", version = "2.1.3", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version >= '3.10'" }, - { name = "platformdirs", marker = "python_full_version >= '3.10'" }, - { name = "py-cpuinfo", marker = "python_full_version >= '3.10' and platform_machine != 'wasm32'" }, - { name = "requests", marker = "python_full_version >= '3.10'" }, + { name = "msgpack" }, + { name = "ndindex" }, + { name = "numexpr", marker = "platform_machine != 'wasm32'" }, + { name = "numpy" }, + { name = "platformdirs" }, + { name = "py-cpuinfo", marker = "platform_machine != 'wasm32'" }, + { name = "requests" }, ] sdist = { url = "https://files.pythonhosted.org/packages/1b/ba/c1d8a8303197d8d8d4ec9b83851fcabd2d57dc5d17af2406643dd015544e/blosc2-3.7.2.tar.gz", hash = "sha256:3e80bd0399241829e4a2100bef9d4de042da979514f5df6aa3378981823f1d9b", size = 3804422, upload-time = "2025-08-19T10:21:46.164Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/ca/40/94e2a330f92d61e61e258dabc4ce76b8b4838e8cfa44f8b69e44fedc09d5/blosc2-3.7.2-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:f2059d43521cdb5535af2c7b5656afc879502e12769a43178415327507aeb16f", size = 3942041, upload-time = "2025-08-19T10:21:04.382Z" }, - { url = "https://files.pythonhosted.org/packages/b2/1e/368789ab00752bfa3be9ace9e93cba7f434355c06ccabecae68cb9e2e156/blosc2-3.7.2-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:c7be5e9d2d47f11f0e22f1a0dc8eb8cc94f388ebf0babb0cd2736b90a541f796", size = 3425736, upload-time = "2025-08-19T10:21:06.235Z" }, - { url = "https://files.pythonhosted.org/packages/db/2f/72ba2d0d2261f11cdccd060658232d092163446a6773a6c4568912076a49/blosc2-3.7.2-cp310-cp310-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:ac311f60750fd7cb3526cfb139af363ed17b41c7f54ce2c8866b01029a925624", size = 4324634, upload-time = "2025-08-19T10:21:07.359Z" }, - { url = "https://files.pythonhosted.org/packages/6a/21/62c230beebe2a7d9104e3a8765bb1fa2d0337ee7a2ebaf5258ce23f66c61/blosc2-3.7.2-cp310-cp310-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:4dfdde206c34a18c5a5c1abaf765c6aee39776946998a017bf48f5fb6ea6eaa6", size = 4461245, upload-time = "2025-08-19T10:21:08.603Z" }, - { url = "https://files.pythonhosted.org/packages/6b/76/b67437bc6f43c07a2068f4e47a101c53da55293c439a937175d2631e37d6/blosc2-3.7.2-cp310-cp310-win_amd64.whl", hash = "sha256:c3566c0c2710a4ff7342a9536b55d8998f5b1c81a1bbec530a42934923c290b2", size = 2256284, upload-time = "2025-08-19T10:21:10.209Z" }, { url = "https://files.pythonhosted.org/packages/2e/6d/27aaf8c1b270498d9132482c17651fe47c2262e9537b2d4d22ac894f83d1/blosc2-3.7.2-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:be0a789d7a5a802e212e529fc33702b978e1a04d89007efa4efe8afc0c4b8da2", size = 3945327, upload-time = "2025-08-19T10:21:11.615Z" }, { url = "https://files.pythonhosted.org/packages/3f/8a/dae4ff33f37ec371ac6f62caf15bf0a863d66ac71d1b8a58df46a0cd9cda/blosc2-3.7.2-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:bee13af632090512868e4e7dc37e5f2dbe426041437562af967c4f1948d36d6a", size = 3429381, upload-time = "2025-08-19T10:21:13.313Z" }, { url = "https://files.pythonhosted.org/packages/1b/1b/1858397556ae354041242e3137183400c625c3b53e584b29c3b19d805480/blosc2-3.7.2-cp311-cp311-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:e7092e26a12f62f25ae3cef8c6ee7abff67ba6c46b73a71305880f236d050600", size = 4330058, upload-time = "2025-08-19T10:21:14.575Z" }, @@ -225,10 +155,8 @@ version = "1.3.0" source = { registry = "https://pypi.org/simple" } dependencies = [ { name = "colorama", marker = "os_name == 'nt'" }, - { name = "importlib-metadata", marker = "python_full_version < '3.10.2'" }, { name = "packaging" }, { name = "pyproject-hooks" }, - { name = "tomli", marker = "python_full_version < '3.11'" }, ] sdist = { url = "https://files.pythonhosted.org/packages/25/1c/23e33405a7c9eac261dff640926b8b5adaed6a6eb3e1767d441ed611d0c0/build-1.3.0.tar.gz", hash = "sha256:698edd0ea270bde950f53aed21f3a0135672206f3911e0176261a31e0e07b397", size = 48544, upload-time = "2025-08-01T21:27:09.268Z" } wheels = [ @@ -265,18 +193,6 @@ dependencies = [ ] 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" }, @@ -336,18 +252,6 @@ wheels = [ { 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]] @@ -356,17 +260,6 @@ version = "3.4.3" source = { registry = "https://pypi.org/simple" } sdist = { url = "https://files.pythonhosted.org/packages/83/2d/5fd176ceb9b2fc619e63405525573493ca23441330fcdaee6bef9460e924/charset_normalizer-3.4.3.tar.gz", hash = "sha256:6fce4b8500244f6fcb71465d4a4930d132ba9ab8e71a7859e6a5d59851068d14", size = 122371, upload-time = "2025-08-09T07:57:28.46Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/d6/98/f3b8013223728a99b908c9344da3aa04ee6e3fa235f19409033eda92fb78/charset_normalizer-3.4.3-cp310-cp310-macosx_10_9_universal2.whl", hash = "sha256:fb7f67a1bfa6e40b438170ebdc8158b78dc465a5a67b6dde178a46987b244a72", size = 207695, upload-time = "2025-08-09T07:55:36.452Z" }, - { url = "https://files.pythonhosted.org/packages/21/40/5188be1e3118c82dcb7c2a5ba101b783822cfb413a0268ed3be0468532de/charset_normalizer-3.4.3-cp310-cp310-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:cc9370a2da1ac13f0153780040f465839e6cccb4a1e44810124b4e22483c93fe", size = 147153, upload-time = "2025-08-09T07:55:38.467Z" }, - { url = "https://files.pythonhosted.org/packages/37/60/5d0d74bc1e1380f0b72c327948d9c2aca14b46a9efd87604e724260f384c/charset_normalizer-3.4.3-cp310-cp310-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:07a0eae9e2787b586e129fdcbe1af6997f8d0e5abaa0bc98c0e20e124d67e601", size = 160428, upload-time = "2025-08-09T07:55:40.072Z" }, - { url = "https://files.pythonhosted.org/packages/85/9a/d891f63722d9158688de58d050c59dc3da560ea7f04f4c53e769de5140f5/charset_normalizer-3.4.3-cp310-cp310-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:74d77e25adda8581ffc1c720f1c81ca082921329452eba58b16233ab1842141c", size = 157627, upload-time = "2025-08-09T07:55:41.706Z" }, - { url = "https://files.pythonhosted.org/packages/65/1a/7425c952944a6521a9cfa7e675343f83fd82085b8af2b1373a2409c683dc/charset_normalizer-3.4.3-cp310-cp310-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:d0e909868420b7049dafd3a31d45125b31143eec59235311fc4c57ea26a4acd2", size = 152388, upload-time = "2025-08-09T07:55:43.262Z" }, - { url = "https://files.pythonhosted.org/packages/f0/c9/a2c9c2a355a8594ce2446085e2ec97fd44d323c684ff32042e2a6b718e1d/charset_normalizer-3.4.3-cp310-cp310-musllinux_1_2_aarch64.whl", hash = "sha256:c6f162aabe9a91a309510d74eeb6507fab5fff92337a15acbe77753d88d9dcf0", size = 150077, upload-time = "2025-08-09T07:55:44.903Z" }, - { url = "https://files.pythonhosted.org/packages/3b/38/20a1f44e4851aa1c9105d6e7110c9d020e093dfa5836d712a5f074a12bf7/charset_normalizer-3.4.3-cp310-cp310-musllinux_1_2_ppc64le.whl", hash = "sha256:4ca4c094de7771a98d7fbd67d9e5dbf1eb73efa4f744a730437d8a3a5cf994f0", size = 161631, upload-time = "2025-08-09T07:55:46.346Z" }, - { url = "https://files.pythonhosted.org/packages/a4/fa/384d2c0f57edad03d7bec3ebefb462090d8905b4ff5a2d2525f3bb711fac/charset_normalizer-3.4.3-cp310-cp310-musllinux_1_2_s390x.whl", hash = "sha256:02425242e96bcf29a49711b0ca9f37e451da7c70562bc10e8ed992a5a7a25cc0", size = 159210, upload-time = "2025-08-09T07:55:47.539Z" }, - { url = "https://files.pythonhosted.org/packages/33/9e/eca49d35867ca2db336b6ca27617deed4653b97ebf45dfc21311ce473c37/charset_normalizer-3.4.3-cp310-cp310-musllinux_1_2_x86_64.whl", hash = "sha256:78deba4d8f9590fe4dae384aeff04082510a709957e968753ff3c48399f6f92a", size = 153739, upload-time = "2025-08-09T07:55:48.744Z" }, - { url = "https://files.pythonhosted.org/packages/2a/91/26c3036e62dfe8de8061182d33be5025e2424002125c9500faff74a6735e/charset_normalizer-3.4.3-cp310-cp310-win32.whl", hash = "sha256:d79c198e27580c8e958906f803e63cddb77653731be08851c7df0b1a14a8fc0f", size = 99825, upload-time = "2025-08-09T07:55:50.305Z" }, - { url = "https://files.pythonhosted.org/packages/e2/c6/f05db471f81af1fa01839d44ae2a8bfeec8d2a8b4590f16c4e7393afd323/charset_normalizer-3.4.3-cp310-cp310-win_amd64.whl", hash = "sha256:c6e490913a46fa054e03699c70019ab869e990270597018cef1d8562132c2669", size = 107452, upload-time = "2025-08-09T07:55:51.461Z" }, { url = "https://files.pythonhosted.org/packages/7f/b5/991245018615474a60965a7c9cd2b4efbaabd16d582a5547c47ee1c7730b/charset_normalizer-3.4.3-cp311-cp311-macosx_10_9_universal2.whl", hash = "sha256:b256ee2e749283ef3ddcff51a675ff43798d92d746d1a6e4631bf8c707d22d0b", size = 204483, upload-time = "2025-08-09T07:55:53.12Z" }, { url = "https://files.pythonhosted.org/packages/c7/2a/ae245c41c06299ec18262825c1569c5d3298fc920e4ddf56ab011b417efd/charset_normalizer-3.4.3-cp311-cp311-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:13faeacfe61784e2559e690fc53fa4c5ae97c6fcedb8eb6fb8d0a15b475d2c64", size = 145520, upload-time = "2025-08-09T07:55:54.712Z" }, { url = "https://files.pythonhosted.org/packages/3a/a4/b3b6c76e7a635748c4421d2b92c7b8f90a432f98bda5082049af37ffc8e3/charset_normalizer-3.4.3-cp311-cp311-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:00237675befef519d9af72169d8604a067d92755e84fe76492fef5441db05b91", size = 158876, upload-time = "2025-08-09T07:55:56.024Z" }, @@ -411,48 +304,15 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/72/2a/aff5dd112b2f14bcc3462c312dce5445806bfc8ab3a7328555da95330e4b/charset_normalizer-3.4.3-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:d716a916938e03231e86e43782ca7878fb602a125a91e7acb8b5112e2e96ac16", size = 152224, upload-time = "2025-08-09T07:56:51.369Z" }, { url = "https://files.pythonhosted.org/packages/b7/8c/9839225320046ed279c6e839d51f028342eb77c91c89b8ef2549f951f3ec/charset_normalizer-3.4.3-cp314-cp314-win32.whl", hash = "sha256:c6dbd0ccdda3a2ba7c2ecd9d77b37f3b5831687d8dc1b6ca5f56a4880cc7b7ce", size = 100086, upload-time = "2025-08-09T07:56:52.722Z" }, { url = "https://files.pythonhosted.org/packages/ee/7a/36fbcf646e41f710ce0a563c1c9a343c6edf9be80786edeb15b6f62e17db/charset_normalizer-3.4.3-cp314-cp314-win_amd64.whl", hash = "sha256:73dc19b562516fc9bcf6e5d6e596df0b4eb98d87e4f79f3ae71840e6ed21361c", size = 107400, upload-time = "2025-08-09T07:56:55.172Z" }, - { url = "https://files.pythonhosted.org/packages/c2/ca/9a0983dd5c8e9733565cf3db4df2b0a2e9a82659fd8aa2a868ac6e4a991f/charset_normalizer-3.4.3-cp39-cp39-macosx_10_9_universal2.whl", hash = "sha256:70bfc5f2c318afece2f5838ea5e4c3febada0be750fcf4775641052bbba14d05", size = 207520, upload-time = "2025-08-09T07:57:11.026Z" }, - { url = "https://files.pythonhosted.org/packages/39/c6/99271dc37243a4f925b09090493fb96c9333d7992c6187f5cfe5312008d2/charset_normalizer-3.4.3-cp39-cp39-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:23b6b24d74478dc833444cbd927c338349d6ae852ba53a0d02a2de1fce45b96e", size = 147307, upload-time = "2025-08-09T07:57:12.4Z" }, - { url = "https://files.pythonhosted.org/packages/e4/69/132eab043356bba06eb333cc2cc60c6340857d0a2e4ca6dc2b51312886b3/charset_normalizer-3.4.3-cp39-cp39-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:34a7f768e3f985abdb42841e20e17b330ad3aaf4bb7e7aeeb73db2e70f077b99", size = 160448, upload-time = "2025-08-09T07:57:13.712Z" }, - { url = "https://files.pythonhosted.org/packages/04/9a/914d294daa4809c57667b77470533e65def9c0be1ef8b4c1183a99170e9d/charset_normalizer-3.4.3-cp39-cp39-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:fb731e5deb0c7ef82d698b0f4c5bb724633ee2a489401594c5c88b02e6cb15f7", size = 157758, upload-time = "2025-08-09T07:57:14.979Z" }, - { url = "https://files.pythonhosted.org/packages/b0/a8/6f5bcf1bcf63cb45625f7c5cadca026121ff8a6c8a3256d8d8cd59302663/charset_normalizer-3.4.3-cp39-cp39-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:257f26fed7d7ff59921b78244f3cd93ed2af1800ff048c33f624c87475819dd7", size = 152487, upload-time = "2025-08-09T07:57:16.332Z" }, - { url = "https://files.pythonhosted.org/packages/c4/72/d3d0e9592f4e504f9dea08b8db270821c909558c353dc3b457ed2509f2fb/charset_normalizer-3.4.3-cp39-cp39-musllinux_1_2_aarch64.whl", hash = "sha256:1ef99f0456d3d46a50945c98de1774da86f8e992ab5c77865ea8b8195341fc19", size = 150054, upload-time = "2025-08-09T07:57:17.576Z" }, - { url = "https://files.pythonhosted.org/packages/20/30/5f64fe3981677fe63fa987b80e6c01042eb5ff653ff7cec1b7bd9268e54e/charset_normalizer-3.4.3-cp39-cp39-musllinux_1_2_ppc64le.whl", hash = "sha256:2c322db9c8c89009a990ef07c3bcc9f011a3269bc06782f916cd3d9eed7c9312", size = 161703, upload-time = "2025-08-09T07:57:20.012Z" }, - { url = "https://files.pythonhosted.org/packages/e1/ef/dd08b2cac9284fd59e70f7d97382c33a3d0a926e45b15fc21b3308324ffd/charset_normalizer-3.4.3-cp39-cp39-musllinux_1_2_s390x.whl", hash = "sha256:511729f456829ef86ac41ca78c63a5cb55240ed23b4b737faca0eb1abb1c41bc", size = 159096, upload-time = "2025-08-09T07:57:21.329Z" }, - { url = "https://files.pythonhosted.org/packages/45/8c/dcef87cfc2b3f002a6478f38906f9040302c68aebe21468090e39cde1445/charset_normalizer-3.4.3-cp39-cp39-musllinux_1_2_x86_64.whl", hash = "sha256:88ab34806dea0671532d3f82d82b85e8fc23d7b2dd12fa837978dad9bb392a34", size = 153852, upload-time = "2025-08-09T07:57:22.608Z" }, - { url = "https://files.pythonhosted.org/packages/63/86/9cbd533bd37883d467fcd1bd491b3547a3532d0fbb46de2b99feeebf185e/charset_normalizer-3.4.3-cp39-cp39-win32.whl", hash = "sha256:16a8770207946ac75703458e2c743631c79c59c5890c80011d536248f8eaa432", size = 99840, upload-time = "2025-08-09T07:57:23.883Z" }, - { url = "https://files.pythonhosted.org/packages/ce/d6/7e805c8e5c46ff9729c49950acc4ee0aeb55efb8b3a56687658ad10c3216/charset_normalizer-3.4.3-cp39-cp39-win_amd64.whl", hash = "sha256:d22dbedd33326a4a5190dd4fe9e9e693ef12160c77382d9e87919bce54f3d4ca", size = 107438, upload-time = "2025-08-09T07:57:25.287Z" }, { url = "https://files.pythonhosted.org/packages/8a/1f/f041989e93b001bc4e44bb1669ccdcf54d3f00e628229a85b08d330615c5/charset_normalizer-3.4.3-py3-none-any.whl", hash = "sha256:ce571ab16d890d23b5c278547ba694193a45011ff86a9162a71307ed9f86759a", size = 53175, upload-time = "2025-08-09T07:57:26.864Z" }, ] -[[package]] -name = "click" -version = "8.1.8" -source = { registry = "https://pypi.org/simple" } -resolution-markers = [ - "python_full_version > '3.9' and python_full_version < '3.10'", - "python_full_version <= '3.9'", -] -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.2.1" source = { registry = "https://pypi.org/simple" } -resolution-markers = [ - "python_full_version >= '3.13'", - "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 = "colorama", marker = "sys_platform == 'win32'" }, ] sdist = { url = "https://files.pythonhosted.org/packages/60/6c/8ca2efa64cf75a977a0d7fac081354553ebe483345c734fb6b6515d96bbc/click-8.2.1.tar.gz", hash = "sha256:27c491cc05d968d271d5a1db13e3b5a184636d9d930f148c50b038f0d0646202", size = 286342, upload-time = "2025-05-20T23:19:49.832Z" } wheels = [ @@ -477,152 +337,12 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/60/97/891a0971e1e4a8c5d2b20bbe0e524dc04548d2307fee33cdeba148fd4fc7/comm-0.2.3-py3-none-any.whl", hash = "sha256:c615d91d75f7f04f095b30d1c1711babd43bdc6419c1be9886a85f2f4e489417", size = 7294, upload-time = "2025-07-25T14:02:02.896Z" }, ] -[[package]] -name = "coverage" -version = "7.10.7" -source = { registry = "https://pypi.org/simple" } -resolution-markers = [ - "python_full_version > '3.9' and python_full_version < '3.10'", - "python_full_version <= '3.9'", -] -sdist = { url = "https://files.pythonhosted.org/packages/51/26/d22c300112504f5f9a9fd2297ce33c35f3d353e4aeb987c8419453b2a7c2/coverage-7.10.7.tar.gz", hash = "sha256:f4ab143ab113be368a3e9b795f9cd7906c5ef407d6173fe9675a902e1fffc239", size = 827704, upload-time = "2025-09-21T20:03:56.815Z" } -wheels = [ - { url = "https://files.pythonhosted.org/packages/e5/6c/3a3f7a46888e69d18abe3ccc6fe4cb16cccb1e6a2f99698931dafca489e6/coverage-7.10.7-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:fc04cc7a3db33664e0c2d10eb8990ff6b3536f6842c9590ae8da4c614b9ed05a", size = 217987, upload-time = "2025-09-21T20:00:57.218Z" }, - { url = "https://files.pythonhosted.org/packages/03/94/952d30f180b1a916c11a56f5c22d3535e943aa22430e9e3322447e520e1c/coverage-7.10.7-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:e201e015644e207139f7e2351980feb7040e6f4b2c2978892f3e3789d1c125e5", size = 218388, upload-time = "2025-09-21T20:01:00.081Z" }, - { url = "https://files.pythonhosted.org/packages/50/2b/9e0cf8ded1e114bcd8b2fd42792b57f1c4e9e4ea1824cde2af93a67305be/coverage-7.10.7-cp310-cp310-manylinux1_i686.manylinux_2_28_i686.manylinux_2_5_i686.whl", hash = "sha256:240af60539987ced2c399809bd34f7c78e8abe0736af91c3d7d0e795df633d17", size = 245148, upload-time = "2025-09-21T20:01:01.768Z" }, - { url = "https://files.pythonhosted.org/packages/19/20/d0384ac06a6f908783d9b6aa6135e41b093971499ec488e47279f5b846e6/coverage-7.10.7-cp310-cp310-manylinux1_x86_64.manylinux_2_28_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:8421e088bc051361b01c4b3a50fd39a4b9133079a2229978d9d30511fd05231b", size = 246958, upload-time = "2025-09-21T20:01:03.355Z" }, - { url = "https://files.pythonhosted.org/packages/60/83/5c283cff3d41285f8eab897651585db908a909c572bdc014bcfaf8a8b6ae/coverage-7.10.7-cp310-cp310-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:6be8ed3039ae7f7ac5ce058c308484787c86e8437e72b30bf5e88b8ea10f3c87", size = 248819, upload-time = "2025-09-21T20:01:04.968Z" }, - { url = "https://files.pythonhosted.org/packages/60/22/02eb98fdc5ff79f423e990d877693e5310ae1eab6cb20ae0b0b9ac45b23b/coverage-7.10.7-cp310-cp310-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:e28299d9f2e889e6d51b1f043f58d5f997c373cc12e6403b90df95b8b047c13e", size = 245754, upload-time = "2025-09-21T20:01:06.321Z" }, - { url = "https://files.pythonhosted.org/packages/b4/bc/25c83bcf3ad141b32cd7dc45485ef3c01a776ca3aa8ef0a93e77e8b5bc43/coverage-7.10.7-cp310-cp310-musllinux_1_2_aarch64.whl", hash = "sha256:c4e16bd7761c5e454f4efd36f345286d6f7c5fa111623c355691e2755cae3b9e", size = 246860, upload-time = "2025-09-21T20:01:07.605Z" }, - { url = "https://files.pythonhosted.org/packages/3c/b7/95574702888b58c0928a6e982038c596f9c34d52c5e5107f1eef729399b5/coverage-7.10.7-cp310-cp310-musllinux_1_2_i686.whl", hash = "sha256:b1c81d0e5e160651879755c9c675b974276f135558cf4ba79fee7b8413a515df", size = 244877, upload-time = "2025-09-21T20:01:08.829Z" }, - { url = "https://files.pythonhosted.org/packages/47/b6/40095c185f235e085df0e0b158f6bd68cc6e1d80ba6c7721dc81d97ec318/coverage-7.10.7-cp310-cp310-musllinux_1_2_riscv64.whl", hash = "sha256:606cc265adc9aaedcc84f1f064f0e8736bc45814f15a357e30fca7ecc01504e0", size = 245108, upload-time = "2025-09-21T20:01:10.527Z" }, - { url = "https://files.pythonhosted.org/packages/c8/50/4aea0556da7a4b93ec9168420d170b55e2eb50ae21b25062513d020c6861/coverage-7.10.7-cp310-cp310-musllinux_1_2_x86_64.whl", hash = "sha256:10b24412692df990dbc34f8fb1b6b13d236ace9dfdd68df5b28c2e39cafbba13", size = 245752, upload-time = "2025-09-21T20:01:11.857Z" }, - { url = "https://files.pythonhosted.org/packages/6a/28/ea1a84a60828177ae3b100cb6723838523369a44ec5742313ed7db3da160/coverage-7.10.7-cp310-cp310-win32.whl", hash = "sha256:b51dcd060f18c19290d9b8a9dd1e0181538df2ce0717f562fff6cf74d9fc0b5b", size = 220497, upload-time = "2025-09-21T20:01:13.459Z" }, - { url = "https://files.pythonhosted.org/packages/fc/1a/a81d46bbeb3c3fd97b9602ebaa411e076219a150489bcc2c025f151bd52d/coverage-7.10.7-cp310-cp310-win_amd64.whl", hash = "sha256:3a622ac801b17198020f09af3eaf45666b344a0d69fc2a6ffe2ea83aeef1d807", size = 221392, upload-time = "2025-09-21T20:01:14.722Z" }, - { url = "https://files.pythonhosted.org/packages/d2/5d/c1a17867b0456f2e9ce2d8d4708a4c3a089947d0bec9c66cdf60c9e7739f/coverage-7.10.7-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:a609f9c93113be646f44c2a0256d6ea375ad047005d7f57a5c15f614dc1b2f59", size = 218102, upload-time = "2025-09-21T20:01:16.089Z" }, - { url = "https://files.pythonhosted.org/packages/54/f0/514dcf4b4e3698b9a9077f084429681bf3aad2b4a72578f89d7f643eb506/coverage-7.10.7-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:65646bb0359386e07639c367a22cf9b5bf6304e8630b565d0626e2bdf329227a", size = 218505, upload-time = "2025-09-21T20:01:17.788Z" }, - { url = "https://files.pythonhosted.org/packages/20/f6/9626b81d17e2a4b25c63ac1b425ff307ecdeef03d67c9a147673ae40dc36/coverage-7.10.7-cp311-cp311-manylinux1_i686.manylinux_2_28_i686.manylinux_2_5_i686.whl", hash = "sha256:5f33166f0dfcce728191f520bd2692914ec70fac2713f6bf3ce59c3deacb4699", size = 248898, upload-time = "2025-09-21T20:01:19.488Z" }, - { url = "https://files.pythonhosted.org/packages/b0/ef/bd8e719c2f7417ba03239052e099b76ea1130ac0cbb183ee1fcaa58aaff3/coverage-7.10.7-cp311-cp311-manylinux1_x86_64.manylinux_2_28_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:35f5e3f9e455bb17831876048355dca0f758b6df22f49258cb5a91da23ef437d", size = 250831, upload-time = "2025-09-21T20:01:20.817Z" }, - { url = "https://files.pythonhosted.org/packages/a5/b6/bf054de41ec948b151ae2b79a55c107f5760979538f5fb80c195f2517718/coverage-7.10.7-cp311-cp311-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:4da86b6d62a496e908ac2898243920c7992499c1712ff7c2b6d837cc69d9467e", size = 252937, upload-time = "2025-09-21T20:01:22.171Z" }, - { url = "https://files.pythonhosted.org/packages/0f/e5/3860756aa6f9318227443c6ce4ed7bf9e70bb7f1447a0353f45ac5c7974b/coverage-7.10.7-cp311-cp311-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:6b8b09c1fad947c84bbbc95eca841350fad9cbfa5a2d7ca88ac9f8d836c92e23", size = 249021, upload-time = "2025-09-21T20:01:23.907Z" }, - { url = "https://files.pythonhosted.org/packages/26/0f/bd08bd042854f7fd07b45808927ebcce99a7ed0f2f412d11629883517ac2/coverage-7.10.7-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:4376538f36b533b46f8971d3a3e63464f2c7905c9800db97361c43a2b14792ab", size = 250626, upload-time = "2025-09-21T20:01:25.721Z" }, - { url = "https://files.pythonhosted.org/packages/8e/a7/4777b14de4abcc2e80c6b1d430f5d51eb18ed1d75fca56cbce5f2db9b36e/coverage-7.10.7-cp311-cp311-musllinux_1_2_i686.whl", hash = "sha256:121da30abb574f6ce6ae09840dae322bef734480ceafe410117627aa54f76d82", size = 248682, upload-time = "2025-09-21T20:01:27.105Z" }, - { url = "https://files.pythonhosted.org/packages/34/72/17d082b00b53cd45679bad682fac058b87f011fd8b9fe31d77f5f8d3a4e4/coverage-7.10.7-cp311-cp311-musllinux_1_2_riscv64.whl", hash = "sha256:88127d40df529336a9836870436fc2751c339fbaed3a836d42c93f3e4bd1d0a2", size = 248402, upload-time = "2025-09-21T20:01:28.629Z" }, - { url = "https://files.pythonhosted.org/packages/81/7a/92367572eb5bdd6a84bfa278cc7e97db192f9f45b28c94a9ca1a921c3577/coverage-7.10.7-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:ba58bbcd1b72f136080c0bccc2400d66cc6115f3f906c499013d065ac33a4b61", size = 249320, upload-time = "2025-09-21T20:01:30.004Z" }, - { url = "https://files.pythonhosted.org/packages/2f/88/a23cc185f6a805dfc4fdf14a94016835eeb85e22ac3a0e66d5e89acd6462/coverage-7.10.7-cp311-cp311-win32.whl", hash = "sha256:972b9e3a4094b053a4e46832b4bc829fc8a8d347160eb39d03f1690316a99c14", size = 220536, upload-time = "2025-09-21T20:01:32.184Z" }, - { url = "https://files.pythonhosted.org/packages/fe/ef/0b510a399dfca17cec7bc2f05ad8bd78cf55f15c8bc9a73ab20c5c913c2e/coverage-7.10.7-cp311-cp311-win_amd64.whl", hash = "sha256:a7b55a944a7f43892e28ad4bc0561dfd5f0d73e605d1aa5c3c976b52aea121d2", size = 221425, upload-time = "2025-09-21T20:01:33.557Z" }, - { url = "https://files.pythonhosted.org/packages/51/7f/023657f301a276e4ba1850f82749bc136f5a7e8768060c2e5d9744a22951/coverage-7.10.7-cp311-cp311-win_arm64.whl", hash = "sha256:736f227fb490f03c6488f9b6d45855f8e0fd749c007f9303ad30efab0e73c05a", size = 220103, upload-time = "2025-09-21T20:01:34.929Z" }, - { url = "https://files.pythonhosted.org/packages/13/e4/eb12450f71b542a53972d19117ea5a5cea1cab3ac9e31b0b5d498df1bd5a/coverage-7.10.7-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:7bb3b9ddb87ef7725056572368040c32775036472d5a033679d1fa6c8dc08417", size = 218290, upload-time = "2025-09-21T20:01:36.455Z" }, - { url = "https://files.pythonhosted.org/packages/37/66/593f9be12fc19fb36711f19a5371af79a718537204d16ea1d36f16bd78d2/coverage-7.10.7-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:18afb24843cbc175687225cab1138c95d262337f5473512010e46831aa0c2973", size = 218515, upload-time = "2025-09-21T20:01:37.982Z" }, - { url = "https://files.pythonhosted.org/packages/66/80/4c49f7ae09cafdacc73fbc30949ffe77359635c168f4e9ff33c9ebb07838/coverage-7.10.7-cp312-cp312-manylinux1_i686.manylinux_2_28_i686.manylinux_2_5_i686.whl", hash = "sha256:399a0b6347bcd3822be369392932884b8216d0944049ae22925631a9b3d4ba4c", size = 250020, upload-time = "2025-09-21T20:01:39.617Z" }, - { url = "https://files.pythonhosted.org/packages/a6/90/a64aaacab3b37a17aaedd83e8000142561a29eb262cede42d94a67f7556b/coverage-7.10.7-cp312-cp312-manylinux1_x86_64.manylinux_2_28_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:314f2c326ded3f4b09be11bc282eb2fc861184bc95748ae67b360ac962770be7", size = 252769, upload-time = "2025-09-21T20:01:41.341Z" }, - { url = "https://files.pythonhosted.org/packages/98/2e/2dda59afd6103b342e096f246ebc5f87a3363b5412609946c120f4e7750d/coverage-7.10.7-cp312-cp312-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:c41e71c9cfb854789dee6fc51e46743a6d138b1803fab6cb860af43265b42ea6", size = 253901, upload-time = "2025-09-21T20:01:43.042Z" }, - { url = "https://files.pythonhosted.org/packages/53/dc/8d8119c9051d50f3119bb4a75f29f1e4a6ab9415cd1fa8bf22fcc3fb3b5f/coverage-7.10.7-cp312-cp312-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:bc01f57ca26269c2c706e838f6422e2a8788e41b3e3c65e2f41148212e57cd59", size = 250413, upload-time = "2025-09-21T20:01:44.469Z" }, - { url = "https://files.pythonhosted.org/packages/98/b3/edaff9c5d79ee4d4b6d3fe046f2b1d799850425695b789d491a64225d493/coverage-7.10.7-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:a6442c59a8ac8b85812ce33bc4d05bde3fb22321fa8294e2a5b487c3505f611b", size = 251820, upload-time = "2025-09-21T20:01:45.915Z" }, - { url = "https://files.pythonhosted.org/packages/11/25/9a0728564bb05863f7e513e5a594fe5ffef091b325437f5430e8cfb0d530/coverage-7.10.7-cp312-cp312-musllinux_1_2_i686.whl", hash = "sha256:78a384e49f46b80fb4c901d52d92abe098e78768ed829c673fbb53c498bef73a", size = 249941, upload-time = "2025-09-21T20:01:47.296Z" }, - { url = "https://files.pythonhosted.org/packages/e0/fd/ca2650443bfbef5b0e74373aac4df67b08180d2f184b482c41499668e258/coverage-7.10.7-cp312-cp312-musllinux_1_2_riscv64.whl", hash = "sha256:5e1e9802121405ede4b0133aa4340ad8186a1d2526de5b7c3eca519db7bb89fb", size = 249519, upload-time = "2025-09-21T20:01:48.73Z" }, - { url = "https://files.pythonhosted.org/packages/24/79/f692f125fb4299b6f963b0745124998ebb8e73ecdfce4ceceb06a8c6bec5/coverage-7.10.7-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:d41213ea25a86f69efd1575073d34ea11aabe075604ddf3d148ecfec9e1e96a1", size = 251375, upload-time = "2025-09-21T20:01:50.529Z" }, - { url = "https://files.pythonhosted.org/packages/5e/75/61b9bbd6c7d24d896bfeec57acba78e0f8deac68e6baf2d4804f7aae1f88/coverage-7.10.7-cp312-cp312-win32.whl", hash = "sha256:77eb4c747061a6af8d0f7bdb31f1e108d172762ef579166ec84542f711d90256", size = 220699, upload-time = "2025-09-21T20:01:51.941Z" }, - { url = "https://files.pythonhosted.org/packages/ca/f3/3bf7905288b45b075918d372498f1cf845b5b579b723c8fd17168018d5f5/coverage-7.10.7-cp312-cp312-win_amd64.whl", hash = "sha256:f51328ffe987aecf6d09f3cd9d979face89a617eacdaea43e7b3080777f647ba", size = 221512, upload-time = "2025-09-21T20:01:53.481Z" }, - { url = "https://files.pythonhosted.org/packages/5c/44/3e32dbe933979d05cf2dac5e697c8599cfe038aaf51223ab901e208d5a62/coverage-7.10.7-cp312-cp312-win_arm64.whl", hash = "sha256:bda5e34f8a75721c96085903c6f2197dc398c20ffd98df33f866a9c8fd95f4bf", size = 220147, upload-time = "2025-09-21T20:01:55.2Z" }, - { url = "https://files.pythonhosted.org/packages/9a/94/b765c1abcb613d103b64fcf10395f54d69b0ef8be6a0dd9c524384892cc7/coverage-7.10.7-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:981a651f543f2854abd3b5fcb3263aac581b18209be49863ba575de6edf4c14d", size = 218320, upload-time = "2025-09-21T20:01:56.629Z" }, - { url = "https://files.pythonhosted.org/packages/72/4f/732fff31c119bb73b35236dd333030f32c4bfe909f445b423e6c7594f9a2/coverage-7.10.7-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:73ab1601f84dc804f7812dc297e93cd99381162da39c47040a827d4e8dafe63b", size = 218575, upload-time = "2025-09-21T20:01:58.203Z" }, - { url = "https://files.pythonhosted.org/packages/87/02/ae7e0af4b674be47566707777db1aa375474f02a1d64b9323e5813a6cdd5/coverage-7.10.7-cp313-cp313-manylinux1_i686.manylinux_2_28_i686.manylinux_2_5_i686.whl", hash = "sha256:a8b6f03672aa6734e700bbcd65ff050fd19cddfec4b031cc8cf1c6967de5a68e", size = 249568, upload-time = "2025-09-21T20:01:59.748Z" }, - { url = "https://files.pythonhosted.org/packages/a2/77/8c6d22bf61921a59bce5471c2f1f7ac30cd4ac50aadde72b8c48d5727902/coverage-7.10.7-cp313-cp313-manylinux1_x86_64.manylinux_2_28_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:10b6ba00ab1132a0ce4428ff68cf50a25efd6840a42cdf4239c9b99aad83be8b", size = 252174, upload-time = "2025-09-21T20:02:01.192Z" }, - { url = "https://files.pythonhosted.org/packages/b1/20/b6ea4f69bbb52dac0aebd62157ba6a9dddbfe664f5af8122dac296c3ee15/coverage-7.10.7-cp313-cp313-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:c79124f70465a150e89340de5963f936ee97097d2ef76c869708c4248c63ca49", size = 253447, upload-time = "2025-09-21T20:02:02.701Z" }, - { url = "https://files.pythonhosted.org/packages/f9/28/4831523ba483a7f90f7b259d2018fef02cb4d5b90bc7c1505d6e5a84883c/coverage-7.10.7-cp313-cp313-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:69212fbccdbd5b0e39eac4067e20a4a5256609e209547d86f740d68ad4f04911", size = 249779, upload-time = "2025-09-21T20:02:04.185Z" }, - { url = "https://files.pythonhosted.org/packages/a7/9f/4331142bc98c10ca6436d2d620c3e165f31e6c58d43479985afce6f3191c/coverage-7.10.7-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:7ea7c6c9d0d286d04ed3541747e6597cbe4971f22648b68248f7ddcd329207f0", size = 251604, upload-time = "2025-09-21T20:02:06.034Z" }, - { url = "https://files.pythonhosted.org/packages/ce/60/bda83b96602036b77ecf34e6393a3836365481b69f7ed7079ab85048202b/coverage-7.10.7-cp313-cp313-musllinux_1_2_i686.whl", hash = "sha256:b9be91986841a75042b3e3243d0b3cb0b2434252b977baaf0cd56e960fe1e46f", size = 249497, upload-time = "2025-09-21T20:02:07.619Z" }, - { url = "https://files.pythonhosted.org/packages/5f/af/152633ff35b2af63977edd835d8e6430f0caef27d171edf2fc76c270ef31/coverage-7.10.7-cp313-cp313-musllinux_1_2_riscv64.whl", hash = "sha256:b281d5eca50189325cfe1f365fafade89b14b4a78d9b40b05ddd1fc7d2a10a9c", size = 249350, upload-time = "2025-09-21T20:02:10.34Z" }, - { url = "https://files.pythonhosted.org/packages/9d/71/d92105d122bd21cebba877228990e1646d862e34a98bb3374d3fece5a794/coverage-7.10.7-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:99e4aa63097ab1118e75a848a28e40d68b08a5e19ce587891ab7fd04475e780f", size = 251111, upload-time = "2025-09-21T20:02:12.122Z" }, - { url = "https://files.pythonhosted.org/packages/a2/9e/9fdb08f4bf476c912f0c3ca292e019aab6712c93c9344a1653986c3fd305/coverage-7.10.7-cp313-cp313-win32.whl", hash = "sha256:dc7c389dce432500273eaf48f410b37886be9208b2dd5710aaf7c57fd442c698", size = 220746, upload-time = "2025-09-21T20:02:13.919Z" }, - { url = "https://files.pythonhosted.org/packages/b1/b1/a75fd25df44eab52d1931e89980d1ada46824c7a3210be0d3c88a44aaa99/coverage-7.10.7-cp313-cp313-win_amd64.whl", hash = "sha256:cac0fdca17b036af3881a9d2729a850b76553f3f716ccb0360ad4dbc06b3b843", size = 221541, upload-time = "2025-09-21T20:02:15.57Z" }, - { url = "https://files.pythonhosted.org/packages/14/3a/d720d7c989562a6e9a14b2c9f5f2876bdb38e9367126d118495b89c99c37/coverage-7.10.7-cp313-cp313-win_arm64.whl", hash = "sha256:4b6f236edf6e2f9ae8fcd1332da4e791c1b6ba0dc16a2dc94590ceccb482e546", size = 220170, upload-time = "2025-09-21T20:02:17.395Z" }, - { url = "https://files.pythonhosted.org/packages/bb/22/e04514bf2a735d8b0add31d2b4ab636fc02370730787c576bb995390d2d5/coverage-7.10.7-cp313-cp313t-macosx_10_13_x86_64.whl", hash = "sha256:a0ec07fd264d0745ee396b666d47cef20875f4ff2375d7c4f58235886cc1ef0c", size = 219029, upload-time = "2025-09-21T20:02:18.936Z" }, - { url = "https://files.pythonhosted.org/packages/11/0b/91128e099035ece15da3445d9015e4b4153a6059403452d324cbb0a575fa/coverage-7.10.7-cp313-cp313t-macosx_11_0_arm64.whl", hash = "sha256:dd5e856ebb7bfb7672b0086846db5afb4567a7b9714b8a0ebafd211ec7ce6a15", size = 219259, upload-time = "2025-09-21T20:02:20.44Z" }, - { url = "https://files.pythonhosted.org/packages/8b/51/66420081e72801536a091a0c8f8c1f88a5c4bf7b9b1bdc6222c7afe6dc9b/coverage-7.10.7-cp313-cp313t-manylinux1_i686.manylinux_2_28_i686.manylinux_2_5_i686.whl", hash = "sha256:f57b2a3c8353d3e04acf75b3fed57ba41f5c0646bbf1d10c7c282291c97936b4", size = 260592, upload-time = "2025-09-21T20:02:22.313Z" }, - { url = "https://files.pythonhosted.org/packages/5d/22/9b8d458c2881b22df3db5bb3e7369e63d527d986decb6c11a591ba2364f7/coverage-7.10.7-cp313-cp313t-manylinux1_x86_64.manylinux_2_28_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:1ef2319dd15a0b009667301a3f84452a4dc6fddfd06b0c5c53ea472d3989fbf0", size = 262768, upload-time = "2025-09-21T20:02:24.287Z" }, - { url = "https://files.pythonhosted.org/packages/f7/08/16bee2c433e60913c610ea200b276e8eeef084b0d200bdcff69920bd5828/coverage-7.10.7-cp313-cp313t-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:83082a57783239717ceb0ad584de3c69cf581b2a95ed6bf81ea66034f00401c0", size = 264995, upload-time = "2025-09-21T20:02:26.133Z" }, - { url = "https://files.pythonhosted.org/packages/20/9d/e53eb9771d154859b084b90201e5221bca7674ba449a17c101a5031d4054/coverage-7.10.7-cp313-cp313t-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:50aa94fb1fb9a397eaa19c0d5ec15a5edd03a47bf1a3a6111a16b36e190cff65", size = 259546, upload-time = "2025-09-21T20:02:27.716Z" }, - { url = "https://files.pythonhosted.org/packages/ad/b0/69bc7050f8d4e56a89fb550a1577d5d0d1db2278106f6f626464067b3817/coverage-7.10.7-cp313-cp313t-musllinux_1_2_aarch64.whl", hash = "sha256:2120043f147bebb41c85b97ac45dd173595ff14f2a584f2963891cbcc3091541", size = 262544, upload-time = "2025-09-21T20:02:29.216Z" }, - { url = "https://files.pythonhosted.org/packages/ef/4b/2514b060dbd1bc0aaf23b852c14bb5818f244c664cb16517feff6bb3a5ab/coverage-7.10.7-cp313-cp313t-musllinux_1_2_i686.whl", hash = "sha256:2fafd773231dd0378fdba66d339f84904a8e57a262f583530f4f156ab83863e6", size = 260308, upload-time = "2025-09-21T20:02:31.226Z" }, - { url = "https://files.pythonhosted.org/packages/54/78/7ba2175007c246d75e496f64c06e94122bdb914790a1285d627a918bd271/coverage-7.10.7-cp313-cp313t-musllinux_1_2_riscv64.whl", hash = "sha256:0b944ee8459f515f28b851728ad224fa2d068f1513ef6b7ff1efafeb2185f999", size = 258920, upload-time = "2025-09-21T20:02:32.823Z" }, - { url = "https://files.pythonhosted.org/packages/c0/b3/fac9f7abbc841409b9a410309d73bfa6cfb2e51c3fada738cb607ce174f8/coverage-7.10.7-cp313-cp313t-musllinux_1_2_x86_64.whl", hash = "sha256:4b583b97ab2e3efe1b3e75248a9b333bd3f8b0b1b8e5b45578e05e5850dfb2c2", size = 261434, upload-time = "2025-09-21T20:02:34.86Z" }, - { url = "https://files.pythonhosted.org/packages/ee/51/a03bec00d37faaa891b3ff7387192cef20f01604e5283a5fabc95346befa/coverage-7.10.7-cp313-cp313t-win32.whl", hash = "sha256:2a78cd46550081a7909b3329e2266204d584866e8d97b898cd7fb5ac8d888b1a", size = 221403, upload-time = "2025-09-21T20:02:37.034Z" }, - { url = "https://files.pythonhosted.org/packages/53/22/3cf25d614e64bf6d8e59c7c669b20d6d940bb337bdee5900b9ca41c820bb/coverage-7.10.7-cp313-cp313t-win_amd64.whl", hash = "sha256:33a5e6396ab684cb43dc7befa386258acb2d7fae7f67330ebb85ba4ea27938eb", size = 222469, upload-time = "2025-09-21T20:02:39.011Z" }, - { url = "https://files.pythonhosted.org/packages/49/a1/00164f6d30d8a01c3c9c48418a7a5be394de5349b421b9ee019f380df2a0/coverage-7.10.7-cp313-cp313t-win_arm64.whl", hash = "sha256:86b0e7308289ddde73d863b7683f596d8d21c7d8664ce1dee061d0bcf3fbb4bb", size = 220731, upload-time = "2025-09-21T20:02:40.939Z" }, - { url = "https://files.pythonhosted.org/packages/23/9c/5844ab4ca6a4dd97a1850e030a15ec7d292b5c5cb93082979225126e35dd/coverage-7.10.7-cp314-cp314-macosx_10_13_x86_64.whl", hash = "sha256:b06f260b16ead11643a5a9f955bd4b5fd76c1a4c6796aeade8520095b75de520", size = 218302, upload-time = "2025-09-21T20:02:42.527Z" }, - { url = "https://files.pythonhosted.org/packages/f0/89/673f6514b0961d1f0e20ddc242e9342f6da21eaba3489901b565c0689f34/coverage-7.10.7-cp314-cp314-macosx_11_0_arm64.whl", hash = "sha256:212f8f2e0612778f09c55dd4872cb1f64a1f2b074393d139278ce902064d5b32", size = 218578, upload-time = "2025-09-21T20:02:44.468Z" }, - { url = "https://files.pythonhosted.org/packages/05/e8/261cae479e85232828fb17ad536765c88dd818c8470aca690b0ac6feeaa3/coverage-7.10.7-cp314-cp314-manylinux1_i686.manylinux_2_28_i686.manylinux_2_5_i686.whl", hash = "sha256:3445258bcded7d4aa630ab8296dea4d3f15a255588dd535f980c193ab6b95f3f", size = 249629, upload-time = "2025-09-21T20:02:46.503Z" }, - { url = "https://files.pythonhosted.org/packages/82/62/14ed6546d0207e6eda876434e3e8475a3e9adbe32110ce896c9e0c06bb9a/coverage-7.10.7-cp314-cp314-manylinux1_x86_64.manylinux_2_28_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:bb45474711ba385c46a0bfe696c695a929ae69ac636cda8f532be9e8c93d720a", size = 252162, upload-time = "2025-09-21T20:02:48.689Z" }, - { url = "https://files.pythonhosted.org/packages/ff/49/07f00db9ac6478e4358165a08fb41b469a1b053212e8a00cb02f0d27a05f/coverage-7.10.7-cp314-cp314-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:813922f35bd800dca9994c5971883cbc0d291128a5de6b167c7aa697fcf59360", size = 253517, upload-time = "2025-09-21T20:02:50.31Z" }, - { url = "https://files.pythonhosted.org/packages/a2/59/c5201c62dbf165dfbc91460f6dbbaa85a8b82cfa6131ac45d6c1bfb52deb/coverage-7.10.7-cp314-cp314-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:93c1b03552081b2a4423091d6fb3787265b8f86af404cff98d1b5342713bdd69", size = 249632, upload-time = "2025-09-21T20:02:51.971Z" }, - { url = "https://files.pythonhosted.org/packages/07/ae/5920097195291a51fb00b3a70b9bbd2edbfe3c84876a1762bd1ef1565ebc/coverage-7.10.7-cp314-cp314-musllinux_1_2_aarch64.whl", hash = "sha256:cc87dd1b6eaf0b848eebb1c86469b9f72a1891cb42ac7adcfbce75eadb13dd14", size = 251520, upload-time = "2025-09-21T20:02:53.858Z" }, - { url = "https://files.pythonhosted.org/packages/b9/3c/a815dde77a2981f5743a60b63df31cb322c944843e57dbd579326625a413/coverage-7.10.7-cp314-cp314-musllinux_1_2_i686.whl", hash = "sha256:39508ffda4f343c35f3236fe8d1a6634a51f4581226a1262769d7f970e73bffe", size = 249455, upload-time = "2025-09-21T20:02:55.807Z" }, - { url = "https://files.pythonhosted.org/packages/aa/99/f5cdd8421ea656abefb6c0ce92556709db2265c41e8f9fc6c8ae0f7824c9/coverage-7.10.7-cp314-cp314-musllinux_1_2_riscv64.whl", hash = "sha256:925a1edf3d810537c5a3abe78ec5530160c5f9a26b1f4270b40e62cc79304a1e", size = 249287, upload-time = "2025-09-21T20:02:57.784Z" }, - { url = "https://files.pythonhosted.org/packages/c3/7a/e9a2da6a1fc5d007dd51fca083a663ab930a8c4d149c087732a5dbaa0029/coverage-7.10.7-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:2c8b9a0636f94c43cd3576811e05b89aa9bc2d0a85137affc544ae5cb0e4bfbd", size = 250946, upload-time = "2025-09-21T20:02:59.431Z" }, - { url = "https://files.pythonhosted.org/packages/ef/5b/0b5799aa30380a949005a353715095d6d1da81927d6dbed5def2200a4e25/coverage-7.10.7-cp314-cp314-win32.whl", hash = "sha256:b7b8288eb7cdd268b0304632da8cb0bb93fadcfec2fe5712f7b9cc8f4d487be2", size = 221009, upload-time = "2025-09-21T20:03:01.324Z" }, - { url = "https://files.pythonhosted.org/packages/da/b0/e802fbb6eb746de006490abc9bb554b708918b6774b722bb3a0e6aa1b7de/coverage-7.10.7-cp314-cp314-win_amd64.whl", hash = "sha256:1ca6db7c8807fb9e755d0379ccc39017ce0a84dcd26d14b5a03b78563776f681", size = 221804, upload-time = "2025-09-21T20:03:03.4Z" }, - { url = "https://files.pythonhosted.org/packages/9e/e8/71d0c8e374e31f39e3389bb0bd19e527d46f00ea8571ec7ec8fd261d8b44/coverage-7.10.7-cp314-cp314-win_arm64.whl", hash = "sha256:097c1591f5af4496226d5783d036bf6fd6cd0cbc132e071b33861de756efb880", size = 220384, upload-time = "2025-09-21T20:03:05.111Z" }, - { url = "https://files.pythonhosted.org/packages/62/09/9a5608d319fa3eba7a2019addeacb8c746fb50872b57a724c9f79f146969/coverage-7.10.7-cp314-cp314t-macosx_10_13_x86_64.whl", hash = "sha256:a62c6ef0d50e6de320c270ff91d9dd0a05e7250cac2a800b7784bae474506e63", size = 219047, upload-time = "2025-09-21T20:03:06.795Z" }, - { url = "https://files.pythonhosted.org/packages/f5/6f/f58d46f33db9f2e3647b2d0764704548c184e6f5e014bef528b7f979ef84/coverage-7.10.7-cp314-cp314t-macosx_11_0_arm64.whl", hash = "sha256:9fa6e4dd51fe15d8738708a973470f67a855ca50002294852e9571cdbd9433f2", size = 219266, upload-time = "2025-09-21T20:03:08.495Z" }, - { url = "https://files.pythonhosted.org/packages/74/5c/183ffc817ba68e0b443b8c934c8795553eb0c14573813415bd59941ee165/coverage-7.10.7-cp314-cp314t-manylinux1_i686.manylinux_2_28_i686.manylinux_2_5_i686.whl", hash = "sha256:8fb190658865565c549b6b4706856d6a7b09302c797eb2cf8e7fe9dabb043f0d", size = 260767, upload-time = "2025-09-21T20:03:10.172Z" }, - { url = "https://files.pythonhosted.org/packages/0f/48/71a8abe9c1ad7e97548835e3cc1adbf361e743e9d60310c5f75c9e7bf847/coverage-7.10.7-cp314-cp314t-manylinux1_x86_64.manylinux_2_28_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:affef7c76a9ef259187ef31599a9260330e0335a3011732c4b9effa01e1cd6e0", size = 262931, upload-time = "2025-09-21T20:03:11.861Z" }, - { url = "https://files.pythonhosted.org/packages/84/fd/193a8fb132acfc0a901f72020e54be5e48021e1575bb327d8ee1097a28fd/coverage-7.10.7-cp314-cp314t-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:6e16e07d85ca0cf8bafe5f5d23a0b850064e8e945d5677492b06bbe6f09cc699", size = 265186, upload-time = "2025-09-21T20:03:13.539Z" }, - { url = "https://files.pythonhosted.org/packages/b1/8f/74ecc30607dd95ad50e3034221113ccb1c6d4e8085cc761134782995daae/coverage-7.10.7-cp314-cp314t-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:03ffc58aacdf65d2a82bbeb1ffe4d01ead4017a21bfd0454983b88ca73af94b9", size = 259470, upload-time = "2025-09-21T20:03:15.584Z" }, - { url = "https://files.pythonhosted.org/packages/0f/55/79ff53a769f20d71b07023ea115c9167c0bb56f281320520cf64c5298a96/coverage-7.10.7-cp314-cp314t-musllinux_1_2_aarch64.whl", hash = "sha256:1b4fd784344d4e52647fd7857b2af5b3fbe6c239b0b5fa63e94eb67320770e0f", size = 262626, upload-time = "2025-09-21T20:03:17.673Z" }, - { url = "https://files.pythonhosted.org/packages/88/e2/dac66c140009b61ac3fc13af673a574b00c16efdf04f9b5c740703e953c0/coverage-7.10.7-cp314-cp314t-musllinux_1_2_i686.whl", hash = "sha256:0ebbaddb2c19b71912c6f2518e791aa8b9f054985a0769bdb3a53ebbc765c6a1", size = 260386, upload-time = "2025-09-21T20:03:19.36Z" }, - { url = "https://files.pythonhosted.org/packages/a2/f1/f48f645e3f33bb9ca8a496bc4a9671b52f2f353146233ebd7c1df6160440/coverage-7.10.7-cp314-cp314t-musllinux_1_2_riscv64.whl", hash = "sha256:a2d9a3b260cc1d1dbdb1c582e63ddcf5363426a1a68faa0f5da28d8ee3c722a0", size = 258852, upload-time = "2025-09-21T20:03:21.007Z" }, - { url = "https://files.pythonhosted.org/packages/bb/3b/8442618972c51a7affeead957995cfa8323c0c9bcf8fa5a027421f720ff4/coverage-7.10.7-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:a3cc8638b2480865eaa3926d192e64ce6c51e3d29c849e09d5b4ad95efae5399", size = 261534, upload-time = "2025-09-21T20:03:23.12Z" }, - { url = "https://files.pythonhosted.org/packages/b2/dc/101f3fa3a45146db0cb03f5b4376e24c0aac818309da23e2de0c75295a91/coverage-7.10.7-cp314-cp314t-win32.whl", hash = "sha256:67f8c5cbcd3deb7a60b3345dffc89a961a484ed0af1f6f73de91705cc6e31235", size = 221784, upload-time = "2025-09-21T20:03:24.769Z" }, - { url = "https://files.pythonhosted.org/packages/4c/a1/74c51803fc70a8a40d7346660379e144be772bab4ac7bb6e6b905152345c/coverage-7.10.7-cp314-cp314t-win_amd64.whl", hash = "sha256:e1ed71194ef6dea7ed2d5cb5f7243d4bcd334bfb63e59878519be558078f848d", size = 222905, upload-time = "2025-09-21T20:03:26.93Z" }, - { url = "https://files.pythonhosted.org/packages/12/65/f116a6d2127df30bcafbceef0302d8a64ba87488bf6f73a6d8eebf060873/coverage-7.10.7-cp314-cp314t-win_arm64.whl", hash = "sha256:7fe650342addd8524ca63d77b2362b02345e5f1a093266787d210c70a50b471a", size = 220922, upload-time = "2025-09-21T20:03:28.672Z" }, - { url = "https://files.pythonhosted.org/packages/a3/ad/d1c25053764b4c42eb294aae92ab617d2e4f803397f9c7c8295caa77a260/coverage-7.10.7-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:fff7b9c3f19957020cac546c70025331113d2e61537f6e2441bc7657913de7d3", size = 217978, upload-time = "2025-09-21T20:03:30.362Z" }, - { url = "https://files.pythonhosted.org/packages/52/2f/b9f9daa39b80ece0b9548bbb723381e29bc664822d9a12c2135f8922c22b/coverage-7.10.7-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:bc91b314cef27742da486d6839b677b3f2793dfe52b51bbbb7cf736d5c29281c", size = 218370, upload-time = "2025-09-21T20:03:32.147Z" }, - { url = "https://files.pythonhosted.org/packages/dd/6e/30d006c3b469e58449650642383dddf1c8fb63d44fdf92994bfd46570695/coverage-7.10.7-cp39-cp39-manylinux1_i686.manylinux_2_28_i686.manylinux_2_5_i686.whl", hash = "sha256:567f5c155eda8df1d3d439d40a45a6a5f029b429b06648235f1e7e51b522b396", size = 244802, upload-time = "2025-09-21T20:03:33.919Z" }, - { url = "https://files.pythonhosted.org/packages/b0/49/8a070782ce7e6b94ff6a0b6d7c65ba6bc3091d92a92cef4cd4eb0767965c/coverage-7.10.7-cp39-cp39-manylinux1_x86_64.manylinux_2_28_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:2af88deffcc8a4d5974cf2d502251bc3b2db8461f0b66d80a449c33757aa9f40", size = 246625, upload-time = "2025-09-21T20:03:36.09Z" }, - { url = "https://files.pythonhosted.org/packages/6a/92/1c1c5a9e8677ce56d42b97bdaca337b2d4d9ebe703d8c174ede52dbabd5f/coverage-7.10.7-cp39-cp39-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:c7315339eae3b24c2d2fa1ed7d7a38654cba34a13ef19fbcb9425da46d3dc594", size = 248399, upload-time = "2025-09-21T20:03:38.342Z" }, - { url = "https://files.pythonhosted.org/packages/c0/54/b140edee7257e815de7426d5d9846b58505dffc29795fff2dfb7f8a1c5a0/coverage-7.10.7-cp39-cp39-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:912e6ebc7a6e4adfdbb1aec371ad04c68854cd3bf3608b3514e7ff9062931d8a", size = 245142, upload-time = "2025-09-21T20:03:40.591Z" }, - { url = "https://files.pythonhosted.org/packages/e4/9e/6d6b8295940b118e8b7083b29226c71f6154f7ff41e9ca431f03de2eac0d/coverage-7.10.7-cp39-cp39-musllinux_1_2_aarch64.whl", hash = "sha256:f49a05acd3dfe1ce9715b657e28d138578bc40126760efb962322c56e9ca344b", size = 246284, upload-time = "2025-09-21T20:03:42.355Z" }, - { url = "https://files.pythonhosted.org/packages/db/e5/5e957ca747d43dbe4d9714358375c7546cb3cb533007b6813fc20fce37ad/coverage-7.10.7-cp39-cp39-musllinux_1_2_i686.whl", hash = "sha256:cce2109b6219f22ece99db7644b9622f54a4e915dad65660ec435e89a3ea7cc3", size = 244353, upload-time = "2025-09-21T20:03:44.218Z" }, - { url = "https://files.pythonhosted.org/packages/9a/45/540fc5cc92536a1b783b7ef99450bd55a4b3af234aae35a18a339973ce30/coverage-7.10.7-cp39-cp39-musllinux_1_2_riscv64.whl", hash = "sha256:f3c887f96407cea3916294046fc7dab611c2552beadbed4ea901cbc6a40cc7a0", size = 244430, upload-time = "2025-09-21T20:03:46.065Z" }, - { url = "https://files.pythonhosted.org/packages/75/0b/8287b2e5b38c8fe15d7e3398849bb58d382aedc0864ea0fa1820e8630491/coverage-7.10.7-cp39-cp39-musllinux_1_2_x86_64.whl", hash = "sha256:635adb9a4507c9fd2ed65f39693fa31c9a3ee3a8e6dc64df033e8fdf52a7003f", size = 245311, upload-time = "2025-09-21T20:03:48.19Z" }, - { url = "https://files.pythonhosted.org/packages/0c/1d/29724999984740f0c86d03e6420b942439bf5bd7f54d4382cae386a9d1e9/coverage-7.10.7-cp39-cp39-win32.whl", hash = "sha256:5a02d5a850e2979b0a014c412573953995174743a3f7fa4ea5a6e9a3c5617431", size = 220500, upload-time = "2025-09-21T20:03:50.024Z" }, - { url = "https://files.pythonhosted.org/packages/43/11/4b1e6b129943f905ca54c339f343877b55b365ae2558806c1be4f7476ed5/coverage-7.10.7-cp39-cp39-win_amd64.whl", hash = "sha256:c134869d5ffe34547d14e174c866fd8fe2254918cc0a95e99052903bc1543e07", size = 221408, upload-time = "2025-09-21T20:03:51.803Z" }, - { url = "https://files.pythonhosted.org/packages/ec/16/114df1c291c22cac3b0c127a73e0af5c12ed7bbb6558d310429a0ae24023/coverage-7.10.7-py3-none-any.whl", hash = "sha256:f7941f6f2fe6dd6807a1208737b8a0cbcf1cc6d7b07d24998ad2d63590868260", size = 209952, upload-time = "2025-09-21T20:03:53.918Z" }, -] - -[package.optional-dependencies] -toml = [ - { name = "tomli", marker = "python_full_version < '3.10'" }, -] - [[package]] name = "coverage" version = "7.13.5" source = { registry = "https://pypi.org/simple" } -resolution-markers = [ - "python_full_version >= '3.13'", - "python_full_version == '3.12.*'", - "python_full_version == '3.11.*'", - "python_full_version == '3.10.*'", -] sdist = { url = "https://files.pythonhosted.org/packages/9d/e0/70553e3000e345daff267cec284ce4cbf3fc141b6da229ac52775b5428f1/coverage-7.13.5.tar.gz", hash = "sha256:c81f6515c4c40141f83f502b07bbfa5c240ba25bbe73da7b33f1e5b6120ff179", size = 915967, upload-time = "2026-03-17T10:33:18.341Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/69/33/e8c48488c29a73fd089f9d71f9653c1be7478f2ad6b5bc870db11a55d23d/coverage-7.13.5-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:e0723d2c96324561b9aa76fb982406e11d93cdb388a7a7da2b16e04719cf7ca5", size = 219255, upload-time = "2026-03-17T10:29:51.081Z" }, - { url = "https://files.pythonhosted.org/packages/da/bd/b0ebe9f677d7f4b74a3e115eec7ddd4bcf892074963a00d91e8b164a6386/coverage-7.13.5-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:52f444e86475992506b32d4e5ca55c24fc88d73bcbda0e9745095b28ef4dc0cf", size = 219772, upload-time = "2026-03-17T10:29:52.867Z" }, - { url = "https://files.pythonhosted.org/packages/48/cc/5cb9502f4e01972f54eedd48218bb203fe81e294be606a2bc93970208013/coverage-7.13.5-cp310-cp310-manylinux1_i686.manylinux_2_28_i686.manylinux_2_5_i686.whl", hash = "sha256:704de6328e3d612a8f6c07000a878ff38181ec3263d5a11da1db294fa6a9bdf8", size = 246532, upload-time = "2026-03-17T10:29:54.688Z" }, - { url = "https://files.pythonhosted.org/packages/7d/d8/3217636d86c7e7b12e126e4f30ef1581047da73140614523af7495ed5f2d/coverage-7.13.5-cp310-cp310-manylinux1_x86_64.manylinux_2_28_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:a1a6d79a14e1ec1832cabc833898636ad5f3754a678ef8bb4908515208bf84f4", size = 248333, upload-time = "2026-03-17T10:29:56.221Z" }, - { url = "https://files.pythonhosted.org/packages/2b/30/2002ac6729ba2d4357438e2ed3c447ad8562866c8c63fc16f6dfc33afe56/coverage-7.13.5-cp310-cp310-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:79060214983769c7ba3f0cee10b54c97609dca4d478fa1aa32b914480fd5738d", size = 250211, upload-time = "2026-03-17T10:29:57.938Z" }, - { url = "https://files.pythonhosted.org/packages/6c/85/552496626d6b9359eb0e2f86f920037c9cbfba09b24d914c6e1528155f7d/coverage-7.13.5-cp310-cp310-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:356e76b46783a98c2a2fe81ec79df4883a1e62895ea952968fb253c114e7f930", size = 252125, upload-time = "2026-03-17T10:29:59.388Z" }, - { url = "https://files.pythonhosted.org/packages/44/21/40256eabdcbccdb6acf6b381b3016a154399a75fe39d406f790ae84d1f3c/coverage-7.13.5-cp310-cp310-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:0cef0cdec915d11254a7f549c1170afecce708d30610c6abdded1f74e581666d", size = 247219, upload-time = "2026-03-17T10:30:01.199Z" }, - { url = "https://files.pythonhosted.org/packages/b1/e8/96e2a6c3f21a0ea77d7830b254a1542d0328acc8d7bdf6a284ba7e529f77/coverage-7.13.5-cp310-cp310-musllinux_1_2_aarch64.whl", hash = "sha256:dc022073d063b25a402454e5712ef9e007113e3a676b96c5f29b2bda29352f40", size = 248248, upload-time = "2026-03-17T10:30:03.317Z" }, - { url = "https://files.pythonhosted.org/packages/da/ba/8477f549e554827da390ec659f3c38e4b6d95470f4daafc2d8ff94eaa9c2/coverage-7.13.5-cp310-cp310-musllinux_1_2_i686.whl", hash = "sha256:9b74db26dfea4f4e50d48a4602207cd1e78be33182bc9cbf22da94f332f99878", size = 246254, upload-time = "2026-03-17T10:30:04.832Z" }, - { url = "https://files.pythonhosted.org/packages/55/59/bc22aef0e6aa179d5b1b001e8b3654785e9adf27ef24c93dc4228ebd5d68/coverage-7.13.5-cp310-cp310-musllinux_1_2_ppc64le.whl", hash = "sha256:ad146744ca4fd09b50c482650e3c1b1f4dfa1d4792e0a04a369c7f23336f0400", size = 250067, upload-time = "2026-03-17T10:30:06.535Z" }, - { url = "https://files.pythonhosted.org/packages/de/1b/c6a023a160806a5137dca53468fd97530d6acad24a22003b1578a9c2e429/coverage-7.13.5-cp310-cp310-musllinux_1_2_riscv64.whl", hash = "sha256:c555b48be1853fe3997c11c4bd521cdd9a9612352de01fa4508f16ec341e6fe0", size = 246521, upload-time = "2026-03-17T10:30:08.486Z" }, - { url = "https://files.pythonhosted.org/packages/2d/3f/3532c85a55aa2f899fa17c186f831cfa1aa434d88ff792a709636f64130e/coverage-7.13.5-cp310-cp310-musllinux_1_2_x86_64.whl", hash = "sha256:7034b5c56a58ae5e85f23949d52c14aca2cfc6848a31764995b7de88f13a1ea0", size = 247126, upload-time = "2026-03-17T10:30:09.966Z" }, - { url = "https://files.pythonhosted.org/packages/aa/2e/b9d56af4a24ef45dfbcda88e06870cb7d57b2b0bfa3a888d79b4c8debd76/coverage-7.13.5-cp310-cp310-win32.whl", hash = "sha256:eb7fdf1ef130660e7415e0253a01a7d5a88c9c4d158bcf75cbbd922fd65a5b58", size = 221860, upload-time = "2026-03-17T10:30:11.393Z" }, - { url = "https://files.pythonhosted.org/packages/9f/cc/d938417e7a4d7f0433ad4edee8bb2acdc60dc7ac5af19e2a07a048ecbee3/coverage-7.13.5-cp310-cp310-win_amd64.whl", hash = "sha256:3e1bb5f6c78feeb1be3475789b14a0f0a5b47d505bfc7267126ccbd50289999e", size = 222788, upload-time = "2026-03-17T10:30:12.886Z" }, { url = "https://files.pythonhosted.org/packages/4b/37/d24c8f8220ff07b839b2c043ea4903a33b0f455abe673ae3c03bbdb7f212/coverage-7.13.5-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:66a80c616f80181f4d643b0f9e709d97bcea413ecd9631e1dedc7401c8e6695d", size = 219381, upload-time = "2026-03-17T10:30:14.68Z" }, { url = "https://files.pythonhosted.org/packages/35/8b/cd129b0ca4afe886a6ce9d183c44d8301acbd4ef248622e7c49a23145605/coverage-7.13.5-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:145ede53ccbafb297c1c9287f788d1bc3efd6c900da23bf6931b09eafc931587", size = 219880, upload-time = "2026-03-17T10:30:16.231Z" }, { url = "https://files.pythonhosted.org/packages/55/2f/e0e5b237bffdb5d6c530ce87cc1d413a5b7d7dfd60fb067ad6d254c35c76/coverage-7.13.5-cp311-cp311-manylinux1_i686.manylinux_2_28_i686.manylinux_2_5_i686.whl", hash = "sha256:0672854dc733c342fa3e957e0605256d2bf5934feeac328da9e0b5449634a642", size = 250303, upload-time = "2026-03-17T10:30:17.748Z" }, @@ -718,7 +438,7 @@ wheels = [ [package.optional-dependencies] toml = [ - { name = "tomli", marker = "python_full_version >= '3.10' and python_full_version <= '3.11'" }, + { name = "tomli", marker = "python_full_version <= '3.11'" }, ] [[package]] @@ -740,10 +460,6 @@ version = "1.8.16" source = { registry = "https://pypi.org/simple" } sdist = { url = "https://files.pythonhosted.org/packages/ca/d4/722d0bcc7986172ac2ef3c979ad56a1030e3afd44ced136d45f8142b1f4a/debugpy-1.8.16.tar.gz", hash = "sha256:31e69a1feb1cf6b51efbed3f6c9b0ef03bc46ff050679c4be7ea6d2e23540870", size = 1643809, upload-time = "2025-08-06T18:00:02.647Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/ce/fd/f1b75ebc61d90882595b81d808efd3573c082e1c3407850d9dccac4ae904/debugpy-1.8.16-cp310-cp310-macosx_14_0_x86_64.whl", hash = "sha256:2a3958fb9c2f40ed8ea48a0d34895b461de57a1f9862e7478716c35d76f56c65", size = 2085511, upload-time = "2025-08-06T18:00:05.067Z" }, - { url = "https://files.pythonhosted.org/packages/df/5e/c5c1934352871128b30a1a144a58b5baa546e1b57bd47dbed788bad4431c/debugpy-1.8.16-cp310-cp310-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:e5ca7314042e8a614cc2574cd71f6ccd7e13a9708ce3c6d8436959eae56f2378", size = 3562094, upload-time = "2025-08-06T18:00:06.66Z" }, - { url = "https://files.pythonhosted.org/packages/c9/d5/2ebe42377e5a78dc786afc25e61ee83c5628d63f32dfa41092597d52fe83/debugpy-1.8.16-cp310-cp310-win32.whl", hash = "sha256:8624a6111dc312ed8c363347a0b59c5acc6210d897e41a7c069de3c53235c9a6", size = 5234277, upload-time = "2025-08-06T18:00:08.429Z" }, - { url = "https://files.pythonhosted.org/packages/54/f8/e774ad16a60b9913213dbabb7472074c5a7b0d84f07c1f383040a9690057/debugpy-1.8.16-cp310-cp310-win_amd64.whl", hash = "sha256:fee6db83ea5c978baf042440cfe29695e1a5d48a30147abf4c3be87513609817", size = 5266011, upload-time = "2025-08-06T18:00:10.162Z" }, { url = "https://files.pythonhosted.org/packages/63/d6/ad70ba8b49b23fa286fb21081cf732232cc19374af362051da9c7537ae52/debugpy-1.8.16-cp311-cp311-macosx_14_0_universal2.whl", hash = "sha256:67371b28b79a6a12bcc027d94a06158f2fde223e35b5c4e0783b6f9d3b39274a", size = 2184063, upload-time = "2025-08-06T18:00:11.885Z" }, { url = "https://files.pythonhosted.org/packages/aa/49/7b03e88dea9759a4c7910143f87f92beb494daaae25560184ff4ae883f9e/debugpy-1.8.16-cp311-cp311-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:b2abae6dd02523bec2dee16bd6b0781cccb53fd4995e5c71cc659b5f45581898", size = 3134837, upload-time = "2025-08-06T18:00:13.782Z" }, { url = "https://files.pythonhosted.org/packages/5d/52/b348930316921de7565fbe37a487d15409041713004f3d74d03eb077dbd4/debugpy-1.8.16-cp311-cp311-win32.whl", hash = "sha256:f8340a3ac2ed4f5da59e064aa92e39edd52729a88fbde7bbaa54e08249a04493", size = 5159142, upload-time = "2025-08-06T18:00:15.391Z" }, @@ -756,10 +472,6 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/4d/a0/c95baae08a75bceabb79868d663a0736655e427ab9c81fb848da29edaeac/debugpy-1.8.16-cp313-cp313-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:bee89e948bc236a5c43c4214ac62d28b29388453f5fd328d739035e205365f0b", size = 4222491, upload-time = "2025-08-06T18:00:25.806Z" }, { url = "https://files.pythonhosted.org/packages/5b/2f/1c8db6ddd8a257c3cd2c46413b267f1d5fa3df910401c899513ce30392d6/debugpy-1.8.16-cp313-cp313-win32.whl", hash = "sha256:cf358066650439847ec5ff3dae1da98b5461ea5da0173d93d5e10f477c94609a", size = 5281126, upload-time = "2025-08-06T18:00:27.207Z" }, { url = "https://files.pythonhosted.org/packages/d3/ba/c3e154ab307366d6c5a9c1b68de04914e2ce7fa2f50d578311d8cc5074b2/debugpy-1.8.16-cp313-cp313-win_amd64.whl", hash = "sha256:b5aea1083f6f50023e8509399d7dc6535a351cc9f2e8827d1e093175e4d9fa4c", size = 5323094, upload-time = "2025-08-06T18:00:29.03Z" }, - { url = "https://files.pythonhosted.org/packages/35/40/acdad5944e508d5e936979ad3e96e56b78ba6d7fa75aaffc4426cb921e12/debugpy-1.8.16-cp39-cp39-macosx_14_0_x86_64.whl", hash = "sha256:135ccd2b1161bade72a7a099c9208811c137a150839e970aeaf121c2467debe8", size = 2086696, upload-time = "2025-08-06T18:00:36.469Z" }, - { url = "https://files.pythonhosted.org/packages/2d/eb/8d6a2cf3b29e272b5dfebe6f384f8457977d4fd7a02dab2cae4d421dbae2/debugpy-1.8.16-cp39-cp39-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:211238306331a9089e253fd997213bc4a4c65f949271057d6695953254095376", size = 3557329, upload-time = "2025-08-06T18:00:38.189Z" }, - { url = "https://files.pythonhosted.org/packages/00/7b/63b9cc4d3c6980c702911c0f6a9748933ce4e4f16ae0ec4fdef7690f6662/debugpy-1.8.16-cp39-cp39-win32.whl", hash = "sha256:88eb9ffdfb59bf63835d146c183d6dba1f722b3ae2a5f4b9fc03e925b3358922", size = 5235114, upload-time = "2025-08-06T18:00:39.586Z" }, - { url = "https://files.pythonhosted.org/packages/05/cf/80947f57e0ef4d6e33ec9c3a109a542678eba465723bf8b599719238eb93/debugpy-1.8.16-cp39-cp39-win_amd64.whl", hash = "sha256:c2c47c2e52b40449552843b913786499efcc3dbc21d6c49287d939cd0dbc49fd", size = 5266799, upload-time = "2025-08-06T18:00:41.013Z" }, { url = "https://files.pythonhosted.org/packages/52/57/ecc9ae29fa5b2d90107cd1d9bf8ed19aacb74b2264d986ae9d44fe9bdf87/debugpy-1.8.16-py2.py3-none-any.whl", hash = "sha256:19c9521962475b87da6f673514f7fd610328757ec993bf7ec0d8c96f9a325f9e", size = 5287700, upload-time = "2025-08-06T18:00:42.333Z" }, ] @@ -799,18 +511,6 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/c1/8b/5fe2cc11fee489817272089c4203e679c63b570a5aaeb18d852ae3cbba6a/et_xmlfile-2.0.0-py3-none-any.whl", hash = "sha256:7a91720bc756843502c3b7504c77b8fe44217c85c537d85037f0f536151b2caa", size = 18059, upload-time = "2024-10-25T17:25:39.051Z" }, ] -[[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 = "executing" version = "2.2.1" @@ -869,16 +569,6 @@ version = "3.2.4" source = { registry = "https://pypi.org/simple" } 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/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" }, @@ -917,81 +607,17 @@ wheels = [ { 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/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 = "h5py" -version = "3.14.0" -source = { registry = "https://pypi.org/simple" } -resolution-markers = [ - "python_full_version > '3.9' and python_full_version < '3.10'", - "python_full_version <= '3.9'", -] -dependencies = [ - { name = "numpy", version = "2.0.2", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version < '3.10'" }, -] -sdist = { url = "https://files.pythonhosted.org/packages/5d/57/dfb3c5c3f1bf5f5ef2e59a22dec4ff1f3d7408b55bfcefcfb0ea69ef21c6/h5py-3.14.0.tar.gz", hash = "sha256:2372116b2e0d5d3e5e705b7f663f7c8d96fa79a4052d250484ef91d24d6a08f4", size = 424323, upload-time = "2025-06-06T14:06:15.01Z" } -wheels = [ - { url = "https://files.pythonhosted.org/packages/52/89/06cbb421e01dea2e338b3154326523c05d9698f89a01f9d9b65e1ec3fb18/h5py-3.14.0-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:24df6b2622f426857bda88683b16630014588a0e4155cba44e872eb011c4eaed", size = 3332522, upload-time = "2025-06-06T14:04:13.775Z" }, - { url = "https://files.pythonhosted.org/packages/c3/e7/6c860b002329e408348735bfd0459e7b12f712c83d357abeef3ef404eaa9/h5py-3.14.0-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:6ff2389961ee5872de697054dd5a033b04284afc3fb52dc51d94561ece2c10c6", size = 2831051, upload-time = "2025-06-06T14:04:18.206Z" }, - { url = "https://files.pythonhosted.org/packages/fa/cd/3dd38cdb7cc9266dc4d85f27f0261680cb62f553f1523167ad7454e32b11/h5py-3.14.0-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:016e89d3be4c44f8d5e115fab60548e518ecd9efe9fa5c5324505a90773e6f03", size = 4324677, upload-time = "2025-06-06T14:04:23.438Z" }, - { url = "https://files.pythonhosted.org/packages/b1/45/e1a754dc7cd465ba35e438e28557119221ac89b20aaebef48282654e3dc7/h5py-3.14.0-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:1223b902ef0b5d90bcc8a4778218d6d6cd0f5561861611eda59fa6c52b922f4d", size = 4557272, upload-time = "2025-06-06T14:04:28.863Z" }, - { url = "https://files.pythonhosted.org/packages/5c/06/f9506c1531645829d302c420851b78bb717af808dde11212c113585fae42/h5py-3.14.0-cp310-cp310-win_amd64.whl", hash = "sha256:852b81f71df4bb9e27d407b43071d1da330d6a7094a588efa50ef02553fa7ce4", size = 2866734, upload-time = "2025-06-06T14:04:33.5Z" }, - { url = "https://files.pythonhosted.org/packages/61/1b/ad24a8ce846cf0519695c10491e99969d9d203b9632c4fcd5004b1641c2e/h5py-3.14.0-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:f30dbc58f2a0efeec6c8836c97f6c94afd769023f44e2bb0ed7b17a16ec46088", size = 3352382, upload-time = "2025-06-06T14:04:37.95Z" }, - { url = "https://files.pythonhosted.org/packages/36/5b/a066e459ca48b47cc73a5c668e9924d9619da9e3c500d9fb9c29c03858ec/h5py-3.14.0-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:543877d7f3d8f8a9828ed5df6a0b78ca3d8846244b9702e99ed0d53610b583a8", size = 2852492, upload-time = "2025-06-06T14:04:42.092Z" }, - { url = "https://files.pythonhosted.org/packages/08/0c/5e6aaf221557314bc15ba0e0da92e40b24af97ab162076c8ae009320a42b/h5py-3.14.0-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:8c497600c0496548810047257e36360ff551df8b59156d3a4181072eed47d8ad", size = 4298002, upload-time = "2025-06-06T14:04:47.106Z" }, - { url = "https://files.pythonhosted.org/packages/21/d4/d461649cafd5137088fb7f8e78fdc6621bb0c4ff2c090a389f68e8edc136/h5py-3.14.0-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:723a40ee6505bd354bfd26385f2dae7bbfa87655f4e61bab175a49d72ebfc06b", size = 4516618, upload-time = "2025-06-06T14:04:52.467Z" }, - { url = "https://files.pythonhosted.org/packages/db/0c/6c3f879a0f8e891625817637fad902da6e764e36919ed091dc77529004ac/h5py-3.14.0-cp311-cp311-win_amd64.whl", hash = "sha256:d2744b520440a996f2dae97f901caa8a953afc055db4673a993f2d87d7f38713", size = 2874888, upload-time = "2025-06-06T14:04:56.95Z" }, - { url = "https://files.pythonhosted.org/packages/3e/77/8f651053c1843391e38a189ccf50df7e261ef8cd8bfd8baba0cbe694f7c3/h5py-3.14.0-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:e0045115d83272090b0717c555a31398c2c089b87d212ceba800d3dc5d952e23", size = 3312740, upload-time = "2025-06-06T14:05:01.193Z" }, - { url = "https://files.pythonhosted.org/packages/ff/10/20436a6cf419b31124e59fefc78d74cb061ccb22213226a583928a65d715/h5py-3.14.0-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:6da62509b7e1d71a7d110478aa25d245dd32c8d9a1daee9d2a42dba8717b047a", size = 2829207, upload-time = "2025-06-06T14:05:05.061Z" }, - { url = "https://files.pythonhosted.org/packages/3f/19/c8bfe8543bfdd7ccfafd46d8cfd96fce53d6c33e9c7921f375530ee1d39a/h5py-3.14.0-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:554ef0ced3571366d4d383427c00c966c360e178b5fb5ee5bb31a435c424db0c", size = 4708455, upload-time = "2025-06-06T14:05:11.528Z" }, - { url = "https://files.pythonhosted.org/packages/86/f9/f00de11c82c88bfc1ef22633557bfba9e271e0cb3189ad704183fc4a2644/h5py-3.14.0-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:0cbd41f4e3761f150aa5b662df991868ca533872c95467216f2bec5fcad84882", size = 4929422, upload-time = "2025-06-06T14:05:18.399Z" }, - { url = "https://files.pythonhosted.org/packages/7a/6d/6426d5d456f593c94b96fa942a9b3988ce4d65ebaf57d7273e452a7222e8/h5py-3.14.0-cp312-cp312-win_amd64.whl", hash = "sha256:bf4897d67e613ecf5bdfbdab39a1158a64df105827da70ea1d90243d796d367f", size = 2862845, upload-time = "2025-06-06T14:05:23.699Z" }, - { url = "https://files.pythonhosted.org/packages/6c/c2/7efe82d09ca10afd77cd7c286e42342d520c049a8c43650194928bcc635c/h5py-3.14.0-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:aa4b7bbce683379b7bf80aaba68e17e23396100336a8d500206520052be2f812", size = 3289245, upload-time = "2025-06-06T14:05:28.24Z" }, - { url = "https://files.pythonhosted.org/packages/4f/31/f570fab1239b0d9441024b92b6ad03bb414ffa69101a985e4c83d37608bd/h5py-3.14.0-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:ef9603a501a04fcd0ba28dd8f0995303d26a77a980a1f9474b3417543d4c6174", size = 2807335, upload-time = "2025-06-06T14:05:31.997Z" }, - { url = "https://files.pythonhosted.org/packages/0d/ce/3a21d87896bc7e3e9255e0ad5583ae31ae9e6b4b00e0bcb2a67e2b6acdbc/h5py-3.14.0-cp313-cp313-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:e8cbaf6910fa3983c46172666b0b8da7b7bd90d764399ca983236f2400436eeb", size = 4700675, upload-time = "2025-06-06T14:05:37.38Z" }, - { url = "https://files.pythonhosted.org/packages/e7/ec/86f59025306dcc6deee5fda54d980d077075b8d9889aac80f158bd585f1b/h5py-3.14.0-cp313-cp313-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:d90e6445ab7c146d7f7981b11895d70bc1dd91278a4f9f9028bc0c95e4a53f13", size = 4921632, upload-time = "2025-06-06T14:05:43.464Z" }, - { url = "https://files.pythonhosted.org/packages/3f/6d/0084ed0b78d4fd3e7530c32491f2884140d9b06365dac8a08de726421d4a/h5py-3.14.0-cp313-cp313-win_amd64.whl", hash = "sha256:ae18e3de237a7a830adb76aaa68ad438d85fe6e19e0d99944a3ce46b772c69b3", size = 2852929, upload-time = "2025-06-06T14:05:47.659Z" }, - { url = "https://files.pythonhosted.org/packages/ec/ac/9ea82488c8790ee5b6ad1a807cd7dc3b9dadfece1cd0e0e369f68a7a8937/h5py-3.14.0-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:f5cc1601e78027cedfec6dd50efb4802f018551754191aeb58d948bd3ec3bd7a", size = 3345097, upload-time = "2025-06-06T14:05:51.984Z" }, - { url = "https://files.pythonhosted.org/packages/6c/bc/a172ecaaf287e3af2f837f23b470b0a2229c79555a0da9ac8b5cc5bed078/h5py-3.14.0-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:5e59d2136a8b302afd25acdf7a89b634e0eb7c66b1a211ef2d0457853768a2ef", size = 2843320, upload-time = "2025-06-06T14:05:55.754Z" }, - { url = "https://files.pythonhosted.org/packages/66/40/b423b57696514e05aa7bb06150ef96667d0e0006cc6de7ab52c71734ab51/h5py-3.14.0-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:573c33ad056ac7c1ab6d567b6db9df3ffc401045e3f605736218f96c1e0490c6", size = 4326368, upload-time = "2025-06-06T14:06:00.782Z" }, - { url = "https://files.pythonhosted.org/packages/f7/07/e088f89f04fdbe57ddf9de377f857158d3daa38cf5d0fb20ef9bd489e313/h5py-3.14.0-cp39-cp39-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:ccbe17dc187c0c64178f1a10aa274ed3a57d055117588942b8a08793cc448216", size = 4559686, upload-time = "2025-06-06T14:06:07.416Z" }, - { url = "https://files.pythonhosted.org/packages/b4/e4/fb8032d0e5480b1db9b419b5b50737b61bb3c7187c49d809975d62129fb0/h5py-3.14.0-cp39-cp39-win_amd64.whl", hash = "sha256:4f025cf30ae738c4c4e38c7439a761a71ccfcce04c2b87b2a2ac64e8c5171d43", size = 2877166, upload-time = "2025-06-06T14:06:13.05Z" }, ] [[package]] name = "h5py" version = "3.16.0" source = { registry = "https://pypi.org/simple" } -resolution-markers = [ - "python_full_version >= '3.13'", - "python_full_version == '3.12.*'", - "python_full_version == '3.11.*'", - "python_full_version == '3.10.*'", -] dependencies = [ - { name = "numpy", version = "2.1.3", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version >= '3.10'" }, + { name = "numpy" }, ] sdist = { url = "https://files.pythonhosted.org/packages/db/33/acd0ce6863b6c0d7735007df01815403f5589a21ff8c2e1ee2587a38f548/h5py-3.16.0.tar.gz", hash = "sha256:a0dbaad796840ccaa67a4c144a0d0c8080073c34c76d5a6941d6818678ef2738", size = 446526, upload-time = "2026-03-06T13:49:08.07Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/3a/6b/231413e58a787a89b316bb0d1777da3c62257e4797e09afd8d17ad3549dc/h5py-3.16.0-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:e06f864bedb2c8e7c1358e6c73af48519e317457c444d6f3d332bb4e8fa6d7d9", size = 3724137, upload-time = "2026-03-06T13:47:35.242Z" }, - { url = "https://files.pythonhosted.org/packages/74/f9/557ce3aad0fe8471fb5279bab0fc56ea473858a022c4ce8a0b8f303d64e9/h5py-3.16.0-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:ec86d4fffd87a0f4cb3d5796ceb5a50123a2a6d99b43e616e5504e66a953eca3", size = 3090112, upload-time = "2026-03-06T13:47:37.634Z" }, - { url = "https://files.pythonhosted.org/packages/7a/f5/e15b3d0dc8a18e56409a839e6468d6fb589bc5207c917399c2e0706eeb44/h5py-3.16.0-cp310-cp310-manylinux_2_28_aarch64.whl", hash = "sha256:86385ea895508220b8a7e45efa428aeafaa586bd737c7af9ee04661d8d84a10d", size = 4844847, upload-time = "2026-03-06T13:47:39.811Z" }, - { url = "https://files.pythonhosted.org/packages/cb/92/a8851d936547efe30cc0ce5245feac01f3ec6171f7899bc3f775c72030b3/h5py-3.16.0-cp310-cp310-manylinux_2_28_x86_64.whl", hash = "sha256:8975273c2c5921c25700193b408e28d6bdd0111c37468b2d4e25dcec4cd1d84d", size = 5065352, upload-time = "2026-03-06T13:47:41.489Z" }, - { url = "https://files.pythonhosted.org/packages/2b/ae/f2adc5d0ca9626db3277a3d87516e124cbc5d0eea0bd79bc085702d04f2c/h5py-3.16.0-cp310-cp310-musllinux_1_2_aarch64.whl", hash = "sha256:1677ad48b703f44efc9ea0c3ab284527f81bc4f318386aaaebc5fede6bbae56f", size = 4839173, upload-time = "2026-03-06T13:47:43.586Z" }, - { url = "https://files.pythonhosted.org/packages/64/0b/e0c8c69da1d8838da023a50cd3080eae5d475691f7636b35eff20bb6ef20/h5py-3.16.0-cp310-cp310-musllinux_1_2_x86_64.whl", hash = "sha256:7c4dd4cf5f0a4e36083f73172f6cfc25a5710789269547f132a20975bfe2434c", size = 5076216, upload-time = "2026-03-06T13:47:45.315Z" }, - { url = "https://files.pythonhosted.org/packages/66/35/d88fd6718832133c885004c61ceeeb24dbd6397ef877dbed6b3a64d6a286/h5py-3.16.0-cp310-cp310-win_amd64.whl", hash = "sha256:bdef06507725b455fccba9c16529121a5e1fbf56aa375f7d9713d9e8ff42454d", size = 3183639, upload-time = "2026-03-06T13:47:47.041Z" }, { url = "https://files.pythonhosted.org/packages/ba/95/a825894f3e45cbac7554c4e97314ce886b233a20033787eda755ca8fecc7/h5py-3.16.0-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:719439d14b83f74eeb080e9650a6c7aa6d0d9ea0ca7f804347b05fac6fbf18af", size = 3721663, upload-time = "2026-03-06T13:47:49.599Z" }, { url = "https://files.pythonhosted.org/packages/bf/3b/38ff88b347c3e346cda1d3fc1b65a7aa75d40632228d8b8a5d7b58508c24/h5py-3.16.0-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:c3f0a0e136f2e95dd0b67146abb6668af4f1a69c81ef8651a2d316e8e01de447", size = 3087630, upload-time = "2026-03-06T13:47:51.249Z" }, { url = "https://files.pythonhosted.org/packages/98/a8/2594cef906aee761601eff842c7dc598bea2b394a3e1c00966832b8eeb7c/h5py-3.16.0-cp311-cp311-manylinux_2_28_aarch64.whl", hash = "sha256:a6fbc5367d4046801f9b7db9191b31895f22f1c6df1f9987d667854cac493538", size = 4823472, upload-time = "2026-03-06T13:47:53.085Z" }, @@ -1098,18 +724,6 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/20/b0/36bd937216ec521246249be3bf9855081de4c5e06a0c9b4219dbeda50373/importlib_metadata-8.7.0-py3-none-any.whl", hash = "sha256:e5dd1551894c77868a30651cef00984d50e1002d06942a7101d34870c5f02afd", size = 27656, upload-time = "2025-04-27T15:29:00.214Z" }, ] -[[package]] -name = "importlib-resources" -version = "6.5.2" -source = { registry = "https://pypi.org/simple" } -dependencies = [ - { name = "zipp", marker = "python_full_version < '3.10'" }, -] -sdist = { url = "https://files.pythonhosted.org/packages/cf/8c/f834fbf984f691b4f7ff60f50b514cc3de5cc08abfc3295564dd89c5e2e7/importlib_resources-6.5.2.tar.gz", hash = "sha256:185f87adef5bcc288449d98fb4fba07cea78bc036455dd44c5fc4a2fe78fed2c", size = 44693, upload-time = "2025-01-03T18:51:56.698Z" } -wheels = [ - { url = "https://files.pythonhosted.org/packages/a4/ed/1f1afb2e9e7f38a545d628f864d562a5ae64fe6f7a10e28ffb9b185b4e89/importlib_resources-6.5.2-py3-none-any.whl", hash = "sha256:789cfdc3ed28c78b67a06acb8126751ced69a3d5f79c095a98298cd8a760ccec", size = 37461, upload-time = "2025-01-03T18:51:54.306Z" }, -] - [[package]] name = "iniconfig" version = "2.1.0" @@ -1127,8 +741,7 @@ dependencies = [ { name = "appnope", marker = "sys_platform == 'darwin'" }, { name = "comm" }, { name = "debugpy" }, - { name = "ipython", version = "8.18.1", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version < '3.10'" }, - { name = "ipython", version = "8.37.0", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version >= '3.10'" }, + { name = "ipython" }, { name = "jupyter-client" }, { name = "jupyter-core" }, { name = "matplotlib-inline" }, @@ -1144,54 +757,21 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/fc/c7/b445faca8deb954fe536abebff4ece5b097b923de482b26e78448c89d1dd/ipykernel-6.30.1-py3-none-any.whl", hash = "sha256:aa6b9fb93dca949069d8b85b6c79b2518e32ac583ae9c7d37c51d119e18b3fb4", size = 117484, upload-time = "2025-08-04T15:47:32.622Z" }, ] -[[package]] -name = "ipython" -version = "8.18.1" -source = { registry = "https://pypi.org/simple" } -resolution-markers = [ - "python_full_version > '3.9' and python_full_version < '3.10'", - "python_full_version <= '3.9'", -] -dependencies = [ - { name = "colorama", marker = "python_full_version < '3.10' and sys_platform == 'win32'" }, - { name = "decorator", marker = "python_full_version < '3.10'" }, - { name = "exceptiongroup", marker = "python_full_version < '3.10'" }, - { name = "jedi", marker = "python_full_version < '3.10'" }, - { name = "matplotlib-inline", marker = "python_full_version < '3.10'" }, - { name = "pexpect", marker = "python_full_version < '3.10' and sys_platform != 'win32'" }, - { name = "prompt-toolkit", marker = "python_full_version < '3.10'" }, - { name = "pygments", marker = "python_full_version < '3.10'" }, - { name = "stack-data", marker = "python_full_version < '3.10'" }, - { name = "traitlets", marker = "python_full_version < '3.10'" }, - { name = "typing-extensions", marker = "python_full_version < '3.10'" }, -] -sdist = { url = "https://files.pythonhosted.org/packages/b1/b9/3ba6c45a6df813c09a48bac313c22ff83efa26cbb55011218d925a46e2ad/ipython-8.18.1.tar.gz", hash = "sha256:ca6f079bb33457c66e233e4580ebfc4128855b4cf6370dddd73842a9563e8a27", size = 5486330, upload-time = "2023-11-27T09:58:34.596Z" } -wheels = [ - { url = "https://files.pythonhosted.org/packages/47/6b/d9fdcdef2eb6a23f391251fde8781c38d42acd82abe84d054cb74f7863b0/ipython-8.18.1-py3-none-any.whl", hash = "sha256:e8267419d72d81955ec1177f8a29aaa90ac80ad647499201119e2f05e99aa397", size = 808161, upload-time = "2023-11-27T09:58:30.538Z" }, -] - [[package]] name = "ipython" version = "8.37.0" source = { registry = "https://pypi.org/simple" } -resolution-markers = [ - "python_full_version >= '3.13'", - "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 = "decorator", marker = "python_full_version >= '3.10'" }, - { name = "exceptiongroup", marker = "python_full_version == '3.10.*'" }, - { name = "jedi", marker = "python_full_version >= '3.10'" }, - { name = "matplotlib-inline", marker = "python_full_version >= '3.10'" }, - { name = "pexpect", marker = "python_full_version >= '3.10' and sys_platform != 'emscripten' and sys_platform != 'win32'" }, - { name = "prompt-toolkit", marker = "python_full_version >= '3.10'" }, - { name = "pygments", marker = "python_full_version >= '3.10'" }, - { name = "stack-data", marker = "python_full_version >= '3.10'" }, - { name = "traitlets", marker = "python_full_version >= '3.10'" }, - { name = "typing-extensions", marker = "python_full_version >= '3.10' and python_full_version < '3.12'" }, + { name = "colorama", marker = "sys_platform == 'win32'" }, + { name = "decorator" }, + { name = "jedi" }, + { name = "matplotlib-inline" }, + { name = "pexpect", marker = "sys_platform != 'emscripten' and sys_platform != 'win32'" }, + { name = "prompt-toolkit" }, + { name = "pygments" }, + { name = "stack-data" }, + { name = "traitlets" }, + { name = "typing-extensions", marker = "python_full_version < '3.12'" }, ] sdist = { url = "https://files.pythonhosted.org/packages/85/31/10ac88f3357fc276dc8a64e8880c82e80e7459326ae1d0a211b40abf6665/ipython-8.37.0.tar.gz", hash = "sha256:ca815841e1a41a1e6b73a0b08f3038af9b2252564d01fc405356d34033012216", size = 5606088, upload-time = "2025-05-31T16:39:09.613Z" } wheels = [ @@ -1203,10 +783,8 @@ name = "itables" version = "2.5.2" source = { registry = "https://pypi.org/simple" } dependencies = [ - { name = "ipython", version = "8.18.1", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version < '3.10'" }, - { name = "ipython", version = "8.37.0", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version >= '3.10'" }, - { name = "numpy", version = "2.0.2", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version < '3.10'" }, - { name = "numpy", version = "2.1.3", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version >= '3.10'" }, + { name = "ipython" }, + { name = "numpy" }, { name = "pandas" }, ] sdist = { url = "https://files.pythonhosted.org/packages/02/a2/4652db589b5767ead6d1dd8016e94e6adc5ec9e9552ccd17cf1886900b04/itables-2.5.2.tar.gz", hash = "sha256:ec34bbacfbf4305570ea75b36970de442f924126f3701c323a5a46018de84c8a", size = 2356416, upload-time = "2025-09-02T20:14:41.722Z" } @@ -1232,16 +810,6 @@ version = "1.2.1" source = { registry = "https://pypi.org/simple" } sdist = { url = "https://files.pythonhosted.org/packages/0b/14/fc5bdb637996df181e5c4fa3b15dcc27d33215e6c41753564ae453bdb40f/jellyfish-1.2.1.tar.gz", hash = "sha256:72d2fda61b23babe862018729be73c8b0dc12e3e6601f36f6e65d905e249f4db", size = 364417, upload-time = "2025-10-11T19:36:37.219Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/3d/3f/b1734e45d4fe1620916616829a87486b26b7455352a23b9939110a26dbde/jellyfish-1.2.1-cp310-cp310-macosx_10_12_x86_64.whl", hash = "sha256:b35d4b5b688f759ffd075190a9850b04671bad14c5b37124eb43e99306ec16ea", size = 325627, upload-time = "2025-10-11T19:34:53.199Z" }, - { url = "https://files.pythonhosted.org/packages/c5/a3/eb770060544a553281654aa6b03f379231c1ef677e6b9a62735c4a9b3c69/jellyfish-1.2.1-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:b37b76ea338c4a473c34a9b9e1e033a78aafb9040a8c0eea579fc5805d8e4b46", size = 320325, upload-time = "2025-10-11T19:34:54.914Z" }, - { url = "https://files.pythonhosted.org/packages/4b/81/461c20d8e60801a4da33c81f970991910db0dc398e7ddfad9a25bf0da764/jellyfish-1.2.1-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:137cfcc26396d0f2e1265ac61f800bb921921ea722a43dd897e58190f767c474", size = 353308, upload-time = "2025-10-11T19:34:56.195Z" }, - { url = "https://files.pythonhosted.org/packages/bc/ca/b53c9869d4cc8183f0d09b073459937d8f39df2b02d1ab05b25c6f4cfc14/jellyfish-1.2.1-cp310-cp310-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:ab1bfea271ce4bda09d975080d5465cf5a8b127e7c0ea61ea3f972417a7a2193", size = 362931, upload-time = "2025-10-11T19:34:57.527Z" }, - { url = "https://files.pythonhosted.org/packages/b3/5f/a08be31c51249d13b324be4c443acd18e172f9f511b70d63c791209ec9f9/jellyfish-1.2.1-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:2348f698f9c1d72023afc8d39939045421a01da9b7e3078e3029227e35f28419", size = 360672, upload-time = "2025-10-11T19:34:58.821Z" }, - { url = "https://files.pythonhosted.org/packages/ec/7e/a04b682872295507437f96567cfea5cc71199cbbd8a541c8310b9b48126e/jellyfish-1.2.1-cp310-cp310-musllinux_1_1_aarch64.whl", hash = "sha256:4072e21ad4036af41bd57b447b1dda64fe60aa679cfa8854ba0a0338152439f1", size = 533943, upload-time = "2025-10-11T19:35:00.839Z" }, - { url = "https://files.pythonhosted.org/packages/70/8d/a4f6e6b8fc626e4a42ec190428efe74806290bf7fd9d61a864e68b8a9f74/jellyfish-1.2.1-cp310-cp310-musllinux_1_1_i686.whl", hash = "sha256:cf6cd68921f2bacc547ba1cf64ad0e76bc1727f3bab13bba2e5f5869aba038b1", size = 554469, upload-time = "2025-10-11T19:35:02.152Z" }, - { url = "https://files.pythonhosted.org/packages/f9/80/66ad92b292e0a8096ebd2c2d6363219cf23df02e90a275b6d3b9df740d29/jellyfish-1.2.1-cp310-cp310-musllinux_1_1_x86_64.whl", hash = "sha256:01647c12261bc1f7b102e918e7665497176d87f6fc96271439c8855872bc2606", size = 523928, upload-time = "2025-10-11T19:35:03.47Z" }, - { url = "https://files.pythonhosted.org/packages/9f/de/fab776c8c4eb997d147bc6a1374d14e7229f42915ccd3471619e761020f2/jellyfish-1.2.1-cp310-cp310-win32.whl", hash = "sha256:ddf05ea471da2808d77ecfa425d8884124b4754f4d483afa7703b6655530cf5c", size = 209286, upload-time = "2025-10-11T19:35:04.438Z" }, - { url = "https://files.pythonhosted.org/packages/f3/95/3257c5483c9a31819aad394db64451b81937f7749aed98b26a51928da2cb/jellyfish-1.2.1-cp310-cp310-win_amd64.whl", hash = "sha256:e4a210a960f3917da757b0581750b6e0a8db9acef68dafbc1b6e2ae39e847ba8", size = 213572, upload-time = "2025-10-11T19:35:05.605Z" }, { url = "https://files.pythonhosted.org/packages/ee/67/5d5ec4004d92573cbccd33fc84d0ad61e523b29f7b17b062913b183961e1/jellyfish-1.2.1-cp311-cp311-macosx_10_12_x86_64.whl", hash = "sha256:9913789a98ccf49213fbb1dabc597847a0ec33d3b0e151689498f4b38ba9be0f", size = 325488, upload-time = "2025-10-11T19:35:06.87Z" }, { url = "https://files.pythonhosted.org/packages/83/21/6cf3add349cd0002cc586178bd8f1fd006894e5c70f959a8db5507cfe075/jellyfish-1.2.1-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:4e36d9000d4f7e1a35689a74ec7749d27a216dfa6c47cac2e5ad3de8a523bd69", size = 320226, upload-time = "2025-10-11T19:35:08.314Z" }, { url = "https://files.pythonhosted.org/packages/1c/ed/b5458b09482913caece2e9f807599318e48490b01c3c3134b636ecd7af8c/jellyfish-1.2.1-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:7853d2ed7d6929c029312ec849410f1ea7ae76ce72ad1140fb73f6e8a1e6aa4f", size = 353091, upload-time = "2025-10-11T19:35:09.395Z" }, @@ -1282,24 +850,6 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/f9/e1/fcc7c5919d871537942425f707b764af65b76c7b88377aa71083c5280e37/jellyfish-1.2.1-cp314-cp314-musllinux_1_1_x86_64.whl", hash = "sha256:29cfa8bfb72aacf2d611a3313b358ed4d4140fa3d3efcffea750c8e7f8acb1aa", size = 523057, upload-time = "2025-10-11T19:35:54.423Z" }, { url = "https://files.pythonhosted.org/packages/95/65/ee5289540b2015643493cc29b50350dbe63ca1977a902de5295a4df8c25a/jellyfish-1.2.1-cp314-cp314-win32.whl", hash = "sha256:f121218dc33fb318c34ddd889dc7362606ce1316af2bb63b73cc1df81523ca34", size = 209340, upload-time = "2025-10-11T19:35:55.69Z" }, { url = "https://files.pythonhosted.org/packages/bc/e2/fa5de38380b0f5bd531b27a78acb0dc6118dab0b21f56d36008b829aa7de/jellyfish-1.2.1-cp314-cp314-win_amd64.whl", hash = "sha256:9a73b5c6425a70ebd440579a677eb4f03b327b2f59090db34e6c937aeea5aabd", size = 213399, upload-time = "2025-10-11T19:35:56.776Z" }, - { url = "https://files.pythonhosted.org/packages/7c/0c/2255426e82d24ec16bed2d146d659c0c32717860b131749c8738fed46b1b/jellyfish-1.2.1-cp39-cp39-macosx_10_12_x86_64.whl", hash = "sha256:5335f622458aa105289a8e358bc32ecd1b9634b6ffec3e77ea3577e49c297171", size = 325549, upload-time = "2025-10-11T19:35:58.004Z" }, - { url = "https://files.pythonhosted.org/packages/07/74/42370f075b63425d461b7f9d56e2b46007e58bec869c7baa76fb311e87b2/jellyfish-1.2.1-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:6c51e565f85ce38cf9388c4f916d53888b0fa34788fcebe3aff3db24948e0960", size = 320368, upload-time = "2025-10-11T19:35:59.139Z" }, - { url = "https://files.pythonhosted.org/packages/6c/0e/6bf5a04a256e129e2c8bb3b22bf2a41126f3937c27f1c5671f7b7bc3aef4/jellyfish-1.2.1-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:14bbb30d988dec1d12183cf5d4621c908f98add2009c72a185e8c3e8d00b804f", size = 353549, upload-time = "2025-10-11T19:36:00.13Z" }, - { url = "https://files.pythonhosted.org/packages/b3/19/3a1c7f99bcfb2281460d166624876d388c452b1e78536b90c27b40444ac9/jellyfish-1.2.1-cp39-cp39-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:9930e20f0e9f65ad1d57d98290c2be3abd75812d058815605f44a56056fb9a66", size = 362965, upload-time = "2025-10-11T19:36:01.144Z" }, - { url = "https://files.pythonhosted.org/packages/3a/ff/f9cc3182e3da6797b58451a9607575c0e3707dea171142459d4b0bb8d6e1/jellyfish-1.2.1-cp39-cp39-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:0028857c5381c9d55e21cc6cb0d7f9545c3a9a7bb7dbca3960fe0a898c691ac2", size = 361231, upload-time = "2025-10-11T19:36:02.155Z" }, - { url = "https://files.pythonhosted.org/packages/13/98/8ef4064872d87162c29f9227cb55936c2bb3942ce89ad032ad2a6bc7e6bc/jellyfish-1.2.1-cp39-cp39-musllinux_1_1_aarch64.whl", hash = "sha256:56da7632e029912af25e25422fae3b6df318400297d552791f4b21da6d815ed6", size = 534251, upload-time = "2025-10-11T19:36:03.461Z" }, - { url = "https://files.pythonhosted.org/packages/2e/71/17e93464e23aa3eab9b1c781b091e40221c64f36433da393d5e285efc150/jellyfish-1.2.1-cp39-cp39-musllinux_1_1_i686.whl", hash = "sha256:a3cab91020e3ff7565e55a611ec3e3257c093ac950d55778a48bfc8c57562b6e", size = 554514, upload-time = "2025-10-11T19:36:04.585Z" }, - { url = "https://files.pythonhosted.org/packages/3e/1b/4d993f0c1d79fdf62ff76cb5b6ba8b591766ed8f03baae395bb31c534a6e/jellyfish-1.2.1-cp39-cp39-musllinux_1_1_x86_64.whl", hash = "sha256:0b21c1596ce283fd7ee954eb0eeb007d59e480364324bcd91ad55146e91f3936", size = 524005, upload-time = "2025-10-11T19:36:05.632Z" }, - { url = "https://files.pythonhosted.org/packages/94/d9/47e84d15927babd9c4b9f6635e81e085c7062467b9b1f536ed40ebf1a201/jellyfish-1.2.1-cp39-cp39-win32.whl", hash = "sha256:1098ce1f84ae3f147f0a18a6803ffb09b9c8cd5fedce42465643ca0b5c9d0224", size = 209401, upload-time = "2025-10-11T19:36:06.658Z" }, - { url = "https://files.pythonhosted.org/packages/d2/d2/c2f103e8a70d54553d7df28fc65db371d2f684cdfdc5666d7ae34246f714/jellyfish-1.2.1-cp39-cp39-win_amd64.whl", hash = "sha256:4b013876109d91fa6fc871ffa4e0dbfda11820c33dc4ad0e2967b3fc1187f804", size = 213806, upload-time = "2025-10-11T19:36:07.557Z" }, - { url = "https://files.pythonhosted.org/packages/f5/2b/ef99cc9ed0c14171b62dfb68dca7e1cecec02b4c007ea47709a3d51ffba6/jellyfish-1.2.1-pp310-pypy310_pp73-macosx_10_12_x86_64.whl", hash = "sha256:c499ea3a134130797c50e367687a6a46a12653c59af381bee92c41a5ab0bd55d", size = 327121, upload-time = "2025-10-11T19:36:09.777Z" }, - { url = "https://files.pythonhosted.org/packages/c1/c9/1159831bf5c2356affab834039cb6fce596ea74e39fc77533ea349f1eef2/jellyfish-1.2.1-pp310-pypy310_pp73-macosx_11_0_arm64.whl", hash = "sha256:91cad49a4fb731b726afc5ae385a3217a7016ed88a04da40c131cff8136a5db5", size = 321825, upload-time = "2025-10-11T19:36:10.887Z" }, - { url = "https://files.pythonhosted.org/packages/bf/66/4328b3ecfb79266b261d195b920c78975e9a0ee793c9fe1e08904e924ad6/jellyfish-1.2.1-pp310-pypy310_pp73-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:5bda2275f31a64adf3483e39f7a4e2107f7dfe3a3f85f0d2c0cb6ae5fbe4a443", size = 355093, upload-time = "2025-10-11T19:36:11.921Z" }, - { url = "https://files.pythonhosted.org/packages/bb/9d/5bbc2ec2e3fd0f93e54642aa1a0772bd909e7bb2755a6a8ad8b7d75e97b8/jellyfish-1.2.1-pp310-pypy310_pp73-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:98a133b40dc00cfda6609e1b0cb0ab0b77796fc2719aae886a12009514f73499", size = 364202, upload-time = "2025-10-11T19:36:12.867Z" }, - { url = "https://files.pythonhosted.org/packages/99/88/bec6bd9899194355f8007e376bbf0824ff6bb9dd78754a5e8b095ee00c1c/jellyfish-1.2.1-pp310-pypy310_pp73-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:baa30c7b59bd1c5e105693108a6d7a98f3e7a1a59e23e15bc5897b91fd5849f5", size = 362053, upload-time = "2025-10-11T19:36:14.086Z" }, - { url = "https://files.pythonhosted.org/packages/6a/15/ca2f05b39145460579b7bfb164d210ce30a68bc1136b74f48bae33e3442b/jellyfish-1.2.1-pp310-pypy310_pp73-musllinux_1_1_aarch64.whl", hash = "sha256:db97d873f23b0c15b4ed911ece10e5cc0bb96cdc53666d5c3788bd0af81807f1", size = 535693, upload-time = "2025-10-11T19:36:15.205Z" }, - { url = "https://files.pythonhosted.org/packages/d1/82/8889c441d768a80eeea575e471d54613ea88c579883a2c30afa04fa0987b/jellyfish-1.2.1-pp310-pypy310_pp73-musllinux_1_1_i686.whl", hash = "sha256:393f609fd6139ce782e747e22c399483ffc58341009e6a97e39ffe5f5b2c674c", size = 555789, upload-time = "2025-10-11T19:36:16.433Z" }, - { url = "https://files.pythonhosted.org/packages/af/65/abca7f780fb66840500ab7842c2285de6c09c5130a769fe3712a3a647af9/jellyfish-1.2.1-pp310-pypy310_pp73-musllinux_1_1_x86_64.whl", hash = "sha256:fb3c6e537cb4605c22895a8d4a10cdb26611ba2bbfc7f0b4c1d06bb9d8aad648", size = 525431, upload-time = "2025-10-11T19:36:17.788Z" }, { url = "https://files.pythonhosted.org/packages/96/72/e4897449abd844d501412873d1d15bd846bcc919648c0b1043e225268b21/jellyfish-1.2.1-pp311-pypy311_pp73-macosx_10_12_x86_64.whl", hash = "sha256:748dc45a0394fbe9120b8b3b9a39fab0967c7e2d6ecdd5304af018e774f80f96", size = 326967, upload-time = "2025-10-11T19:36:18.851Z" }, { url = "https://files.pythonhosted.org/packages/60/3f/c7a550abd212ae40c2a555055a3f16ba39376e486ba0189e150fb25cf6b1/jellyfish-1.2.1-pp311-pypy311_pp73-macosx_11_0_arm64.whl", hash = "sha256:13f1ac9caba22af10bfe42f674822643c0266009f882e0fe652079706dc5d13a", size = 321759, upload-time = "2025-10-11T19:36:19.882Z" }, { url = "https://files.pythonhosted.org/packages/19/58/a268365ba659f04d4db0c94325042aa9aee69c3a9a5823a5b2a2db308a5c/jellyfish-1.2.1-pp311-pypy311_pp73-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:1ffeeb6c78c45fbb6d2a22b0173fb8a6af849001d6c26fab49c525136dbd9734", size = 354847, upload-time = "2025-10-11T19:36:20.976Z" }, @@ -1308,14 +858,6 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/74/5c/82455195b77cd1996c3618bd5aa8f25a9fc254d401a3c1425fb60cf97742/jellyfish-1.2.1-pp311-pypy311_pp73-musllinux_1_1_aarch64.whl", hash = "sha256:536c80d8d4ec7f39cbb10b85d926ff96cef3cde4a83ca0991c07cd9835d5dc13", size = 535488, upload-time = "2025-10-11T19:36:24.252Z" }, { url = "https://files.pythonhosted.org/packages/74/3d/295468c5df5a8d03f522b0c21fc3e694d6be376602a6d755bf7815947522/jellyfish-1.2.1-pp311-pypy311_pp73-musllinux_1_1_i686.whl", hash = "sha256:21baa92d4a5112167721156f6d061c2ae105f2995b3a5e19cec6662928f0c439", size = 555818, upload-time = "2025-10-11T19:36:25.667Z" }, { url = "https://files.pythonhosted.org/packages/41/6d/4029265138a5a0b18e4df381560e467339bec477f4efaa7614736dbc433e/jellyfish-1.2.1-pp311-pypy311_pp73-musllinux_1_1_x86_64.whl", hash = "sha256:68ea3ddd4dae1152a7f7155ef02a7bfad919611158d71b301f9aa167685819af", size = 525259, upload-time = "2025-10-11T19:36:26.726Z" }, - { url = "https://files.pythonhosted.org/packages/3a/31/42f4f1b80f2de28ac455a04d7d4d15765003d41ff790485dd65467618973/jellyfish-1.2.1-pp39-pypy39_pp73-macosx_10_12_x86_64.whl", hash = "sha256:d7be8021658b46b22500a77f1707901bd98fc210f185c229b81c74efd3c1baf2", size = 327035, upload-time = "2025-10-11T19:36:28.134Z" }, - { url = "https://files.pythonhosted.org/packages/31/bd/e926937450a1064dd2240b14fd4f5bea2e8a4c5b4809427b4c98daa0e112/jellyfish-1.2.1-pp39-pypy39_pp73-macosx_11_0_arm64.whl", hash = "sha256:bcdcd603a7737cd3f5a2ab10ce9b49844329deb81c2daafcd8131e54fc730205", size = 321844, upload-time = "2025-10-11T19:36:29.14Z" }, - { url = "https://files.pythonhosted.org/packages/0d/65/a7bee935f8e9c7fb1968eedb39bff2ffd440ca924148b40f1cd82e8e6f1e/jellyfish-1.2.1-pp39-pypy39_pp73-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:2c28a4ae3e201e1c1b7bacacd40e2e76c4068b90c9ae3a0d525e0ac98206f1cc", size = 355124, upload-time = "2025-10-11T19:36:30.187Z" }, - { url = "https://files.pythonhosted.org/packages/5f/ad/3b3d60001ba7b168fc78c275470e7cf03387962c600437304232ead06f56/jellyfish-1.2.1-pp39-pypy39_pp73-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:bebccd0652ac1c7e438ae1f451edefde63d14b3af6f6daa30c599919dcb92886", size = 364462, upload-time = "2025-10-11T19:36:31.243Z" }, - { url = "https://files.pythonhosted.org/packages/7b/43/e63cccdbf4616e1160c2d3ca350b49e31a3c151766b6c7da1db34dee985b/jellyfish-1.2.1-pp39-pypy39_pp73-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:05be396aebe3dce7a8cb2f97727ecdf99e86457c48e97190775dce33f8b7e39d", size = 362230, upload-time = "2025-10-11T19:36:32.303Z" }, - { url = "https://files.pythonhosted.org/packages/04/49/621afee4f6908166a7d1862b6dada4f76a0efc43ab9d89799502ccac06d5/jellyfish-1.2.1-pp39-pypy39_pp73-musllinux_1_1_aarch64.whl", hash = "sha256:9d4448c874959ae012cda0f6d570ac0bd7f0fcf12007714eaebf86b86919b66f", size = 535739, upload-time = "2025-10-11T19:36:33.698Z" }, - { url = "https://files.pythonhosted.org/packages/f5/29/f913b8199b9f85e432085271e29081bf68111969a69d7c46127e38994374/jellyfish-1.2.1-pp39-pypy39_pp73-musllinux_1_1_i686.whl", hash = "sha256:4a21d7eda5e6996772055f798e3fe1de1b33b3edad7f6cf0567097a21585a812", size = 555991, upload-time = "2025-10-11T19:36:34.911Z" }, - { url = "https://files.pythonhosted.org/packages/04/be/2e3ebd88d0937d5e69e2ad09a4e31ed419211d702051a8f09cb509eac282/jellyfish-1.2.1-pp39-pypy39_pp73-musllinux_1_1_x86_64.whl", hash = "sha256:a0ef6f0ecc085c1f8fddb048f538c8bb89989e5d470eab45d4e9bd48ee73a40d", size = 525411, upload-time = "2025-10-11T19:36:36.032Z" }, ] [[package]] @@ -1371,8 +913,7 @@ name = "jupyter-book" version = "1.0.4.post1" source = { registry = "https://pypi.org/simple" } dependencies = [ - { name = "click", version = "8.1.8", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version < '3.10'" }, - { name = "click", version = "8.2.1", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version >= '3.10'" }, + { name = "click" }, { name = "jinja2" }, { name = "jsonschema" }, { name = "linkify-it-py" }, @@ -1402,8 +943,7 @@ version = "1.0.1" source = { registry = "https://pypi.org/simple" } dependencies = [ { name = "attrs" }, - { name = "click", version = "8.1.8", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version < '3.10'" }, - { name = "click", version = "8.2.1", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version >= '3.10'" }, + { name = "click" }, { name = "importlib-metadata" }, { name = "nbclient" }, { name = "nbformat" }, @@ -1421,7 +961,6 @@ name = "jupyter-client" version = "8.6.3" source = { registry = "https://pypi.org/simple" } dependencies = [ - { name = "importlib-metadata", marker = "python_full_version < '3.10'" }, { name = "jupyter-core" }, { name = "python-dateutil" }, { name = "pyzmq" }, @@ -1462,18 +1001,6 @@ version = "0.8.1" source = { registry = "https://pypi.org/simple" } sdist = { url = "https://files.pythonhosted.org/packages/56/9c/b4b0c54d84da4a94b37bd44151e46d5e583c9534c7e02250b961b1b6d8a8/librt-0.8.1.tar.gz", hash = "sha256:be46a14693955b3bd96014ccbdb8339ee8c9346fbe11c1b78901b55125f14c73", size = 177471, upload-time = "2026-02-17T16:13:06.101Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/7c/5f/63f5fa395c7a8a93558c0904ba8f1c8d1b997ca6a3de61bc7659970d66bf/librt-0.8.1-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:81fd938344fecb9373ba1b155968c8a329491d2ce38e7ddb76f30ffb938f12dc", size = 65697, upload-time = "2026-02-17T16:11:06.903Z" }, - { url = "https://files.pythonhosted.org/packages/ff/e0/0472cf37267b5920eff2f292ccfaede1886288ce35b7f3203d8de00abfe6/librt-0.8.1-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:5db05697c82b3a2ec53f6e72b2ed373132b0c2e05135f0696784e97d7f5d48e7", size = 68376, upload-time = "2026-02-17T16:11:08.395Z" }, - { url = "https://files.pythonhosted.org/packages/c8/be/8bd1359fdcd27ab897cd5963294fa4a7c83b20a8564678e4fd12157e56a5/librt-0.8.1-cp310-cp310-manylinux1_i686.manylinux_2_28_i686.manylinux_2_5_i686.whl", hash = "sha256:d56bc4011975f7460bea7b33e1ff425d2f1adf419935ff6707273c77f8a4ada6", size = 197084, upload-time = "2026-02-17T16:11:09.774Z" }, - { url = "https://files.pythonhosted.org/packages/e2/fe/163e33fdd091d0c2b102f8a60cc0a61fd730ad44e32617cd161e7cd67a01/librt-0.8.1-cp310-cp310-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:5cdc0f588ff4b663ea96c26d2a230c525c6fc62b28314edaaaca8ed5af931ad0", size = 207337, upload-time = "2026-02-17T16:11:11.311Z" }, - { url = "https://files.pythonhosted.org/packages/01/99/f85130582f05dcf0c8902f3d629270231d2f4afdfc567f8305a952ac7f14/librt-0.8.1-cp310-cp310-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:97c2b54ff6717a7a563b72627990bec60d8029df17df423f0ed37d56a17a176b", size = 219980, upload-time = "2026-02-17T16:11:12.499Z" }, - { url = "https://files.pythonhosted.org/packages/6f/54/cb5e4d03659e043a26c74e08206412ac9a3742f0477d96f9761a55313b5f/librt-0.8.1-cp310-cp310-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:8f1125e6bbf2f1657d9a2f3ccc4a2c9b0c8b176965bb565dd4d86be67eddb4b6", size = 212921, upload-time = "2026-02-17T16:11:14.484Z" }, - { url = "https://files.pythonhosted.org/packages/b1/81/a3a01e4240579c30f3487f6fed01eb4bc8ef0616da5b4ebac27ca19775f3/librt-0.8.1-cp310-cp310-musllinux_1_2_aarch64.whl", hash = "sha256:8f4bb453f408137d7581be309b2fbc6868a80e7ef60c88e689078ee3a296ae71", size = 221381, upload-time = "2026-02-17T16:11:17.459Z" }, - { url = "https://files.pythonhosted.org/packages/08/b0/fc2d54b4b1c6fb81e77288ff31ff25a2c1e62eaef4424a984f228839717b/librt-0.8.1-cp310-cp310-musllinux_1_2_i686.whl", hash = "sha256:c336d61d2fe74a3195edc1646d53ff1cddd3a9600b09fa6ab75e5514ba4862a7", size = 216714, upload-time = "2026-02-17T16:11:19.197Z" }, - { url = "https://files.pythonhosted.org/packages/96/96/85daa73ffbd87e1fb287d7af6553ada66bf25a2a6b0de4764344a05469f6/librt-0.8.1-cp310-cp310-musllinux_1_2_riscv64.whl", hash = "sha256:eb5656019db7c4deacf0c1a55a898c5bb8f989be904597fcb5232a2f4828fa05", size = 214777, upload-time = "2026-02-17T16:11:20.443Z" }, - { url = "https://files.pythonhosted.org/packages/12/9c/c3aa7a2360383f4bf4f04d98195f2739a579128720c603f4807f006a4225/librt-0.8.1-cp310-cp310-musllinux_1_2_x86_64.whl", hash = "sha256:c25d9e338d5bed46c1632f851babf3d13c78f49a225462017cf5e11e845c5891", size = 237398, upload-time = "2026-02-17T16:11:22.083Z" }, - { url = "https://files.pythonhosted.org/packages/61/19/d350ea89e5274665185dabc4bbb9c3536c3411f862881d316c8b8e00eb66/librt-0.8.1-cp310-cp310-win32.whl", hash = "sha256:aaab0e307e344cb28d800957ef3ec16605146ef0e59e059a60a176d19543d1b7", size = 54285, upload-time = "2026-02-17T16:11:23.27Z" }, - { url = "https://files.pythonhosted.org/packages/4f/d6/45d587d3d41c112e9543a0093d883eb57a24a03e41561c127818aa2a6bcc/librt-0.8.1-cp310-cp310-win_amd64.whl", hash = "sha256:56e04c14b696300d47b3bc5f1d10a00e86ae978886d0cee14e5714fafb5df5d2", size = 61352, upload-time = "2026-02-17T16:11:24.207Z" }, { url = "https://files.pythonhosted.org/packages/1d/01/0e748af5e4fee180cf7cd12bd12b0513ad23b045dccb2a83191bde82d168/librt-0.8.1-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:681dc2451d6d846794a828c16c22dc452d924e9f700a485b7ecb887a30aad1fd", size = 65315, upload-time = "2026-02-17T16:11:25.152Z" }, { url = "https://files.pythonhosted.org/packages/9d/4d/7184806efda571887c798d573ca4134c80ac8642dcdd32f12c31b939c595/librt-0.8.1-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:a3b4350b13cc0e6f5bec8fa7caf29a8fb8cdc051a3bae45cfbfd7ce64f009965", size = 68021, upload-time = "2026-02-17T16:11:26.129Z" }, { url = "https://files.pythonhosted.org/packages/ae/88/c3c52d2a5d5101f28d3dc89298444626e7874aa904eed498464c2af17627/librt-0.8.1-cp311-cp311-manylinux1_i686.manylinux_2_28_i686.manylinux_2_5_i686.whl", hash = "sha256:ac1e7817fd0ed3d14fd7c5df91daed84c48e4c2a11ee99c0547f9f62fdae13da", size = 194500, upload-time = "2026-02-17T16:11:27.177Z" }, @@ -1539,18 +1066,6 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/b9/eb/7697f60fbe7042ab4e88f4ee6af496b7f222fffb0a4e3593ef1f29f81652/librt-0.8.1-cp314-cp314t-win32.whl", hash = "sha256:738f08021b3142c2918c03692608baed43bc51144c29e35807682f8070ee2a3a", size = 51328, upload-time = "2026-02-17T16:12:45.148Z" }, { url = "https://files.pythonhosted.org/packages/7c/72/34bf2eb7a15414a23e5e70ecb9440c1d3179f393d9349338a91e2781c0fb/librt-0.8.1-cp314-cp314t-win_amd64.whl", hash = "sha256:89815a22daf9c51884fb5dbe4f1ef65ee6a146e0b6a8df05f753e2e4a9359bf4", size = 58722, upload-time = "2026-02-17T16:12:46.85Z" }, { url = "https://files.pythonhosted.org/packages/b2/c8/d148e041732d631fc76036f8b30fae4e77b027a1e95b7a84bb522481a940/librt-0.8.1-cp314-cp314t-win_arm64.whl", hash = "sha256:bf512a71a23504ed08103a13c941f763db13fb11177beb3d9244c98c29fb4a61", size = 48755, upload-time = "2026-02-17T16:12:47.943Z" }, - { url = "https://files.pythonhosted.org/packages/01/1f/c7d8b66a3ca3ca3ed8ded4b32c96ee58a45920ebbbaa934355c74adcc33e/librt-0.8.1-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:3dff3d3ca8db20e783b1bc7de49c0a2ab0b8387f31236d6a026597d07fcd68ac", size = 65990, upload-time = "2026-02-17T16:12:48.972Z" }, - { url = "https://files.pythonhosted.org/packages/56/be/ee9ba1730052313d08457f19beaa1b878619978863fba09b40aed5b5c123/librt-0.8.1-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:08eec3a1fc435f0d09c87b6bf1ec798986a3544f446b864e4099633a56fcd9ed", size = 68640, upload-time = "2026-02-17T16:12:50.24Z" }, - { url = "https://files.pythonhosted.org/packages/81/27/b7309298b96f7690cec3ceee38004c1a7f60fcd96d952d3ac344a1e3e8b3/librt-0.8.1-cp39-cp39-manylinux1_i686.manylinux_2_28_i686.manylinux_2_5_i686.whl", hash = "sha256:e3f0a41487fd5fad7e760b9e8a90e251e27c2816fbc2cff36a22a0e6bcbbd9dd", size = 196099, upload-time = "2026-02-17T16:12:52.788Z" }, - { url = "https://files.pythonhosted.org/packages/10/48/160a5aacdcb21824b10a52378c39e88c46a29bb31efdaf3910dd1f9b670e/librt-0.8.1-cp39-cp39-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:bacdb58d9939d95cc557b4dbaa86527c9db2ac1ed76a18bc8d26f6dc8647d851", size = 206663, upload-time = "2026-02-17T16:12:55.017Z" }, - { url = "https://files.pythonhosted.org/packages/ee/65/33dd1d8caabb7c6805d87d095b143417dc96b0277c06ffa0508361422c82/librt-0.8.1-cp39-cp39-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:b6d7ab1f01aa753188605b09a51faa44a3327400b00b8cce424c71910fc0a128", size = 219318, upload-time = "2026-02-17T16:12:56.145Z" }, - { url = "https://files.pythonhosted.org/packages/09/d4/353805aa6181c7950a2462bd6e855366eeca21a501f375228d72a51547df/librt-0.8.1-cp39-cp39-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:4998009e7cb9e896569f4be7004f09d0ed70d386fa99d42b6d363f6d200501ac", size = 212191, upload-time = "2026-02-17T16:12:57.326Z" }, - { url = "https://files.pythonhosted.org/packages/06/08/725b3f304d61eba56c713c251fb833a06d84bf93381caad5152366f5d2bb/librt-0.8.1-cp39-cp39-musllinux_1_2_aarch64.whl", hash = "sha256:2cc68eeeef5e906839c7bb0815748b5b0a974ec27125beefc0f942715785b551", size = 220672, upload-time = "2026-02-17T16:12:58.497Z" }, - { url = "https://files.pythonhosted.org/packages/0e/55/e8cdf04145872b3b97cb9b68287b22d1c08348227063f305aec11a3e6ce7/librt-0.8.1-cp39-cp39-musllinux_1_2_i686.whl", hash = "sha256:0bf69d79a23f4f40b8673a947a234baeeb133b5078b483b7297c5916539cf5d5", size = 216172, upload-time = "2026-02-17T16:12:59.751Z" }, - { url = "https://files.pythonhosted.org/packages/8f/d8/23b1c6592d2422dd6829c672f45b1f1c257f219926b0d216fedb572d0184/librt-0.8.1-cp39-cp39-musllinux_1_2_riscv64.whl", hash = "sha256:22b46eabd76c1986ee7d231b0765ad387d7673bbd996aa0d0d054b38ac65d8f6", size = 214116, upload-time = "2026-02-17T16:13:01.056Z" }, - { url = "https://files.pythonhosted.org/packages/c9/92/2b44fd3cc3313f44e43bdbb41343735b568fa675fa351642b408ee48d418/librt-0.8.1-cp39-cp39-musllinux_1_2_x86_64.whl", hash = "sha256:237796479f4d0637d6b9cbcb926ff424a97735e68ade6facf402df4ec93375ed", size = 236664, upload-time = "2026-02-17T16:13:02.314Z" }, - { url = "https://files.pythonhosted.org/packages/00/23/92313ecdab80e142d8ea10e8dfa6297694359dbaacc9e81679bdc8cbceb6/librt-0.8.1-cp39-cp39-win32.whl", hash = "sha256:4beb04b8c66c6ae62f8c1e0b2f097c1ebad9295c929a8d5286c05eae7c2fc7dc", size = 54368, upload-time = "2026-02-17T16:13:03.549Z" }, - { url = "https://files.pythonhosted.org/packages/68/36/18f6e768afad6b55a690d38427c53251b69b7ba8795512730fd2508b31a9/librt-0.8.1-cp39-cp39-win_amd64.whl", hash = "sha256:64548cde61b692dc0dc379f4b5f59a2f582c2ebe7890d09c1ae3b9e66fa015b7", size = 61507, upload-time = "2026-02-17T16:13:04.556Z" }, ] [[package]] @@ -1583,16 +1098,6 @@ version = "3.0.2" source = { registry = "https://pypi.org/simple" } sdist = { url = "https://files.pythonhosted.org/packages/b2/97/5d42485e71dfc078108a86d6de8fa46db44a1a9295e89c5d6d4a06e23a62/markupsafe-3.0.2.tar.gz", hash = "sha256:ee55d3edf80167e48ea11a923c7386f4669df67d7994554387f84e7d8b0a2bf0", size = 20537, upload-time = "2024-10-18T15:21:54.129Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/04/90/d08277ce111dd22f77149fd1a5d4653eeb3b3eaacbdfcbae5afb2600eebd/MarkupSafe-3.0.2-cp310-cp310-macosx_10_9_universal2.whl", hash = "sha256:7e94c425039cde14257288fd61dcfb01963e658efbc0ff54f5306b06054700f8", size = 14357, upload-time = "2024-10-18T15:20:51.44Z" }, - { url = "https://files.pythonhosted.org/packages/04/e1/6e2194baeae0bca1fae6629dc0cbbb968d4d941469cbab11a3872edff374/MarkupSafe-3.0.2-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:9e2d922824181480953426608b81967de705c3cef4d1af983af849d7bd619158", size = 12393, upload-time = "2024-10-18T15:20:52.426Z" }, - { url = "https://files.pythonhosted.org/packages/1d/69/35fa85a8ece0a437493dc61ce0bb6d459dcba482c34197e3efc829aa357f/MarkupSafe-3.0.2-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:38a9ef736c01fccdd6600705b09dc574584b89bea478200c5fbf112a6b0d5579", size = 21732, upload-time = "2024-10-18T15:20:53.578Z" }, - { url = "https://files.pythonhosted.org/packages/22/35/137da042dfb4720b638d2937c38a9c2df83fe32d20e8c8f3185dbfef05f7/MarkupSafe-3.0.2-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:bbcb445fa71794da8f178f0f6d66789a28d7319071af7a496d4d507ed566270d", size = 20866, upload-time = "2024-10-18T15:20:55.06Z" }, - { 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", hash = "sha256:57cb5a3cf367aeb1d316576250f65edec5bb3be939e9247ae594b4bcbc317dfb", size = 20964, upload-time = "2024-10-18T15:20:55.906Z" }, - { url = "https://files.pythonhosted.org/packages/cc/cd/07438f95f83e8bc028279909d9c9bd39e24149b0d60053a97b2bc4f8aa51/MarkupSafe-3.0.2-cp310-cp310-musllinux_1_2_aarch64.whl", hash = "sha256:3809ede931876f5b2ec92eef964286840ed3540dadf803dd570c3b7e13141a3b", size = 21977, upload-time = "2024-10-18T15:20:57.189Z" }, - { url = "https://files.pythonhosted.org/packages/29/01/84b57395b4cc062f9c4c55ce0df7d3108ca32397299d9df00fedd9117d3d/MarkupSafe-3.0.2-cp310-cp310-musllinux_1_2_i686.whl", hash = "sha256:e07c3764494e3776c602c1e78e298937c3315ccc9043ead7e685b7f2b8d47b3c", size = 21366, upload-time = "2024-10-18T15:20:58.235Z" }, - { url = "https://files.pythonhosted.org/packages/bd/6e/61ebf08d8940553afff20d1fb1ba7294b6f8d279df9fd0c0db911b4bbcfd/MarkupSafe-3.0.2-cp310-cp310-musllinux_1_2_x86_64.whl", hash = "sha256:b424c77b206d63d500bcb69fa55ed8d0e6a3774056bdc4839fc9298a7edca171", size = 21091, upload-time = "2024-10-18T15:20:59.235Z" }, - { url = "https://files.pythonhosted.org/packages/11/23/ffbf53694e8c94ebd1e7e491de185124277964344733c45481f32ede2499/MarkupSafe-3.0.2-cp310-cp310-win32.whl", hash = "sha256:fcabf5ff6eea076f859677f5f0b6b5c1a51e70a376b0579e0eadef8db48c6b50", size = 15065, upload-time = "2024-10-18T15:21:00.307Z" }, - { url = "https://files.pythonhosted.org/packages/44/06/e7175d06dd6e9172d4a69a72592cb3f7a996a9c396eee29082826449bbc3/MarkupSafe-3.0.2-cp310-cp310-win_amd64.whl", hash = "sha256:6af100e168aa82a50e186c82875a5893c5597a0c1ccdb0d8b40240b1f28b969a", size = 15514, upload-time = "2024-10-18T15:21:01.122Z" }, { url = "https://files.pythonhosted.org/packages/6b/28/bbf83e3f76936960b850435576dd5e67034e200469571be53f69174a2dfd/MarkupSafe-3.0.2-cp311-cp311-macosx_10_9_universal2.whl", hash = "sha256:9025b4018f3a1314059769c7bf15441064b2207cb3f065e6ea1e7359cb46db9d", size = 14353, upload-time = "2024-10-18T15:21:02.187Z" }, { url = "https://files.pythonhosted.org/packages/6c/30/316d194b093cde57d448a4c3209f22e3046c5bb2fb0820b118292b334be7/MarkupSafe-3.0.2-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:93335ca3812df2f366e80509ae119189886b0f3c2b81325d39efdb84a1e2ae93", size = 12392, upload-time = "2024-10-18T15:21:02.941Z" }, { url = "https://files.pythonhosted.org/packages/f2/96/9cdafba8445d3a53cae530aaf83c38ec64c4d5427d975c974084af5bc5d2/MarkupSafe-3.0.2-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:2cb8438c3cbb25e220c2ab33bb226559e7afb3baec11c4f218ffa7308603c832", size = 23984, upload-time = "2024-10-18T15:21:03.953Z" }, @@ -1633,16 +1138,6 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/0d/80/0985960e4b89922cb5a0bac0ed39c5b96cbc1a536a99f30e8c220a996ed9/MarkupSafe-3.0.2-cp313-cp313t-musllinux_1_2_x86_64.whl", hash = "sha256:131a3c7689c85f5ad20f9f6fb1b866f402c445b220c19fe4308c0b147ccd2ad9", size = 24098, upload-time = "2024-10-18T15:21:40.813Z" }, { url = "https://files.pythonhosted.org/packages/82/78/fedb03c7d5380df2427038ec8d973587e90561b2d90cd472ce9254cf348b/MarkupSafe-3.0.2-cp313-cp313t-win32.whl", hash = "sha256:ba8062ed2cf21c07a9e295d5b8a2a5ce678b913b45fdf68c32d95d6c1291e0b6", size = 15208, upload-time = "2024-10-18T15:21:41.814Z" }, { url = "https://files.pythonhosted.org/packages/4f/65/6079a46068dfceaeabb5dcad6d674f5f5c61a6fa5673746f42a9f4c233b3/MarkupSafe-3.0.2-cp313-cp313t-win_amd64.whl", hash = "sha256:e444a31f8db13eb18ada366ab3cf45fd4b31e4db1236a4448f68778c1d1a5a2f", size = 15739, upload-time = "2024-10-18T15:21:42.784Z" }, - { url = "https://files.pythonhosted.org/packages/a7/ea/9b1530c3fdeeca613faeb0fb5cbcf2389d816072fab72a71b45749ef6062/MarkupSafe-3.0.2-cp39-cp39-macosx_10_9_universal2.whl", hash = "sha256:eaa0a10b7f72326f1372a713e73c3f739b524b3af41feb43e4921cb529f5929a", size = 14344, upload-time = "2024-10-18T15:21:43.721Z" }, - { url = "https://files.pythonhosted.org/packages/4b/c2/fbdbfe48848e7112ab05e627e718e854d20192b674952d9042ebd8c9e5de/MarkupSafe-3.0.2-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:48032821bbdf20f5799ff537c7ac3d1fba0ba032cfc06194faffa8cda8b560ff", size = 12389, upload-time = "2024-10-18T15:21:44.666Z" }, - { url = "https://files.pythonhosted.org/packages/f0/25/7a7c6e4dbd4f867d95d94ca15449e91e52856f6ed1905d58ef1de5e211d0/MarkupSafe-3.0.2-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:1a9d3f5f0901fdec14d8d2f66ef7d035f2157240a433441719ac9a3fba440b13", size = 21607, upload-time = "2024-10-18T15:21:45.452Z" }, - { url = "https://files.pythonhosted.org/packages/53/8f/f339c98a178f3c1e545622206b40986a4c3307fe39f70ccd3d9df9a9e425/MarkupSafe-3.0.2-cp39-cp39-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:88b49a3b9ff31e19998750c38e030fc7bb937398b1f78cfa599aaef92d693144", size = 20728, upload-time = "2024-10-18T15:21:46.295Z" }, - { url = "https://files.pythonhosted.org/packages/1a/03/8496a1a78308456dbd50b23a385c69b41f2e9661c67ea1329849a598a8f9/MarkupSafe-3.0.2-cp39-cp39-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:cfad01eed2c2e0c01fd0ecd2ef42c492f7f93902e39a42fc9ee1692961443a29", size = 20826, upload-time = "2024-10-18T15:21:47.134Z" }, - { url = "https://files.pythonhosted.org/packages/e6/cf/0a490a4bd363048c3022f2f475c8c05582179bb179defcee4766fb3dcc18/MarkupSafe-3.0.2-cp39-cp39-musllinux_1_2_aarch64.whl", hash = "sha256:1225beacc926f536dc82e45f8a4d68502949dc67eea90eab715dea3a21c1b5f0", size = 21843, upload-time = "2024-10-18T15:21:48.334Z" }, - { url = "https://files.pythonhosted.org/packages/19/a3/34187a78613920dfd3cdf68ef6ce5e99c4f3417f035694074beb8848cd77/MarkupSafe-3.0.2-cp39-cp39-musllinux_1_2_i686.whl", hash = "sha256:3169b1eefae027567d1ce6ee7cae382c57fe26e82775f460f0b2778beaad66c0", size = 21219, upload-time = "2024-10-18T15:21:49.587Z" }, - { url = "https://files.pythonhosted.org/packages/17/d8/5811082f85bb88410ad7e452263af048d685669bbbfb7b595e8689152498/MarkupSafe-3.0.2-cp39-cp39-musllinux_1_2_x86_64.whl", hash = "sha256:eb7972a85c54febfb25b5c4b4f3af4dcc731994c7da0d8a0b4a6eb0640e1d178", size = 20946, upload-time = "2024-10-18T15:21:50.441Z" }, - { url = "https://files.pythonhosted.org/packages/7c/31/bd635fb5989440d9365c5e3c47556cfea121c7803f5034ac843e8f37c2f2/MarkupSafe-3.0.2-cp39-cp39-win32.whl", hash = "sha256:8c4e8c3ce11e1f92f6536ff07154f9d49677ebaaafc32db9db4620bc11ed480f", size = 15063, upload-time = "2024-10-18T15:21:51.385Z" }, - { url = "https://files.pythonhosted.org/packages/b3/73/085399401383ce949f727afec55ec3abd76648d04b9f22e1c0e99cb4bec3/MarkupSafe-3.0.2-cp39-cp39-win_amd64.whl", hash = "sha256:6e296a513ca3d94054c2c881cc913116e90fd030ad1c656b3869762b754f5f8a", size = 15506, upload-time = "2024-10-18T15:21:52.974Z" }, ] [[package]] @@ -1657,34 +1152,12 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/8f/8e/9ad090d3553c280a8060fbf6e24dc1c0c29704ee7d1c372f0c174aa59285/matplotlib_inline-0.1.7-py3-none-any.whl", hash = "sha256:df192d39a4ff8f21b1895d72e6a13f5fcc5099f00fa84384e0ea28c2cc0653ca", size = 9899, upload-time = "2024-04-15T13:44:43.265Z" }, ] -[[package]] -name = "mdit-py-plugins" -version = "0.4.2" -source = { registry = "https://pypi.org/simple" } -resolution-markers = [ - "python_full_version > '3.9' and python_full_version < '3.10'", - "python_full_version <= '3.9'", -] -dependencies = [ - { name = "markdown-it-py", marker = "python_full_version < '3.10'" }, -] -sdist = { url = "https://files.pythonhosted.org/packages/19/03/a2ecab526543b152300717cf232bb4bb8605b6edb946c845016fa9c9c9fd/mdit_py_plugins-0.4.2.tar.gz", hash = "sha256:5f2cd1fdb606ddf152d37ec30e46101a60512bc0e5fa1a7002c36647b09e26b5", size = 43542, upload-time = "2024-09-09T20:27:49.564Z" } -wheels = [ - { url = "https://files.pythonhosted.org/packages/a7/f7/7782a043553ee469c1ff49cfa1cdace2d6bf99a1f333cf38676b3ddf30da/mdit_py_plugins-0.4.2-py3-none-any.whl", hash = "sha256:0c673c3f889399a33b95e88d2f0d111b4447bdfea7f237dab2d488f459835636", size = 55316, upload-time = "2024-09-09T20:27:48.397Z" }, -] - [[package]] name = "mdit-py-plugins" version = "0.5.0" source = { registry = "https://pypi.org/simple" } -resolution-markers = [ - "python_full_version >= '3.13'", - "python_full_version == '3.12.*'", - "python_full_version == '3.11.*'", - "python_full_version == '3.10.*'", -] dependencies = [ - { name = "markdown-it-py", marker = "python_full_version >= '3.10'" }, + { name = "markdown-it-py" }, ] sdist = { url = "https://files.pythonhosted.org/packages/b2/fd/a756d36c0bfba5f6e39a1cdbdbfdd448dc02692467d83816dff4592a1ebc/mdit_py_plugins-0.5.0.tar.gz", hash = "sha256:f4918cb50119f50446560513a8e311d574ff6aaed72606ddae6d35716fe809c6", size = 44655, upload-time = "2025-08-11T07:25:49.083Z" } wheels = [ @@ -1705,8 +1178,7 @@ name = "microdf-python" version = "1.3.0" source = { registry = "https://pypi.org/simple" } dependencies = [ - { name = "numpy", version = "2.0.2", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version < '3.10'" }, - { name = "numpy", version = "2.1.3", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version >= '3.10'" }, + { name = "numpy" }, { name = "pandas" }, ] sdist = { url = "https://files.pythonhosted.org/packages/a3/1b/a4b6a53c8e6668c190e1928ebf9f8c67d6a4995ff4a4b0e3403ded30d4ab/microdf_python-1.3.0.tar.gz", hash = "sha256:f352fe32c25a4fd6d6188ef6aaca0214b53b29a8a7913aca6af0362dd9c922bd", size = 28141, upload-time = "2026-04-17T23:20:57.018Z" } @@ -1720,16 +1192,6 @@ version = "1.1.1" source = { registry = "https://pypi.org/simple" } sdist = { url = "https://files.pythonhosted.org/packages/45/b1/ea4f68038a18c77c9467400d166d74c4ffa536f34761f7983a104357e614/msgpack-1.1.1.tar.gz", hash = "sha256:77b79ce34a2bdab2594f490c8e80dd62a02d650b91a75159a63ec413b8d104cd", size = 173555, upload-time = "2025-06-13T06:52:51.324Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/33/52/f30da112c1dc92cf64f57d08a273ac771e7b29dea10b4b30369b2d7e8546/msgpack-1.1.1-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:353b6fc0c36fde68b661a12949d7d49f8f51ff5fa019c1e47c87c4ff34b080ed", size = 81799, upload-time = "2025-06-13T06:51:37.228Z" }, - { url = "https://files.pythonhosted.org/packages/e4/35/7bfc0def2f04ab4145f7f108e3563f9b4abae4ab0ed78a61f350518cc4d2/msgpack-1.1.1-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:79c408fcf76a958491b4e3b103d1c417044544b68e96d06432a189b43d1215c8", size = 78278, upload-time = "2025-06-13T06:51:38.534Z" }, - { url = "https://files.pythonhosted.org/packages/e8/c5/df5d6c1c39856bc55f800bf82778fd4c11370667f9b9e9d51b2f5da88f20/msgpack-1.1.1-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:78426096939c2c7482bf31ef15ca219a9e24460289c00dd0b94411040bb73ad2", size = 402805, upload-time = "2025-06-13T06:51:39.538Z" }, - { url = "https://files.pythonhosted.org/packages/20/8e/0bb8c977efecfe6ea7116e2ed73a78a8d32a947f94d272586cf02a9757db/msgpack-1.1.1-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:8b17ba27727a36cb73aabacaa44b13090feb88a01d012c0f4be70c00f75048b4", size = 408642, upload-time = "2025-06-13T06:51:41.092Z" }, - { url = "https://files.pythonhosted.org/packages/59/a1/731d52c1aeec52006be6d1f8027c49fdc2cfc3ab7cbe7c28335b2910d7b6/msgpack-1.1.1-cp310-cp310-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:7a17ac1ea6ec3c7687d70201cfda3b1e8061466f28f686c24f627cae4ea8efd0", size = 395143, upload-time = "2025-06-13T06:51:42.575Z" }, - { url = "https://files.pythonhosted.org/packages/2b/92/b42911c52cda2ba67a6418ffa7d08969edf2e760b09015593c8a8a27a97d/msgpack-1.1.1-cp310-cp310-musllinux_1_2_aarch64.whl", hash = "sha256:88d1e966c9235c1d4e2afac21ca83933ba59537e2e2727a999bf3f515ca2af26", size = 395986, upload-time = "2025-06-13T06:51:43.807Z" }, - { url = "https://files.pythonhosted.org/packages/61/dc/8ae165337e70118d4dab651b8b562dd5066dd1e6dd57b038f32ebc3e2f07/msgpack-1.1.1-cp310-cp310-musllinux_1_2_i686.whl", hash = "sha256:f6d58656842e1b2ddbe07f43f56b10a60f2ba5826164910968f5933e5178af75", size = 402682, upload-time = "2025-06-13T06:51:45.534Z" }, - { url = "https://files.pythonhosted.org/packages/58/27/555851cb98dcbd6ce041df1eacb25ac30646575e9cd125681aa2f4b1b6f1/msgpack-1.1.1-cp310-cp310-musllinux_1_2_x86_64.whl", hash = "sha256:96decdfc4adcbc087f5ea7ebdcfd3dee9a13358cae6e81d54be962efc38f6338", size = 406368, upload-time = "2025-06-13T06:51:46.97Z" }, - { url = "https://files.pythonhosted.org/packages/d4/64/39a26add4ce16f24e99eabb9005e44c663db00e3fce17d4ae1ae9d61df99/msgpack-1.1.1-cp310-cp310-win32.whl", hash = "sha256:6640fd979ca9a212e4bcdf6eb74051ade2c690b862b679bfcb60ae46e6dc4bfd", size = 65004, upload-time = "2025-06-13T06:51:48.582Z" }, - { url = "https://files.pythonhosted.org/packages/7d/18/73dfa3e9d5d7450d39debde5b0d848139f7de23bd637a4506e36c9800fd6/msgpack-1.1.1-cp310-cp310-win_amd64.whl", hash = "sha256:8b65b53204fe1bd037c40c4148d00ef918eb2108d24c9aaa20bc31f9810ce0a8", size = 71548, upload-time = "2025-06-13T06:51:49.558Z" }, { url = "https://files.pythonhosted.org/packages/7f/83/97f24bf9848af23fe2ba04380388216defc49a8af6da0c28cc636d722502/msgpack-1.1.1-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:71ef05c1726884e44f8b1d1773604ab5d4d17729d8491403a705e649116c9558", size = 82728, upload-time = "2025-06-13T06:51:50.68Z" }, { url = "https://files.pythonhosted.org/packages/aa/7f/2eaa388267a78401f6e182662b08a588ef4f3de6f0eab1ec09736a7aaa2b/msgpack-1.1.1-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:36043272c6aede309d29d56851f8841ba907a1a3d04435e43e8a19928e243c1d", size = 79279, upload-time = "2025-06-13T06:51:51.72Z" }, { url = "https://files.pythonhosted.org/packages/f8/46/31eb60f4452c96161e4dfd26dbca562b4ec68c72e4ad07d9566d7ea35e8a/msgpack-1.1.1-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:a32747b1b39c3ac27d0670122b57e6e57f28eefb725e0b625618d1b59bf9d1e0", size = 423859, upload-time = "2025-06-13T06:51:52.749Z" }, @@ -1760,100 +1222,20 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/bc/66/36c78af2efaffcc15a5a61ae0df53a1d025f2680122e2a9eb8442fed3ae4/msgpack-1.1.1-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:4147151acabb9caed4e474c3344181e91ff7a388b888f1e19ea04f7e73dc7ad5", size = 424172, upload-time = "2025-06-13T06:52:25.704Z" }, { url = "https://files.pythonhosted.org/packages/8c/87/a75eb622b555708fe0427fab96056d39d4c9892b0c784b3a721088c7ee37/msgpack-1.1.1-cp313-cp313-win32.whl", hash = "sha256:500e85823a27d6d9bba1d057c871b4210c1dd6fb01fbb764e37e4e8847376323", size = 65347, upload-time = "2025-06-13T06:52:26.846Z" }, { url = "https://files.pythonhosted.org/packages/ca/91/7dc28d5e2a11a5ad804cf2b7f7a5fcb1eb5a4966d66a5d2b41aee6376543/msgpack-1.1.1-cp313-cp313-win_amd64.whl", hash = "sha256:6d489fba546295983abd142812bda76b57e33d0b9f5d5b71c09a583285506f69", size = 72341, upload-time = "2025-06-13T06:52:27.835Z" }, - { url = "https://files.pythonhosted.org/packages/1f/bd/0792be119d7fe7dc2148689ef65c90507d82d20a204aab3b98c74a1f8684/msgpack-1.1.1-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:f5be6b6bc52fad84d010cb45433720327ce886009d862f46b26d4d154001994b", size = 81882, upload-time = "2025-06-13T06:52:39.316Z" }, - { url = "https://files.pythonhosted.org/packages/75/77/ce06c8e26a816ae8730a8e030d263c5289adcaff9f0476f9b270bdd7c5c2/msgpack-1.1.1-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:3a89cd8c087ea67e64844287ea52888239cbd2940884eafd2dcd25754fb72232", size = 78414, upload-time = "2025-06-13T06:52:40.341Z" }, - { url = "https://files.pythonhosted.org/packages/73/27/190576c497677fb4a0d05d896b24aea6cdccd910f206aaa7b511901befed/msgpack-1.1.1-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:1d75f3807a9900a7d575d8d6674a3a47e9f227e8716256f35bc6f03fc597ffbf", size = 400927, upload-time = "2025-06-13T06:52:41.399Z" }, - { url = "https://files.pythonhosted.org/packages/ed/af/6a0aa5a06762e70726ec3c10fb966600d84a7220b52635cb0ab2dc64d32f/msgpack-1.1.1-cp39-cp39-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:d182dac0221eb8faef2e6f44701812b467c02674a322c739355c39e94730cdbf", size = 405903, upload-time = "2025-06-13T06:52:42.699Z" }, - { url = "https://files.pythonhosted.org/packages/1e/80/3f3da358cecbbe8eb12360814bd1277d59d2608485934742a074d99894a9/msgpack-1.1.1-cp39-cp39-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:1b13fe0fb4aac1aa5320cd693b297fe6fdef0e7bea5518cbc2dd5299f873ae90", size = 393192, upload-time = "2025-06-13T06:52:43.986Z" }, - { url = "https://files.pythonhosted.org/packages/98/c6/3a0ec7fdebbb4f3f8f254696cd91d491c29c501dbebd86286c17e8f68cd7/msgpack-1.1.1-cp39-cp39-musllinux_1_2_aarch64.whl", hash = "sha256:435807eeb1bc791ceb3247d13c79868deb22184e1fc4224808750f0d7d1affc1", size = 393851, upload-time = "2025-06-13T06:52:45.177Z" }, - { url = "https://files.pythonhosted.org/packages/39/37/df50d5f8e68514b60fbe70f6e8337ea2b32ae2be030871bcd9d1cf7d4b62/msgpack-1.1.1-cp39-cp39-musllinux_1_2_i686.whl", hash = "sha256:4835d17af722609a45e16037bb1d4d78b7bdf19d6c0128116d178956618c4e88", size = 400292, upload-time = "2025-06-13T06:52:46.381Z" }, - { url = "https://files.pythonhosted.org/packages/fc/ec/1e067292e02d2ceb4c8cb5ba222c4f7bb28730eef5676740609dc2627e0f/msgpack-1.1.1-cp39-cp39-musllinux_1_2_x86_64.whl", hash = "sha256:a8ef6e342c137888ebbfb233e02b8fbd689bb5b5fcc59b34711ac47ebd504478", size = 401873, upload-time = "2025-06-13T06:52:47.957Z" }, - { url = "https://files.pythonhosted.org/packages/d3/31/e8c9c6b5b58d64c9efa99c8d181fcc25f38ead357b0360379fbc8a4234ad/msgpack-1.1.1-cp39-cp39-win32.whl", hash = "sha256:61abccf9de335d9efd149e2fff97ed5974f2481b3353772e8e2dd3402ba2bd57", size = 65028, upload-time = "2025-06-13T06:52:49.166Z" }, - { url = "https://files.pythonhosted.org/packages/20/d6/cd62cded572e5e25892747a5d27850170bcd03c855e9c69c538e024de6f9/msgpack-1.1.1-cp39-cp39-win_amd64.whl", hash = "sha256:40eae974c873b2992fd36424a5d9407f93e97656d999f43fca9d29f820899084", size = 71700, upload-time = "2025-06-13T06:52:50.244Z" }, -] - -[[package]] -name = "mypy" -version = "1.19.1" -source = { registry = "https://pypi.org/simple" } -resolution-markers = [ - "python_full_version > '3.9' and python_full_version < '3.10'", - "python_full_version <= '3.9'", -] -dependencies = [ - { name = "librt", marker = "python_full_version < '3.10' and platform_python_implementation != 'PyPy'" }, - { name = "mypy-extensions", marker = "python_full_version < '3.10'" }, - { name = "pathspec", marker = "python_full_version < '3.10'" }, - { name = "tomli", marker = "python_full_version < '3.10'" }, - { name = "typing-extensions", marker = "python_full_version < '3.10'" }, -] -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" version = "1.20.0" source = { registry = "https://pypi.org/simple" } -resolution-markers = [ - "python_full_version >= '3.13'", - "python_full_version == '3.12.*'", - "python_full_version == '3.11.*'", - "python_full_version == '3.10.*'", -] dependencies = [ - { name = "librt", marker = "python_full_version >= '3.10' and platform_python_implementation != 'PyPy'" }, - { name = "mypy-extensions", marker = "python_full_version >= '3.10'" }, - { name = "pathspec", marker = "python_full_version >= '3.10'" }, - { name = "tomli", marker = "python_full_version == '3.10.*'" }, - { name = "typing-extensions", marker = "python_full_version >= '3.10'" }, + { name = "librt", marker = "platform_python_implementation != 'PyPy'" }, + { name = "mypy-extensions" }, + { name = "pathspec" }, + { name = "typing-extensions" }, ] sdist = { url = "https://files.pythonhosted.org/packages/f8/5c/b0089fe7fef0a994ae5ee07029ced0526082c6cfaaa4c10d40a10e33b097/mypy-1.20.0.tar.gz", hash = "sha256:eb96c84efcc33f0b5e0e04beacf00129dd963b67226b01c00b9dfc8affb464c3", size = 3815028, upload-time = "2026-03-31T16:55:14.959Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/4d/a2/a965c8c3fcd4fa8b84ba0d46606181b0d0a1d50f274c67877f3e9ed4882c/mypy-1.20.0-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:d99f515f95fd03a90875fdb2cca12ff074aa04490db4d190905851bdf8a549a8", size = 14430138, upload-time = "2026-03-31T16:52:37.843Z" }, - { url = "https://files.pythonhosted.org/packages/53/6e/043477501deeb8eabbab7f1a2f6cac62cfb631806dc1d6862a04a7f5011b/mypy-1.20.0-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:bd0212976dc57a5bfeede7c219e7cd66568a32c05c9129686dd487c059c1b88a", size = 13311282, upload-time = "2026-03-31T16:55:11.021Z" }, - { url = "https://files.pythonhosted.org/packages/65/aa/bd89b247b83128197a214f29f0632ff3c14f54d4cd70d144d157bd7d7d6e/mypy-1.20.0-cp310-cp310-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:f8426d4d75d68714abc17a4292d922f6ba2cfb984b72c2278c437f6dae797865", size = 13750889, upload-time = "2026-03-31T16:52:02.909Z" }, - { url = "https://files.pythonhosted.org/packages/fa/9d/2860be7355c45247ccc0be1501c91176318964c2a137bd4743f58ce6200e/mypy-1.20.0-cp310-cp310-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:02cca0761c75b42a20a2757ae58713276605eb29a08dd8a6e092aa347c4115ca", size = 14619788, upload-time = "2026-03-31T16:50:48.928Z" }, - { url = "https://files.pythonhosted.org/packages/75/7f/3ef3e360c91f3de120f205c8ce405e9caf9fc52ef14b65d37073e322c114/mypy-1.20.0-cp310-cp310-musllinux_1_2_x86_64.whl", hash = "sha256:b3a49064504be59e59da664c5e149edc1f26c67c4f8e8456f6ba6aba55033018", size = 14918849, upload-time = "2026-03-31T16:51:10.478Z" }, - { url = "https://files.pythonhosted.org/packages/ae/72/af970dfe167ef788df7c5e6109d2ed0229f164432ce828bc9741a4250e64/mypy-1.20.0-cp310-cp310-win_amd64.whl", hash = "sha256:ebea00201737ad4391142808ed16e875add5c17f676e0912b387739f84991e13", size = 10822007, upload-time = "2026-03-31T16:50:25.268Z" }, - { url = "https://files.pythonhosted.org/packages/93/94/ba9065c2ebe5421619aff684b793d953e438a8bfe31a320dd6d1e0706e81/mypy-1.20.0-cp310-cp310-win_arm64.whl", hash = "sha256:e80cf77847d0d3e6e3111b7b25db32a7f8762fd4b9a3a72ce53fe16a2863b281", size = 9756158, upload-time = "2026-03-31T16:48:36.213Z" }, { url = "https://files.pythonhosted.org/packages/6e/1c/74cb1d9993236910286865679d1c616b136b2eae468493aa939431eda410/mypy-1.20.0-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:4525e7010b1b38334516181c5b81e16180b8e149e6684cee5a727c78186b4e3b", size = 14343972, upload-time = "2026-03-31T16:49:04.887Z" }, { url = "https://files.pythonhosted.org/packages/d5/0d/01399515eca280386e308cf57901e68d3a52af18691941b773b3380c1df8/mypy-1.20.0-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:a17c5d0bdcca61ce24a35beb828a2d0d323d3fcf387d7512206888c900193367", size = 13225007, upload-time = "2026-03-31T16:50:08.151Z" }, { url = "https://files.pythonhosted.org/packages/56/ac/b4ba5094fb2d7fe9d2037cd8d18bbe02bcf68fd22ab9ff013f55e57ba095/mypy-1.20.0-cp311-cp311-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:f75ff57defcd0f1d6e006d721ccdec6c88d4f6a7816eb92f1c4890d979d9ee62", size = 13663752, upload-time = "2026-03-31T16:49:26.064Z" }, @@ -1908,8 +1290,7 @@ source = { registry = "https://pypi.org/simple" } dependencies = [ { name = "importlib-metadata" }, { name = "ipykernel" }, - { name = "ipython", version = "8.18.1", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version < '3.10'" }, - { name = "ipython", version = "8.37.0", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version >= '3.10'" }, + { name = "ipython" }, { name = "jupyter-cache" }, { name = "myst-parser" }, { name = "nbclient" }, @@ -1931,8 +1312,7 @@ dependencies = [ { name = "docutils" }, { name = "jinja2" }, { name = "markdown-it-py" }, - { name = "mdit-py-plugins", version = "0.4.2", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version < '3.10'" }, - { name = "mdit-py-plugins", version = "0.5.0", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version >= '3.10'" }, + { name = "mdit-py-plugins" }, { name = "pyyaml" }, { name = "sphinx" }, ] @@ -1977,14 +1357,6 @@ version = "1.10.0" source = { registry = "https://pypi.org/simple" } sdist = { url = "https://files.pythonhosted.org/packages/dc/a0/f584c0b6b998e4981201a1383200663a725f556f439cf58d02a093cb9f91/ndindex-1.10.0.tar.gz", hash = "sha256:20e3a2f0a8ed4646abf0f13296aab0b5b9cc8c5bc182b71b5945e76eb6f558bb", size = 258688, upload-time = "2025-05-21T17:42:22.718Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/61/de/45af0f9b0abe5795228ca79577541c1c79b664996a5c9d15df21789e2ced/ndindex-1.10.0-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:3d96dc319c39dce679d85a997f4eeb439f6de73c0793956b66598954ca61365c", size = 162311, upload-time = "2025-05-21T17:40:36.873Z" }, - { url = "https://files.pythonhosted.org/packages/d9/dd/d950718536c3898580c3f903888209d75057659b862b3d8d8af17bdb4fa8/ndindex-1.10.0-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:b082de3c042b6da7ca327f17d088de3695333c30e0f9717d2ed5de5dc4d70802", size = 161621, upload-time = "2025-05-21T17:40:38.792Z" }, - { url = "https://files.pythonhosted.org/packages/bd/00/462ef86c63590e1f2e56d31ce46e9f13ae6aebd7506d33c08b927d2f1594/ndindex-1.10.0-cp310-cp310-manylinux_2_12_i686.manylinux2010_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:69cf517d138f47163d6c94cd9ccaafb91606a2aab386c05aaa0718975da09c88", size = 482241, upload-time = "2025-05-21T17:40:40.671Z" }, - { url = "https://files.pythonhosted.org/packages/e3/a6/975bfec7bec7f274853b0c33953b5f2df4ad51f62d1aab0c7142fee98261/ndindex-1.10.0-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:f9cea2a5f7a432dafadb6c5732a9af3e7139adbf9085320f284885fe5d4776e4", size = 501603, upload-time = "2025-05-21T17:40:42.394Z" }, - { url = "https://files.pythonhosted.org/packages/13/4a/8d39f8ab1d20cd246360d7af707107bc4a332c6758ea45780a5bff6ec29f/ndindex-1.10.0-cp310-cp310-musllinux_1_2_i686.whl", hash = "sha256:a3d2ea706c80e21022f6661524efb0aeed89a714a8fda4712df8d4a90ef507f5", size = 1620040, upload-time = "2025-05-21T17:40:44.638Z" }, - { url = "https://files.pythonhosted.org/packages/87/83/ba24c57073c29ba3f69c52767bec64dc818e90ac23f6ee43c98172b9f888/ndindex-1.10.0-cp310-cp310-musllinux_1_2_x86_64.whl", hash = "sha256:d5b3b8f99970ce40fbff1e55ad9ddf9ea708e82ace91271784e7ff1d08707c4c", size = 1529863, upload-time = "2025-05-21T17:40:46.886Z" }, - { url = "https://files.pythonhosted.org/packages/cd/2c/61e88acae938898994a6cfe83716db0e440f44f7b0c821a7adb2ab4cedbd/ndindex-1.10.0-cp310-cp310-win32.whl", hash = "sha256:6a5a401b867530fe4f1022cc8d578c8092cfdc726348e6d1569ec91881da365f", size = 149122, upload-time = "2025-05-21T17:40:48.921Z" }, - { url = "https://files.pythonhosted.org/packages/a9/61/2bc88b2b5f71649f9e07fcf3509ce8eb187adbb3e787e4600b28ce00139c/ndindex-1.10.0-cp310-cp310-win_amd64.whl", hash = "sha256:88504651ddcb6733ba0caf0cdfc214d8ba9f140609b69f6566ad143322ce5a96", size = 156550, upload-time = "2025-05-21T17:40:50.819Z" }, { url = "https://files.pythonhosted.org/packages/b4/1c/a53253d68bb269e5591c39b96ae2c4dd671132a82f63d70aea486f76d70c/ndindex-1.10.0-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:2e42198c8636eaf468cf28b7e1700738de37841853f5f15a0671bad4c3876a85", size = 162556, upload-time = "2025-05-21T17:40:52.668Z" }, { url = "https://files.pythonhosted.org/packages/0d/2a/4e268ff5992d4b42755ee19cf46c3e954632aadd57810db7173fe945ad47/ndindex-1.10.0-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:ec9865e787eababc9aa1be973bf8545c044e2b68297fe37adf7aeefe0ec61f59", size = 161769, upload-time = "2025-05-21T17:40:54.55Z" }, { url = "https://files.pythonhosted.org/packages/14/67/28ef988483e1ff446873150979b20fa87833c711fbe3a816e0e6a3e6e7d3/ndindex-1.10.0-cp311-cp311-manylinux_2_12_i686.manylinux2010_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:72377bc5d15229eeefa73a4370212d0bdb8992c76c2228df0771e0dcdeb5354a", size = 504542, upload-time = "2025-05-21T17:40:56.771Z" }, @@ -2015,29 +1387,11 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/19/6c/f9b449d0d9db404637d026798a208b677c04c349ab740db33ab78065603d/ndindex-1.10.0-cp313-cp313t-musllinux_1_2_x86_64.whl", hash = "sha256:cb68232e58ca6cc92ddc8cdddcff8dcdfa5de030e89de8457e5d43de77bcc331", size = 1639541, upload-time = "2025-05-21T17:41:42.33Z" }, { url = "https://files.pythonhosted.org/packages/2c/14/0bfe948a092ddba3c23f18a6f4e3fc2029adfc3e433e634410ba98b7700f/ndindex-1.10.0-cp313-cp313t-win32.whl", hash = "sha256:af8ecd5a0221482e9b467918b90e78f85241572102fdcf0a941ef087e7dcf2e4", size = 157843, upload-time = "2025-05-21T17:41:43.981Z" }, { url = "https://files.pythonhosted.org/packages/50/49/0e7d831e918db3e8819f7327e835e4b106fe91ed0c865e96fb952f936b7f/ndindex-1.10.0-cp313-cp313t-win_amd64.whl", hash = "sha256:2fb32342379547032fd25dbf5bfc7003ebc1bde582779e9a171373a738d6fb8b", size = 166116, upload-time = "2025-05-21T17:41:45.506Z" }, - { url = "https://files.pythonhosted.org/packages/77/85/66a504a430e28cf0ed12a5092253814114dae2b842ec2894e5a81ee6df45/ndindex-1.10.0-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:4542247a231a8a998636109d6241bc692b583de06afa3fc2b8fbbbd6424bb965", size = 163972, upload-time = "2025-05-21T17:41:47.653Z" }, - { url = "https://files.pythonhosted.org/packages/bb/59/ced90fe3e5d2a695310334162dec2f7bf00659a1d2c760e089cef1e58dbe/ndindex-1.10.0-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:442ee28aaaa471bcc845406d7f006715e685087217e1d58b516b8d58a8c778eb", size = 162983, upload-time = "2025-05-21T17:41:49.186Z" }, - { url = "https://files.pythonhosted.org/packages/2c/97/d7e08e27ac65a1869659985ec4e026f154d3b11e5298bdc97cdbec9b0aae/ndindex-1.10.0-cp39-cp39-manylinux_2_12_i686.manylinux2010_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:8f0274114cc986f017093d1f92e8c3c38cc4aaf7ed39facecdbb4bf636d1bc95", size = 489636, upload-time = "2025-05-21T17:41:50.766Z" }, - { url = "https://files.pythonhosted.org/packages/84/45/0beba49f64c9e06398c037b580467bf1001d186dc73de5aeafbad55e1ca7/ndindex-1.10.0-cp39-cp39-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:009dde7c7216ea784744ed1d4bc1606c39f6d1b192395377e95a657b59e524c7", size = 510063, upload-time = "2025-05-21T17:41:52.427Z" }, - { url = "https://files.pythonhosted.org/packages/d7/57/255a54840d0cac8f3e735e2ce2bba8bbe8dc33bf80b909d1cc39b9f97fce/ndindex-1.10.0-cp39-cp39-musllinux_1_2_i686.whl", hash = "sha256:660cae3a9fae4fbdb12417fa4efe1f476c37793a5230a0fb28ea9cf38ad71658", size = 1628733, upload-time = "2025-05-21T17:41:54.197Z" }, - { url = "https://files.pythonhosted.org/packages/2d/27/da2fd1a1e2b670e3fab1ab217561b3bb6fe38b6b52f0edb56762f2a80f6f/ndindex-1.10.0-cp39-cp39-musllinux_1_2_x86_64.whl", hash = "sha256:304de39ba6bbea3bf0b4069c6240c87e9697fe355128324bc74cb6e92813df58", size = 1538961, upload-time = "2025-05-21T17:41:56.326Z" }, - { url = "https://files.pythonhosted.org/packages/e7/18/eecb8a9df326b861fe021d1401026d7694f7d910c9f9baef4ae95448d3fe/ndindex-1.10.0-cp39-cp39-win32.whl", hash = "sha256:bf7aba235a994a0c488d9edfd00558a96eef3ae0f18c1504d9e50677ba82fbd5", size = 149916, upload-time = "2025-05-21T17:41:57.904Z" }, - { url = "https://files.pythonhosted.org/packages/89/de/83add3ed9a94c6e5f7401d2a3547a51ef9c89f69ff9b8fd8a8ece598886d/ndindex-1.10.0-cp39-cp39-win_amd64.whl", hash = "sha256:041be825a10e4bd7368e481a7893a96e96680a9623860e58d65c843e7734cccc", size = 157607, upload-time = "2025-05-21T17:41:59.346Z" }, - { url = "https://files.pythonhosted.org/packages/b0/61/afde1bf918386625e477a7ac0fa518ca83f9239e2675affccf8d48d05e25/ndindex-1.10.0-pp310-pypy310_pp73-macosx_10_15_x86_64.whl", hash = "sha256:1851d2d490413edc5c5734f5f74e8d3b59cfc23eae561d10bd4db6e4162dcf02", size = 146659, upload-time = "2025-05-21T17:42:00.855Z" }, - { url = "https://files.pythonhosted.org/packages/63/22/90a3e3aa613d4d7e5432e8d7cf0188049f61b34b104eef7f014b7e35a3aa/ndindex-1.10.0-pp310-pypy310_pp73-macosx_11_0_arm64.whl", hash = "sha256:490c577e6915f8d2d045239a14e70b1dfd14b703028a41f6a3713821598d0db8", size = 146160, upload-time = "2025-05-21T17:42:02.227Z" }, - { url = "https://files.pythonhosted.org/packages/80/a5/677dc41756ac9b2ac3bd0b458abda4dee0c74ee1c6560be3a1b36cc2c9d1/ndindex-1.10.0-pp310-pypy310_pp73-manylinux_2_12_i686.manylinux2010_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:21f4c61db28b7ba8dc03548a3b2c3561feb8d61f7293dfc310df52aa2676463f", size = 163067, upload-time = "2025-05-21T17:42:03.615Z" }, - { url = "https://files.pythonhosted.org/packages/01/8d/319499a3f9da41695a75243b8fd8576d42c1e382f5dc935b885f590a42be/ndindex-1.10.0-pp310-pypy310_pp73-manylinux_2_12_x86_64.manylinux2010_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:afd41c7cce386bc21a38a2153427ce47f92d6bdb097dc3c5c42fa24e75090c8f", size = 160109, upload-time = "2025-05-21T17:42:05.137Z" }, - { url = "https://files.pythonhosted.org/packages/7c/66/a6721aac78028ee1dd35106a20a2f5c940f17587bc8c8fb9d98040eeddec/ndindex-1.10.0-pp310-pypy310_pp73-win_amd64.whl", hash = "sha256:1ba5f6d09ad320e0045ea39d7efd66a21d73cd4875d114be08e7ba6204a8feb7", size = 148094, upload-time = "2025-05-21T17:42:06.563Z" }, { url = "https://files.pythonhosted.org/packages/c3/61/1333424bdfcebdcea63f5ed86ac98dccaf07ebb7e1463ca845a06e321d91/ndindex-1.10.0-pp311-pypy311_pp73-macosx_10_15_x86_64.whl", hash = "sha256:aa17ea725f85af9285b298f72ccc8012949c0916d4426b0215d1c556dd995246", size = 146929, upload-time = "2025-05-21T17:42:08.04Z" }, { url = "https://files.pythonhosted.org/packages/eb/7c/0813615d958ec78c521b9c09518b1f49ec553a0bec0646b5f4ebbf33bdcb/ndindex-1.10.0-pp311-pypy311_pp73-macosx_11_0_arm64.whl", hash = "sha256:219fdef9d6a557913fd92418275088b46c727944356f3fe59f4f72d62efd6f3d", size = 146417, upload-time = "2025-05-21T17:42:09.534Z" }, { url = "https://files.pythonhosted.org/packages/d8/a1/b340a47409253f05c78d400f98b43477549ad1a1f7a5358acb784c79ed48/ndindex-1.10.0-pp311-pypy311_pp73-manylinux_2_12_i686.manylinux2010_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:f1962137fcb69c00e2db42d5d045f9b7413fc37f44b143e7ae4a8c2c68ba3832", size = 163867, upload-time = "2025-05-21T17:42:10.994Z" }, { url = "https://files.pythonhosted.org/packages/02/24/e5192ffb87070e9ff2328d715e5aa3a7f6b673e86c1ee8f48136815564e1/ndindex-1.10.0-pp311-pypy311_pp73-manylinux_2_12_x86_64.manylinux2010_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:18c9c8271926fb16c59e827b61bb77f45ee31a824eaa50b386edcd77a6a7c9a3", size = 160644, upload-time = "2025-05-21T17:42:12.415Z" }, { url = "https://files.pythonhosted.org/packages/09/c5/b894cc961460e608b869d91164e9f825e3bb0579defb37c0eea61dce584e/ndindex-1.10.0-pp311-pypy311_pp73-win_amd64.whl", hash = "sha256:76e4fb082c83ccbc67c7a64b80e33bc5dfe9379f30c3b40a865914ae79947071", size = 147721, upload-time = "2025-05-21T17:42:13.825Z" }, - { url = "https://files.pythonhosted.org/packages/09/90/0a8e43e9ade1ff08dfe60ffe8bbc7751dbb6d19ffb45717b126445aa4ac7/ndindex-1.10.0-pp39-pypy39_pp73-macosx_10_15_x86_64.whl", hash = "sha256:074be9fc105e87f0eab6b464e78ecea6ffbb78d8bee07c5bbc2476f7900796ce", size = 146460, upload-time = "2025-05-21T17:42:15.269Z" }, - { url = "https://files.pythonhosted.org/packages/70/ce/8796f89989ad6fc3603991bb20e55f28a9279ea905a9d63c127a0440df7c/ndindex-1.10.0-pp39-pypy39_pp73-macosx_11_0_arm64.whl", hash = "sha256:058002945c1e956761d9a9ae620a1ae1c01b613fc9808d09e6f65efd977e169d", size = 145977, upload-time = "2025-05-21T17:42:16.771Z" }, - { url = "https://files.pythonhosted.org/packages/19/5a/67d0e21d21ccf19d396cb84dbf65b089f7076406400281e0b0bfc269602a/ndindex-1.10.0-pp39-pypy39_pp73-manylinux_2_12_i686.manylinux2010_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:004cfdde6106891e63f1a22e5d53d2884c277fd749839c6869e0bfffe5be2ed0", size = 162824, upload-time = "2025-05-21T17:42:18.214Z" }, - { url = "https://files.pythonhosted.org/packages/bc/ac/72dbdbedce254317e83bcaec269f166450cf10cf5130296c0a7ca65a1bb9/ndindex-1.10.0-pp39-pypy39_pp73-manylinux_2_12_x86_64.manylinux2010_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:a0a9bbd54500cebe3d481611acd420a46099ce65181b81e8347e11a6bac68445", size = 159876, upload-time = "2025-05-21T17:42:19.706Z" }, - { url = "https://files.pythonhosted.org/packages/d4/84/5d74108950086ea209672dff368c42bd47e9d2d4890438e763a906a4a401/ndindex-1.10.0-pp39-pypy39_pp73-win_amd64.whl", hash = "sha256:367a2af7fbb84395a9d66f92dd0c4287b86e7b2c60cb96505c13ff36571bc9af", size = 147961, upload-time = "2025-05-21T17:42:21.2Z" }, ] [[package]] @@ -2049,116 +1403,24 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/a0/c4/c2971a3ba4c6103a3d10c4b0f24f461ddc027f0f09763220cf35ca1401b3/nest_asyncio-1.6.0-py3-none-any.whl", hash = "sha256:87af6efd6b5e897c81050477ef65c62e2b2f35d51703cae01aff2905b1852e1c", size = 5195, upload-time = "2024-01-21T14:25:17.223Z" }, ] -[[package]] -name = "networkx" -version = "3.2.1" -source = { registry = "https://pypi.org/simple" } -resolution-markers = [ - "python_full_version > '3.9' and python_full_version < '3.10'", - "python_full_version <= '3.9'", -] -sdist = { url = "https://files.pythonhosted.org/packages/c4/80/a84676339aaae2f1cfdf9f418701dd634aef9cc76f708ef55c36ff39c3ca/networkx-3.2.1.tar.gz", hash = "sha256:9f1bb5cf3409bf324e0a722c20bdb4c20ee39bf1c30ce8ae499c8502b0b5e0c6", size = 2073928, upload-time = "2023-10-28T08:41:39.364Z" } -wheels = [ - { url = "https://files.pythonhosted.org/packages/d5/f0/8fbc882ca80cf077f1b246c0e3c3465f7f415439bdea6b899f6b19f61f70/networkx-3.2.1-py3-none-any.whl", hash = "sha256:f18c69adc97877c42332c170849c96cefa91881c99a7cb3e95b7c659ebdc1ec2", size = 1647772, upload-time = "2023-10-28T08:41:36.945Z" }, -] - -[[package]] -name = "networkx" -version = "3.4.2" -source = { registry = "https://pypi.org/simple" } -resolution-markers = [ - "python_full_version == '3.10.*'", -] -sdist = { url = "https://files.pythonhosted.org/packages/fd/1d/06475e1cd5264c0b870ea2cc6fdb3e37177c1e565c43f56ff17a10e3937f/networkx-3.4.2.tar.gz", hash = "sha256:307c3669428c5362aab27c8a1260aa8f47c4e91d3891f48be0141738d8d053e1", size = 2151368, upload-time = "2024-10-21T12:39:38.695Z" } -wheels = [ - { url = "https://files.pythonhosted.org/packages/b9/54/dd730b32ea14ea797530a4479b2ed46a6fb250f682a9cfb997e968bf0261/networkx-3.4.2-py3-none-any.whl", hash = "sha256:df5d4365b724cf81b8c6a7312509d0c22386097011ad1abe274afd5e9d3bbc5f", size = 1723263, upload-time = "2024-10-21T12:39:36.247Z" }, -] - [[package]] name = "networkx" version = "3.5" 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/6c/4f/ccdb8ad3a38e583f214547fd2f7ff1fc160c43a75af88e6aec213404b96a/networkx-3.5.tar.gz", hash = "sha256:d4c6f9cf81f52d69230866796b82afbccdec3db7ae4fbd1b65ea750feed50037", size = 2471065, upload-time = "2025-05-29T11:35:07.804Z" } wheels = [ { url = "https://files.pythonhosted.org/packages/eb/8d/776adee7bbf76365fdd7f2552710282c79a4ead5d2a46408c9043a2b70ba/networkx-3.5-py3-none-any.whl", hash = "sha256:0030d386a9a06dee3565298b4a734b68589749a544acbb6c412dc9e2489ec6ec", size = 2034406, upload-time = "2025-05-29T11:35:04.961Z" }, ] -[[package]] -name = "numexpr" -version = "2.10.2" -source = { registry = "https://pypi.org/simple" } -resolution-markers = [ - "python_full_version > '3.9' and python_full_version < '3.10'", - "python_full_version <= '3.9'", -] -dependencies = [ - { name = "numpy", version = "2.0.2", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version < '3.10'" }, -] -sdist = { url = "https://files.pythonhosted.org/packages/21/67/c7415cf04ebe418193cfd6595ae03e3a64d76dac7b9c010098b39cc7992e/numexpr-2.10.2.tar.gz", hash = "sha256:b0aff6b48ebc99d2f54f27b5f73a58cb92fde650aeff1b397c71c8788b4fff1a", size = 106787, upload-time = "2024-11-23T13:34:23.127Z" } -wheels = [ - { url = "https://files.pythonhosted.org/packages/fd/dc/bd84219318826d138b7e729ac3ffce3c706ab9d810ce74326a55c7252dd1/numexpr-2.10.2-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:b5b0e82d2109c1d9e63fcd5ea177d80a11b881157ab61178ddbdebd4c561ea46", size = 145011, upload-time = "2024-11-23T13:33:24.846Z" }, - { url = "https://files.pythonhosted.org/packages/31/6a/b1f08141283327478a57490c0ab3f26a634d4741ff33b9e22f760a7cedb0/numexpr-2.10.2-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:3fc2b8035a0c2cdc352e58c3875cb668836018065cbf5752cb531015d9a568d8", size = 134777, upload-time = "2024-11-23T13:33:26.693Z" }, - { url = "https://files.pythonhosted.org/packages/7c/d6/6641864b0446ce472330de7644c78f90bd7e55d902046b44161f92721279/numexpr-2.10.2-cp310-cp310-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:0db5ff5183935d1612653559c319922143e8fa3019007696571b13135f216458", size = 408893, upload-time = "2024-11-23T13:33:28.44Z" }, - { url = "https://files.pythonhosted.org/packages/25/ab/cb5809cb1f66431632d63dc028c58cb91492725c74dddc4b97ba62e88a92/numexpr-2.10.2-cp310-cp310-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:15f59655458056fdb3a621b1bb8e071581ccf7e823916c7568bb7c9a3e393025", size = 397305, upload-time = "2024-11-23T13:33:30.181Z" }, - { url = "https://files.pythonhosted.org/packages/9c/a0/29bcb31a9debb743e3dc46bacd55f4f6ee6a77d95eda5c8dca19a29c0627/numexpr-2.10.2-cp310-cp310-musllinux_1_2_x86_64.whl", hash = "sha256:ce8cccf944339051e44a49a124a06287fe3066d0acbff33d1aa5aee10a96abb7", size = 1378789, upload-time = "2024-11-23T13:33:32.263Z" }, - { url = "https://files.pythonhosted.org/packages/cc/72/415262a7bdda706c41bf8254311a5ca13d3b8532341ab478be4583d7061a/numexpr-2.10.2-cp310-cp310-win32.whl", hash = "sha256:ba85371c9a8d03e115f4dfb6d25dfbce05387002b9bc85016af939a1da9624f0", size = 151935, upload-time = "2024-11-23T13:33:33.653Z" }, - { url = "https://files.pythonhosted.org/packages/71/fa/0124f0c2a502a0bac4553c8a171c551f154cf80a83a15e40d30c43e48a7e/numexpr-2.10.2-cp310-cp310-win_amd64.whl", hash = "sha256:deb64235af9eeba59fcefa67e82fa80cfc0662e1b0aa373b7118a28da124d51d", size = 144961, upload-time = "2024-11-23T13:33:34.883Z" }, - { url = "https://files.pythonhosted.org/packages/de/b7/f25d6166f92ef23737c1c90416144492a664f0a56510d90f7c6577c2cd14/numexpr-2.10.2-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:6b360eb8d392483410fe6a3d5a7144afa298c9a0aa3e9fe193e89590b47dd477", size = 145055, upload-time = "2024-11-23T13:33:36.154Z" }, - { url = "https://files.pythonhosted.org/packages/66/64/428361ea6415826332f38ef2dd5c3abf4e7e601f033bfc9be68b680cb765/numexpr-2.10.2-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:d9a42f5c24880350d88933c4efee91b857c378aaea7e8b86221fff569069841e", size = 134743, upload-time = "2024-11-23T13:33:37.273Z" }, - { url = "https://files.pythonhosted.org/packages/3f/fb/639ec91d2ea7b4a5d66e26e8ef8e06b020c8e9b9ebaf3bab7b0a9bee472e/numexpr-2.10.2-cp311-cp311-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:83fcb11988b57cc25b028a36d285287d706d1f536ebf2662ea30bd990e0de8b9", size = 410397, upload-time = "2024-11-23T13:33:38.474Z" }, - { url = "https://files.pythonhosted.org/packages/89/5a/0f5c5b8a3a6d34eeecb30d0e2f722d50b9b38c0e175937e7c6268ffab997/numexpr-2.10.2-cp311-cp311-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:4213a92efa9770bc28e3792134e27c7e5c7e97068bdfb8ba395baebbd12f991b", size = 398902, upload-time = "2024-11-23T13:33:39.802Z" }, - { url = "https://files.pythonhosted.org/packages/a2/d5/ec734e735eba5a753efed5be3707ee7447ebd371772f8081b65a4153fb97/numexpr-2.10.2-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:ebdbef5763ca057eea0c2b5698e4439d084a0505d9d6e94f4804f26e8890c45e", size = 1380354, upload-time = "2024-11-23T13:33:41.68Z" }, - { url = "https://files.pythonhosted.org/packages/30/51/406e572531d817480bd612ee08239a36ee82865fea02fce569f15631f4ee/numexpr-2.10.2-cp311-cp311-win32.whl", hash = "sha256:3bf01ec502d89944e49e9c1b5cc7c7085be8ca2eb9dd46a0eafd218afbdbd5f5", size = 151938, upload-time = "2024-11-23T13:33:43.998Z" }, - { url = "https://files.pythonhosted.org/packages/04/32/5882ed1dbd96234f327a73316a481add151ff827cfaf2ea24fb4d5ad04db/numexpr-2.10.2-cp311-cp311-win_amd64.whl", hash = "sha256:e2d0ae24b0728e4bc3f1d3f33310340d67321d36d6043f7ce26897f4f1042db0", size = 144961, upload-time = "2024-11-23T13:33:45.329Z" }, - { url = "https://files.pythonhosted.org/packages/2b/96/d5053dea06d8298ae8052b4b049cbf8ef74998e28d57166cc27b8ae909e2/numexpr-2.10.2-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:b5323a46e75832334f1af86da1ef6ff0add00fbacdd266250be872b438bdf2be", size = 145029, upload-time = "2024-11-23T13:33:46.892Z" }, - { url = "https://files.pythonhosted.org/packages/3e/3c/fcd5a812ed5dda757b2d9ef2764a3e1cca6f6d1f02dbf113dc23a2c7702a/numexpr-2.10.2-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:a42963bd4c62d8afa4f51e7974debfa39a048383f653544ab54f50a2f7ec6c42", size = 134851, upload-time = "2024-11-23T13:33:47.986Z" }, - { url = "https://files.pythonhosted.org/packages/0a/52/0ed3b306d8c9944129bce97fec73a2caff13adbd7e1df148d546d7eb2d4d/numexpr-2.10.2-cp312-cp312-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:5191ba8f2975cb9703afc04ae845a929e193498c0e8bcd408ecb147b35978470", size = 411837, upload-time = "2024-11-23T13:33:49.223Z" }, - { url = "https://files.pythonhosted.org/packages/7d/9c/6b671dd3fb67d7e7da93cb76b7c5277743f310a216b7856bb18776bb3371/numexpr-2.10.2-cp312-cp312-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:97298b14f0105a794bea06fd9fbc5c423bd3ff4d88cbc618860b83eb7a436ad6", size = 400577, upload-time = "2024-11-23T13:33:50.559Z" }, - { url = "https://files.pythonhosted.org/packages/ea/4d/a167d1a215fe10ce58c45109f2869fd13aa0eef66f7e8c69af68be45d436/numexpr-2.10.2-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:f9d7805ccb6be2d3b0f7f6fad3707a09ac537811e8e9964f4074d28cb35543db", size = 1381735, upload-time = "2024-11-23T13:33:51.918Z" }, - { url = "https://files.pythonhosted.org/packages/c1/d4/17e4434f989e4917d31cbd88a043e1c9c16958149cf43fa622987111392b/numexpr-2.10.2-cp312-cp312-win32.whl", hash = "sha256:cb845b2d4f9f8ef0eb1c9884f2b64780a85d3b5ae4eeb26ae2b0019f489cd35e", size = 152102, upload-time = "2024-11-23T13:33:53.93Z" }, - { url = "https://files.pythonhosted.org/packages/b8/25/9ae599994076ef2a42d35ff6b0430da002647f212567851336a6c7b132d6/numexpr-2.10.2-cp312-cp312-win_amd64.whl", hash = "sha256:57b59cbb5dcce4edf09cd6ce0b57ff60312479930099ca8d944c2fac896a1ead", size = 145061, upload-time = "2024-11-23T13:33:55.161Z" }, - { url = "https://files.pythonhosted.org/packages/8c/cb/2ea1848c46e4d75073c038dd75628d1aa442975303264ed230bf90f74f44/numexpr-2.10.2-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:a37d6a51ec328c561b2ca8a2bef07025642eca995b8553a5267d0018c732976d", size = 145035, upload-time = "2024-11-23T13:33:56.778Z" }, - { url = "https://files.pythonhosted.org/packages/ec/cf/bb2bcd81d6f3243590e19ac3e7795a1a370f3ebcd8ecec1f46dcd5333f37/numexpr-2.10.2-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:81d1dde7dd6166d8ff5727bb46ab42a6b0048db0e97ceb84a121334a404a800f", size = 134858, upload-time = "2024-11-23T13:33:57.953Z" }, - { url = "https://files.pythonhosted.org/packages/48/9b/c9128ffb453205c2a4c84a3abed35447c7591c2c2812e77e34fd238cb2bb/numexpr-2.10.2-cp313-cp313-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:5b3f814437d5a10797f8d89d2037cca2c9d9fa578520fc911f894edafed6ea3e", size = 415517, upload-time = "2024-11-23T13:33:59.163Z" }, - { url = "https://files.pythonhosted.org/packages/7e/b0/64c04c9f8b4a563218d00daa1ec4563364961b79025162c5276ab2c7c407/numexpr-2.10.2-cp313-cp313-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:9309f2e43fe6e4560699ef5c27d7a848b3ff38549b6b57194207cf0e88900527", size = 403846, upload-time = "2024-11-23T13:34:01.006Z" }, - { url = "https://files.pythonhosted.org/packages/80/35/60e9041fd709fe98dd3109d73a03cdffaeb6ee2089179155f5c3754e9934/numexpr-2.10.2-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:ebb73b93f5c4d6994f357fa5a47a9f7a5485577e633b3c46a603cb01445bbb19", size = 1381659, upload-time = "2024-11-23T13:34:02.979Z" }, - { url = "https://files.pythonhosted.org/packages/bd/5a/955bf5b5cf8f3de7b044a999e36327e14191fa073ed0e329456ed0f8161d/numexpr-2.10.2-cp313-cp313-win32.whl", hash = "sha256:ec04c9a3c050c175348801e27c18c68d28673b7bfb865ef88ce333be523bbc01", size = 152105, upload-time = "2024-11-23T13:34:04.374Z" }, - { url = "https://files.pythonhosted.org/packages/be/7a/8ce360a1848bb5bcc30a414493371678f43790ece397f8652d5f65757e57/numexpr-2.10.2-cp313-cp313-win_amd64.whl", hash = "sha256:d7a3fc83c959288544db3adc70612475d8ad53a66c69198105c74036182d10dd", size = 145060, upload-time = "2024-11-23T13:34:06.112Z" }, - { url = "https://files.pythonhosted.org/packages/41/6a/06a225ac970c5921f41bc069a30438ff64fd79ef5e828f5ec2d4f6658307/numexpr-2.10.2-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:0495f8111c3633e265248709b8b3b521bbfa646ba384909edd10e2b9a588a83a", size = 145100, upload-time = "2024-11-23T13:33:12.013Z" }, - { url = "https://files.pythonhosted.org/packages/bb/c5/9ecfa0da1d93d57e3f447d10da8cf6d695c93131cec085625e5092b37631/numexpr-2.10.2-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:2aa05ac71bee3b1253e73173c4d7fa96a09a18970c0226f1c2c07a71ffe988dc", size = 134839, upload-time = "2024-11-23T13:33:13.608Z" }, - { url = "https://files.pythonhosted.org/packages/f5/30/f1a48c485183da517bd28e0df6fee337d12bbb0cd2d6bf13f8f5695afd37/numexpr-2.10.2-cp39-cp39-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:c3a23c3002ab330056fbdd2785871937a6f2f2fa85d06c8d0ff74ea8418119d1", size = 408149, upload-time = "2024-11-23T13:33:15.549Z" }, - { url = "https://files.pythonhosted.org/packages/ed/f2/009d9dd8cd22f253fd6ead4165f81fafbe22489c1cfea612e18aa3dcb0fa/numexpr-2.10.2-cp39-cp39-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:a018a7d81326f4c73d8b5aee61794d7d8514512f43957c0db61eb2a8a86848c7", size = 396740, upload-time = "2024-11-23T13:33:17.396Z" }, - { url = "https://files.pythonhosted.org/packages/47/90/e3f12670b3cca9bed85096671265e0f65cde6cf4646baadd4ee9c33944a8/numexpr-2.10.2-cp39-cp39-musllinux_1_2_x86_64.whl", hash = "sha256:037859b17a0abe2b489d4c2cfdadd2bf458ec80dd83f338ea5544c7987e06b85", size = 1377883, upload-time = "2024-11-23T13:33:20.385Z" }, - { url = "https://files.pythonhosted.org/packages/06/1d/068c09a3c013c1178495f73a21ebd6ee25b9f0fc4202cea38b7a826c43a2/numexpr-2.10.2-cp39-cp39-win32.whl", hash = "sha256:eb278ccda6f893a312aa0452701bb17d098b7b14eb7c9381517d509cce0a39a3", size = 151878, upload-time = "2024-11-23T13:33:21.67Z" }, - { url = "https://files.pythonhosted.org/packages/70/81/affb9ff26e8accb210fe5585b095bd6872f5614d18b76cd53888e955ed9a/numexpr-2.10.2-cp39-cp39-win_amd64.whl", hash = "sha256:734b64c6d6a597601ce9d0ef7b666e678ec015b446f1d1412c23903c021436c3", size = 144960, upload-time = "2024-11-23T13:33:23.617Z" }, -] - [[package]] name = "numexpr" version = "2.11.0" source = { registry = "https://pypi.org/simple" } -resolution-markers = [ - "python_full_version >= '3.13'", - "python_full_version == '3.12.*'", - "python_full_version == '3.11.*'", - "python_full_version == '3.10.*'", -] dependencies = [ - { name = "numpy", version = "2.1.3", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version >= '3.10'" }, + { name = "numpy" }, ] sdist = { url = "https://files.pythonhosted.org/packages/d2/8f/2cc977e91adbfbcdb6b49fdb9147e1d1c7566eb2c0c1e737e9a47020b5ca/numexpr-2.11.0.tar.gz", hash = "sha256:75b2c01a4eda2e7c357bc67a3f5c3dd76506c15b5fd4dc42845ef2e182181bad", size = 108960, upload-time = "2025-06-09T11:05:56.79Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/aa/3a/99d5c9fb7f1cbb465798b79b9fd6d5df5ab10fee0d499c2b72a76634c80e/numexpr-2.11.0-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:7f471fd055a9e13cf5f4337ee12379b30b4dcda1ae0d85018d4649e841578c02", size = 147492, upload-time = "2025-06-09T11:04:59.605Z" }, - { url = "https://files.pythonhosted.org/packages/f4/32/914b8bb3d9a40e27ee56bfa915dcdfd60a460a6a9006bab80aa25df91c91/numexpr-2.11.0-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:6e68a9800a3fa37c438b73a669f507c4973801a456a864ac56b62c3bd63d08af", size = 136741, upload-time = "2025-06-09T11:05:01.096Z" }, - { url = "https://files.pythonhosted.org/packages/5c/89/177fae13baaa9380a9f714bdf8b88ae941ed2c2f89bd228f2f089a651afa/numexpr-2.11.0-cp310-cp310-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:ad5cf0ebc3cdb12edb5aa50472108807ffd0a0ce95f87c0366a479fa83a7c346", size = 409327, upload-time = "2025-06-09T11:05:02.706Z" }, - { url = "https://files.pythonhosted.org/packages/83/03/0718f1ac2d7cc0422096ab0ac16cc04597539a2c69a22616d781a2be4359/numexpr-2.11.0-cp310-cp310-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:8c9e6b07c136d06495c792f603099039bb1e7c6c29854cc5eb3d7640268df016", size = 399827, upload-time = "2025-06-09T11:05:04.33Z" }, - { url = "https://files.pythonhosted.org/packages/81/7d/8225d6fcafaa937606543bee6e985966c91d8741d25a8eb6d0143f64ce77/numexpr-2.11.0-cp310-cp310-win32.whl", hash = "sha256:4aba2f640d9d45b986a613ce94fcf008c42cc72eeba2990fefdb575228b1d3d1", size = 153165, upload-time = "2025-06-09T11:05:06.583Z" }, - { url = "https://files.pythonhosted.org/packages/8d/c8/abd6371906c2690852dbbd4cb8faa3d26c51bc8ce849cb4b16dc24e799c1/numexpr-2.11.0-cp310-cp310-win_amd64.whl", hash = "sha256:7f75797bc75a2e7edf52a1c9e68a1295fa84250161c8f4e41df9e72723332c65", size = 146233, upload-time = "2025-06-09T11:05:07.614Z" }, { url = "https://files.pythonhosted.org/packages/d8/d1/1cf8137990b3f3d445556ed63b9bc347aec39bde8c41146b02d3b35c1adc/numexpr-2.11.0-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:450eba3c93c3e3e8070566ad8d70590949d6e574b1c960bf68edd789811e7da8", size = 147535, upload-time = "2025-06-09T11:05:08.929Z" }, { url = "https://files.pythonhosted.org/packages/b6/5e/bac7649d043f47c7c14c797efe60dbd19476468a149399cd706fe2e47f8c/numexpr-2.11.0-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:f0eb88dbac8a7e61ee433006d0ddfd6eb921f5c6c224d1b50855bc98fb304c44", size = 136710, upload-time = "2025-06-09T11:05:10.366Z" }, { url = "https://files.pythonhosted.org/packages/1b/9f/c88fc34d82d23c66ea0b78b00a1fb3b64048e0f7ac7791b2cd0d2a4ce14d/numexpr-2.11.0-cp311-cp311-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:a194e3684b3553ea199c3f4837f422a521c7e2f0cce13527adc3a6b4049f9e7c", size = 411169, upload-time = "2025-06-09T11:05:11.797Z" }, @@ -2185,84 +1447,12 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/75/d7/41287384e413e8d20457d35e264d9c9754e65eb13a988af51ceb7057f61b/numexpr-2.11.0-cp313-cp313t-win_amd64.whl", hash = "sha256:a69b5c02014448a412012752dc46091902d28932c3be0c6e02e73cecceffb700", size = 147207, upload-time = "2025-06-09T11:05:39.011Z" }, ] -[[package]] -name = "numpy" -version = "2.0.2" -source = { registry = "https://pypi.org/simple" } -resolution-markers = [ - "python_full_version > '3.9' and python_full_version < '3.10'", - "python_full_version <= '3.9'", -] -sdist = { url = "https://files.pythonhosted.org/packages/a9/75/10dd1f8116a8b796cb2c737b674e02d02e80454bda953fa7e65d8c12b016/numpy-2.0.2.tar.gz", hash = "sha256:883c987dee1880e2a864ab0dc9892292582510604156762362d9326444636e78", size = 18902015, upload-time = "2024-08-26T20:19:40.945Z" } -wheels = [ - { url = "https://files.pythonhosted.org/packages/21/91/3495b3237510f79f5d81f2508f9f13fea78ebfdf07538fc7444badda173d/numpy-2.0.2-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:51129a29dbe56f9ca83438b706e2e69a39892b5eda6cedcb6b0c9fdc9b0d3ece", size = 21165245, upload-time = "2024-08-26T20:04:14.625Z" }, - { url = "https://files.pythonhosted.org/packages/05/33/26178c7d437a87082d11019292dce6d3fe6f0e9026b7b2309cbf3e489b1d/numpy-2.0.2-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:f15975dfec0cf2239224d80e32c3170b1d168335eaedee69da84fbe9f1f9cd04", size = 13738540, upload-time = "2024-08-26T20:04:36.784Z" }, - { url = "https://files.pythonhosted.org/packages/ec/31/cc46e13bf07644efc7a4bf68df2df5fb2a1a88d0cd0da9ddc84dc0033e51/numpy-2.0.2-cp310-cp310-macosx_14_0_arm64.whl", hash = "sha256:8c5713284ce4e282544c68d1c3b2c7161d38c256d2eefc93c1d683cf47683e66", size = 5300623, upload-time = "2024-08-26T20:04:46.491Z" }, - { url = "https://files.pythonhosted.org/packages/6e/16/7bfcebf27bb4f9d7ec67332ffebee4d1bf085c84246552d52dbb548600e7/numpy-2.0.2-cp310-cp310-macosx_14_0_x86_64.whl", hash = "sha256:becfae3ddd30736fe1889a37f1f580e245ba79a5855bff5f2a29cb3ccc22dd7b", size = 6901774, upload-time = "2024-08-26T20:04:58.173Z" }, - { url = "https://files.pythonhosted.org/packages/f9/a3/561c531c0e8bf082c5bef509d00d56f82e0ea7e1e3e3a7fc8fa78742a6e5/numpy-2.0.2-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:2da5960c3cf0df7eafefd806d4e612c5e19358de82cb3c343631188991566ccd", size = 13907081, upload-time = "2024-08-26T20:05:19.098Z" }, - { url = "https://files.pythonhosted.org/packages/fa/66/f7177ab331876200ac7563a580140643d1179c8b4b6a6b0fc9838de2a9b8/numpy-2.0.2-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:496f71341824ed9f3d2fd36cf3ac57ae2e0165c143b55c3a035ee219413f3318", size = 19523451, upload-time = "2024-08-26T20:05:47.479Z" }, - { url = "https://files.pythonhosted.org/packages/25/7f/0b209498009ad6453e4efc2c65bcdf0ae08a182b2b7877d7ab38a92dc542/numpy-2.0.2-cp310-cp310-musllinux_1_1_x86_64.whl", hash = "sha256:a61ec659f68ae254e4d237816e33171497e978140353c0c2038d46e63282d0c8", size = 19927572, upload-time = "2024-08-26T20:06:17.137Z" }, - { url = "https://files.pythonhosted.org/packages/3e/df/2619393b1e1b565cd2d4c4403bdd979621e2c4dea1f8532754b2598ed63b/numpy-2.0.2-cp310-cp310-musllinux_1_2_aarch64.whl", hash = "sha256:d731a1c6116ba289c1e9ee714b08a8ff882944d4ad631fd411106a30f083c326", size = 14400722, upload-time = "2024-08-26T20:06:39.16Z" }, - { url = "https://files.pythonhosted.org/packages/22/ad/77e921b9f256d5da36424ffb711ae79ca3f451ff8489eeca544d0701d74a/numpy-2.0.2-cp310-cp310-win32.whl", hash = "sha256:984d96121c9f9616cd33fbd0618b7f08e0cfc9600a7ee1d6fd9b239186d19d97", size = 6472170, upload-time = "2024-08-26T20:06:50.361Z" }, - { url = "https://files.pythonhosted.org/packages/10/05/3442317535028bc29cf0c0dd4c191a4481e8376e9f0db6bcf29703cadae6/numpy-2.0.2-cp310-cp310-win_amd64.whl", hash = "sha256:c7b0be4ef08607dd04da4092faee0b86607f111d5ae68036f16cc787e250a131", size = 15905558, upload-time = "2024-08-26T20:07:13.881Z" }, - { url = "https://files.pythonhosted.org/packages/8b/cf/034500fb83041aa0286e0fb16e7c76e5c8b67c0711bb6e9e9737a717d5fe/numpy-2.0.2-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:49ca4decb342d66018b01932139c0961a8f9ddc7589611158cb3c27cbcf76448", size = 21169137, upload-time = "2024-08-26T20:07:45.345Z" }, - { url = "https://files.pythonhosted.org/packages/4a/d9/32de45561811a4b87fbdee23b5797394e3d1504b4a7cf40c10199848893e/numpy-2.0.2-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:11a76c372d1d37437857280aa142086476136a8c0f373b2e648ab2c8f18fb195", size = 13703552, upload-time = "2024-08-26T20:08:06.666Z" }, - { url = "https://files.pythonhosted.org/packages/c1/ca/2f384720020c7b244d22508cb7ab23d95f179fcfff33c31a6eeba8d6c512/numpy-2.0.2-cp311-cp311-macosx_14_0_arm64.whl", hash = "sha256:807ec44583fd708a21d4a11d94aedf2f4f3c3719035c76a2bbe1fe8e217bdc57", size = 5298957, upload-time = "2024-08-26T20:08:15.83Z" }, - { url = "https://files.pythonhosted.org/packages/0e/78/a3e4f9fb6aa4e6fdca0c5428e8ba039408514388cf62d89651aade838269/numpy-2.0.2-cp311-cp311-macosx_14_0_x86_64.whl", hash = "sha256:8cafab480740e22f8d833acefed5cc87ce276f4ece12fdaa2e8903db2f82897a", size = 6905573, upload-time = "2024-08-26T20:08:27.185Z" }, - { url = "https://files.pythonhosted.org/packages/a0/72/cfc3a1beb2caf4efc9d0b38a15fe34025230da27e1c08cc2eb9bfb1c7231/numpy-2.0.2-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:a15f476a45e6e5a3a79d8a14e62161d27ad897381fecfa4a09ed5322f2085669", size = 13914330, upload-time = "2024-08-26T20:08:48.058Z" }, - { url = "https://files.pythonhosted.org/packages/ba/a8/c17acf65a931ce551fee11b72e8de63bf7e8a6f0e21add4c937c83563538/numpy-2.0.2-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:13e689d772146140a252c3a28501da66dfecd77490b498b168b501835041f951", size = 19534895, upload-time = "2024-08-26T20:09:16.536Z" }, - { url = "https://files.pythonhosted.org/packages/ba/86/8767f3d54f6ae0165749f84648da9dcc8cd78ab65d415494962c86fac80f/numpy-2.0.2-cp311-cp311-musllinux_1_1_x86_64.whl", hash = "sha256:9ea91dfb7c3d1c56a0e55657c0afb38cf1eeae4544c208dc465c3c9f3a7c09f9", size = 19937253, upload-time = "2024-08-26T20:09:46.263Z" }, - { url = "https://files.pythonhosted.org/packages/df/87/f76450e6e1c14e5bb1eae6836478b1028e096fd02e85c1c37674606ab752/numpy-2.0.2-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:c1c9307701fec8f3f7a1e6711f9089c06e6284b3afbbcd259f7791282d660a15", size = 14414074, upload-time = "2024-08-26T20:10:08.483Z" }, - { url = "https://files.pythonhosted.org/packages/5c/ca/0f0f328e1e59f73754f06e1adfb909de43726d4f24c6a3f8805f34f2b0fa/numpy-2.0.2-cp311-cp311-win32.whl", hash = "sha256:a392a68bd329eafac5817e5aefeb39038c48b671afd242710b451e76090e81f4", size = 6470640, upload-time = "2024-08-26T20:10:19.732Z" }, - { url = "https://files.pythonhosted.org/packages/eb/57/3a3f14d3a759dcf9bf6e9eda905794726b758819df4663f217d658a58695/numpy-2.0.2-cp311-cp311-win_amd64.whl", hash = "sha256:286cd40ce2b7d652a6f22efdfc6d1edf879440e53e76a75955bc0c826c7e64dc", size = 15910230, upload-time = "2024-08-26T20:10:43.413Z" }, - { url = "https://files.pythonhosted.org/packages/45/40/2e117be60ec50d98fa08c2f8c48e09b3edea93cfcabd5a9ff6925d54b1c2/numpy-2.0.2-cp312-cp312-macosx_10_9_x86_64.whl", hash = "sha256:df55d490dea7934f330006d0f81e8551ba6010a5bf035a249ef61a94f21c500b", size = 20895803, upload-time = "2024-08-26T20:11:13.916Z" }, - { url = "https://files.pythonhosted.org/packages/46/92/1b8b8dee833f53cef3e0a3f69b2374467789e0bb7399689582314df02651/numpy-2.0.2-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:8df823f570d9adf0978347d1f926b2a867d5608f434a7cff7f7908c6570dcf5e", size = 13471835, upload-time = "2024-08-26T20:11:34.779Z" }, - { url = "https://files.pythonhosted.org/packages/7f/19/e2793bde475f1edaea6945be141aef6c8b4c669b90c90a300a8954d08f0a/numpy-2.0.2-cp312-cp312-macosx_14_0_arm64.whl", hash = "sha256:9a92ae5c14811e390f3767053ff54eaee3bf84576d99a2456391401323f4ec2c", size = 5038499, upload-time = "2024-08-26T20:11:43.902Z" }, - { url = "https://files.pythonhosted.org/packages/e3/ff/ddf6dac2ff0dd50a7327bcdba45cb0264d0e96bb44d33324853f781a8f3c/numpy-2.0.2-cp312-cp312-macosx_14_0_x86_64.whl", hash = "sha256:a842d573724391493a97a62ebbb8e731f8a5dcc5d285dfc99141ca15a3302d0c", size = 6633497, upload-time = "2024-08-26T20:11:55.09Z" }, - { url = "https://files.pythonhosted.org/packages/72/21/67f36eac8e2d2cd652a2e69595a54128297cdcb1ff3931cfc87838874bd4/numpy-2.0.2-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:c05e238064fc0610c840d1cf6a13bf63d7e391717d247f1bf0318172e759e692", size = 13621158, upload-time = "2024-08-26T20:12:14.95Z" }, - { url = "https://files.pythonhosted.org/packages/39/68/e9f1126d757653496dbc096cb429014347a36b228f5a991dae2c6b6cfd40/numpy-2.0.2-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:0123ffdaa88fa4ab64835dcbde75dcdf89c453c922f18dced6e27c90d1d0ec5a", size = 19236173, upload-time = "2024-08-26T20:12:44.049Z" }, - { url = "https://files.pythonhosted.org/packages/d1/e9/1f5333281e4ebf483ba1c888b1d61ba7e78d7e910fdd8e6499667041cc35/numpy-2.0.2-cp312-cp312-musllinux_1_1_x86_64.whl", hash = "sha256:96a55f64139912d61de9137f11bf39a55ec8faec288c75a54f93dfd39f7eb40c", size = 19634174, upload-time = "2024-08-26T20:13:13.634Z" }, - { url = "https://files.pythonhosted.org/packages/71/af/a469674070c8d8408384e3012e064299f7a2de540738a8e414dcfd639996/numpy-2.0.2-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:ec9852fb39354b5a45a80bdab5ac02dd02b15f44b3804e9f00c556bf24b4bded", size = 14099701, upload-time = "2024-08-26T20:13:34.851Z" }, - { url = "https://files.pythonhosted.org/packages/d0/3d/08ea9f239d0e0e939b6ca52ad403c84a2bce1bde301a8eb4888c1c1543f1/numpy-2.0.2-cp312-cp312-win32.whl", hash = "sha256:671bec6496f83202ed2d3c8fdc486a8fc86942f2e69ff0e986140339a63bcbe5", size = 6174313, upload-time = "2024-08-26T20:13:45.653Z" }, - { url = "https://files.pythonhosted.org/packages/b2/b5/4ac39baebf1fdb2e72585c8352c56d063b6126be9fc95bd2bb5ef5770c20/numpy-2.0.2-cp312-cp312-win_amd64.whl", hash = "sha256:cfd41e13fdc257aa5778496b8caa5e856dc4896d4ccf01841daee1d96465467a", size = 15606179, upload-time = "2024-08-26T20:14:08.786Z" }, - { url = "https://files.pythonhosted.org/packages/43/c1/41c8f6df3162b0c6ffd4437d729115704bd43363de0090c7f913cfbc2d89/numpy-2.0.2-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:9059e10581ce4093f735ed23f3b9d283b9d517ff46009ddd485f1747eb22653c", size = 21169942, upload-time = "2024-08-26T20:14:40.108Z" }, - { url = "https://files.pythonhosted.org/packages/39/bc/fd298f308dcd232b56a4031fd6ddf11c43f9917fbc937e53762f7b5a3bb1/numpy-2.0.2-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:423e89b23490805d2a5a96fe40ec507407b8ee786d66f7328be214f9679df6dd", size = 13711512, upload-time = "2024-08-26T20:15:00.985Z" }, - { url = "https://files.pythonhosted.org/packages/96/ff/06d1aa3eeb1c614eda245c1ba4fb88c483bee6520d361641331872ac4b82/numpy-2.0.2-cp39-cp39-macosx_14_0_arm64.whl", hash = "sha256:2b2955fa6f11907cf7a70dab0d0755159bca87755e831e47932367fc8f2f2d0b", size = 5306976, upload-time = "2024-08-26T20:15:10.876Z" }, - { url = "https://files.pythonhosted.org/packages/2d/98/121996dcfb10a6087a05e54453e28e58694a7db62c5a5a29cee14c6e047b/numpy-2.0.2-cp39-cp39-macosx_14_0_x86_64.whl", hash = "sha256:97032a27bd9d8988b9a97a8c4d2c9f2c15a81f61e2f21404d7e8ef00cb5be729", size = 6906494, upload-time = "2024-08-26T20:15:22.055Z" }, - { url = "https://files.pythonhosted.org/packages/15/31/9dffc70da6b9bbf7968f6551967fc21156207366272c2a40b4ed6008dc9b/numpy-2.0.2-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:1e795a8be3ddbac43274f18588329c72939870a16cae810c2b73461c40718ab1", size = 13912596, upload-time = "2024-08-26T20:15:42.452Z" }, - { url = "https://files.pythonhosted.org/packages/b9/14/78635daab4b07c0930c919d451b8bf8c164774e6a3413aed04a6d95758ce/numpy-2.0.2-cp39-cp39-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:f26b258c385842546006213344c50655ff1555a9338e2e5e02a0756dc3e803dd", size = 19526099, upload-time = "2024-08-26T20:16:11.048Z" }, - { url = "https://files.pythonhosted.org/packages/26/4c/0eeca4614003077f68bfe7aac8b7496f04221865b3a5e7cb230c9d055afd/numpy-2.0.2-cp39-cp39-musllinux_1_1_x86_64.whl", hash = "sha256:5fec9451a7789926bcf7c2b8d187292c9f93ea30284802a0ab3f5be8ab36865d", size = 19932823, upload-time = "2024-08-26T20:16:40.171Z" }, - { url = "https://files.pythonhosted.org/packages/f1/46/ea25b98b13dccaebddf1a803f8c748680d972e00507cd9bc6dcdb5aa2ac1/numpy-2.0.2-cp39-cp39-musllinux_1_2_aarch64.whl", hash = "sha256:9189427407d88ff25ecf8f12469d4d39d35bee1db5d39fc5c168c6f088a6956d", size = 14404424, upload-time = "2024-08-26T20:17:02.604Z" }, - { url = "https://files.pythonhosted.org/packages/c8/a6/177dd88d95ecf07e722d21008b1b40e681a929eb9e329684d449c36586b2/numpy-2.0.2-cp39-cp39-win32.whl", hash = "sha256:905d16e0c60200656500c95b6b8dca5d109e23cb24abc701d41c02d74c6b3afa", size = 6476809, upload-time = "2024-08-26T20:17:13.553Z" }, - { url = "https://files.pythonhosted.org/packages/ea/2b/7fc9f4e7ae5b507c1a3a21f0f15ed03e794c1242ea8a242ac158beb56034/numpy-2.0.2-cp39-cp39-win_amd64.whl", hash = "sha256:a3f4ab0caa7f053f6797fcd4e1e25caee367db3112ef2b6ef82d749530768c73", size = 15911314, upload-time = "2024-08-26T20:17:36.72Z" }, - { url = "https://files.pythonhosted.org/packages/8f/3b/df5a870ac6a3be3a86856ce195ef42eec7ae50d2a202be1f5a4b3b340e14/numpy-2.0.2-pp39-pypy39_pp73-macosx_10_9_x86_64.whl", hash = "sha256:7f0a0c6f12e07fa94133c8a67404322845220c06a9e80e85999afe727f7438b8", size = 21025288, upload-time = "2024-08-26T20:18:07.732Z" }, - { url = "https://files.pythonhosted.org/packages/2c/97/51af92f18d6f6f2d9ad8b482a99fb74e142d71372da5d834b3a2747a446e/numpy-2.0.2-pp39-pypy39_pp73-macosx_14_0_x86_64.whl", hash = "sha256:312950fdd060354350ed123c0e25a71327d3711584beaef30cdaa93320c392d4", size = 6762793, upload-time = "2024-08-26T20:18:19.125Z" }, - { url = "https://files.pythonhosted.org/packages/12/46/de1fbd0c1b5ccaa7f9a005b66761533e2f6a3e560096682683a223631fe9/numpy-2.0.2-pp39-pypy39_pp73-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:26df23238872200f63518dd2aa984cfca675d82469535dc7162dc2ee52d9dd5c", size = 19334885, upload-time = "2024-08-26T20:18:47.237Z" }, - { url = "https://files.pythonhosted.org/packages/cc/dc/d330a6faefd92b446ec0f0dfea4c3207bb1fef3c4771d19cf4543efd2c78/numpy-2.0.2-pp39-pypy39_pp73-win_amd64.whl", hash = "sha256:a46288ec55ebbd58947d31d72be2c63cbf839f0a63b49cb755022310792a3385", size = 15828784, upload-time = "2024-08-26T20:19:11.19Z" }, -] - [[package]] name = "numpy" version = "2.1.3" source = { registry = "https://pypi.org/simple" } -resolution-markers = [ - "python_full_version >= '3.13'", - "python_full_version == '3.12.*'", - "python_full_version == '3.11.*'", - "python_full_version == '3.10.*'", -] sdist = { url = "https://files.pythonhosted.org/packages/25/ca/1166b75c21abd1da445b97bf1fa2f14f423c6cfb4fc7c4ef31dccf9f6a94/numpy-2.1.3.tar.gz", hash = "sha256:aa08e04e08aaf974d4458def539dece0d28146d866a39da5639596f4921fd761", size = 20166090, upload-time = "2024-11-02T17:48:55.832Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/f1/80/d572a4737626372915bca41c3afbfec9d173561a39a0a61bacbbfd1dafd4/numpy-2.1.3-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:c894b4305373b9c5576d7a12b473702afdf48ce5369c074ba304cc5ad8730dff", size = 21152472, upload-time = "2024-11-02T17:30:37.354Z" }, - { url = "https://files.pythonhosted.org/packages/6f/bb/7bfba10c791ae3bb6716da77ad85a82d5fac07fc96fb0023ef0571df9d20/numpy-2.1.3-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:b47fbb433d3260adcd51eb54f92a2ffbc90a4595f8970ee00e064c644ac788f5", size = 13747967, upload-time = "2024-11-02T17:30:59.602Z" }, - { url = "https://files.pythonhosted.org/packages/da/d6/2df7bde35f0478455f0be5934877b3e5a505f587b00230f54a519a6b55a5/numpy-2.1.3-cp310-cp310-macosx_14_0_arm64.whl", hash = "sha256:825656d0743699c529c5943554d223c021ff0494ff1442152ce887ef4f7561a1", size = 5354921, upload-time = "2024-11-02T17:31:09.428Z" }, - { url = "https://files.pythonhosted.org/packages/d1/bb/75b945874f931494891eac6ca06a1764d0e8208791f3addadb2963b83527/numpy-2.1.3-cp310-cp310-macosx_14_0_x86_64.whl", hash = "sha256:6a4825252fcc430a182ac4dee5a505053d262c807f8a924603d411f6718b88fd", size = 6888603, upload-time = "2024-11-02T17:31:20.835Z" }, - { url = "https://files.pythonhosted.org/packages/68/a7/fde73636f6498dbfa6d82fc336164635fe592f1ad0d13285fcb6267fdc1c/numpy-2.1.3-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:e711e02f49e176a01d0349d82cb5f05ba4db7d5e7e0defd026328e5cfb3226d3", size = 13889862, upload-time = "2024-11-02T17:31:41.486Z" }, - { url = "https://files.pythonhosted.org/packages/05/db/5d9c91b2e1e2e72be1369278f696356d44975befcae830daf2e667dcb54f/numpy-2.1.3-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:78574ac2d1a4a02421f25da9559850d59457bac82f2b8d7a44fe83a64f770098", size = 16328151, upload-time = "2024-11-02T17:32:08.262Z" }, - { url = "https://files.pythonhosted.org/packages/3e/6a/7eb732109b53ae64a29e25d7e68eb9d6611037f6354875497008a49e74d3/numpy-2.1.3-cp310-cp310-musllinux_1_1_x86_64.whl", hash = "sha256:c7662f0e3673fe4e832fe07b65c50342ea27d989f92c80355658c7f888fcc83c", size = 16704107, upload-time = "2024-11-02T17:32:34.361Z" }, - { url = "https://files.pythonhosted.org/packages/88/cc/278113b66a1141053cbda6f80e4200c6da06b3079c2d27bda1fde41f2c1f/numpy-2.1.3-cp310-cp310-musllinux_1_2_aarch64.whl", hash = "sha256:fa2d1337dc61c8dc417fbccf20f6d1e139896a30721b7f1e832b2bb6ef4eb6c4", size = 14385789, upload-time = "2024-11-02T17:32:57.152Z" }, - { url = "https://files.pythonhosted.org/packages/f5/69/eb20f5e1bfa07449bc67574d2f0f7c1e6b335fb41672e43861a7727d85f2/numpy-2.1.3-cp310-cp310-win32.whl", hash = "sha256:72dcc4a35a8515d83e76b58fdf8113a5c969ccd505c8a946759b24e3182d1f23", size = 6536706, upload-time = "2024-11-02T17:33:09.12Z" }, - { url = "https://files.pythonhosted.org/packages/8e/8b/1c131ab5a94c1086c289c6e1da1d843de9dbd95fe5f5ee6e61904c9518e2/numpy-2.1.3-cp310-cp310-win_amd64.whl", hash = "sha256:ecc76a9ba2911d8d37ac01de72834d8849e55473457558e12995f4cd53e778e0", size = 12864165, upload-time = "2024-11-02T17:33:28.974Z" }, { url = "https://files.pythonhosted.org/packages/ad/81/c8167192eba5247593cd9d305ac236847c2912ff39e11402e72ae28a4985/numpy-2.1.3-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:4d1167c53b93f1f5d8a139a742b3c6f4d429b54e74e6b57d0eff40045187b15d", size = 21156252, upload-time = "2024-11-02T17:34:01.372Z" }, { url = "https://files.pythonhosted.org/packages/da/74/5a60003fc3d8a718d830b08b654d0eea2d2db0806bab8f3c2aca7e18e010/numpy-2.1.3-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:c80e4a09b3d95b4e1cac08643f1152fa71a0a821a2d4277334c88d54b2219a41", size = 13784119, upload-time = "2024-11-02T17:34:23.809Z" }, { url = "https://files.pythonhosted.org/packages/47/7c/864cb966b96fce5e63fcf25e1e4d957fe5725a635e5f11fe03f39dd9d6b5/numpy-2.1.3-cp311-cp311-macosx_14_0_arm64.whl", hash = "sha256:576a1c1d25e9e02ed7fa5477f30a127fe56debd53b8d2c89d5578f9857d03ca9", size = 5352978, upload-time = "2024-11-02T17:34:34.001Z" }, @@ -2303,10 +1493,6 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/ef/62/1d3204313357591c913c32132a28f09a26357e33ea3c4e2fe81269e0dca1/numpy-2.1.3-cp313-cp313t-musllinux_1_2_aarch64.whl", hash = "sha256:14e253bd43fc6b37af4921b10f6add6925878a42a0c5fe83daee390bca80bc17", size = 14067180, upload-time = "2024-11-02T17:45:37.234Z" }, { url = "https://files.pythonhosted.org/packages/24/d7/78a40ed1d80e23a774cb8a34ae8a9493ba1b4271dde96e56ccdbab1620ef/numpy-2.1.3-cp313-cp313t-win32.whl", hash = "sha256:08788d27a5fd867a663f6fc753fd7c3ad7e92747efc73c53bca2f19f8bc06f48", size = 6291907, upload-time = "2024-11-02T17:45:48.951Z" }, { url = "https://files.pythonhosted.org/packages/86/09/a5ab407bd7f5f5599e6a9261f964ace03a73e7c6928de906981c31c38082/numpy-2.1.3-cp313-cp313t-win_amd64.whl", hash = "sha256:2564fbdf2b99b3f815f2107c1bbc93e2de8ee655a69c261363a1172a79a257d4", size = 12644098, upload-time = "2024-11-02T17:46:07.941Z" }, - { url = "https://files.pythonhosted.org/packages/00/e7/8d8bb791b62586cc432ecbb70632b4f23b7b7c88df41878de7528264f6d7/numpy-2.1.3-pp310-pypy310_pp73-macosx_10_15_x86_64.whl", hash = "sha256:4f2015dfe437dfebbfce7c85c7b53d81ba49e71ba7eadbf1df40c915af75979f", size = 20983893, upload-time = "2024-11-02T17:47:09.365Z" }, - { url = "https://files.pythonhosted.org/packages/5e/f3/cb8118a044b5007586245a650360c9f5915b2f4232dd7658bb7a63dd1d02/numpy-2.1.3-pp310-pypy310_pp73-macosx_14_0_x86_64.whl", hash = "sha256:3522b0dfe983a575e6a9ab3a4a4dfe156c3e428468ff08ce582b9bb6bd1d71d4", size = 6752501, upload-time = "2024-11-02T17:47:21.52Z" }, - { url = "https://files.pythonhosted.org/packages/53/f5/365b46439b518d2ec6ebb880cc0edf90f225145dfd4db7958334f7164530/numpy-2.1.3-pp310-pypy310_pp73-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:c006b607a865b07cd981ccb218a04fc86b600411d83d6fc261357f1c0966755d", size = 16142601, upload-time = "2024-11-02T17:47:45.575Z" }, - { url = "https://files.pythonhosted.org/packages/03/c2/d1fee6ba999aa7cd41ca6856937f2baaf604c3eec1565eae63451ec31e5e/numpy-2.1.3-pp310-pypy310_pp73-win_amd64.whl", hash = "sha256:e14e26956e6f1696070788252dcdff11b4aca4c3e8bd166e0df1bb8f315a67cb", size = 12771397, upload-time = "2024-11-02T17:48:05.988Z" }, ] [[package]] @@ -2335,21 +1521,13 @@ name = "pandas" version = "2.3.2" source = { registry = "https://pypi.org/simple" } dependencies = [ - { name = "numpy", version = "2.0.2", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version < '3.10'" }, - { name = "numpy", version = "2.1.3", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version >= '3.10'" }, + { name = "numpy" }, { name = "python-dateutil" }, { name = "pytz" }, { name = "tzdata" }, ] sdist = { url = "https://files.pythonhosted.org/packages/79/8e/0e90233ac205ad182bd6b422532695d2b9414944a280488105d598c70023/pandas-2.3.2.tar.gz", hash = "sha256:ab7b58f8f82706890924ccdfb5f48002b83d2b5a3845976a9fb705d36c34dcdb", size = 4488684, upload-time = "2025-08-21T10:28:29.257Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/2e/16/a8eeb70aad84ccbf14076793f90e0031eded63c1899aeae9fdfbf37881f4/pandas-2.3.2-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:52bc29a946304c360561974c6542d1dd628ddafa69134a7131fdfd6a5d7a1a35", size = 11539648, upload-time = "2025-08-21T10:26:36.236Z" }, - { url = "https://files.pythonhosted.org/packages/47/f1/c5bdaea13bf3708554d93e948b7ea74121ce6e0d59537ca4c4f77731072b/pandas-2.3.2-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:220cc5c35ffaa764dd5bb17cf42df283b5cb7fdf49e10a7b053a06c9cb48ee2b", size = 10786923, upload-time = "2025-08-21T10:26:40.518Z" }, - { url = "https://files.pythonhosted.org/packages/bb/10/811fa01476d29ffed692e735825516ad0e56d925961819e6126b4ba32147/pandas-2.3.2-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:42c05e15111221384019897df20c6fe893b2f697d03c811ee67ec9e0bb5a3424", size = 11726241, upload-time = "2025-08-21T10:26:43.175Z" }, - { url = "https://files.pythonhosted.org/packages/c4/6a/40b043b06e08df1ea1b6d20f0e0c2f2c4ec8c4f07d1c92948273d943a50b/pandas-2.3.2-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:cc03acc273c5515ab69f898df99d9d4f12c4d70dbfc24c3acc6203751d0804cf", size = 12349533, upload-time = "2025-08-21T10:26:46.611Z" }, - { url = "https://files.pythonhosted.org/packages/e2/ea/2e081a2302e41a9bca7056659fdd2b85ef94923723e41665b42d65afd347/pandas-2.3.2-cp310-cp310-musllinux_1_2_aarch64.whl", hash = "sha256:d25c20a03e8870f6339bcf67281b946bd20b86f1a544ebbebb87e66a8d642cba", size = 13202407, upload-time = "2025-08-21T10:26:49.068Z" }, - { url = "https://files.pythonhosted.org/packages/f4/12/7ff9f6a79e2ee8869dcf70741ef998b97ea20050fe25f83dc759764c1e32/pandas-2.3.2-cp310-cp310-musllinux_1_2_x86_64.whl", hash = "sha256:21bb612d148bb5860b7eb2c10faacf1a810799245afd342cf297d7551513fbb6", size = 13837212, upload-time = "2025-08-21T10:26:51.832Z" }, - { url = "https://files.pythonhosted.org/packages/d8/df/5ab92fcd76455a632b3db34a746e1074d432c0cdbbd28d7cd1daba46a75d/pandas-2.3.2-cp310-cp310-win_amd64.whl", hash = "sha256:b62d586eb25cb8cb70a5746a378fc3194cb7f11ea77170d59f889f5dfe3cec7a", size = 11338099, upload-time = "2025-08-21T10:26:54.382Z" }, { url = "https://files.pythonhosted.org/packages/7a/59/f3e010879f118c2d400902d2d871c2226cef29b08c09fb8dc41111730400/pandas-2.3.2-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:1333e9c299adcbb68ee89a9bb568fc3f20f9cbb419f1dd5225071e6cddb2a743", size = 11563308, upload-time = "2025-08-21T10:26:56.656Z" }, { url = "https://files.pythonhosted.org/packages/38/18/48f10f1cc5c397af59571d638d211f494dba481f449c19adbd282aa8f4ca/pandas-2.3.2-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:76972bcbd7de8e91ad5f0ca884a9f2c477a2125354af624e022c49e5bd0dfff4", size = 10820319, upload-time = "2025-08-21T10:26:59.162Z" }, { url = "https://files.pythonhosted.org/packages/95/3b/1e9b69632898b048e223834cd9702052bcf06b15e1ae716eda3196fb972e/pandas-2.3.2-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:b98bdd7c456a05eef7cd21fd6b29e3ca243591fe531c62be94a2cc987efb5ac2", size = 11790097, upload-time = "2025-08-21T10:27:02.204Z" }, @@ -2377,13 +1555,6 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/15/d5/f0486090eb18dd8710bf60afeaf638ba6817047c0c8ae5c6a25598665609/pandas-2.3.2-cp313-cp313t-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:b37205ad6f00d52f16b6d09f406434ba928c1a1966e2771006a9033c736d30d2", size = 11883216, upload-time = "2025-08-21T10:27:59.302Z" }, { url = "https://files.pythonhosted.org/packages/10/86/692050c119696da19e20245bbd650d8dfca6ceb577da027c3a73c62a047e/pandas-2.3.2-cp313-cp313t-musllinux_1_2_aarch64.whl", hash = "sha256:837248b4fc3a9b83b9c6214699a13f069dc13510a6a6d7f9ba33145d2841a012", size = 12699743, upload-time = "2025-08-21T10:28:02.447Z" }, { url = "https://files.pythonhosted.org/packages/cd/d7/612123674d7b17cf345aad0a10289b2a384bff404e0463a83c4a3a59d205/pandas-2.3.2-cp313-cp313t-musllinux_1_2_x86_64.whl", hash = "sha256:d2c3554bd31b731cd6490d94a28f3abb8dd770634a9e06eb6d2911b9827db370", size = 13186141, upload-time = "2025-08-21T10:28:05.377Z" }, - { url = "https://files.pythonhosted.org/packages/e0/c3/b37e090d0aceda9b4dd85c8dbd1bea65b1de9e7a4f690d6bd3a40bd16390/pandas-2.3.2-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:88080a0ff8a55eac9c84e3ff3c7665b3b5476c6fbc484775ca1910ce1c3e0b87", size = 11551511, upload-time = "2025-08-21T10:28:11.111Z" }, - { url = "https://files.pythonhosted.org/packages/b9/47/381fb1e7adcfcf4230fa6dc3a741acbac6c6fe072f19f4e7a46bddf3e5f6/pandas-2.3.2-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:d4a558c7620340a0931828d8065688b3cc5b4c8eb674bcaf33d18ff4a6870b4a", size = 10797930, upload-time = "2025-08-21T10:28:13.436Z" }, - { url = "https://files.pythonhosted.org/packages/36/ca/d42467829080b92fc46d451288af8068f129fbcfb6578d573f45120de5cf/pandas-2.3.2-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:45178cf09d1858a1509dc73ec261bf5b25a625a389b65be2e47b559905f0ab6a", size = 11738470, upload-time = "2025-08-21T10:28:16.065Z" }, - { url = "https://files.pythonhosted.org/packages/60/76/7d0f0a0deed7867c51163982d7b79c0a089096cd7ad50e1b87c2c82220e9/pandas-2.3.2-cp39-cp39-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:77cefe00e1b210f9c76c697fedd8fdb8d3dd86563e9c8adc9fa72b90f5e9e4c2", size = 12366640, upload-time = "2025-08-21T10:28:18.557Z" }, - { url = "https://files.pythonhosted.org/packages/21/31/56784743e421cf51e34358fe7e5954345e5942168897bf8eb5707b71eedb/pandas-2.3.2-cp39-cp39-musllinux_1_2_aarch64.whl", hash = "sha256:13bd629c653856f00c53dc495191baa59bcafbbf54860a46ecc50d3a88421a96", size = 13211567, upload-time = "2025-08-21T10:28:20.998Z" }, - { url = "https://files.pythonhosted.org/packages/7a/4e/50a399dc7d9dd4aa09a03b163751d428026cf0f16c419b4010f6aca26ebd/pandas-2.3.2-cp39-cp39-musllinux_1_2_x86_64.whl", hash = "sha256:36d627906fd44b5fd63c943264e11e96e923f8de77d6016dc2f667b9ad193438", size = 13854073, upload-time = "2025-08-21T10:28:24.056Z" }, - { url = "https://files.pythonhosted.org/packages/29/72/8978a84861a5124e56ce1048376569545412501fcb9a83f035393d6d85bc/pandas-2.3.2-cp39-cp39-win_amd64.whl", hash = "sha256:a9d7ec92d71a420185dec44909c32e9a362248c4ae2238234b76d5be37f208cc", size = 11346452, upload-time = "2025-08-21T10:28:26.691Z" }, ] [[package]] @@ -2458,11 +1629,10 @@ wheels = [ [[package]] name = "policyengine" -version = "5.3.0" +version = "5.3.1" source = { editable = "." } dependencies = [ - { name = "h5py", version = "3.14.0", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version < '3.10'" }, - { name = "h5py", version = "3.16.0", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version >= '3.10'" }, + { name = "h5py" }, { name = "jsonschema" }, { name = "microdf-python" }, { name = "packaging" }, @@ -2479,8 +1649,7 @@ dev = [ { name = "furo" }, { name = "itables" }, { name = "jupyter-book" }, - { name = "mypy", version = "1.19.1", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version < '3.10'" }, - { name = "mypy", version = "1.20.0", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version >= '3.10'" }, + { name = "mypy" }, { name = "plotly" }, { name = "policyengine-core" }, { name = "policyengine-uk" }, @@ -2494,9 +1663,7 @@ dev = [ { name = "yaml-changelog" }, ] graph = [ - { name = "networkx", version = "3.2.1", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version < '3.10'" }, - { name = "networkx", version = "3.4.2", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version == '3.10.*'" }, - { name = "networkx", version = "3.5", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version >= '3.11'" }, + { name = "networkx" }, ] models = [ { name = "policyengine-core" }, @@ -2564,16 +1731,12 @@ version = "3.30.1" source = { registry = "https://pypi.org/simple" } dependencies = [ { name = "dpath" }, - { name = "h5py", version = "3.14.0", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version < '3.10'" }, - { name = "h5py", version = "3.16.0", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version >= '3.10'" }, + { name = "h5py" }, { name = "huggingface-hub" }, - { name = "ipython", version = "8.18.1", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version < '3.10'" }, - { name = "ipython", version = "8.37.0", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version >= '3.10'" }, + { name = "ipython" }, { name = "microdf-python" }, - { name = "numexpr", version = "2.10.2", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version < '3.10'" }, - { name = "numexpr", version = "2.11.0", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version >= '3.10'" }, - { name = "numpy", version = "2.0.2", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version < '3.10'" }, - { name = "numpy", version = "2.1.3", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version >= '3.10'" }, + { name = "numexpr" }, + { name = "numpy" }, { name = "pandas" }, { name = "plotly" }, { name = "psutil" }, @@ -2597,9 +1760,7 @@ dependencies = [ { name = "microdf-python" }, { name = "policyengine-core" }, { name = "pydantic" }, - { name = "tables", version = "3.9.2", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version < '3.10'" }, - { name = "tables", version = "3.10.1", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version == '3.10.*'" }, - { name = "tables", version = "3.11.1", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version >= '3.11'" }, + { name = "tables" }, ] sdist = { url = "https://files.pythonhosted.org/packages/40/e1/6652fd4ff8806d81b7c846ce2baeef4b9a8457ed380bb0853419d3cf36d9/policyengine_uk-2.90.2.tar.gz", hash = "sha256:c9baeaded1bb6d6f5ca5fcd962b80a198509513737c194aa867b0ef6d13d6a09", size = 1237478, upload-time = "2026-08-13T05:30:05.961Z" } wheels = [ @@ -2615,9 +1776,7 @@ dependencies = [ { name = "pandas" }, { name = "policyengine-core" }, { name = "spm-calculator" }, - { name = "tables", version = "3.9.2", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version < '3.10'" }, - { name = "tables", version = "3.10.1", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version == '3.10.*'" }, - { name = "tables", version = "3.11.1", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version >= '3.11'" }, + { name = "tables" }, { name = "tqdm" }, ] sdist = { url = "https://files.pythonhosted.org/packages/6e/cc/9e65a9586069beab1f02102a23f7a0e6332f1a9c2d590cb9f0e68f9ac987/policyengine_us-1.764.6.tar.gz", hash = "sha256:9817d48abc5b7d690b8abe33047005e5cf40b71654a85a2534bb5e3bcf921f79", size = 11009555, upload-time = "2026-07-06T13:13:07.254Z" } @@ -2684,7 +1843,6 @@ name = "pybtex" version = "0.25.1" source = { registry = "https://pypi.org/simple" } dependencies = [ - { name = "importlib-metadata", marker = "python_full_version < '3.10'" }, { name = "latexcodec" }, { name = "pyyaml" }, ] @@ -2739,19 +1897,6 @@ dependencies = [ ] 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" }, @@ -2822,19 +1967,6 @@ wheels = [ { 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/54/db/160dffb57ed9a3705c4cbcbff0ac03bdae45f1ca7d58ab74645550df3fbd/pydantic_core-2.41.5-cp39-cp39-macosx_10_12_x86_64.whl", hash = "sha256:8bfeaf8735be79f225f3fefab7f941c712aaca36f1128c9d7e2352ee1aa87bdf", size = 2107999, upload-time = "2025-11-04T13:42:03.885Z" }, - { url = "https://files.pythonhosted.org/packages/a3/7d/88e7de946f60d9263cc84819f32513520b85c0f8322f9b8f6e4afc938383/pydantic_core-2.41.5-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:346285d28e4c8017da95144c7f3acd42740d637ff41946af5ce6e5e420502dd5", size = 1929745, upload-time = "2025-11-04T13:42:06.075Z" }, - { url = "https://files.pythonhosted.org/packages/d5/c2/aef51e5b283780e85e99ff19db0f05842d2d4a8a8cd15e63b0280029b08f/pydantic_core-2.41.5-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:a75dafbf87d6276ddc5b2bf6fae5254e3d0876b626eb24969a574fff9149ee5d", size = 1920220, upload-time = "2025-11-04T13:42:08.457Z" }, - { url = "https://files.pythonhosted.org/packages/c7/97/492ab10f9ac8695cd76b2fdb24e9e61f394051df71594e9bcc891c9f586e/pydantic_core-2.41.5-cp39-cp39-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:7b93a4d08587e2b7e7882de461e82b6ed76d9026ce91ca7915e740ecc7855f60", size = 2067296, upload-time = "2025-11-04T13:42:10.817Z" }, - { url = "https://files.pythonhosted.org/packages/ec/23/984149650e5269c59a2a4c41d234a9570adc68ab29981825cfaf4cfad8f4/pydantic_core-2.41.5-cp39-cp39-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:e8465ab91a4bd96d36dde3263f06caa6a8a6019e4113f24dc753d79a8b3a3f82", size = 2231548, upload-time = "2025-11-04T13:42:13.843Z" }, - { url = "https://files.pythonhosted.org/packages/71/0c/85bcbb885b9732c28bec67a222dbed5ed2d77baee1f8bba2002e8cd00c5c/pydantic_core-2.41.5-cp39-cp39-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:299e0a22e7ae2b85c1a57f104538b2656e8ab1873511fd718a1c1c6f149b77b5", size = 2362571, upload-time = "2025-11-04T13:42:16.208Z" }, - { url = "https://files.pythonhosted.org/packages/c0/4a/412d2048be12c334003e9b823a3fa3d038e46cc2d64dd8aab50b31b65499/pydantic_core-2.41.5-cp39-cp39-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:707625ef0983fcfb461acfaf14de2067c5942c6bb0f3b4c99158bed6fedd3cf3", size = 2068175, upload-time = "2025-11-04T13:42:18.911Z" }, - { url = "https://files.pythonhosted.org/packages/73/f4/c58b6a776b502d0a5540ad02e232514285513572060f0d78f7832ca3c98b/pydantic_core-2.41.5-cp39-cp39-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:f41eb9797986d6ebac5e8edff36d5cef9de40def462311b3eb3eeded1431e425", size = 2177203, upload-time = "2025-11-04T13:42:22.578Z" }, - { url = "https://files.pythonhosted.org/packages/ed/ae/f06ea4c7e7a9eead3d165e7623cd2ea0cb788e277e4f935af63fc98fa4e6/pydantic_core-2.41.5-cp39-cp39-musllinux_1_1_aarch64.whl", hash = "sha256:0384e2e1021894b1ff5a786dbf94771e2986ebe2869533874d7e43bc79c6f504", size = 2148191, upload-time = "2025-11-04T13:42:24.89Z" }, - { url = "https://files.pythonhosted.org/packages/c1/57/25a11dcdc656bf5f8b05902c3c2934ac3ea296257cc4a3f79a6319e61856/pydantic_core-2.41.5-cp39-cp39-musllinux_1_1_armv7l.whl", hash = "sha256:f0cd744688278965817fd0839c4a4116add48d23890d468bc436f78beb28abf5", size = 2343907, upload-time = "2025-11-04T13:42:27.683Z" }, - { url = "https://files.pythonhosted.org/packages/96/82/e33d5f4933d7a03327c0c43c65d575e5919d4974ffc026bc917a5f7b9f61/pydantic_core-2.41.5-cp39-cp39-musllinux_1_1_x86_64.whl", hash = "sha256:753e230374206729bf0a807954bcc6c150d3743928a73faffee51ac6557a03c3", size = 2322174, upload-time = "2025-11-04T13:42:30.776Z" }, - { url = "https://files.pythonhosted.org/packages/81/45/4091be67ce9f469e81656f880f3506f6a5624121ec5eb3eab37d7581897d/pydantic_core-2.41.5-cp39-cp39-win32.whl", hash = "sha256:873e0d5b4fb9b89ef7c2d2a963ea7d02879d9da0da8d9d4933dee8ee86a8b460", size = 1990353, upload-time = "2025-11-04T13:42:33.111Z" }, - { url = "https://files.pythonhosted.org/packages/44/8a/a98aede18db6e9cd5d66bcacd8a409fcf8134204cdede2e7de35c5a2c5ef/pydantic_core-2.41.5-cp39-cp39-win_amd64.whl", hash = "sha256:e4f4a984405e91527a0d62649ee21138f8e3d0ef103be488c1dc11a80d7f184b", size = 2015698, upload-time = "2025-11-04T13:42:35.484Z" }, { 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" }, @@ -2843,14 +1975,6 @@ wheels = [ { 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" }, @@ -2918,12 +2042,10 @@ version = "8.4.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/08/ba/45911d754e8eba3d5a841a5ce61a65a685ff1798421ac054f85aa8747dfb/pytest-8.4.1.tar.gz", hash = "sha256:7c67fd69174877359ed9371ec3af8a3d2b04741818c51e5e99cc1742251fa93c", size = 1517714, upload-time = "2025-06-18T05:48:06.109Z" } wheels = [ @@ -2935,9 +2057,7 @@ name = "pytest-asyncio" version = "1.1.0" source = { registry = "https://pypi.org/simple" } dependencies = [ - { name = "backports-asyncio-runner", marker = "python_full_version < '3.11'" }, { name = "pytest" }, - { name = "typing-extensions", marker = "python_full_version < '3.10'" }, ] sdist = { url = "https://files.pythonhosted.org/packages/4e/51/f8794af39eeb870e87a8c8068642fc07bce0c854d6865d7dd0f2a9d338c2/pytest_asyncio-1.1.0.tar.gz", hash = "sha256:796aa822981e01b68c12e4827b8697108f7205020f24b5793b3c41555dab68ea", size = 46652, upload-time = "2025-07-16T04:29:26.393Z" } wheels = [ @@ -2949,8 +2069,7 @@ name = "pytest-cov" version = "7.1.0" source = { registry = "https://pypi.org/simple" } dependencies = [ - { name = "coverage", version = "7.10.7", source = { registry = "https://pypi.org/simple" }, extra = ["toml"], marker = "python_full_version < '3.10'" }, - { name = "coverage", version = "7.13.5", source = { registry = "https://pypi.org/simple" }, extra = ["toml"], marker = "python_full_version >= '3.10'" }, + { name = "coverage", extra = ["toml"] }, { name = "pluggy" }, { name = "pytest" }, ] @@ -2994,13 +2113,10 @@ name = "pyvis" version = "0.3.2" source = { registry = "https://pypi.org/simple" } dependencies = [ - { name = "ipython", version = "8.18.1", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version < '3.10'" }, - { name = "ipython", version = "8.37.0", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version >= '3.10'" }, + { name = "ipython" }, { name = "jinja2" }, { name = "jsonpickle" }, - { name = "networkx", version = "3.2.1", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version < '3.10'" }, - { name = "networkx", version = "3.4.2", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version == '3.10.*'" }, - { name = "networkx", version = "3.5", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version >= '3.11'" }, + { name = "networkx" }, ] wheels = [ { url = "https://files.pythonhosted.org/packages/ab/4b/e37e4e5d5ee1179694917b445768bdbfb084f5a59ecd38089d3413d4c70f/pyvis-0.3.2-py3-none-any.whl", hash = "sha256:5720c4ca8161dc5d9ab352015723abb7a8bb8fb443edeb07f7a322db34a97555", size = 756038, upload-time = "2023-02-24T20:29:46.758Z" }, @@ -3011,9 +2127,6 @@ name = "pywin32" version = "311" source = { registry = "https://pypi.org/simple" } wheels = [ - { url = "https://files.pythonhosted.org/packages/7b/40/44efbb0dfbd33aca6a6483191dae0716070ed99e2ecb0c53683f400a0b4f/pywin32-311-cp310-cp310-win32.whl", hash = "sha256:d03ff496d2a0cd4a5893504789d4a15399133fe82517455e78bad62efbb7f0a3", size = 8760432, upload-time = "2025-07-14T20:13:05.9Z" }, - { url = "https://files.pythonhosted.org/packages/5e/bf/360243b1e953bd254a82f12653974be395ba880e7ec23e3731d9f73921cc/pywin32-311-cp310-cp310-win_amd64.whl", hash = "sha256:797c2772017851984b97180b0bebe4b620bb86328e8a884bb626156295a63b3b", size = 9590103, upload-time = "2025-07-14T20:13:07.698Z" }, - { url = "https://files.pythonhosted.org/packages/57/38/d290720e6f138086fb3d5ffe0b6caa019a791dd57866940c82e4eeaf2012/pywin32-311-cp310-cp310-win_arm64.whl", hash = "sha256:0502d1facf1fed4839a9a51ccbcc63d952cf318f78ffc00a7e78528ac27d7a2b", size = 8778557, upload-time = "2025-07-14T20:13:11.11Z" }, { url = "https://files.pythonhosted.org/packages/7c/af/449a6a91e5d6db51420875c54f6aff7c97a86a3b13a0b4f1a5c13b988de3/pywin32-311-cp311-cp311-win32.whl", hash = "sha256:184eb5e436dea364dcd3d2316d577d625c0351bf237c4e9a5fabbcfa5a58b151", size = 8697031, upload-time = "2025-07-14T20:13:13.266Z" }, { url = "https://files.pythonhosted.org/packages/51/8f/9bb81dd5bb77d22243d33c8397f09377056d5c687aa6d4042bea7fbf8364/pywin32-311-cp311-cp311-win_amd64.whl", hash = "sha256:3ce80b34b22b17ccbd937a6e78e7225d80c52f5ab9940fe0506a1a16f3dab503", size = 9508308, upload-time = "2025-07-14T20:13:15.147Z" }, { url = "https://files.pythonhosted.org/packages/44/7b/9c2ab54f74a138c491aba1b1cd0795ba61f144c711daea84a88b63dc0f6c/pywin32-311-cp311-cp311-win_arm64.whl", hash = "sha256:a733f1388e1a842abb67ffa8e7aad0e70ac519e09b0f6a784e65a136ec7cefd2", size = 8703930, upload-time = "2025-07-14T20:13:16.945Z" }, @@ -3026,9 +2139,6 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/c9/31/097f2e132c4f16d99a22bfb777e0fd88bd8e1c634304e102f313af69ace5/pywin32-311-cp314-cp314-win32.whl", hash = "sha256:b7a2c10b93f8986666d0c803ee19b5990885872a7de910fc460f9b0c2fbf92ee", size = 8840714, upload-time = "2025-07-14T20:13:32.449Z" }, { url = "https://files.pythonhosted.org/packages/90/4b/07c77d8ba0e01349358082713400435347df8426208171ce297da32c313d/pywin32-311-cp314-cp314-win_amd64.whl", hash = "sha256:3aca44c046bd2ed8c90de9cb8427f581c479e594e99b5c0bb19b29c10fd6cb87", size = 9656800, upload-time = "2025-07-14T20:13:34.312Z" }, { url = "https://files.pythonhosted.org/packages/c0/d2/21af5c535501a7233e734b8af901574572da66fcc254cb35d0609c9080dd/pywin32-311-cp314-cp314-win_arm64.whl", hash = "sha256:a508e2d9025764a8270f93111a970e1d0fbfc33f4153b388bb649b7eec4f9b42", size = 8932540, upload-time = "2025-07-14T20:13:36.379Z" }, - { url = "https://files.pythonhosted.org/packages/59/42/b86689aac0cdaee7ae1c58d464b0ff04ca909c19bb6502d4973cdd9f9544/pywin32-311-cp39-cp39-win32.whl", hash = "sha256:aba8f82d551a942cb20d4a83413ccbac30790b50efb89a75e4f586ac0bb8056b", size = 8760837, upload-time = "2025-07-14T20:12:59.59Z" }, - { url = "https://files.pythonhosted.org/packages/9f/8a/1403d0353f8c5a2f0829d2b1c4becbf9da2f0a4d040886404fc4a5431e4d/pywin32-311-cp39-cp39-win_amd64.whl", hash = "sha256:e0c4cfb0621281fe40387df582097fd796e80430597cb9944f0ae70447bacd91", size = 9590187, upload-time = "2025-07-14T20:13:01.419Z" }, - { url = "https://files.pythonhosted.org/packages/60/22/e0e8d802f124772cec9c75430b01a212f86f9de7546bda715e54140d5aeb/pywin32-311-cp39-cp39-win_arm64.whl", hash = "sha256:62ea666235135fee79bb154e695f3ff67370afefd71bd7fea7512fc70ef31e3d", size = 8778162, upload-time = "2025-07-14T20:13:03.544Z" }, ] [[package]] @@ -3037,15 +2147,6 @@ version = "6.0.2" source = { registry = "https://pypi.org/simple" } sdist = { url = "https://files.pythonhosted.org/packages/54/ed/79a089b6be93607fa5cdaedf301d7dfb23af5f25c398d5ead2525b063e17/pyyaml-6.0.2.tar.gz", hash = "sha256:d584d9ec91ad65861cc08d42e834324ef890a082e591037abe114850ff7bbc3e", size = 130631, upload-time = "2024-08-06T20:33:50.674Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/9b/95/a3fac87cb7158e231b5a6012e438c647e1a87f09f8e0d123acec8ab8bf71/PyYAML-6.0.2-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:0a9a2848a5b7feac301353437eb7d5957887edbf81d56e903999a75a3d743086", size = 184199, upload-time = "2024-08-06T20:31:40.178Z" }, - { url = "https://files.pythonhosted.org/packages/c7/7a/68bd47624dab8fd4afbfd3c48e3b79efe09098ae941de5b58abcbadff5cb/PyYAML-6.0.2-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:29717114e51c84ddfba879543fb232a6ed60086602313ca38cce623c1d62cfbf", size = 171758, upload-time = "2024-08-06T20:31:42.173Z" }, - { url = "https://files.pythonhosted.org/packages/49/ee/14c54df452143b9ee9f0f29074d7ca5516a36edb0b4cc40c3f280131656f/PyYAML-6.0.2-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:8824b5a04a04a047e72eea5cec3bc266db09e35de6bdfe34c9436ac5ee27d237", size = 718463, upload-time = "2024-08-06T20:31:44.263Z" }, - { url = "https://files.pythonhosted.org/packages/4d/61/de363a97476e766574650d742205be468921a7b532aa2499fcd886b62530/PyYAML-6.0.2-cp310-cp310-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:7c36280e6fb8385e520936c3cb3b8042851904eba0e58d277dca80a5cfed590b", size = 719280, upload-time = "2024-08-06T20:31:50.199Z" }, - { url = "https://files.pythonhosted.org/packages/6b/4e/1523cb902fd98355e2e9ea5e5eb237cbc5f3ad5f3075fa65087aa0ecb669/PyYAML-6.0.2-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:ec031d5d2feb36d1d1a24380e4db6d43695f3748343d99434e6f5f9156aaa2ed", size = 751239, upload-time = "2024-08-06T20:31:52.292Z" }, - { url = "https://files.pythonhosted.org/packages/b7/33/5504b3a9a4464893c32f118a9cc045190a91637b119a9c881da1cf6b7a72/PyYAML-6.0.2-cp310-cp310-musllinux_1_1_aarch64.whl", hash = "sha256:936d68689298c36b53b29f23c6dbb74de12b4ac12ca6cfe0e047bedceea56180", size = 695802, upload-time = "2024-08-06T20:31:53.836Z" }, - { url = "https://files.pythonhosted.org/packages/5c/20/8347dcabd41ef3a3cdc4f7b7a2aff3d06598c8779faa189cdbf878b626a4/PyYAML-6.0.2-cp310-cp310-musllinux_1_1_x86_64.whl", hash = "sha256:23502f431948090f597378482b4812b0caae32c22213aecf3b55325e049a6c68", size = 720527, upload-time = "2024-08-06T20:31:55.565Z" }, - { url = "https://files.pythonhosted.org/packages/be/aa/5afe99233fb360d0ff37377145a949ae258aaab831bde4792b32650a4378/PyYAML-6.0.2-cp310-cp310-win32.whl", hash = "sha256:2e99c6826ffa974fe6e27cdb5ed0021786b03fc98e5ee3c5bfe1fd5015f42b99", size = 144052, upload-time = "2024-08-06T20:31:56.914Z" }, - { url = "https://files.pythonhosted.org/packages/b5/84/0fa4b06f6d6c958d207620fc60005e241ecedceee58931bb20138e1e5776/PyYAML-6.0.2-cp310-cp310-win_amd64.whl", hash = "sha256:a4d3091415f010369ae4ed1fc6b79def9416358877534caf6a0fdd2146c87a3e", size = 161774, upload-time = "2024-08-06T20:31:58.304Z" }, { url = "https://files.pythonhosted.org/packages/f8/aa/7af4e81f7acba21a4c6be026da38fd2b872ca46226673c89a758ebdc4fd2/PyYAML-6.0.2-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:cc1c1159b3d456576af7a3e4d1ba7e6924cb39de8f67111c735f6fc832082774", size = 184612, upload-time = "2024-08-06T20:32:03.408Z" }, { url = "https://files.pythonhosted.org/packages/8b/62/b9faa998fd185f65c1371643678e4d58254add437edb764a08c5a98fb986/PyYAML-6.0.2-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:1e2120ef853f59c7419231f3bf4e7021f1b936f6ebd222406c3b60212205d2ee", size = 172040, upload-time = "2024-08-06T20:32:04.926Z" }, { url = "https://files.pythonhosted.org/packages/ad/0c/c804f5f922a9a6563bab712d8dcc70251e8af811fce4524d57c2c0fd49a4/PyYAML-6.0.2-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:5d225db5a45f21e78dd9358e58a98702a0302f2659a3c6cd320564b75b86f47c", size = 736829, upload-time = "2024-08-06T20:32:06.459Z" }, @@ -3073,15 +2174,6 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/fe/0f/25911a9f080464c59fab9027482f822b86bf0608957a5fcc6eaac85aa515/PyYAML-6.0.2-cp313-cp313-musllinux_1_1_x86_64.whl", hash = "sha256:68ccc6023a3400877818152ad9a1033e3db8625d899c72eacb5a668902e4d652", size = 751597, upload-time = "2024-08-06T20:32:56.985Z" }, { url = "https://files.pythonhosted.org/packages/14/0d/e2c3b43bbce3cf6bd97c840b46088a3031085179e596d4929729d8d68270/PyYAML-6.0.2-cp313-cp313-win32.whl", hash = "sha256:bc2fa7c6b47d6bc618dd7fb02ef6fdedb1090ec036abab80d4681424b84c1183", size = 140527, upload-time = "2024-08-06T20:33:03.001Z" }, { url = "https://files.pythonhosted.org/packages/fa/de/02b54f42487e3d3c6efb3f89428677074ca7bf43aae402517bc7cca949f3/PyYAML-6.0.2-cp313-cp313-win_amd64.whl", hash = "sha256:8388ee1976c416731879ac16da0aff3f63b286ffdd57cdeb95f3f2e085687563", size = 156446, upload-time = "2024-08-06T20:33:04.33Z" }, - { url = "https://files.pythonhosted.org/packages/65/d8/b7a1db13636d7fb7d4ff431593c510c8b8fca920ade06ca8ef20015493c5/PyYAML-6.0.2-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:688ba32a1cffef67fd2e9398a2efebaea461578b0923624778664cc1c914db5d", size = 184777, upload-time = "2024-08-06T20:33:25.896Z" }, - { url = "https://files.pythonhosted.org/packages/0a/02/6ec546cd45143fdf9840b2c6be8d875116a64076218b61d68e12548e5839/PyYAML-6.0.2-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:a8786accb172bd8afb8be14490a16625cbc387036876ab6ba70912730faf8e1f", size = 172318, upload-time = "2024-08-06T20:33:27.212Z" }, - { url = "https://files.pythonhosted.org/packages/0e/9a/8cc68be846c972bda34f6c2a93abb644fb2476f4dcc924d52175786932c9/PyYAML-6.0.2-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:d8e03406cac8513435335dbab54c0d385e4a49e4945d2909a581c83647ca0290", size = 720891, upload-time = "2024-08-06T20:33:28.974Z" }, - { url = "https://files.pythonhosted.org/packages/e9/6c/6e1b7f40181bc4805e2e07f4abc10a88ce4648e7e95ff1abe4ae4014a9b2/PyYAML-6.0.2-cp39-cp39-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:f753120cb8181e736c57ef7636e83f31b9c0d1722c516f7e86cf15b7aa57ff12", size = 722614, upload-time = "2024-08-06T20:33:34.157Z" }, - { url = "https://files.pythonhosted.org/packages/3d/32/e7bd8535d22ea2874cef6a81021ba019474ace0d13a4819c2a4bce79bd6a/PyYAML-6.0.2-cp39-cp39-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:3b1fdb9dc17f5a7677423d508ab4f243a726dea51fa5e70992e59a7411c89d19", size = 737360, upload-time = "2024-08-06T20:33:35.84Z" }, - { url = "https://files.pythonhosted.org/packages/d7/12/7322c1e30b9be969670b672573d45479edef72c9a0deac3bb2868f5d7469/PyYAML-6.0.2-cp39-cp39-musllinux_1_1_aarch64.whl", hash = "sha256:0b69e4ce7a131fe56b7e4d770c67429700908fc0752af059838b1cfb41960e4e", size = 699006, upload-time = "2024-08-06T20:33:37.501Z" }, - { url = "https://files.pythonhosted.org/packages/82/72/04fcad41ca56491995076630c3ec1e834be241664c0c09a64c9a2589b507/PyYAML-6.0.2-cp39-cp39-musllinux_1_1_x86_64.whl", hash = "sha256:a9f8c2e67970f13b16084e04f134610fd1d374bf477b17ec1599185cf611d725", size = 723577, upload-time = "2024-08-06T20:33:39.389Z" }, - { url = "https://files.pythonhosted.org/packages/ed/5e/46168b1f2757f1fcd442bc3029cd8767d88a98c9c05770d8b420948743bb/PyYAML-6.0.2-cp39-cp39-win32.whl", hash = "sha256:6395c297d42274772abc367baaa79683958044e5d3835486c16da75d2a694631", size = 144593, upload-time = "2024-08-06T20:33:46.63Z" }, - { url = "https://files.pythonhosted.org/packages/19/87/5124b1c1f2412bb95c59ec481eaf936cd32f0fe2a7b16b97b81c4c017a6a/PyYAML-6.0.2-cp39-cp39-win_amd64.whl", hash = "sha256:39693e1f8320ae4f43943590b49779ffb98acb81f788220ea932a6b6c51004d8", size = 162312, upload-time = "2024-08-06T20:33:49.073Z" }, ] [[package]] @@ -3093,16 +2185,6 @@ dependencies = [ ] sdist = { url = "https://files.pythonhosted.org/packages/f8/66/159f38d184f08b5f971b467f87b1ab142ab1320d5200825c824b32b84b66/pyzmq-27.0.2.tar.gz", hash = "sha256:b398dd713b18de89730447347e96a0240225e154db56e35b6bb8447ffdb07798", size = 281440, upload-time = "2025-08-21T04:23:26.334Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/83/4d/2081cd7e41e340004d2051821efe1d0d67d31bdb5ac33bffc7e628d5f1bd/pyzmq-27.0.2-cp310-cp310-macosx_10_15_universal2.whl", hash = "sha256:8b32c4636ced87dce0ac3d671e578b3400215efab372f1b4be242e8cf0b11384", size = 1329839, upload-time = "2025-08-21T04:20:55.8Z" }, - { url = "https://files.pythonhosted.org/packages/ad/f1/1300b7e932671e31accb3512c19b43e6a3e8d08c54ab8b920308e53427ce/pyzmq-27.0.2-cp310-cp310-manylinux2014_i686.manylinux_2_17_i686.whl", hash = "sha256:f9528a4b3e24189cb333a9850fddbbafaa81df187297cfbddee50447cdb042cf", size = 906367, upload-time = "2025-08-21T04:20:58.476Z" }, - { url = "https://files.pythonhosted.org/packages/e6/80/61662db85eb3255a58c1bb59f6d4fc0d31c9c75b9a14983deafab12b2329/pyzmq-27.0.2-cp310-cp310-manylinux_2_26_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:3b02ba0c0b2b9ebe74688002e6c56c903429924a25630804b9ede1f178aa5a3f", size = 666545, upload-time = "2025-08-21T04:20:59.775Z" }, - { url = "https://files.pythonhosted.org/packages/5c/6e/49fb9c75b039978cbb1f3657811d8056b0ebe6ecafd78a4457fc6de19799/pyzmq-27.0.2-cp310-cp310-manylinux_2_26_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:9e4dc5c9a6167617251dea0d024d67559795761aabb4b7ea015518be898be076", size = 854219, upload-time = "2025-08-21T04:21:01.807Z" }, - { url = "https://files.pythonhosted.org/packages/b0/3c/9951b302d221e471b7c659e70f9cb64db5f68fa3b7da45809ec4e6c6ef17/pyzmq-27.0.2-cp310-cp310-musllinux_1_2_aarch64.whl", hash = "sha256:f1151b33aaf3b4fa9da26f4d696e38eebab67d1b43c446184d733c700b3ff8ce", size = 1655103, upload-time = "2025-08-21T04:21:03.239Z" }, - { url = "https://files.pythonhosted.org/packages/88/ca/d7adea6100fdf7f87f3856db02d2a0a45ce2764b9f60ba08c48c655b762f/pyzmq-27.0.2-cp310-cp310-musllinux_1_2_i686.whl", hash = "sha256:4ecfc7999ac44c9ef92b5ae8f0b44fb935297977df54d8756b195a3cd12f38f0", size = 2033712, upload-time = "2025-08-21T04:21:05.121Z" }, - { url = "https://files.pythonhosted.org/packages/e9/63/b34e601b36ba4864d02ac1460443fc39bf533dedbdeead2a4e0df7dfc8ee/pyzmq-27.0.2-cp310-cp310-musllinux_1_2_x86_64.whl", hash = "sha256:31c26a5d0b00befcaeeb600d8b15ad09f5604b6f44e2057ec5e521a9e18dcd9a", size = 1891847, upload-time = "2025-08-21T04:21:06.586Z" }, - { url = "https://files.pythonhosted.org/packages/cf/a2/9479e6af779da44f788d5fcda5f77dff1af988351ef91682b92524eab2db/pyzmq-27.0.2-cp310-cp310-win32.whl", hash = "sha256:25a100d2de2ac0c644ecf4ce0b509a720d12e559c77aff7e7e73aa684f0375bc", size = 567136, upload-time = "2025-08-21T04:21:07.885Z" }, - { url = "https://files.pythonhosted.org/packages/58/46/e1c2be469781fc56ba092fecb1bb336cedde0fd87d9e1a547aaeb5d1a968/pyzmq-27.0.2-cp310-cp310-win_amd64.whl", hash = "sha256:a1acf091f53bb406e9e5e7383e467d1dd1b94488b8415b890917d30111a1fef3", size = 631969, upload-time = "2025-08-21T04:21:09.5Z" }, - { url = "https://files.pythonhosted.org/packages/d5/8d/d20a62f1f77e3f04633a80bb83df085e4314f0e9404619cc458d0005d6ab/pyzmq-27.0.2-cp310-cp310-win_arm64.whl", hash = "sha256:b38e01f11e9e95f6668dc8a62dccf9483f454fed78a77447507a0e8dcbd19a63", size = 559459, upload-time = "2025-08-21T04:21:11.208Z" }, { url = "https://files.pythonhosted.org/packages/42/73/034429ab0f4316bf433eb6c20c3f49d1dc13b2ed4e4d951b283d300a0f35/pyzmq-27.0.2-cp311-cp311-macosx_10_15_universal2.whl", hash = "sha256:063845960df76599ad4fad69fa4d884b3ba38304272104fdcd7e3af33faeeb1d", size = 1333169, upload-time = "2025-08-21T04:21:12.483Z" }, { url = "https://files.pythonhosted.org/packages/35/02/c42b3b526eb03a570c889eea85a5602797f800a50ba8b09ddbf7db568b78/pyzmq-27.0.2-cp311-cp311-manylinux2014_i686.manylinux_2_17_i686.whl", hash = "sha256:845a35fb21b88786aeb38af8b271d41ab0967985410f35411a27eebdc578a076", size = 909176, upload-time = "2025-08-21T04:21:13.835Z" }, { url = "https://files.pythonhosted.org/packages/1b/35/a1c0b988fabbdf2dc5fe94b7c2bcfd61e3533e5109297b8e0daf1d7a8d2d/pyzmq-27.0.2-cp311-cp311-manylinux_2_26_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:515d20b5c3c86db95503faa989853a8ab692aab1e5336db011cd6d35626c4cb1", size = 668972, upload-time = "2025-08-21T04:21:15.315Z" }, @@ -3145,31 +2227,11 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/4b/ed/42de80b7ab4e8fcf13376f81206cf8041740672ac1fd2e1c598d63f595bf/pyzmq-27.0.2-cp314-cp314t-win32.whl", hash = "sha256:0f6e9b00d81b58f859fffc112365d50413954e02aefe36c5b4c8fb4af79f8cc3", size = 587526, upload-time = "2025-08-21T04:22:14.18Z" }, { url = "https://files.pythonhosted.org/packages/ed/c8/8f3c72d6f0bfbf090aa5e283576073ca5c59839b85a5cc8c66ddb9b59801/pyzmq-27.0.2-cp314-cp314t-win_amd64.whl", hash = "sha256:2e73cf3b127a437fef4100eb3ac2ebe6b49e655bb721329f667f59eca0a26221", size = 661368, upload-time = "2025-08-21T04:22:15.677Z" }, { url = "https://files.pythonhosted.org/packages/69/a4/7ee652ea1c77d872f5d99ed937fa8bbd1f6f4b7a39a6d3a0076c286e0c3e/pyzmq-27.0.2-cp314-cp314t-win_arm64.whl", hash = "sha256:4108785f2e5ac865d06f678a07a1901e3465611356df21a545eeea8b45f56265", size = 574901, upload-time = "2025-08-21T04:22:17.423Z" }, - { url = "https://files.pythonhosted.org/packages/c8/57/a4aff0dbb29001cebf94848a980ac36bea8e1cb65ac0f72ff03e484df208/pyzmq-27.0.2-cp39-cp39-macosx_10_15_universal2.whl", hash = "sha256:aa9c1c208c263b84386ac25bed6af5672397dc3c232638114fc09bca5c7addf9", size = 1330291, upload-time = "2025-08-21T04:22:34.433Z" }, - { url = "https://files.pythonhosted.org/packages/e1/a6/8519ed48b5bdd72e1a3946a76987afb6659a355180cd15e51cea6afe1983/pyzmq-27.0.2-cp39-cp39-manylinux2014_i686.manylinux_2_17_i686.whl", hash = "sha256:795c4884cfe7ea59f2b67d82b417e899afab889d332bfda13b02f8e0c155b2e4", size = 906596, upload-time = "2025-08-21T04:22:36.168Z" }, - { url = "https://files.pythonhosted.org/packages/3c/19/cb6f463d6454079a566160d10cf175c3c59274b1378104d2b01a5a1ca560/pyzmq-27.0.2-cp39-cp39-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:47eb65bb25478358ba3113dd9a08344f616f417ad3ffcbb190cd874fae72b1b1", size = 863605, upload-time = "2025-08-21T04:22:37.825Z" }, - { url = "https://files.pythonhosted.org/packages/9d/37/afe25d5f9b91b7422067bb7cd3f75691d78baa0c2c2f76bd0643389ac600/pyzmq-27.0.2-cp39-cp39-manylinux_2_26_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:a6fc24f00293f10aff04d55ca37029b280474c91f4de2cad5e911e5e10d733b7", size = 666777, upload-time = "2025-08-21T04:22:39.551Z" }, - { url = "https://files.pythonhosted.org/packages/15/4a/97b0b59a7eacfd9e61c40abaa9c7e2587bbb39671816a9fb0c5dc32d87bc/pyzmq-27.0.2-cp39-cp39-musllinux_1_2_aarch64.whl", hash = "sha256:58d4cc9b6b768478adfc40a5cbee545303db8dbc81ba688474e0f499cc581028", size = 1655404, upload-time = "2025-08-21T04:22:41.082Z" }, - { url = "https://files.pythonhosted.org/packages/27/db/bcf03bea232839ea749562f76bda3807b16a25c68fdd153339a56699bb1d/pyzmq-27.0.2-cp39-cp39-musllinux_1_2_i686.whl", hash = "sha256:cea2f26c5972796e02b222968a21a378d09eb4ff590eb3c5fafa8913f8c2bdf5", size = 2034051, upload-time = "2025-08-21T04:22:43.216Z" }, - { url = "https://files.pythonhosted.org/packages/57/3f/9cc291101ee766ab41bae25654af84c11d259f27d73b1ed985256dae8a16/pyzmq-27.0.2-cp39-cp39-musllinux_1_2_x86_64.whl", hash = "sha256:a0621ec020c49fc1b6e31304f1a820900d54e7d9afa03ea1634264bf9387519e", size = 1892080, upload-time = "2025-08-21T04:22:44.81Z" }, - { url = "https://files.pythonhosted.org/packages/16/52/51d4c56c553e7368abc2d010a783092c68a1888045f899eabcfd3bc345f3/pyzmq-27.0.2-cp39-cp39-win32.whl", hash = "sha256:1326500792a9cb0992db06bbaf5d0098459133868932b81a6e90d45c39eca99d", size = 567463, upload-time = "2025-08-21T04:22:46.853Z" }, - { url = "https://files.pythonhosted.org/packages/93/20/3b0ac853b5bbe1fd05e1898ea60335f9d402312328fa17514a876418ab11/pyzmq-27.0.2-cp39-cp39-win_amd64.whl", hash = "sha256:5ee9560cb1e3094ef01fc071b361121a57ebb8d4232912b6607a6d7d2d0a97b4", size = 632200, upload-time = "2025-08-21T04:22:48.34Z" }, - { url = "https://files.pythonhosted.org/packages/b0/14/78b192a7864a38c71e30c3fd668eff949b44db13c1906035e6095fecd00d/pyzmq-27.0.2-cp39-cp39-win_arm64.whl", hash = "sha256:85e3c6fb0d25ea046ebcfdc2bcb9683d663dc0280645c79a616ff5077962a15b", size = 559638, upload-time = "2025-08-21T04:22:50.002Z" }, - { url = "https://files.pythonhosted.org/packages/19/d7/e388e80107b7c438c9698ce59c2a3b950021cd4ab3fe641485e4ed6b0960/pyzmq-27.0.2-pp310-pypy310_pp73-macosx_10_15_x86_64.whl", hash = "sha256:d67a0960803a37b60f51b460c58444bc7033a804c662f5735172e21e74ee4902", size = 836008, upload-time = "2025-08-21T04:22:51.842Z" }, - { url = "https://files.pythonhosted.org/packages/65/ef/58d3eb85f1b67a16e22adb07d084f975a7b9641463d18e27230550bb436a/pyzmq-27.0.2-pp310-pypy310_pp73-manylinux2014_i686.manylinux_2_17_i686.whl", hash = "sha256:dd4d3e6a567ffd0d232cfc667c49d0852d0ee7481458a2a1593b9b1bc5acba88", size = 799932, upload-time = "2025-08-21T04:22:53.529Z" }, - { url = "https://files.pythonhosted.org/packages/3c/63/66b9f6db19ee8c86105ffd4475a4f5d93cdd62b1edcb1e894d971df0728c/pyzmq-27.0.2-pp310-pypy310_pp73-manylinux_2_26_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:5e558be423631704803bc6a642e2caa96083df759e25fe6eb01f2d28725f80bd", size = 567458, upload-time = "2025-08-21T04:22:55.289Z" }, - { url = "https://files.pythonhosted.org/packages/10/af/d92207fe8b6e3d9f588d0591219a86dd7b4ed27bb3e825c1d9cf48467fc0/pyzmq-27.0.2-pp310-pypy310_pp73-manylinux_2_26_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:c4c20ba8389f495c7b4f6b896bb1ca1e109a157d4f189267a902079699aaf787", size = 747087, upload-time = "2025-08-21T04:22:56.994Z" }, - { url = "https://files.pythonhosted.org/packages/82/e9/d9f8b4b191c6733e31de28974d608a2475a6598136ac901a8c5b67c11432/pyzmq-27.0.2-pp310-pypy310_pp73-win_amd64.whl", hash = "sha256:c5be232f7219414ff672ff7ab8c5a7e8632177735186d8a42b57b491fafdd64e", size = 544641, upload-time = "2025-08-21T04:22:58.87Z" }, { url = "https://files.pythonhosted.org/packages/c7/60/027d0032a1e3b1aabcef0e309b9ff8a4099bdd5a60ab38b36a676ff2bd7b/pyzmq-27.0.2-pp311-pypy311_pp73-macosx_10_15_x86_64.whl", hash = "sha256:e297784aea724294fe95e442e39a4376c2f08aa4fae4161c669f047051e31b02", size = 836007, upload-time = "2025-08-21T04:23:00.447Z" }, { url = "https://files.pythonhosted.org/packages/25/20/2ed1e6168aaea323df9bb2c451309291f53ba3af372ffc16edd4ce15b9e5/pyzmq-27.0.2-pp311-pypy311_pp73-manylinux2014_i686.manylinux_2_17_i686.whl", hash = "sha256:e3659a79ded9745bc9c2aef5b444ac8805606e7bc50d2d2eb16dc3ab5483d91f", size = 799932, upload-time = "2025-08-21T04:23:02.052Z" }, { url = "https://files.pythonhosted.org/packages/fd/25/5c147307de546b502c9373688ce5b25dc22288d23a1ebebe5d587bf77610/pyzmq-27.0.2-pp311-pypy311_pp73-manylinux_2_26_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:f3dba49ff037d02373a9306b58d6c1e0be031438f822044e8767afccfdac4c6b", size = 567459, upload-time = "2025-08-21T04:23:03.593Z" }, { url = "https://files.pythonhosted.org/packages/71/06/0dc56ffc615c8095cd089c9b98ce5c733e990f09ce4e8eea4aaf1041a532/pyzmq-27.0.2-pp311-pypy311_pp73-manylinux_2_26_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:de84e1694f9507b29e7b263453a2255a73e3d099d258db0f14539bad258abe41", size = 747088, upload-time = "2025-08-21T04:23:05.334Z" }, { url = "https://files.pythonhosted.org/packages/06/f6/4a50187e023b8848edd3f0a8e197b1a7fb08d261d8c60aae7cb6c3d71612/pyzmq-27.0.2-pp311-pypy311_pp73-win_amd64.whl", hash = "sha256:f0944d65ba2b872b9fcece08411d6347f15a874c775b4c3baae7f278550da0fb", size = 544639, upload-time = "2025-08-21T04:23:07.279Z" }, - { url = "https://files.pythonhosted.org/packages/c6/ce/36c82244b111c408c99037b61eedcaac9a3f17300c3f857cf175a7b8832f/pyzmq-27.0.2-pp39-pypy39_pp73-macosx_10_15_x86_64.whl", hash = "sha256:400f34321e3bd89b1165b91ea6b18ad26042ba9ad0dfed8b35049e2e24eeab9b", size = 836001, upload-time = "2025-08-21T04:23:17.532Z" }, - { url = "https://files.pythonhosted.org/packages/6f/dc/4aa8e518f1c0d0ee7fb98cf54cf3854e471815040062d26d948df638d9bc/pyzmq-27.0.2-pp39-pypy39_pp73-manylinux2014_i686.manylinux_2_17_i686.whl", hash = "sha256:9cbad4ef12e4c15c94d2c24ecd15a8ed56bf091c62f121a2b0c618ddd4b7402b", size = 799927, upload-time = "2025-08-21T04:23:19.218Z" }, - { url = "https://files.pythonhosted.org/packages/43/4a/cca781133ce015898b7c4ad63240d05af5c54e8736d099c95b0a6c9aaca9/pyzmq-27.0.2-pp39-pypy39_pp73-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:6b2b74aac3392b8cf508ccb68c980a8555298cd378434a2d065d6ce0f4211dff", size = 758427, upload-time = "2025-08-21T04:23:21.204Z" }, - { url = "https://files.pythonhosted.org/packages/e3/66/772bf1e3aed8b20587f2e0122bc929711d44ddd27dbbd6eb308a5badd0c3/pyzmq-27.0.2-pp39-pypy39_pp73-manylinux_2_26_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:7db5db88c24cf9253065d69229a148ff60821e5d6f8ff72579b1f80f8f348bab", size = 567453, upload-time = "2025-08-21T04:23:22.825Z" }, - { url = "https://files.pythonhosted.org/packages/e0/fb/fdc3dd8ec46f5226b4cde299839f10c625886bd18adbeaa8a59ffe104356/pyzmq-27.0.2-pp39-pypy39_pp73-win_amd64.whl", hash = "sha256:8ffe40c216c41756ca05188c3e24a23142334b304f7aebd75c24210385e35573", size = 544636, upload-time = "2025-08-21T04:23:24.736Z" }, ] [[package]] @@ -3207,20 +2269,6 @@ version = "0.27.1" source = { registry = "https://pypi.org/simple" } sdist = { url = "https://files.pythonhosted.org/packages/e9/dd/2c0cbe774744272b0ae725f44032c77bdcab6e8bcf544bffa3b6e70c8dba/rpds_py-0.27.1.tar.gz", hash = "sha256:26a1c73171d10b7acccbded82bf6a586ab8203601e565badc74bbbf8bc5a10f8", size = 27479, upload-time = "2025-08-27T12:16:36.024Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/a5/ed/3aef893e2dd30e77e35d20d4ddb45ca459db59cead748cad9796ad479411/rpds_py-0.27.1-cp310-cp310-macosx_10_12_x86_64.whl", hash = "sha256:68afeec26d42ab3b47e541b272166a0b4400313946871cba3ed3a4fc0cab1cef", size = 371606, upload-time = "2025-08-27T12:12:25.189Z" }, - { url = "https://files.pythonhosted.org/packages/6d/82/9818b443e5d3eb4c83c3994561387f116aae9833b35c484474769c4a8faf/rpds_py-0.27.1-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:74e5b2f7bb6fa38b1b10546d27acbacf2a022a8b5543efb06cfebc72a59c85be", size = 353452, upload-time = "2025-08-27T12:12:27.433Z" }, - { url = "https://files.pythonhosted.org/packages/99/c7/d2a110ffaaa397fc6793a83c7bd3545d9ab22658b7cdff05a24a4535cc45/rpds_py-0.27.1-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:9024de74731df54546fab0bfbcdb49fae19159ecaecfc8f37c18d2c7e2c0bd61", size = 381519, upload-time = "2025-08-27T12:12:28.719Z" }, - { url = "https://files.pythonhosted.org/packages/5a/bc/e89581d1f9d1be7d0247eaef602566869fdc0d084008ba139e27e775366c/rpds_py-0.27.1-cp310-cp310-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:31d3ebadefcd73b73928ed0b2fd696f7fefda8629229f81929ac9c1854d0cffb", size = 394424, upload-time = "2025-08-27T12:12:30.207Z" }, - { url = "https://files.pythonhosted.org/packages/ac/2e/36a6861f797530e74bb6ed53495f8741f1ef95939eed01d761e73d559067/rpds_py-0.27.1-cp310-cp310-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:b2e7f8f169d775dd9092a1743768d771f1d1300453ddfe6325ae3ab5332b4657", size = 523467, upload-time = "2025-08-27T12:12:31.808Z" }, - { url = "https://files.pythonhosted.org/packages/c4/59/c1bc2be32564fa499f988f0a5c6505c2f4746ef96e58e4d7de5cf923d77e/rpds_py-0.27.1-cp310-cp310-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:3d905d16f77eb6ab2e324e09bfa277b4c8e5e6b8a78a3e7ff8f3cdf773b4c013", size = 402660, upload-time = "2025-08-27T12:12:33.444Z" }, - { url = "https://files.pythonhosted.org/packages/0a/ec/ef8bf895f0628dd0a59e54d81caed6891663cb9c54a0f4bb7da918cb88cf/rpds_py-0.27.1-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:50c946f048209e6362e22576baea09193809f87687a95a8db24e5fbdb307b93a", size = 384062, upload-time = "2025-08-27T12:12:34.857Z" }, - { url = "https://files.pythonhosted.org/packages/69/f7/f47ff154be8d9a5e691c083a920bba89cef88d5247c241c10b9898f595a1/rpds_py-0.27.1-cp310-cp310-manylinux_2_31_riscv64.whl", hash = "sha256:3deab27804d65cd8289eb814c2c0e807c4b9d9916c9225e363cb0cf875eb67c1", size = 401289, upload-time = "2025-08-27T12:12:36.085Z" }, - { url = "https://files.pythonhosted.org/packages/3b/d9/ca410363efd0615814ae579f6829cafb39225cd63e5ea5ed1404cb345293/rpds_py-0.27.1-cp310-cp310-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:8b61097f7488de4be8244c89915da8ed212832ccf1e7c7753a25a394bf9b1f10", size = 417718, upload-time = "2025-08-27T12:12:37.401Z" }, - { url = "https://files.pythonhosted.org/packages/e3/a0/8cb5c2ff38340f221cc067cc093d1270e10658ba4e8d263df923daa18e86/rpds_py-0.27.1-cp310-cp310-musllinux_1_2_aarch64.whl", hash = "sha256:8a3f29aba6e2d7d90528d3c792555a93497fe6538aa65eb675b44505be747808", size = 558333, upload-time = "2025-08-27T12:12:38.672Z" }, - { url = "https://files.pythonhosted.org/packages/6f/8c/1b0de79177c5d5103843774ce12b84caa7164dfc6cd66378768d37db11bf/rpds_py-0.27.1-cp310-cp310-musllinux_1_2_i686.whl", hash = "sha256:dd6cd0485b7d347304067153a6dc1d73f7d4fd995a396ef32a24d24b8ac63ac8", size = 589127, upload-time = "2025-08-27T12:12:41.48Z" }, - { url = "https://files.pythonhosted.org/packages/c8/5e/26abb098d5e01266b0f3a2488d299d19ccc26849735d9d2b95c39397e945/rpds_py-0.27.1-cp310-cp310-musllinux_1_2_x86_64.whl", hash = "sha256:6f4461bf931108c9fa226ffb0e257c1b18dc2d44cd72b125bec50ee0ab1248a9", size = 554899, upload-time = "2025-08-27T12:12:42.925Z" }, - { url = "https://files.pythonhosted.org/packages/de/41/905cc90ced13550db017f8f20c6d8e8470066c5738ba480d7ba63e3d136b/rpds_py-0.27.1-cp310-cp310-win32.whl", hash = "sha256:ee5422d7fb21f6a00c1901bf6559c49fee13a5159d0288320737bbf6585bd3e4", size = 217450, upload-time = "2025-08-27T12:12:44.813Z" }, - { url = "https://files.pythonhosted.org/packages/75/3d/6bef47b0e253616ccdf67c283e25f2d16e18ccddd38f92af81d5a3420206/rpds_py-0.27.1-cp310-cp310-win_amd64.whl", hash = "sha256:3e039aabf6d5f83c745d5f9a0a381d031e9ed871967c0a5c38d201aca41f3ba1", size = 228447, upload-time = "2025-08-27T12:12:46.204Z" }, { url = "https://files.pythonhosted.org/packages/b5/c1/7907329fbef97cbd49db6f7303893bd1dd5a4a3eae415839ffdfb0762cae/rpds_py-0.27.1-cp311-cp311-macosx_10_12_x86_64.whl", hash = "sha256:be898f271f851f68b318872ce6ebebbc62f303b654e43bf72683dbdc25b7c881", size = 371063, upload-time = "2025-08-27T12:12:47.856Z" }, { url = "https://files.pythonhosted.org/packages/11/94/2aab4bc86228bcf7c48760990273653a4900de89c7537ffe1b0d6097ed39/rpds_py-0.27.1-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:62ac3d4e3e07b58ee0ddecd71d6ce3b1637de2d373501412df395a0ec5f9beb5", size = 353210, upload-time = "2025-08-27T12:12:49.187Z" }, { url = "https://files.pythonhosted.org/packages/3a/57/f5eb3ecf434342f4f1a46009530e93fd201a0b5b83379034ebdb1d7c1a58/rpds_py-0.27.1-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:4708c5c0ceb2d034f9991623631d3d23cb16e65c83736ea020cdbe28d57c0a0e", size = 381636, upload-time = "2025-08-27T12:12:50.492Z" }, @@ -3309,33 +2357,6 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/dd/10/6b283707780a81919f71625351182b4f98932ac89a09023cb61865136244/rpds_py-0.27.1-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:f39f58a27cc6e59f432b568ed8429c7e1641324fbe38131de852cd77b2d534b0", size = 555813, upload-time = "2025-08-27T12:15:00.334Z" }, { url = "https://files.pythonhosted.org/packages/04/2e/30b5ea18c01379da6272a92825dd7e53dc9d15c88a19e97932d35d430ef7/rpds_py-0.27.1-cp314-cp314t-win32.whl", hash = "sha256:d5fa0ee122dc09e23607a28e6d7b150da16c662e66409bbe85230e4c85bb528a", size = 217385, upload-time = "2025-08-27T12:15:01.937Z" }, { url = "https://files.pythonhosted.org/packages/32/7d/97119da51cb1dd3f2f3c0805f155a3aa4a95fa44fe7d78ae15e69edf4f34/rpds_py-0.27.1-cp314-cp314t-win_amd64.whl", hash = "sha256:6567d2bb951e21232c2f660c24cf3470bb96de56cdcb3f071a83feeaff8a2772", size = 230097, upload-time = "2025-08-27T12:15:03.961Z" }, - { url = "https://files.pythonhosted.org/packages/7f/6c/252e83e1ce7583c81f26d1d884b2074d40a13977e1b6c9c50bbf9a7f1f5a/rpds_py-0.27.1-cp39-cp39-macosx_10_12_x86_64.whl", hash = "sha256:c918c65ec2e42c2a78d19f18c553d77319119bf43aa9e2edf7fb78d624355527", size = 372140, upload-time = "2025-08-27T12:15:05.441Z" }, - { url = "https://files.pythonhosted.org/packages/9d/71/949c195d927c5aeb0d0629d329a20de43a64c423a6aa53836290609ef7ec/rpds_py-0.27.1-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:1fea2b1a922c47c51fd07d656324531adc787e415c8b116530a1d29c0516c62d", size = 354086, upload-time = "2025-08-27T12:15:07.404Z" }, - { url = "https://files.pythonhosted.org/packages/9f/02/e43e332ad8ce4f6c4342d151a471a7f2900ed1d76901da62eb3762663a71/rpds_py-0.27.1-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:bbf94c58e8e0cd6b6f38d8de67acae41b3a515c26169366ab58bdca4a6883bb8", size = 382117, upload-time = "2025-08-27T12:15:09.275Z" }, - { url = "https://files.pythonhosted.org/packages/d0/05/b0fdeb5b577197ad72812bbdfb72f9a08fa1e64539cc3940b1b781cd3596/rpds_py-0.27.1-cp39-cp39-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:c2a8fed130ce946d5c585eddc7c8eeef0051f58ac80a8ee43bd17835c144c2cc", size = 394520, upload-time = "2025-08-27T12:15:10.727Z" }, - { url = "https://files.pythonhosted.org/packages/67/1f/4cfef98b2349a7585181e99294fa2a13f0af06902048a5d70f431a66d0b9/rpds_py-0.27.1-cp39-cp39-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:037a2361db72ee98d829bc2c5b7cc55598ae0a5e0ec1823a56ea99374cfd73c1", size = 522657, upload-time = "2025-08-27T12:15:12.613Z" }, - { url = "https://files.pythonhosted.org/packages/44/55/ccf37ddc4c6dce7437b335088b5ca18da864b334890e2fe9aa6ddc3f79a9/rpds_py-0.27.1-cp39-cp39-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:5281ed1cc1d49882f9997981c88df1a22e140ab41df19071222f7e5fc4e72125", size = 402967, upload-time = "2025-08-27T12:15:14.113Z" }, - { url = "https://files.pythonhosted.org/packages/74/e5/5903f92e41e293b07707d5bf00ef39a0eb2af7190aff4beaf581a6591510/rpds_py-0.27.1-cp39-cp39-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:2fd50659a069c15eef8aa3d64bbef0d69fd27bb4a50c9ab4f17f83a16cbf8905", size = 384372, upload-time = "2025-08-27T12:15:15.842Z" }, - { url = "https://files.pythonhosted.org/packages/8f/e3/fbb409e18aeefc01e49f5922ac63d2d914328430e295c12183ce56ebf76b/rpds_py-0.27.1-cp39-cp39-manylinux_2_31_riscv64.whl", hash = "sha256:c4b676c4ae3921649a15d28ed10025548e9b561ded473aa413af749503c6737e", size = 401264, upload-time = "2025-08-27T12:15:17.388Z" }, - { url = "https://files.pythonhosted.org/packages/55/79/529ad07794e05cb0f38e2f965fc5bb20853d523976719400acecc447ec9d/rpds_py-0.27.1-cp39-cp39-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:079bc583a26db831a985c5257797b2b5d3affb0386e7ff886256762f82113b5e", size = 418691, upload-time = "2025-08-27T12:15:19.144Z" }, - { url = "https://files.pythonhosted.org/packages/33/39/6554a7fd6d9906fda2521c6d52f5d723dca123529fb719a5b5e074c15e01/rpds_py-0.27.1-cp39-cp39-musllinux_1_2_aarch64.whl", hash = "sha256:4e44099bd522cba71a2c6b97f68e19f40e7d85399de899d66cdb67b32d7cb786", size = 558989, upload-time = "2025-08-27T12:15:21.087Z" }, - { url = "https://files.pythonhosted.org/packages/19/b2/76fa15173b6f9f445e5ef15120871b945fb8dd9044b6b8c7abe87e938416/rpds_py-0.27.1-cp39-cp39-musllinux_1_2_i686.whl", hash = "sha256:e202e6d4188e53c6661af813b46c37ca2c45e497fc558bacc1a7630ec2695aec", size = 589835, upload-time = "2025-08-27T12:15:22.696Z" }, - { url = "https://files.pythonhosted.org/packages/ee/9e/5560a4b39bab780405bed8a88ee85b30178061d189558a86003548dea045/rpds_py-0.27.1-cp39-cp39-musllinux_1_2_x86_64.whl", hash = "sha256:f41f814b8eaa48768d1bb551591f6ba45f87ac76899453e8ccd41dba1289b04b", size = 555227, upload-time = "2025-08-27T12:15:24.278Z" }, - { url = "https://files.pythonhosted.org/packages/52/d7/cd9c36215111aa65724c132bf709c6f35175973e90b32115dedc4ced09cb/rpds_py-0.27.1-cp39-cp39-win32.whl", hash = "sha256:9e71f5a087ead99563c11fdaceee83ee982fd39cf67601f4fd66cb386336ee52", size = 217899, upload-time = "2025-08-27T12:15:25.926Z" }, - { url = "https://files.pythonhosted.org/packages/5b/e0/d75ab7b4dd8ba777f6b365adbdfc7614bbfe7c5f05703031dfa4b61c3d6c/rpds_py-0.27.1-cp39-cp39-win_amd64.whl", hash = "sha256:71108900c9c3c8590697244b9519017a400d9ba26a36c48381b3f64743a44aab", size = 228725, upload-time = "2025-08-27T12:15:27.398Z" }, - { url = "https://files.pythonhosted.org/packages/d5/63/b7cc415c345625d5e62f694ea356c58fb964861409008118f1245f8c3347/rpds_py-0.27.1-pp310-pypy310_pp73-macosx_10_12_x86_64.whl", hash = "sha256:7ba22cb9693df986033b91ae1d7a979bc399237d45fccf875b76f62bb9e52ddf", size = 371360, upload-time = "2025-08-27T12:15:29.218Z" }, - { url = "https://files.pythonhosted.org/packages/e5/8c/12e1b24b560cf378b8ffbdb9dc73abd529e1adcfcf82727dfd29c4a7b88d/rpds_py-0.27.1-pp310-pypy310_pp73-macosx_11_0_arm64.whl", hash = "sha256:5b640501be9288c77738b5492b3fd3abc4ba95c50c2e41273c8a1459f08298d3", size = 353933, upload-time = "2025-08-27T12:15:30.837Z" }, - { url = "https://files.pythonhosted.org/packages/9b/85/1bb2210c1f7a1b99e91fea486b9f0f894aa5da3a5ec7097cbad7dec6d40f/rpds_py-0.27.1-pp310-pypy310_pp73-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:fb08b65b93e0c6dd70aac7f7890a9c0938d5ec71d5cb32d45cf844fb8ae47636", size = 382962, upload-time = "2025-08-27T12:15:32.348Z" }, - { url = "https://files.pythonhosted.org/packages/cc/c9/a839b9f219cf80ed65f27a7f5ddbb2809c1b85c966020ae2dff490e0b18e/rpds_py-0.27.1-pp310-pypy310_pp73-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:d7ff07d696a7a38152ebdb8212ca9e5baab56656749f3d6004b34ab726b550b8", size = 394412, upload-time = "2025-08-27T12:15:33.839Z" }, - { url = "https://files.pythonhosted.org/packages/02/2d/b1d7f928b0b1f4fc2e0133e8051d199b01d7384875adc63b6ddadf3de7e5/rpds_py-0.27.1-pp310-pypy310_pp73-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:fb7c72262deae25366e3b6c0c0ba46007967aea15d1eea746e44ddba8ec58dcc", size = 523972, upload-time = "2025-08-27T12:15:35.377Z" }, - { url = "https://files.pythonhosted.org/packages/a9/af/2cbf56edd2d07716df1aec8a726b3159deb47cb5c27e1e42b71d705a7c2f/rpds_py-0.27.1-pp310-pypy310_pp73-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:7b002cab05d6339716b03a4a3a2ce26737f6231d7b523f339fa061d53368c9d8", size = 403273, upload-time = "2025-08-27T12:15:37.051Z" }, - { url = "https://files.pythonhosted.org/packages/c0/93/425e32200158d44ff01da5d9612c3b6711fe69f606f06e3895511f17473b/rpds_py-0.27.1-pp310-pypy310_pp73-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:23f6b69d1c26c4704fec01311963a41d7de3ee0570a84ebde4d544e5a1859ffc", size = 385278, upload-time = "2025-08-27T12:15:38.571Z" }, - { url = "https://files.pythonhosted.org/packages/eb/1a/1a04a915ecd0551bfa9e77b7672d1937b4b72a0fc204a17deef76001cfb2/rpds_py-0.27.1-pp310-pypy310_pp73-manylinux_2_31_riscv64.whl", hash = "sha256:530064db9146b247351f2a0250b8f00b289accea4596a033e94be2389977de71", size = 402084, upload-time = "2025-08-27T12:15:40.529Z" }, - { url = "https://files.pythonhosted.org/packages/51/f7/66585c0fe5714368b62951d2513b684e5215beaceab2c6629549ddb15036/rpds_py-0.27.1-pp310-pypy310_pp73-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:7b90b0496570bd6b0321724a330d8b545827c4df2034b6ddfc5f5275f55da2ad", size = 419041, upload-time = "2025-08-27T12:15:42.191Z" }, - { url = "https://files.pythonhosted.org/packages/8e/7e/83a508f6b8e219bba2d4af077c35ba0e0cdd35a751a3be6a7cba5a55ad71/rpds_py-0.27.1-pp310-pypy310_pp73-musllinux_1_2_aarch64.whl", hash = "sha256:879b0e14a2da6a1102a3fc8af580fc1ead37e6d6692a781bd8c83da37429b5ab", size = 560084, upload-time = "2025-08-27T12:15:43.839Z" }, - { url = "https://files.pythonhosted.org/packages/66/66/bb945683b958a1b19eb0fe715594630d0f36396ebdef4d9b89c2fa09aa56/rpds_py-0.27.1-pp310-pypy310_pp73-musllinux_1_2_i686.whl", hash = "sha256:0d807710df3b5faa66c731afa162ea29717ab3be17bdc15f90f2d9f183da4059", size = 590115, upload-time = "2025-08-27T12:15:46.647Z" }, - { url = "https://files.pythonhosted.org/packages/12/00/ccfaafaf7db7e7adace915e5c2f2c2410e16402561801e9c7f96683002d3/rpds_py-0.27.1-pp310-pypy310_pp73-musllinux_1_2_x86_64.whl", hash = "sha256:3adc388fc3afb6540aec081fa59e6e0d3908722771aa1e37ffe22b220a436f0b", size = 556561, upload-time = "2025-08-27T12:15:48.219Z" }, - { url = "https://files.pythonhosted.org/packages/e1/b7/92b6ed9aad103bfe1c45df98453dfae40969eef2cb6c6239c58d7e96f1b3/rpds_py-0.27.1-pp310-pypy310_pp73-win_amd64.whl", hash = "sha256:c796c0c1cc68cb08b0284db4229f5af76168172670c74908fdbd4b7d7f515819", size = 229125, upload-time = "2025-08-27T12:15:49.956Z" }, { url = "https://files.pythonhosted.org/packages/0c/ed/e1fba02de17f4f76318b834425257c8ea297e415e12c68b4361f63e8ae92/rpds_py-0.27.1-pp311-pypy311_pp73-macosx_10_12_x86_64.whl", hash = "sha256:cdfe4bb2f9fe7458b7453ad3c33e726d6d1c7c0a72960bcc23800d77384e42df", size = 371402, upload-time = "2025-08-27T12:15:51.561Z" }, { url = "https://files.pythonhosted.org/packages/af/7c/e16b959b316048b55585a697e94add55a4ae0d984434d279ea83442e460d/rpds_py-0.27.1-pp311-pypy311_pp73-macosx_11_0_arm64.whl", hash = "sha256:8fabb8fd848a5f75a2324e4a84501ee3a5e3c78d8603f83475441866e60b94a3", size = 354084, upload-time = "2025-08-27T12:15:53.219Z" }, { url = "https://files.pythonhosted.org/packages/de/c1/ade645f55de76799fdd08682d51ae6724cb46f318573f18be49b1e040428/rpds_py-0.27.1-pp311-pypy311_pp73-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:eda8719d598f2f7f3e0f885cba8646644b55a187762bec091fa14a2b819746a9", size = 383090, upload-time = "2025-08-27T12:15:55.158Z" }, @@ -3348,19 +2369,6 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/6a/24/e3e72d265121e00b063aef3e3501e5b2473cf1b23511d56e529531acf01e/rpds_py-0.27.1-pp311-pypy311_pp73-musllinux_1_2_aarch64.whl", hash = "sha256:94c44ee01fd21c9058f124d2d4f0c9dc7634bec93cd4b38eefc385dabe71acbf", size = 560003, upload-time = "2025-08-27T12:16:08.06Z" }, { url = "https://files.pythonhosted.org/packages/26/ca/f5a344c534214cc2d41118c0699fffbdc2c1bc7046f2a2b9609765ab9c92/rpds_py-0.27.1-pp311-pypy311_pp73-musllinux_1_2_i686.whl", hash = "sha256:df8b74962e35c9249425d90144e721eed198e6555a0e22a563d29fe4486b51f6", size = 590482, upload-time = "2025-08-27T12:16:10.137Z" }, { url = "https://files.pythonhosted.org/packages/ce/08/4349bdd5c64d9d193c360aa9db89adeee6f6682ab8825dca0a3f535f434f/rpds_py-0.27.1-pp311-pypy311_pp73-musllinux_1_2_x86_64.whl", hash = "sha256:dc23e6820e3b40847e2f4a7726462ba0cf53089512abe9ee16318c366494c17a", size = 556523, upload-time = "2025-08-27T12:16:12.188Z" }, - { url = "https://files.pythonhosted.org/packages/4e/ea/5463cd5048a7a2fcdae308b6e96432802132c141bfb9420260142632a0f1/rpds_py-0.27.1-pp39-pypy39_pp73-macosx_10_12_x86_64.whl", hash = "sha256:aa8933159edc50be265ed22b401125c9eebff3171f570258854dbce3ecd55475", size = 371778, upload-time = "2025-08-27T12:16:13.851Z" }, - { url = "https://files.pythonhosted.org/packages/0d/c8/f38c099db07f5114029c1467649d308543906933eebbc226d4527a5f4693/rpds_py-0.27.1-pp39-pypy39_pp73-macosx_11_0_arm64.whl", hash = "sha256:a50431bf02583e21bf273c71b89d710e7a710ad5e39c725b14e685610555926f", size = 354394, upload-time = "2025-08-27T12:16:15.609Z" }, - { url = "https://files.pythonhosted.org/packages/7d/79/b76f97704d9dd8ddbd76fed4c4048153a847c5d6003afe20a6b5c3339065/rpds_py-0.27.1-pp39-pypy39_pp73-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:78af06ddc7fe5cc0e967085a9115accee665fb912c22a3f54bad70cc65b05fe6", size = 382348, upload-time = "2025-08-27T12:16:17.251Z" }, - { url = "https://files.pythonhosted.org/packages/8a/3f/ef23d3c1be1b837b648a3016d5bbe7cfe711422ad110b4081c0a90ef5a53/rpds_py-0.27.1-pp39-pypy39_pp73-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:70d0738ef8fee13c003b100c2fbd667ec4f133468109b3472d249231108283a3", size = 394159, upload-time = "2025-08-27T12:16:19.251Z" }, - { url = "https://files.pythonhosted.org/packages/74/8a/9e62693af1a34fd28b1a190d463d12407bd7cf561748cb4745845d9548d3/rpds_py-0.27.1-pp39-pypy39_pp73-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:e2f6fd8a1cea5bbe599b6e78a6e5ee08db434fc8ffea51ff201c8765679698b3", size = 522775, upload-time = "2025-08-27T12:16:20.929Z" }, - { url = "https://files.pythonhosted.org/packages/36/0d/8d5bb122bf7a60976b54c5c99a739a3819f49f02d69df3ea2ca2aff47d5c/rpds_py-0.27.1-pp39-pypy39_pp73-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:8177002868d1426305bb5de1e138161c2ec9eb2d939be38291d7c431c4712df8", size = 402633, upload-time = "2025-08-27T12:16:22.548Z" }, - { url = "https://files.pythonhosted.org/packages/0f/0e/237948c1f425e23e0cf5a566d702652a6e55c6f8fbd332a1792eb7043daf/rpds_py-0.27.1-pp39-pypy39_pp73-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:008b839781d6c9bf3b6a8984d1d8e56f0ec46dc56df61fd669c49b58ae800400", size = 384867, upload-time = "2025-08-27T12:16:24.29Z" }, - { url = "https://files.pythonhosted.org/packages/d6/0a/da0813efcd998d260cbe876d97f55b0f469ada8ba9cbc47490a132554540/rpds_py-0.27.1-pp39-pypy39_pp73-manylinux_2_31_riscv64.whl", hash = "sha256:a55b9132bb1ade6c734ddd2759c8dc132aa63687d259e725221f106b83a0e485", size = 401791, upload-time = "2025-08-27T12:16:25.954Z" }, - { url = "https://files.pythonhosted.org/packages/51/78/c6c9e8a8aaca416a6f0d1b6b4a6ee35b88fe2c5401d02235d0a056eceed2/rpds_py-0.27.1-pp39-pypy39_pp73-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:a46fdec0083a26415f11d5f236b79fa1291c32aaa4a17684d82f7017a1f818b1", size = 419525, upload-time = "2025-08-27T12:16:27.659Z" }, - { url = "https://files.pythonhosted.org/packages/a3/69/5af37e1d71487cf6d56dd1420dc7e0c2732c1b6ff612aa7a88374061c0a8/rpds_py-0.27.1-pp39-pypy39_pp73-musllinux_1_2_aarch64.whl", hash = "sha256:8a63b640a7845f2bdd232eb0d0a4a2dd939bcdd6c57e6bb134526487f3160ec5", size = 559255, upload-time = "2025-08-27T12:16:29.343Z" }, - { url = "https://files.pythonhosted.org/packages/40/7f/8b7b136069ef7ac3960eda25d832639bdb163018a34c960ed042dd1707c8/rpds_py-0.27.1-pp39-pypy39_pp73-musllinux_1_2_i686.whl", hash = "sha256:7e32721e5d4922deaaf963469d795d5bde6093207c52fec719bd22e5d1bedbc4", size = 590384, upload-time = "2025-08-27T12:16:31.005Z" }, - { url = "https://files.pythonhosted.org/packages/d8/06/c316d3f6ff03f43ccb0eba7de61376f8ec4ea850067dddfafe98274ae13c/rpds_py-0.27.1-pp39-pypy39_pp73-musllinux_1_2_x86_64.whl", hash = "sha256:2c426b99a068601b5f4623573df7a7c3d72e87533a2dd2253353a03e7502566c", size = 555959, upload-time = "2025-08-27T12:16:32.73Z" }, - { url = "https://files.pythonhosted.org/packages/60/94/384cf54c430b9dac742bbd2ec26c23feb78ded0d43d6d78563a281aec017/rpds_py-0.27.1-pp39-pypy39_pp73-win_amd64.whl", hash = "sha256:4fc9b7fe29478824361ead6e14e4f5aed570d477e06088826537e202d25fe859", size = 228784, upload-time = "2025-08-27T12:16:34.428Z" }, ] [[package]] @@ -3444,7 +2452,6 @@ dependencies = [ { name = "colorama", marker = "sys_platform == 'win32'" }, { name = "docutils" }, { name = "imagesize" }, - { name = "importlib-metadata", marker = "python_full_version < '3.10'" }, { name = "jinja2" }, { name = "packaging" }, { name = "pygments" }, @@ -3456,7 +2463,6 @@ dependencies = [ { name = "sphinxcontrib-jsmath" }, { name = "sphinxcontrib-qthelp" }, { name = "sphinxcontrib-serializinghtml" }, - { name = "tomli", marker = "python_full_version < '3.11'" }, ] 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 = [ @@ -3529,8 +2535,7 @@ name = "sphinx-external-toc" version = "1.0.1" source = { registry = "https://pypi.org/simple" } dependencies = [ - { name = "click", version = "8.1.8", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version < '3.10'" }, - { name = "click", version = "8.2.1", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version >= '3.10'" }, + { name = "click" }, { name = "pyyaml" }, { name = "sphinx" }, ] @@ -3606,7 +2611,6 @@ version = "2.6.5" source = { registry = "https://pypi.org/simple" } dependencies = [ { name = "docutils" }, - { name = "importlib-metadata", marker = "python_full_version < '3.10'" }, { name = "pybtex" }, { name = "pybtex-docutils" }, { name = "sphinx" }, @@ -3667,8 +2671,7 @@ version = "0.3.1" source = { registry = "https://pypi.org/simple" } dependencies = [ { name = "census" }, - { name = "numpy", version = "2.0.2", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version < '3.10'" }, - { name = "numpy", version = "2.1.3", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version >= '3.10'" }, + { name = "numpy" }, { name = "openpyxl" }, { name = "pandas" }, { name = "requests" }, @@ -3689,14 +2692,6 @@ dependencies = [ ] sdist = { url = "https://files.pythonhosted.org/packages/d7/bc/d59b5d97d27229b0e009bd9098cd81af71c2fa5549c580a0a67b9bed0496/sqlalchemy-2.0.43.tar.gz", hash = "sha256:788bfcef6787a7764169cfe9859fe425bf44559619e1d9f56f5bddf2ebf6f417", size = 9762949, upload-time = "2025-08-11T14:24:58.438Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/8f/4e/985f7da36f09592c5ade99321c72c15101d23c0bb7eecfd1daaca5714422/sqlalchemy-2.0.43-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:70322986c0c699dca241418fcf18e637a4369e0ec50540a2b907b184c8bca069", size = 2133162, upload-time = "2025-08-11T15:52:17.854Z" }, - { url = "https://files.pythonhosted.org/packages/37/34/798af8db3cae069461e3bc0898a1610dc469386a97048471d364dc8aae1c/sqlalchemy-2.0.43-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:87accdbba88f33efa7b592dc2e8b2a9c2cdbca73db2f9d5c510790428c09c154", size = 2123082, upload-time = "2025-08-11T15:52:19.181Z" }, - { url = "https://files.pythonhosted.org/packages/fb/0f/79cf4d9dad42f61ec5af1e022c92f66c2d110b93bb1dc9b033892971abfa/sqlalchemy-2.0.43-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:c00e7845d2f692ebfc7d5e4ec1a3fd87698e4337d09e58d6749a16aedfdf8612", size = 3208871, upload-time = "2025-08-11T15:50:30.656Z" }, - { url = "https://files.pythonhosted.org/packages/56/b3/59befa58fb0e1a9802c87df02344548e6d007e77e87e6084e2131c29e033/sqlalchemy-2.0.43-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:022e436a1cb39b13756cf93b48ecce7aa95382b9cfacceb80a7d263129dfd019", size = 3209583, upload-time = "2025-08-11T15:57:47.697Z" }, - { url = "https://files.pythonhosted.org/packages/29/d2/124b50c0eb8146e8f0fe16d01026c1a073844f0b454436d8544fe9b33bd7/sqlalchemy-2.0.43-cp310-cp310-musllinux_1_2_aarch64.whl", hash = "sha256:c5e73ba0d76eefc82ec0219d2301cb33bfe5205ed7a2602523111e2e56ccbd20", size = 3148177, upload-time = "2025-08-11T15:50:32.078Z" }, - { url = "https://files.pythonhosted.org/packages/83/f5/e369cd46aa84278107624617034a5825fedfc5c958b2836310ced4d2eadf/sqlalchemy-2.0.43-cp310-cp310-musllinux_1_2_x86_64.whl", hash = "sha256:9c2e02f06c68092b875d5cbe4824238ab93a7fa35d9c38052c033f7ca45daa18", size = 3172276, upload-time = "2025-08-11T15:57:49.477Z" }, - { url = "https://files.pythonhosted.org/packages/de/2b/4602bf4c3477fa4c837c9774e6dd22e0389fc52310c4c4dfb7e7ba05e90d/sqlalchemy-2.0.43-cp310-cp310-win32.whl", hash = "sha256:e7a903b5b45b0d9fa03ac6a331e1c1d6b7e0ab41c63b6217b3d10357b83c8b00", size = 2101491, upload-time = "2025-08-11T15:54:59.191Z" }, - { url = "https://files.pythonhosted.org/packages/38/2d/bfc6b6143adef553a08295490ddc52607ee435b9c751c714620c1b3dd44d/sqlalchemy-2.0.43-cp310-cp310-win_amd64.whl", hash = "sha256:4bf0edb24c128b7be0c61cd17eef432e4bef507013292415f3fb7023f02b7d4b", size = 2125148, upload-time = "2025-08-11T15:55:00.593Z" }, { url = "https://files.pythonhosted.org/packages/9d/77/fa7189fe44114658002566c6fe443d3ed0ec1fa782feb72af6ef7fbe98e7/sqlalchemy-2.0.43-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:52d9b73b8fb3e9da34c2b31e6d99d60f5f99fd8c1225c9dad24aeb74a91e1d29", size = 2136472, upload-time = "2025-08-11T15:52:21.789Z" }, { url = "https://files.pythonhosted.org/packages/99/ea/92ac27f2fbc2e6c1766bb807084ca455265707e041ba027c09c17d697867/sqlalchemy-2.0.43-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:f42f23e152e4545157fa367b2435a1ace7571cab016ca26038867eb7df2c3631", size = 2126535, upload-time = "2025-08-11T15:52:23.109Z" }, { url = "https://files.pythonhosted.org/packages/94/12/536ede80163e295dc57fff69724caf68f91bb40578b6ac6583a293534849/sqlalchemy-2.0.43-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:4fb1a8c5438e0c5ea51afe9c6564f951525795cf432bed0c028c1cb081276685", size = 3297521, upload-time = "2025-08-11T15:50:33.536Z" }, @@ -3721,14 +2716,6 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/86/85/29d216002d4593c2ce1c0ec2cec46dda77bfbcd221e24caa6e85eff53d89/sqlalchemy-2.0.43-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:9df7126fd9db49e3a5a3999442cc67e9ee8971f3cb9644250107d7296cb2a164", size = 3219363, upload-time = "2025-08-11T15:56:39.11Z" }, { url = "https://files.pythonhosted.org/packages/b6/e4/bd78b01919c524f190b4905d47e7630bf4130b9f48fd971ae1c6225b6f6a/sqlalchemy-2.0.43-cp313-cp313-win32.whl", hash = "sha256:7f1ac7828857fcedb0361b48b9ac4821469f7694089d15550bbcf9ab22564a1d", size = 2096718, upload-time = "2025-08-11T15:55:05.349Z" }, { url = "https://files.pythonhosted.org/packages/ac/a5/ca2f07a2a201f9497de1928f787926613db6307992fe5cda97624eb07c2f/sqlalchemy-2.0.43-cp313-cp313-win_amd64.whl", hash = "sha256:971ba928fcde01869361f504fcff3b7143b47d30de188b11c6357c0505824197", size = 2123200, upload-time = "2025-08-11T15:55:07.932Z" }, - { url = "https://files.pythonhosted.org/packages/92/95/ddb5acf74a71e0fa4f9410c7d8555f169204ae054a49693b3cd31d0bf504/sqlalchemy-2.0.43-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:ceb5c832cc30663aeaf5e39657712f4c4241ad1f638d487ef7216258f6d41fe7", size = 2136445, upload-time = "2025-08-12T17:29:06.145Z" }, - { url = "https://files.pythonhosted.org/packages/ea/d4/7d7ea7dfbc1ddb0aa54dd63a686cd43842192b8e1bfb5315bb052925f704/sqlalchemy-2.0.43-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:11f43c39b4b2ec755573952bbcc58d976779d482f6f832d7f33a8d869ae891bf", size = 2126411, upload-time = "2025-08-12T17:29:08.138Z" }, - { url = "https://files.pythonhosted.org/packages/07/bd/123ba09bec14112de10e49d8835e6561feb24fd34131099d98d28d34f106/sqlalchemy-2.0.43-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:413391b2239db55be14fa4223034d7e13325a1812c8396ecd4f2c08696d5ccad", size = 3221776, upload-time = "2025-08-11T16:00:30.938Z" }, - { url = "https://files.pythonhosted.org/packages/ae/35/553e45d5b91b15980c13e1dbcd7591f49047589843fff903c086d7985afb/sqlalchemy-2.0.43-cp39-cp39-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:c379e37b08c6c527181a397212346be39319fb64323741d23e46abd97a400d34", size = 3221665, upload-time = "2025-08-12T17:29:11.307Z" }, - { url = "https://files.pythonhosted.org/packages/07/4d/ff03e516087251da99bd879b5fdb2c697ff20295c836318dda988e12ec19/sqlalchemy-2.0.43-cp39-cp39-musllinux_1_2_aarch64.whl", hash = "sha256:03d73ab2a37d9e40dec4984d1813d7878e01dbdc742448d44a7341b7a9f408c7", size = 3160067, upload-time = "2025-08-11T16:00:33.148Z" }, - { url = "https://files.pythonhosted.org/packages/ae/88/cbc7caa186ecdc5dea013e9ccc00d78b93a6638dc39656a42369a9536458/sqlalchemy-2.0.43-cp39-cp39-musllinux_1_2_x86_64.whl", hash = "sha256:8cee08f15d9e238ede42e9bbc1d6e7158d0ca4f176e4eab21f88ac819ae3bd7b", size = 3184462, upload-time = "2025-08-12T17:29:14.919Z" }, - { url = "https://files.pythonhosted.org/packages/ab/69/f8bbd43080b6fa75cb44ff3a1cc99aaae538dd0ade1a58206912b2565d72/sqlalchemy-2.0.43-cp39-cp39-win32.whl", hash = "sha256:b3edaec7e8b6dc5cd94523c6df4f294014df67097c8217a89929c99975811414", size = 2104031, upload-time = "2025-08-11T15:48:56.453Z" }, - { url = "https://files.pythonhosted.org/packages/36/39/2ec1b0e7a4f44d833d924e7bfca8054c72e37eb73f4d02795d16d8b0230a/sqlalchemy-2.0.43-cp39-cp39-win_amd64.whl", hash = "sha256:227119ce0a89e762ecd882dc661e0aa677a690c914e358f0dd8932a2e8b2765b", size = 2128007, upload-time = "2025-08-11T15:48:57.872Z" }, { url = "https://files.pythonhosted.org/packages/b8/d9/13bdde6521f322861fab67473cec4b1cc8999f3871953531cf61945fad92/sqlalchemy-2.0.43-py3-none-any.whl", hash = "sha256:1681c21dd2ccee222c2fe0bef671d1aef7c504087c9c4e800371cfcc8ac966fc", size = 1924759, upload-time = "2025-08-11T15:39:53.024Z" }, ] @@ -3755,94 +2742,16 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/df/cb/e1da7e340586a078404c7e4328bfefc930867ace8a9a55916fd220cf9547/standard_imghdr-3.13.0-py3-none-any.whl", hash = "sha256:30a1bff5465605bb496f842a6ac3cc1f2131bf3025b0da28d4877d6d4b7cc8e9", size = 4639, upload-time = "2024-10-30T16:01:13.829Z" }, ] -[[package]] -name = "tables" -version = "3.9.2" -source = { registry = "https://pypi.org/simple" } -resolution-markers = [ - "python_full_version > '3.9' and python_full_version < '3.10'", - "python_full_version <= '3.9'", -] -dependencies = [ - { name = "blosc2", version = "2.5.1", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version < '3.10'" }, - { name = "numexpr", version = "2.10.2", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version < '3.10'" }, - { name = "numpy", version = "2.0.2", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version < '3.10'" }, - { name = "packaging", marker = "python_full_version < '3.10'" }, - { name = "py-cpuinfo", marker = "python_full_version < '3.10'" }, -] -sdist = { url = "https://files.pythonhosted.org/packages/31/83/8a13be8338219c3fe0aa7357d1ec4edb27bc346e0f224df7212892b243b5/tables-3.9.2.tar.gz", hash = "sha256:d470263c2e50c4b7c8635a0d99ac1ff2f9e704c24d71e5fa33c4529e7d0ad9c3", size = 4683437, upload-time = "2023-11-27T11:53:17.229Z" } -wheels = [ - { url = "https://files.pythonhosted.org/packages/17/95/dea498a083e1fbe60bf0ea999da38cb8e33b5506ba99237b5416267a9bc0/tables-3.9.2-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:8a4e71fc9d2a3a0cacce4994afd47cd5f4797093ff9cee2cc7dc87e51f308107", size = 7392824, upload-time = "2023-11-27T11:52:19.054Z" }, - { url = "https://files.pythonhosted.org/packages/49/b0/e424d4d409925cdf38689bb85f33e2a7c7bca310fb188de65af031c933b7/tables-3.9.2-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:5fbea426ce9bdd60cda435a265823b31d18f2b36e9045fb2d565679825a7aa46", size = 7113863, upload-time = "2023-11-27T11:52:24.217Z" }, - { url = "https://files.pythonhosted.org/packages/22/86/5a474f002126e37d9ee1062e71dc99223d1618ffcfbd5683a7662766c4af/tables-3.9.2-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:e756c272bb111d016fec1d03a60095403a8fb42a5fbaf5f317dcf6e3b9d8e92e", size = 7481194, upload-time = "2023-11-27T11:52:29.369Z" }, - { url = "https://files.pythonhosted.org/packages/90/eb/8b998ae92009d6e9a696d8ec43a5b5ab9200a715f8da86d9f952a1a1bde5/tables-3.9.2-cp310-cp310-win_amd64.whl", hash = "sha256:eea41cb32dd22b30d6f3dd4e113f6d693384d301c89f3c4b4712f90c9c955875", size = 4391242, upload-time = "2023-11-27T11:52:33.314Z" }, - { url = "https://files.pythonhosted.org/packages/14/41/ea4928ece6c79614d1b61215a52760d88a134e55eca42a3a09815468afa9/tables-3.9.2-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:d71913fb8147dc6132595b94fc82f88f6c2436a3b5c57aadfe26c680f96aa387", size = 7323512, upload-time = "2023-11-27T11:52:37.305Z" }, - { url = "https://files.pythonhosted.org/packages/d2/de/a3f2a4c6ac32b81e4e2789d7364db0f8fff0657f1b767004c3e63b353142/tables-3.9.2-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:9d6bbc477d038a17c5062ab6ccd94c8b1fa365cf017b9a2ad6c2dff1a07abb2b", size = 7112660, upload-time = "2023-11-27T11:52:40.427Z" }, - { url = "https://files.pythonhosted.org/packages/03/76/53d432936c6955de1346b629b9f92213b11ee9e8aeab8831027fac9e053d/tables-3.9.2-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:8e67c71070b871fade3694a4c764504e03836bb1843321766cf2e40b7d280e84", size = 7478756, upload-time = "2023-11-27T11:52:44.182Z" }, - { url = "https://files.pythonhosted.org/packages/64/09/1071a5cef60108dc3fd8d44596371d223834171800f43a652a9bd523588f/tables-3.9.2-cp311-cp311-win_amd64.whl", hash = "sha256:ab9291ff4d243e7966b6706a2675b83138bd9bbe82721d695b78971660d59632", size = 4391764, upload-time = "2023-11-27T11:52:47.4Z" }, - { url = "https://files.pythonhosted.org/packages/01/4f/cb9e44384982889ff720b8fabddb24aa4b0deb69afb1a5f09dd17a93b4ca/tables-3.9.2-cp312-cp312-macosx_10_9_x86_64.whl", hash = "sha256:c14dc879b041cf53be1afe9e5ed581e1aeacdcee9e2e1ee79110dc96a4c8d97c", size = 7351431, upload-time = "2023-11-27T11:52:51.371Z" }, - { url = "https://files.pythonhosted.org/packages/9a/e0/070c3ebc11614f1c3fc413f2327ea9d1818444eab7ae4975a2fea7de03cb/tables-3.9.2-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:2848fb3dce30a7b83fa099d026a91d7b10ad48afae04fa10f974f1da3f1e2bbf", size = 7077135, upload-time = "2023-11-27T11:52:54.361Z" }, - { url = "https://files.pythonhosted.org/packages/dd/50/8efbd7339a1fd69658f623bcee5e4c0318d823cc39bb23dbc6b0ff1a7491/tables-3.9.2-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:b131c9b4e003816a45e2efe5c5c797d01d8308cac4aee72597a15837cedb605c", size = 7442059, upload-time = "2023-11-27T11:52:57.999Z" }, - { url = "https://files.pythonhosted.org/packages/8b/84/93dd082a991c36f3e55e170cb71c4011256e45549c0a3b0d3693cf9053e3/tables-3.9.2-cp312-cp312-win_amd64.whl", hash = "sha256:c6304d321452fd56865e5c309e38373011b0f0f6c714786c5660613ceb623acb", size = 4374051, upload-time = "2023-11-27T11:53:00.801Z" }, - { url = "https://files.pythonhosted.org/packages/64/5d/eb0c1b4401cbd9006589cb1f2a4288e70faa246538c7115ee7e54805c2d7/tables-3.9.2-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:c52087ed8b90a5f6ba87f0adcd1c433e5f5db7c7ca5984b08ff45f2247635f7d", size = 7396210, upload-time = "2023-11-27T11:53:03.627Z" }, - { url = "https://files.pythonhosted.org/packages/a9/9c/fb98d7a5b9e1c7f588bf71ea75c87711212c964f0c1c8e0cb1246bda0a35/tables-3.9.2-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:164b945d0cb731c7232775fd3657f150bcf05413928b86033b023a1dc8dbeb05", size = 7116751, upload-time = "2023-11-27T11:53:07.406Z" }, - { url = "https://files.pythonhosted.org/packages/62/cf/897ad10477897bd55bcb3431833b274adda1c01bc8f856b8de57f6de0b83/tables-3.9.2-cp39-cp39-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:a068d4ad08d5a6b2ad457f60ac6676efdab9e29459e776e433d5537a46e62e41", size = 7483600, upload-time = "2023-11-27T11:53:10.155Z" }, - { url = "https://files.pythonhosted.org/packages/28/67/f710befc6a57a339ac0aa7272c7df58014fff4f8863c16f683894b8c7893/tables-3.9.2-cp39-cp39-win_amd64.whl", hash = "sha256:bca5a6bf162a84a6ef74ca4017b28c59c1526cffdbd93ce94c98ff8f9593f1d5", size = 4394103, upload-time = "2023-11-27T11:53:13.632Z" }, -] - -[[package]] -name = "tables" -version = "3.10.1" -source = { registry = "https://pypi.org/simple" } -resolution-markers = [ - "python_full_version == '3.10.*'", -] -dependencies = [ - { name = "blosc2", version = "3.7.2", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version == '3.10.*'" }, - { name = "numexpr", version = "2.11.0", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version == '3.10.*'" }, - { name = "numpy", version = "2.1.3", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version == '3.10.*'" }, - { name = "packaging", marker = "python_full_version == '3.10.*'" }, - { name = "py-cpuinfo", marker = "python_full_version == '3.10.*'" }, - { name = "typing-extensions", marker = "python_full_version == '3.10.*'" }, -] -sdist = { url = "https://files.pythonhosted.org/packages/0d/5d/96708a84e9fcd29d1f684d56d4c38a23d29b1c934599a072a49f27ccfa71/tables-3.10.1.tar.gz", hash = "sha256:4aa07ac734b9c037baeaf44aec64ec902ad247f57811b59f30c4e31d31f126cf", size = 4762413, upload-time = "2024-08-17T09:57:47.127Z" } -wheels = [ - { url = "https://files.pythonhosted.org/packages/ff/69/a768ec8104ada032c9be09f521f548766ddd0351bc941c9d42fa5db001de/tables-3.10.1-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:bca9d11a570ca1bc57f0845e54e55c3093d5a1ace376faee639e09503a73745b", size = 6823691, upload-time = "2024-08-17T09:56:50.229Z" }, - { url = "https://files.pythonhosted.org/packages/e4/2d/074bc14b39de9b552eec02ee583eff2997d903da1355f4450506335a6055/tables-3.10.1-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:b62881cb682438d1e92b9178db42b160638aef3ca23341f7d98e9b27821b1eb4", size = 5471221, upload-time = "2024-08-17T09:56:54.84Z" }, - { url = "https://files.pythonhosted.org/packages/4a/30/29411ab804b5ac4bee25c82ba38f4e7a8c0b52c6a1cdbeea7d1db33a53fe/tables-3.10.1-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:f9cf1bfd8b0e0195196205fc8a134628219cff85d20da537facd67a291e6b347", size = 7170201, upload-time = "2024-08-17T09:56:59.011Z" }, - { url = "https://files.pythonhosted.org/packages/0a/7d/3165c7538b8e89b22fa17ad68e04106cca7023cf68e94011ae7b3b6d2a78/tables-3.10.1-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:77f0e6dd45b91d99bf3976c8655c48fe3816baf390b9098e4fb2f0fdf9da7078", size = 7571035, upload-time = "2024-08-17T09:57:03.115Z" }, - { url = "https://files.pythonhosted.org/packages/46/b3/985a23d2cf27aad383301a5e99e1851228a1941b868515612b5357bded5f/tables-3.10.1-cp310-cp310-win_amd64.whl", hash = "sha256:d90542ec172d1d60df0b796c48ad446f2b69a5d5cd3077bd6450891b854d1ffb", size = 6311650, upload-time = "2024-08-17T09:57:06.593Z" }, - { url = "https://files.pythonhosted.org/packages/dc/04/957264eb35e60251830a965e2d02332eb36ed14fbd8345df06981bbf3ece/tables-3.10.1-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:f8917262a2bb3cd79d37e108557e34ec4b365fdcc806e01dd10765a84c65dab6", size = 6790492, upload-time = "2024-08-17T09:57:10.247Z" }, - { url = "https://files.pythonhosted.org/packages/b2/19/eb7af9d92aaf6766f5fedfce11a97ab03cf39856561c5f562dc0c769a682/tables-3.10.1-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:f93f6db623b484bb6606537c2a71e95ee34fae19b0d891867642dd8c7be05af6", size = 5506835, upload-time = "2024-08-17T09:57:13.883Z" }, - { url = "https://files.pythonhosted.org/packages/b0/8f/897324e1ad543ca439b2c91f04c406f3eeda6e7ff2f43b4cd939f05043e4/tables-3.10.1-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:01ca51624bca1a87e703d6d6b796368bc3460ff007ea8b1341be03bedd863833", size = 7166960, upload-time = "2024-08-17T09:57:17.463Z" }, - { url = "https://files.pythonhosted.org/packages/4e/5c/3f21d1135bf60af99ac79a17bbffd333d69763df2197ba04f47dd30bbd4e/tables-3.10.1-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:9372516c76be3a05a573df63a69ce38315d03b5816d2a1e89c48129ec8b161b0", size = 7568724, upload-time = "2024-08-17T09:57:23.02Z" }, - { url = "https://files.pythonhosted.org/packages/1f/e3/3ee6b66263902eccadc4e0e23bca7fb480fd190904b7ce0bea4777b5b799/tables-3.10.1-cp311-cp311-win_amd64.whl", hash = "sha256:09190fb504888aeacafb7739c13d5c5a3e87af3d261f4d2f832b1f8407be133a", size = 6312200, upload-time = "2024-08-17T09:57:26.322Z" }, - { url = "https://files.pythonhosted.org/packages/95/ec/ea6c476e33602c172c797fe8f8ab96d007d964137068276d142b142a28e5/tables-3.10.1-cp312-cp312-macosx_10_9_x86_64.whl", hash = "sha256:a7090af37909e3bf229d5599fa442633e5a93b6082960b01038dc0106e07a8da", size = 6791597, upload-time = "2024-08-17T09:57:29.598Z" }, - { url = "https://files.pythonhosted.org/packages/74/02/a967a506e9204e3328a8c03f67e6f3c919defc8df11aba83ae5b2abf7b0f/tables-3.10.1-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:203ed50c0c5f30f007df7633089b2a567b99856cd25d68f19d91624a8db2e7ad", size = 5474779, upload-time = "2024-08-17T09:57:32.43Z" }, - { url = "https://files.pythonhosted.org/packages/c3/26/925793f753664ec698b2c6315c818269313db143da38150897cf260405c2/tables-3.10.1-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:e36ce9f10471c69c1f0b06c6966de762558a35d62592c55df7994a8019adaf0c", size = 7130683, upload-time = "2024-08-17T09:57:36.181Z" }, - { url = "https://files.pythonhosted.org/packages/d8/79/2b34f22284459e940a84e71dba19b2a34c7cc0ce3cdf685923c50d5b9611/tables-3.10.1-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:f233e78cc9fa4157ec4c3ef2abf01a731fe7969bc6ed73539e5f4cd3b94c98b2", size = 7531367, upload-time = "2024-08-17T09:57:39.864Z" }, - { url = "https://files.pythonhosted.org/packages/3d/27/5a23830f611e26dd7ee104096c6bb82e481b16f3f17ccaed3075f8d48312/tables-3.10.1-cp312-cp312-win_amd64.whl", hash = "sha256:34357d2f2f75843a44e6fe54d1f11fc2e35a8fd3cb134df3d3362cff78010adb", size = 6295046, upload-time = "2024-08-17T09:57:43.561Z" }, - { url = "https://files.pythonhosted.org/packages/d3/d4/e7c25df877e054b05f146d6ccb920bcdbe8d39b35a0962868b80547532c7/tables-3.10.1-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:6fc5b46a4f359249c3ab9a0a0a2448d7e680e68cffd63fdf3fb7171781edd46e", size = 6824253, upload-time = "2024-11-09T19:26:06.428Z" }, - { url = "https://files.pythonhosted.org/packages/c6/49/091865d75090a24493bd1b66e52d72f4d9627ff42983a13d4dcd89455d02/tables-3.10.1-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:2ecabd7f459d40b7f9f5256850dd5f43773fda7b789f827de92c3d26df1e320f", size = 5499587, upload-time = "2024-11-09T19:26:12.402Z" }, - { url = "https://files.pythonhosted.org/packages/23/83/9dac8af333149fa01add439f710d4a312b70faf81c2f59a16b8bfaebb75e/tables-3.10.1-cp313-cp313-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:40a4ee18f3c9339d9dd8fd3777c75cda5768f2ff347064a2796f59161a190af8", size = 7128236, upload-time = "2024-11-09T19:26:15.716Z" }, - { url = "https://files.pythonhosted.org/packages/89/fd/62f31643596f6ab71fc6d2a87acdee0bc01a03fbe1a7f3f6dc0c91e2546d/tables-3.10.1-cp313-cp313-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:757c6ea257c174af8036cf8f273ede756bbcd6db5ac7e2a4d64e788b0f371152", size = 7527953, upload-time = "2024-11-09T19:26:20.229Z" }, -] - [[package]] name = "tables" version = "3.11.1" source = { registry = "https://pypi.org/simple" } -resolution-markers = [ - "python_full_version >= '3.13'", - "python_full_version == '3.12.*'", - "python_full_version == '3.11.*'", -] dependencies = [ - { name = "blosc2", version = "3.7.2", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version >= '3.11'" }, - { name = "numexpr", version = "2.11.0", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version >= '3.11'" }, - { name = "numpy", version = "2.1.3", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version >= '3.11'" }, - { name = "packaging", marker = "python_full_version >= '3.11'" }, - { name = "py-cpuinfo", marker = "python_full_version >= '3.11'" }, + { name = "blosc2" }, + { name = "numexpr" }, + { name = "numpy" }, + { name = "packaging" }, + { name = "py-cpuinfo" }, ] sdist = { url = "https://files.pythonhosted.org/packages/cc/a3/d213ebe7376d48055bd55a29cd9f99061afa0dcece608f94a5025d797b0a/tables-3.11.1.tar.gz", hash = "sha256:78abcf413091bc7c1e4e8c10fbbb438d1ac0b5a87436c5b972c3e8253871b6fb", size = 4790533, upload-time = "2026-03-01T11:43:36.036Z" } wheels = [ @@ -3954,12 +2863,8 @@ name = "towncrier" version = "25.8.0" source = { registry = "https://pypi.org/simple" } dependencies = [ - { name = "click", version = "8.1.8", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version < '3.10'" }, - { name = "click", version = "8.2.1", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version >= '3.10'" }, - { name = "importlib-metadata", marker = "python_full_version < '3.10'" }, - { name = "importlib-resources", marker = "python_full_version < '3.10'" }, + { name = "click" }, { name = "jinja2" }, - { name = "tomli", marker = "python_full_version < '3.11'" }, ] sdist = { url = "https://files.pythonhosted.org/packages/c2/eb/5bf25a34123698d3bbab39c5bc5375f8f8bcbcc5a136964ade66935b8b9d/towncrier-25.8.0.tar.gz", hash = "sha256:eef16d29f831ad57abb3ae32a0565739866219f1ebfbdd297d32894eb9940eb1", size = 76322, upload-time = "2025-08-30T11:41:55.393Z" } wheels = [ @@ -4099,12 +3004,6 @@ dependencies = [ ] sdist = { url = "https://files.pythonhosted.org/packages/30/93/9210e7606be57a2dfc6277ac97dcc864fd8d39f142ca194fdc186d596fda/zope.interface-7.2.tar.gz", hash = "sha256:8b49f1a3d1ee4cdaf5b32d2e738362c7f5e40ac8b46dd7d1a65e82a4872728fe", size = 252960, upload-time = "2024-11-28T08:45:39.224Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/76/71/e6177f390e8daa7e75378505c5ab974e0bf59c1d3b19155638c7afbf4b2d/zope.interface-7.2-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:ce290e62229964715f1011c3dbeab7a4a1e4971fd6f31324c4519464473ef9f2", size = 208243, upload-time = "2024-11-28T08:47:29.781Z" }, - { url = "https://files.pythonhosted.org/packages/52/db/7e5f4226bef540f6d55acfd95cd105782bc6ee044d9b5587ce2c95558a5e/zope.interface-7.2-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:05b910a5afe03256b58ab2ba6288960a2892dfeef01336dc4be6f1b9ed02ab0a", size = 208759, upload-time = "2024-11-28T08:47:31.908Z" }, - { url = "https://files.pythonhosted.org/packages/28/ea/fdd9813c1eafd333ad92464d57a4e3a82b37ae57c19497bcffa42df673e4/zope.interface-7.2-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:550f1c6588ecc368c9ce13c44a49b8d6b6f3ca7588873c679bd8fd88a1b557b6", size = 254922, upload-time = "2024-11-28T09:18:11.795Z" }, - { url = "https://files.pythonhosted.org/packages/3b/d3/0000a4d497ef9fbf4f66bb6828b8d0a235e690d57c333be877bec763722f/zope.interface-7.2-cp310-cp310-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:0ef9e2f865721553c6f22a9ff97da0f0216c074bd02b25cf0d3af60ea4d6931d", size = 249367, upload-time = "2024-11-28T08:48:24.238Z" }, - { url = "https://files.pythonhosted.org/packages/3e/e5/0b359e99084f033d413419eff23ee9c2bd33bca2ca9f4e83d11856f22d10/zope.interface-7.2-cp310-cp310-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:27f926f0dcb058211a3bb3e0e501c69759613b17a553788b2caeb991bed3b61d", size = 254488, upload-time = "2024-11-28T08:48:28.816Z" }, - { url = "https://files.pythonhosted.org/packages/7b/90/12d50b95f40e3b2fc0ba7f7782104093b9fd62806b13b98ef4e580f2ca61/zope.interface-7.2-cp310-cp310-win_amd64.whl", hash = "sha256:144964649eba4c5e4410bb0ee290d338e78f179cdbfd15813de1a664e7649b3b", size = 211947, upload-time = "2024-11-28T08:48:18.831Z" }, { url = "https://files.pythonhosted.org/packages/98/7d/2e8daf0abea7798d16a58f2f3a2bf7588872eee54ac119f99393fdd47b65/zope.interface-7.2-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:1909f52a00c8c3dcab6c4fad5d13de2285a4b3c7be063b239b8dc15ddfb73bd2", size = 208776, upload-time = "2024-11-28T08:47:53.009Z" }, { url = "https://files.pythonhosted.org/packages/a0/2a/0c03c7170fe61d0d371e4c7ea5b62b8cb79b095b3d630ca16719bf8b7b18/zope.interface-7.2-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:80ecf2451596f19fd607bb09953f426588fc1e79e93f5968ecf3367550396b22", size = 209296, upload-time = "2024-11-28T08:47:57.993Z" }, { url = "https://files.pythonhosted.org/packages/49/b4/451f19448772b4a1159519033a5f72672221e623b0a1bd2b896b653943d8/zope.interface-7.2-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:033b3923b63474800b04cba480b70f6e6243a62208071fc148354f3f89cc01b7", size = 260997, upload-time = "2024-11-28T09:18:13.935Z" }, @@ -4123,10 +3022,4 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/b8/f6/54548df6dc73e30ac6c8a7ff1da73ac9007ba38f866397091d5a82237bd3/zope.interface-7.2-cp313-cp313-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:eb23f58a446a7f09db85eda09521a498e109f137b85fb278edb2e34841055398", size = 259237, upload-time = "2024-11-28T08:48:31.71Z" }, { url = "https://files.pythonhosted.org/packages/b6/66/ac05b741c2129fdf668b85631d2268421c5cd1a9ff99be1674371139d665/zope.interface-7.2-cp313-cp313-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:a71a5b541078d0ebe373a81a3b7e71432c61d12e660f1d67896ca62d9628045b", size = 264696, upload-time = "2024-11-28T08:48:41.161Z" }, { url = "https://files.pythonhosted.org/packages/0a/2f/1bccc6f4cc882662162a1158cda1a7f616add2ffe322b28c99cb031b4ffc/zope.interface-7.2-cp313-cp313-win_amd64.whl", hash = "sha256:4893395d5dd2ba655c38ceb13014fd65667740f09fa5bb01caa1e6284e48c0cd", size = 212472, upload-time = "2024-11-28T08:49:56.587Z" }, - { url = "https://files.pythonhosted.org/packages/8c/2c/1f49dc8b4843c4f0848d8e43191aed312bad946a1563d1bf9e46cf2816ee/zope.interface-7.2-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:7bd449c306ba006c65799ea7912adbbfed071089461a19091a228998b82b1fdb", size = 208349, upload-time = "2024-11-28T08:49:28.872Z" }, - { url = "https://files.pythonhosted.org/packages/ed/7d/83ddbfc8424c69579a90fc8edc2b797223da2a8083a94d8dfa0e374c5ed4/zope.interface-7.2-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:a19a6cc9c6ce4b1e7e3d319a473cf0ee989cbbe2b39201d7c19e214d2dfb80c7", size = 208799, upload-time = "2024-11-28T08:49:30.616Z" }, - { url = "https://files.pythonhosted.org/packages/36/22/b1abd91854c1be03f5542fe092e6a745096d2eca7704d69432e119100583/zope.interface-7.2-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:72cd1790b48c16db85d51fbbd12d20949d7339ad84fd971427cf00d990c1f137", size = 254267, upload-time = "2024-11-28T09:18:21.059Z" }, - { url = "https://files.pythonhosted.org/packages/2a/dd/fcd313ee216ad0739ae00e6126bc22a0af62a74f76a9ca668d16cd276222/zope.interface-7.2-cp39-cp39-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:52e446f9955195440e787596dccd1411f543743c359eeb26e9b2c02b077b0519", size = 248614, upload-time = "2024-11-28T08:48:41.953Z" }, - { url = "https://files.pythonhosted.org/packages/88/d4/4ba1569b856870527cec4bf22b91fe704b81a3c1a451b2ccf234e9e0666f/zope.interface-7.2-cp39-cp39-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:2ad9913fd858274db8dd867012ebe544ef18d218f6f7d1e3c3e6d98000f14b75", size = 253800, upload-time = "2024-11-28T08:48:46.637Z" }, - { url = "https://files.pythonhosted.org/packages/69/da/c9cfb384c18bd3a26d9fc6a9b5f32ccea49ae09444f097eaa5ca9814aff9/zope.interface-7.2-cp39-cp39-win_amd64.whl", hash = "sha256:1090c60116b3da3bfdd0c03406e2f14a1ff53e5771aebe33fec1edc0a350175d", size = 211980, upload-time = "2024-11-28T08:50:35.681Z" }, ] From 59931d2de078198c1165dfbbbaff0943f1fda3ff Mon Sep 17 00:00:00 2001 From: Max Ghenis Date: Fri, 11 Sep 2026 12:30:23 -0400 Subject: [PATCH 03/46] Add core SPM selection, receipts, and run identity Introduces the dependency-light SPMSelection and SPMProvenance models and threads them through the country-agnostic simulation layer. A Simulation now carries an optional SPM selection plus a detached calculation receipt, resolves the full six-field configuration through spm_config, and folds those resolved settings into storage_id so cache entries, saved output files and run records are keyed by the measurement settings that produced them. The cache stores a snapshot of the receipt, so re-running with a different selection cannot overwrite an earlier key's provenance. Saving writes the configuration and receipt into the result HDF as a UTF-8 JSON dataset rather than an attribute, because receipts can exceed HDF5's attribute-size limit, and loading refuses a file whose recorded settings differ from the selection being requested. Run records reject a receipt that no longer matches the output it describes. calculate_decile_impacts gains an spm kwarg that is passed to both constructed simulations and is rejected when the caller supplies simulations directly. HouseholdResult.to_dict now converts nested mappings recursively so the added provenance section survives JSON export. Co-Authored-By: Claude Fable 5.1 --- changelog.d/canonical-spm-wrapper.breaking.md | 1 + src/policyengine/core/run_record.py | 9 ++ src/policyengine/core/simulation.py | 76 ++++++++++++++++- src/policyengine/core/spm.py | 85 +++++++++++++++++++ src/policyengine/outputs/decile_impact.py | 10 +++ .../common/model_version.py | 61 ++++++++++++- .../tax_benefit_models/common/result.py | 4 +- tests/test_spm_decile.py | 44 ++++++++++ tests/test_spm_selection_schema.py | 52 ++++++++++++ 9 files changed, 332 insertions(+), 10 deletions(-) create mode 100644 changelog.d/canonical-spm-wrapper.breaking.md create mode 100644 src/policyengine/core/spm.py create mode 100644 tests/test_spm_decile.py create mode 100644 tests/test_spm_selection_schema.py diff --git a/changelog.d/canonical-spm-wrapper.breaking.md b/changelog.d/canonical-spm-wrapper.breaking.md new file mode 100644 index 00000000..255a425e --- /dev/null +++ b/changelog.d/canonical-spm-wrapper.breaking.md @@ -0,0 +1 @@ +Use the bundle-pinned canonical SPM configuration across US household and population simulations, require explicit geography for SPM-dependent outputs, reject formula-owned SPM inputs, and retain serialisable selection and calculation provenance across results and replay. diff --git a/src/policyengine/core/run_record.py b/src/policyengine/core/run_record.py index 10118077..c0bf7956 100644 --- a/src/policyengine/core/run_record.py +++ b/src/policyengine/core/run_record.py @@ -175,6 +175,15 @@ def build_simulation_run_record_payloads( output["tables"] = _table_summaries(simulation.output_dataset) results_payload = {"output_dataset": output} + if simulation.spm_config is not None: + recorded_config = simulation.output_dataset.metadata.get("spm_config") + if recorded_config != simulation.spm_config: + raise ValueError( + "SPM settings changed since this output was calculated; run again before writing a run record" + ) + input_payload["spm"] = simulation.spm_config + results_payload["spm"] = simulation.spm_provenance() + return {"reform": reform, "input": input_payload, "results": results_payload} diff --git a/src/policyengine/core/simulation.py b/src/policyengine/core/simulation.py index d9afd5ba..f337a7f2 100644 --- a/src/policyengine/core/simulation.py +++ b/src/policyengine/core/simulation.py @@ -1,3 +1,5 @@ +import hashlib +import json import logging from datetime import datetime from typing import Any, Optional, Union @@ -10,6 +12,7 @@ from .dynamic import Dynamic from .policy import Policy from .scoping_strategy import ScopingStrategy +from .spm import SPMProvenance, SPMSelection from .tax_benefit_model_version import TaxBenefitModelVersion logger = logging.getLogger(__name__) @@ -91,6 +94,43 @@ class Simulation(BaseModel): output_dataset: Optional[Dataset] = None + spm: Optional[SPMSelection] = Field( + default=None, + description="US SPM forecast and geography selection within the release bundle.", + ) + spm_receipt: Optional[SPMProvenance] = Field( + default=None, + description="Detached SPM calculation receipt, populated by a US run or load.", + ) + + @property + def spm_config(self) -> Optional[dict[str, Any]]: + """Resolve the US measurement selection without retaining a country object.""" + if getattr(self.tax_benefit_model_version, "country_code", None) != "us": + if self.spm is not None: + raise ValueError("SPM selection is only supported by the US model") + return None + from policyengine.tax_benefit_models.us.spm import resolve_spm_selection + + return resolve_spm_selection(self.spm) + + def spm_provenance(self) -> Optional[dict[str, Any]]: + """Return a detached JSON receipt, or None before a US calculation.""" + return ( + self.spm_receipt.model_dump(mode="json") + if self.spm_receipt is not None + else None + ) + + @property + def storage_id(self) -> str: + """Include resolved SPM settings in cache and saved-result identity.""" + config = self.spm_config + if config is None: + return self.id + encoded = json.dumps(config, sort_keys=True, separators=(",", ":")).encode() + return f"{self.id}-spm-{hashlib.sha256(encoded).hexdigest()}" + @model_validator(mode="after") def _compile_dict_reforms(self) -> "Simulation": """Coerce dict ``policy`` / ``dynamic`` inputs into proper objects. @@ -128,12 +168,24 @@ def _compile_dict_reforms(self) -> "Simulation": return self def run(self): + # Reject unsupported country settings even on model paths that do not + # use US measurement. Results from an earlier run are no longer valid. + if self.spm_config is not None: + self.spm_receipt = None + self.output_dataset = None self.tax_benefit_model_version.run(self) def ensure(self): - cached_result = _cache.get(self.id) + cache_key = self.storage_id + cached_result = _cache.get(cache_key) if cached_result: self.output_dataset = cached_result.output_dataset + self.spm = cached_result.spm + self.spm_receipt = ( + cached_result.spm_receipt.model_copy(deep=True) + if cached_result.spm_receipt is not None + else None + ) return try: self.tax_benefit_model_version.load(self) @@ -149,7 +201,18 @@ def ensure(self): self.run() self.save() - _cache.add(self.id, self) + # Cache a snapshot: changing this Simulation's selection then running + # it again must not overwrite the receipt for an earlier cache key. + _cache.add( + cache_key, + self.model_copy( + update={ + "spm_receipt": self.spm_receipt.model_copy(deep=True) + if self.spm_receipt is not None + else None + } + ), + ) def save(self): """Save the simulation's output dataset.""" @@ -171,15 +234,20 @@ def write_run_record(self, directory, **kwargs): return write_simulation_run_record(self, directory, **kwargs) @property - def release_bundle(self) -> dict[str, Optional[str]]: + def release_bundle(self) -> dict[str, Any]: bundle = ( self.tax_benefit_model_version.release_bundle if self.tax_benefit_model_version is not None else {} ) - return { + result = { **bundle, "dataset_filepath": self.dataset.filepath if self.dataset is not None else None, } + if self.spm_config is not None: + recorded = getattr(self.output_dataset, "metadata", {}).get("spm_config") + result["spm_config"] = dict(recorded or self.spm_config) + result["spm"] = self.spm_provenance() + return result diff --git a/src/policyengine/core/spm.py b/src/policyengine/core/spm.py new file mode 100644 index 00000000..2d14b86e --- /dev/null +++ b/src/policyengine/core/spm.py @@ -0,0 +1,85 @@ +"""Dependency-light public models for US SPM selection and calculation receipts.""" + +from datetime import date +from typing import Any, Literal, Optional + +from pydantic import ( + BaseModel, + ConfigDict, + Field, + SerializerFunctionWrapHandler, + field_validator, + model_serializer, + model_validator, +) + + +def _selection_schema(schema: dict[str, Any]) -> None: + """Keep generated clients from replacing inherited settings with defaults.""" + for field in schema.get("properties", {}).values(): + field.pop("default", None) + + +class SPMSelection(BaseModel): + """Select from the bundle's pinned artifact; national geography is explicit. + + County mode reads the household's observed ``county_fips``. A state alone + does not identify an SPM area. These settings contain no provider or path. + Serialization preserves omitted options so they still inherit bundle defaults + after a round trip. A resolved selection explicitly contains all six fields. + """ + + model_config = ConfigDict( + frozen=True, extra="forbid", json_schema_extra=_selection_schema + ) + + forecast_content_sha256: Optional[str] = Field( + default=None, pattern=r"^[0-9a-f]{64}$" + ) + scenario: Optional[str] = Field(default=None, min_length=1, pattern=r"^\S+$") + geography_kind: Literal["county", "national", "metro"] = "county" + geography_id: Optional[str] = Field(default=None, min_length=1) + county_vintage: str = Field(default="2020", pattern=r"^[0-9]{4}$") + as_of: Optional[str] = None + + @model_serializer(mode="wrap") + def serialize_selection(self, handler: SerializerFunctionWrapHandler): + """Keep inherited defaults distinct from explicit options in nested JSON.""" + return { + name: value + for name, value in handler(self).items() + if name in self.model_fields_set + } + + @field_validator("as_of") + @classmethod + def validate_as_of(cls, value): + if value is not None: + if date.fromisoformat(value).isoformat() != value: + raise ValueError("as_of must be an ISO calendar date (YYYY-MM-DD)") + return value + + @model_validator(mode="after") + def validate_location(self): + if self.geography_kind == "metro": + if not self.geography_id or not self.geography_id.strip(): + raise ValueError("An SPM area selection requires geography_id") + elif self.geography_id is not None: + raise ValueError("Only an SPM area selection accepts geography_id") + return self + + +class SPMProvenance(BaseModel): + """Detached calculation receipt; data certification is a separate claim.""" + + model_config = ConfigDict(extra="forbid") + + forecast_id: str + forecast_sha256: str + scenario: str + geography_kind: str + runtime_versions: dict[str, Optional[str]] + years: dict[str, dict[str, Any]] + geographies: list[dict[str, Any]] + composition_method: str + storage_method: str diff --git a/src/policyengine/outputs/decile_impact.py b/src/policyengine/outputs/decile_impact.py index b41d5973..097d50df 100644 --- a/src/policyengine/outputs/decile_impact.py +++ b/src/policyengine/outputs/decile_impact.py @@ -9,6 +9,7 @@ from policyengine.core.dataset import Dataset from policyengine.core.dynamic import Dynamic from policyengine.core.policy import Policy +from policyengine.core.spm import SPMSelection from policyengine.core.tax_benefit_model_version import TaxBenefitModelVersion from policyengine.outputs.decile_analysis import ( _prepare_decile_analysis, @@ -151,9 +152,14 @@ def calculate_decile_impacts( quantiles: int = 10, baseline_simulation: Optional[Simulation] = None, reform_simulation: Optional[Simulation] = None, + spm: Optional[SPMSelection] = None, ) -> OutputCollection[DecileImpact]: """Calculate decile-by-decile impact of a reform. + ``spm`` selects canonical US measurement settings when constructing the + baseline/reform simulations. When supplying simulations, select SPM on + each Simulation directly. + By default, changes are measured in ``household_net_income`` and household deciles are computed from that variable using survey weights multiplied by household size. Households with negative values of the computed income @@ -174,6 +180,8 @@ def calculate_decile_impacts( raise ValueError( "baseline_simulation and reform_simulation must be provided together" ) + if baseline_simulation is not None and spm is not None: + raise ValueError("Pass SPM settings on the supplied simulations directly") if baseline_simulation is None: if dataset is None or tax_benefit_model_version is None: @@ -186,12 +194,14 @@ def calculate_decile_impacts( tax_benefit_model_version=tax_benefit_model_version, policy=baseline_policy, dynamic=dynamic, + spm=spm, ) reform_simulation = Simulation( dataset=dataset, tax_benefit_model_version=tax_benefit_model_version, policy=reform_policy, dynamic=dynamic, + spm=spm, ) assert baseline_simulation is not None diff --git a/src/policyengine/tax_benefit_models/common/model_version.py b/src/policyengine/tax_benefit_models/common/model_version.py index c5e6c090..54466728 100644 --- a/src/policyengine/tax_benefit_models/common/model_version.py +++ b/src/policyengine/tax_benefit_models/common/model_version.py @@ -17,6 +17,7 @@ from __future__ import annotations import datetime +import json import os import tempfile import warnings @@ -25,6 +26,7 @@ from pathlib import Path from typing import TYPE_CHECKING, Any, ClassVar, Optional +import h5py import pandas as pd from policyengine.core import ( @@ -60,7 +62,7 @@ def output_dataset_filepath(simulation: Simulation) -> Path: parent = ( Path(input_filepath).parent if input_filepath else Path(tempfile.gettempdir()) ) - return parent / (simulation.id + ".h5") + return parent / (simulation.storage_id + ".h5") def build_runtime_dataset_provenance( @@ -353,7 +355,35 @@ def save(self, simulation: Simulation) -> None: "simulation.run() or simulation.ensure() first so there is " "something to persist." ) + serialized_spm = None + if self.country_code == "us": + from policyengine.core.spm import SPMProvenance + + receipt = SPMProvenance.model_validate(simulation.spm_provenance()) + if ( + simulation.output_dataset.metadata.get("spm_config") + != simulation.spm_config + ): + raise ValueError( + "SPM settings changed since this output was calculated; run again before saving" + ) + serialized_spm = json.dumps( + { + "config": simulation.spm_config, + "provenance": receipt.model_dump(mode="json"), + }, + sort_keys=True, + ) simulation.output_dataset.save() + if serialized_spm is not None: + # Receipts can exceed HDF5's attribute-size limit. Store UTF-8 + # JSON in a dataset, keeping the result file self-contained. + with h5py.File(simulation.output_dataset.filepath, "a") as stream: + stream.create_dataset( + "policyengine_spm", + data=serialized_spm, + dtype=h5py.string_dtype("utf-8"), + ) def load(self, simulation: Simulation) -> None: """Rehydrate the simulation's output dataset from disk. @@ -363,9 +393,26 @@ def load(self, simulation: Simulation) -> None: are not written back on ``save()``, so they're filesystem approximations, not a true round-trip of the original timestamps. """ - filepath = str( - Path(simulation.dataset.filepath).parent / (simulation.id + ".h5") - ) + filepath = str(output_dataset_filepath(simulation)) + + receipt = None + if self.country_code == "us": + from policyengine.core.spm import SPMProvenance + + with h5py.File(filepath, "r") as stream: + raw = ( + stream["policyengine_spm"].asstr()[()] + if "policyengine_spm" in stream + else None + ) + if raw is None: + raise ValueError( + "Saved US simulation has no SPM configuration or receipt" + ) + recorded = json.loads(raw) + if recorded["config"] != simulation.spm_config: + raise ValueError("Saved US simulation uses different SPM settings") + receipt = SPMProvenance.model_validate(recorded["provenance"]) simulation.output_dataset = self._dataset_class( id=simulation.id, @@ -375,6 +422,12 @@ def load(self, simulation: Simulation) -> None: year=simulation.dataset.year, is_output_dataset=True, ) + if self.country_code == "us": + from policyengine.core.spm import SPMSelection + + simulation.spm_receipt = receipt + simulation.spm = SPMSelection.model_validate(recorded["config"]) + simulation.output_dataset.metadata["spm_config"] = recorded["config"] if os.path.exists(filepath): simulation.created_at = datetime.datetime.fromtimestamp( diff --git a/src/policyengine/tax_benefit_models/common/result.py b/src/policyengine/tax_benefit_models/common/result.py index e73fa406..78b5c221 100644 --- a/src/policyengine/tax_benefit_models/common/result.py +++ b/src/policyengine/tax_benefit_models/common/result.py @@ -63,8 +63,8 @@ def to_dict(self) -> dict[str, Any]: """Return a plain ``dict[str, Any]`` copy suitable for JSON dumps.""" def _convert(value: Any) -> Any: - if isinstance(value, EntityResult): - return dict(value) + if isinstance(value, dict): + return {key: _convert(item) for key, item in value.items()} if isinstance(value, list): return [_convert(v) for v in value] return value diff --git a/tests/test_spm_decile.py b/tests/test_spm_decile.py new file mode 100644 index 00000000..db217b61 --- /dev/null +++ b/tests/test_spm_decile.py @@ -0,0 +1,44 @@ +"""Decile convenience construction preserves explicit measurement choices.""" + +from types import SimpleNamespace + +import pytest + +from policyengine.core.spm import SPMSelection +from policyengine.outputs import decile_impact + + +def test_decile_factory_uses_same_explicit_selection_for_baseline_and_reform( + monkeypatch, +): + selection = SPMSelection(geography_kind="national", scenario="zero_real") + created = [] + + def construct(**kwargs): + simulation = SimpleNamespace(**kwargs, ensure=lambda: None) + created.append(simulation) + return simulation + + class AnalysisReached(Exception): + pass + + def analyse(baseline, reform, **kwargs): + assert baseline.spm == reform.spm == selection + raise AnalysisReached + + monkeypatch.setattr(decile_impact, "Simulation", construct) + monkeypatch.setattr(decile_impact, "_prepare_decile_analysis", analyse) + with pytest.raises(AnalysisReached): + decile_impact.calculate_decile_impacts( + dataset=object(), tax_benefit_model_version=object(), spm=selection + ) + assert len(created) == 2 + + +def test_decile_supplied_simulations_cannot_silently_ignore_selection(): + with pytest.raises(ValueError, match="supplied simulations"): + decile_impact.calculate_decile_impacts( + baseline_simulation=object(), + reform_simulation=object(), + spm=SPMSelection(geography_kind="national"), + ) diff --git a/tests/test_spm_selection_schema.py b/tests/test_spm_selection_schema.py new file mode 100644 index 00000000..e665435f --- /dev/null +++ b/tests/test_spm_selection_schema.py @@ -0,0 +1,52 @@ +"""Public schemas must let clients leave bundle-dependent settings omitted.""" + +import pytest +from jsonschema import Draft202012Validator +from pydantic import BaseModel + +from policyengine.core.spm import SPMSelection + + +class SelectionEnvelope(BaseModel): + spm: SPMSelection | None = None + + +@pytest.mark.parametrize("mode", ["validation", "serialization"]) +@pytest.mark.parametrize("nested", [False, True]) +def test_spm_schema_keeps_typed_optional_fields_without_client_defaults(mode, nested): + schema = ( + SelectionEnvelope.model_json_schema(mode=mode)["$defs"]["SPMSelection"] + if nested + else SPMSelection.model_json_schema(mode=mode) + ) + assert set(schema["properties"]) == set(SPMSelection.model_fields) + assert schema.get("required", []) == [] + assert schema["additionalProperties"] is False + for field in schema["properties"].values(): + assert "default" not in field + assert schema["properties"]["geography_kind"]["enum"] == [ + "county", + "national", + "metro", + ] + assert schema["properties"]["county_vintage"]["pattern"] == r"^[0-9]{4}$" + validator = Draft202012Validator(schema) + validator.validate({}) + validator.validate({"scenario": "zero_real"}) + validator.validate({"geography_kind": "county", "as_of": None}) + assert not validator.is_valid({"geography_kind": "state"}) + assert not validator.is_valid({"provider": "/tmp/unpinned.json"}) + + +def test_schema_inheritance_contract_preserves_python_runtime_defaults(): + selection = SPMSelection() + assert selection.geography_kind == "county" + assert selection.county_vintage == "2020" + assert selection.model_dump() == {} + assert selection.model_dump_json() == "{}" + explicit = SPMSelection(geography_kind="county", county_vintage="2020", as_of=None) + assert explicit.model_dump() == { + "geography_kind": "county", + "county_vintage": "2020", + "as_of": None, + } From 62d8eb98f6e616c1055540c0e10fbb2d2b62f741 Mon Sep 17 00:00:00 2001 From: Max Ghenis Date: Fri, 11 Sep 2026 12:30:23 -0400 Subject: [PATCH 04/46] Integrate canonical SPM and native entity data in the US model Adds the US-side resolver that merges a caller's partial selection with the bundle's pinned measurement settings, and uses it everywhere a US population or household simulation is constructed: calculate_household, the microsimulation run path, managed_microsimulation and create_datasets. Default US outputs now include SPM resources and poverty, so a household needs an observed county_fips or an explicit metro/national choice, and every result carries the resolved configuration plus a calculation receipt. Formula-owned SPM inputs are rejected before either the reform or its baseline is built, including through axes, so no set_input can suppress a canonical formula. The same change makes native producer tables safe to consume. Entity IDs are validated for uniqueness and nullity before any join; missing person and group weight columns are projected from calibrated household weights through native membership IDs on in-memory copies, leaving the source HDF read-only and raising instead of producing unweighted rows when a link is missing or a group spans households. Inputs are reordered to the country populations' ID order before set_input, and calculated outputs are mapped back to the native table order before being attached to IDs and weights, so independently shuffled entity tables keep the right geography and results. A standalone script runs the producer's native- input qualification against candidate wheels without publishing anything. Existing US household tests select national geography explicitly, since those fixtures exercise the calculator interface without claiming a local SPM area, and the numeric snapshot drops the additive provenance section it does not assert on. Co-Authored-By: Claude Fable 5.1 --- changelog.d/canonical-spm-review.fixed.md | 6 + changelog.d/native-us-hdf-weights.fixed.md | 1 + scripts/check_native_spm_loading.py | 135 ++++++++ src/policyengine/__init__.py | 1 + .../tax_benefit_models/us/__init__.py | 5 + .../tax_benefit_models/us/datasets.py | 132 +++++--- .../tax_benefit_models/us/household.py | 33 +- .../tax_benefit_models/us/model.py | 84 ++++- src/policyengine/tax_benefit_models/us/spm.py | 49 +++ tests/test_dataset_runtime.py | 5 +- tests/test_household_calculator_snapshot.py | 8 +- tests/test_household_impact.py | 25 ++ tests/test_spm_household.py | 292 ++++++++++++++++++ tests/test_spm_model.py | 245 +++++++++++++++ tests/test_spm_selection.py | 247 +++++++++++++++ tests/test_us_native_alignment.py | 226 ++++++++++++++ tests/test_us_native_hdf_weights.py | 244 +++++++++++++++ 17 files changed, 1678 insertions(+), 60 deletions(-) create mode 100644 changelog.d/canonical-spm-review.fixed.md create mode 100644 changelog.d/native-us-hdf-weights.fixed.md create mode 100644 scripts/check_native_spm_loading.py create mode 100644 src/policyengine/tax_benefit_models/us/spm.py create mode 100644 tests/test_spm_household.py create mode 100644 tests/test_spm_model.py create mode 100644 tests/test_spm_selection.py create mode 100644 tests/test_us_native_alignment.py create mode 100644 tests/test_us_native_hdf_weights.py diff --git a/changelog.d/canonical-spm-review.fixed.md b/changelog.d/canonical-spm-review.fixed.md new file mode 100644 index 00000000..7f84f3f4 --- /dev/null +++ b/changelog.d/canonical-spm-review.fixed.md @@ -0,0 +1,6 @@ +Preserve native entity ID alignment across input/output tables, reject malformed +group IDs, and retain partial SPM selection semantics through JSON round trips. +Validate managed SPM settings before dataset materialization, preserve +dependency-light bundle bootstrap, and enforce published measurement pins in +the release workflow. Authenticate candidate import sources and correct the +measurement serialization example. diff --git a/changelog.d/native-us-hdf-weights.fixed.md b/changelog.d/native-us-hdf-weights.fixed.md new file mode 100644 index 00000000..1c7484c3 --- /dev/null +++ b/changelog.d/native-us-hdf-weights.fixed.md @@ -0,0 +1 @@ +Load native US pandas HDF datasets that store only calibrated household weights by mapping missing person and entity weights through native membership IDs in memory, preserving source files and supplied weights. diff --git a/scripts/check_native_spm_loading.py b/scripts/check_native_spm_loading.py new file mode 100644 index 00000000..0e96f141 --- /dev/null +++ b/scripts/check_native_spm_loading.py @@ -0,0 +1,135 @@ +"""Run the producer's four-wheel native-input qualification without publication. + +Run only after installing the four candidate wheels in an isolated environment. +The wrapper must import from its installed wheel, never a checkout PYTHONPATH. +The explicit development bootstrap does not certify data or published packages. +""" + +from __future__ import annotations + +import argparse +import hashlib +import importlib.util +import json +import sys +from pathlib import Path + + +def sha256_file(path: Path) -> str: + digest = hashlib.sha256() + with path.open("rb") as stream: + for chunk in iter(lambda: stream.read(1024 * 1024), b""): + digest.update(chunk) + return digest.hexdigest() + + +def check_derived_weights(path: Path) -> dict[str, int]: + """Check household weight projection with one additional read-only load.""" + import numpy as np + + from policyengine.tax_benefit_models.us.datasets import PolicyEngineUSDataset + + dataset = PolicyEngineUSDataset( + name="native_spm_weight_qualification", + description="Read-only mapping check; not population model acceptance", + filepath=str(path), + year=2024, + ) + import pandas as pd + + # This is an ID/array qualification, never a population poverty statistic. + person = pd.DataFrame(dataset.data.person) + household = pd.DataFrame(dataset.data.household) + lookup = household.set_index("household_id")["household_weight"] + counts = {} + for entity, frame in dataset.data.entity_data.items(): + frame = pd.DataFrame(frame) + counts[entity] = len(frame) + if entity == "household": + continue + if entity == "person": + expected = person["person_household_id"].map(lookup) + else: + link = f"person_{entity}_id" + relationships = person[[link, "person_household_id"]].drop_duplicates() + assert not relationships[link].duplicated().any() + group_households = relationships.set_index(link)["person_household_id"] + expected = frame[f"{entity}_id"].map(group_households).map(lookup) + if not np.array_equal( + frame[f"{entity}_weight"].to_numpy(), expected.to_numpy() + ): + raise ValueError( + f"Wrapper {entity} weights do not match household projection" + ) + return counts + + +def main() -> None: + parser = argparse.ArgumentParser(description=__doc__) + parser.add_argument("--candidate-h5", required=True, type=Path) + parser.add_argument("--producer-src", required=True, type=Path) + parser.add_argument("--development-manifest", required=True, type=Path) + parser.add_argument("--wheel", required=True, action="append", type=Path) + parser.add_argument("--check-derived-weights", action="store_true") + parser.add_argument("--output", type=Path) + args = parser.parse_args() + if args.output is not None and args.output.exists(): + parser.error("Output must be a new diagnostic file") + + # Import the producer read-only. This path contains Microcosm only, so + # installed-wrapper source ownership remains independently verifiable. + sys.dont_write_bytecode = True + sys.path.insert(0, str(args.producer_src.resolve(strict=True))) + helper = ( + Path(__file__).resolve().parents[1] + / "tests" + / "fixtures" + / "spm_development.py" + ) + spec = importlib.util.spec_from_file_location("spm_qualification_bootstrap", helper) + bootstrap = importlib.util.module_from_spec(spec) + spec.loader.exec_module(bootstrap) + bootstrap.activate_spm_development_manifest(args.development_manifest) + + from microcosm.data.source_enrichment import run_native_loader_compatibility + + before = sha256_file(args.candidate_h5) + try: + receipt = run_native_loader_compatibility( + args.candidate_h5, + require_wheels=True, + compatibility_wheels=tuple(args.wheel), + ) + weight_check = ( + check_derived_weights(args.candidate_h5) + if args.check_derived_weights + else None + ) + finally: + after = sha256_file(args.candidate_h5) + if after != before: + raise RuntimeError("Native candidate H5 changed during qualification") + report = { + "scope": "read_only_native_input_qualification", + "data_certification": "not_certified", + "external_package_publication": "not_attested", + "canonical_spm_model_acceptance": "not_attested", + "candidate_sha256_before": before, + "candidate_sha256_after": after, + "development_manifest": { + "path": str(args.development_manifest.resolve()), + "sha256": sha256_file(args.development_manifest), + }, + "probe_sha256": sha256_file(Path(__file__)), + "derived_weight_check": weight_check, + "producer_compatibility": receipt, + } + text = json.dumps(report, indent=2, sort_keys=True) + "\n" + if args.output is not None: + args.output.parent.mkdir(parents=True, exist_ok=True) + args.output.write_text(text) + print(text, end="") + + +if __name__ == "__main__": + main() diff --git a/src/policyengine/__init__.py b/src/policyengine/__init__.py index 1c4fb0ae..587cc254 100644 --- a/src/policyengine/__init__.py +++ b/src/policyengine/__init__.py @@ -10,6 +10,7 @@ result = pe.us.calculate_household( people=[{"age": 35, "employment_income": 60000}], tax_unit={"filing_status": "SINGLE"}, + household={"state_code": "NY", "county_fips": "36061"}, year=2026, reform={"gov.irs.credits.ctc.amount.adult_dependent": 1000}, ) diff --git a/src/policyengine/tax_benefit_models/us/__init__.py b/src/policyengine/tax_benefit_models/us/__init__.py index d98ba089..bfd62147 100644 --- a/src/policyengine/tax_benefit_models/us/__init__.py +++ b/src/policyengine/tax_benefit_models/us/__init__.py @@ -10,6 +10,7 @@ result = pe.us.calculate_household( people=[{"age": 35, "employment_income": 60000}], tax_unit={"filing_status": "SINGLE"}, + household={"state_code": "NY", "county_fips": "36061"}, year=2026, ) print(result.tax_unit.income_tax) @@ -18,6 +19,7 @@ reformed = pe.us.calculate_household( people=[{"age": 35, "employment_income": 60000}], tax_unit={"filing_status": "SINGLE"}, + household={"state_code": "NY", "county_fips": "36061"}, year=2026, reform={"gov.irs.credits.ctc.amount.adult_dependent": 1000}, extra_variables=["adjusted_gross_income"], @@ -52,6 +54,7 @@ managed_microsimulation, us_latest, ) + from .spm import SPMProvenance, SPMSelection model = us_latest """The pinned US ``TaxBenefitModelVersion`` for this policyengine release.""" @@ -78,6 +81,8 @@ "model", "us_latest", "calculate_household", + "SPMSelection", + "SPMProvenance", "economic_impact_analysis", "calculate_budgetary_impact", "BudgetaryImpact", diff --git a/src/policyengine/tax_benefit_models/us/datasets.py b/src/policyengine/tax_benefit_models/us/datasets.py index 7032ea61..2ff4aa47 100644 --- a/src/policyengine/tax_benefit_models/us/datasets.py +++ b/src/policyengine/tax_benefit_models/us/datasets.py @@ -106,18 +106,17 @@ def load(self) -> None: return with pd.HDFStore(filepath, mode="r") as store: - self.data = USYearData( - person=MicroDataFrame(store["person"], weights="person_weight"), - marital_unit=MicroDataFrame( - store["marital_unit"], weights="marital_unit_weight" - ), - family=MicroDataFrame(store["family"], weights="family_weight"), - spm_unit=MicroDataFrame(store["spm_unit"], weights="spm_unit_weight"), - tax_unit=MicroDataFrame(store["tax_unit"], weights="tax_unit_weight"), - household=MicroDataFrame( - store["household"], weights="household_weight" - ), - ) + data = {entity: store[entity] for entity in US_ENTITY_KEYS} + # Native producer files store calibrated household weights. Project + # missing weights through membership IDs on these in-memory copies, + # before constructing MicroDataFrames; the source HDF remains read-only. + _assign_missing_entity_weights(data) + self.data = USYearData( + **{ + entity: MicroDataFrame(frame, weights=f"{entity}_weight") + for entity, frame in data.items() + } + ) def __repr__(self) -> str: if self.data is None: @@ -201,45 +200,88 @@ def _core_h5_variable_entities() -> dict[str, str]: return {name: variable.entity.key for name, variable in system.variables.items()} +def _validate_entity_ids(data: dict[str, pd.DataFrame]) -> None: + """Reject ambiguous row identities before joining inputs or weights.""" + for entity, frame in data.items(): + entity_id = US_ENTITY_ID_COLUMNS[entity] + if entity_id not in frame: + raise ValueError(f"Missing {entity_id} for native entity alignment") + if frame[entity_id].isna().any() or frame[entity_id].duplicated().any(): + raise ValueError( + f"Native entity alignment requires unique nonmissing {entity_id} values" + ) + + def _assign_missing_entity_weights(data: dict[str, pd.DataFrame]) -> None: + """Project missing weights from calibrated households through native IDs. + + Both supported HDF layouts use this mapping. Existing weight columns are + retained; group weights are one household weight, never sums of person + weights. Missing or ambiguous links cannot silently produce unweighted rows. + """ + _validate_entity_ids(data) + missing = [ + entity for entity in US_ENTITY_KEYS if f"{entity}_weight" not in data[entity] + ] + if not missing: + return household = data["household"] if "household_id" not in household or "household_weight" not in household: - return - - household_weights = household[["household_id", "household_weight"]] - person = data["person"] + raise ValueError( + "Missing household_id or household_weight for deriving entity weights" + ) if ( - "person_weight" not in person - and "person_household_id" in person - and len(person) > 0 + household["household_id"].isna().any() + or household["household_id"].duplicated().any() ): - person.loc[:, "person_weight"] = person["person_household_id"].map( - household_weights.set_index("household_id")["household_weight"] + raise ValueError( + "Deriving entity weights requires unique nonmissing household_id values" + ) + if household["household_weight"].isna().any(): + raise ValueError( + "Cannot derive entity weights from missing household_weight values" ) - for entity, person_entity_id in US_PERSON_ENTITY_ID_COLUMNS.items(): - if entity == "household": - continue - entity_id = US_ENTITY_ID_COLUMNS[entity] - entity_weight = US_ENTITY_WEIGHT_COLUMNS[entity] - if ( - entity_weight in data[entity] - or entity_id not in data[entity] - or person_entity_id not in person - or "person_household_id" not in person - ): - continue + household_weights = household.set_index("household_id")["household_weight"] + person = data["person"] - entity_households = person[ - [person_entity_id, "person_household_id"] - ].drop_duplicates(subset=[person_entity_id]) - weight_lookup = entity_households.merge( - household_weights, - left_on="person_household_id", - right_on="household_id", - how="left", - ).set_index(person_entity_id)["household_weight"] - data[entity].loc[:, entity_weight] = data[entity][entity_id].map(weight_lookup) + def person_link(entity: str) -> str: + native = US_PERSON_ENTITY_ID_COLUMNS[entity] + alias = US_ENTITY_ID_COLUMNS[entity] + if native in person: + return native + if alias in person: + return alias + raise ValueError(f"Missing person {native} link for deriving {entity} weights") + + for entity in missing: + frame = data[entity] + weight = US_ENTITY_WEIGHT_COLUMNS[entity] + if frame.empty: + frame[weight] = pd.Series(index=frame.index, dtype=household_weights.dtype) + continue + household_link = person_link("household") + if entity == "person": + weights = person[household_link].map(household_weights) + else: + entity_id = US_ENTITY_ID_COLUMNS[entity] + if entity_id not in frame: + raise ValueError(f"Missing {entity_id} for deriving {weight}") + entity_link = person_link(entity) + relationships = person[[entity_link, household_link]].drop_duplicates() + if relationships[entity_link].duplicated().any(): + raise ValueError( + f"Cannot derive {weight}: {entity_id} spans multiple households" + ) + weight_lookup = relationships.set_index(entity_link)[household_link].map( + household_weights + ) + weights = frame[entity_id].map(weight_lookup) + if weights.isna().any(): + raise ValueError( + f"Cannot derive {weight}: missing or unknown native household/entity links" + ) + frame[weight] = weights def _load_policyengine_core_h5(path: Path, year: int) -> USYearData: @@ -298,6 +340,8 @@ def create_datasets( """ from policyengine_us import Microsimulation + from policyengine.tax_benefit_models.us.spm import resolve_spm_selection + dataset_requests: list[Optional[str]] = datasets or [None] result = {} for dataset in dataset_requests: @@ -308,7 +352,7 @@ def create_datasets( data_dir=Path(data_folder), ) dataset_stem = source.name - sim = Microsimulation(dataset=source.path) + sim = Microsimulation(dataset=source.path, spm=resolve_spm_selection()) for year in years: # Get all input variables from the simulation diff --git a/src/policyengine/tax_benefit_models/us/household.py b/src/policyengine/tax_benefit_models/us/household.py index 5122bb15..c1b86f23 100644 --- a/src/policyengine/tax_benefit_models/us/household.py +++ b/src/policyengine/tax_benefit_models/us/household.py @@ -15,7 +15,7 @@ {"age": 6, "is_tax_unit_dependent": True}, ], tax_unit={"filing_status": "HEAD_OF_HOUSEHOLD"}, - household={"state_code": "NY"}, + household={"state_code": "NY", "county_fips": "36061"}, year=2026, extra_variables=["adjusted_gross_income"], ) @@ -29,7 +29,7 @@ {"age": 6, "is_tax_unit_dependent": True}, ], tax_unit={"filing_status": "HEAD_OF_HOUSEHOLD"}, - household={"state_code": "NY"}, + household={"state_code": "NY", "county_fips": "36061"}, year=2026, reform={"gov.usda.snap.income.deductions.earned_income": 0}, ) @@ -52,6 +52,7 @@ from policyengine.utils.household_validation import validate_household_input from .model import us_latest +from .spm import SPMSelection, calculation_provenance, resolve_spm_selection _GROUP_ENTITIES = ("marital_unit", "family", "spm_unit", "tax_unit", "household") @@ -68,7 +69,7 @@ def _raise_unexpected_kwargs(unexpected: Mapping[str, Any]) -> None: lines.append(f" - '{name}'{hint}") lines.append( "Valid kwargs: people, marital_unit, family, spm_unit, tax_unit, " - "household, year, reform, extra_variables, axes." + "household, year, reform, extra_variables, axes, spm." ) raise TypeError("\n".join(lines)) @@ -142,6 +143,7 @@ def _group(spec: Mapping[str, Any]) -> dict[str, Any]: "reform", "extra_variables", "axes", + "spm", } ) @@ -158,6 +160,7 @@ def calculate_household( reform: Optional[Mapping[str, Any]] = None, extra_variables: Optional[list[str]] = None, axes: Optional[list[Any]] = None, + spm: Optional[SPMSelection] = None, **unexpected: Any, ) -> HouseholdResult: """Compute tax and benefit variables for a single US household. @@ -184,6 +187,11 @@ def calculate_household( shape or a flat list of axis dictionaries. Missing ``period`` values default to ``year``. When axes are present, result values are lists ordered by the axis grid instead of scalars. + spm: SPMSelection or mapping selecting a scenario and geography from + the bundle's independently pinned artifact. Default outputs require + household county_fips or an explicit metro/national selection, + including when housing assistance is zero. Formula-owned SPM + amounts and measurement counts cannot be supplied as inputs/axes. Returns: :class:`HouseholdResult` with dot-accessible per-entity @@ -218,6 +226,11 @@ def calculate_household( ) from policyengine_us import Simulation + from spm_calculator.policyengine_adapter import validate_policyengine_inputs + + validate_policyengine_inputs( + {"person": people, **{name: [value] for name, value in entities.items()}} + ) validate_household_input( model_version=us_latest, @@ -234,7 +247,16 @@ def calculate_household( output_columns = _default_output_columns(extra_by_entity) reform_dict = compile_reform(reform, year=year, model_version=us_latest) normalized_axes = normalize_axes(axes=axes, year=year, model_version=us_latest) + if normalized_axes is not None: + validate_policyengine_inputs( + { + "axes": [ + {axis["name"]: None} for group in normalized_axes for axis in group + ] + } + ) axes_active = normalized_axes is not None + spm_config = resolve_spm_selection(spm) simulation = Simulation( situation=_build_situation( @@ -248,6 +270,7 @@ def calculate_household( axes=normalized_axes, ), reform=reform_dict, + spm=spm_config, ) result = HouseholdResult() @@ -282,4 +305,8 @@ def calculate_household( for variable in columns } ) + result["provenance"] = { + "spm_config": dict(simulation.spm_config), + "spm": calculation_provenance(simulation), + } return result diff --git a/src/policyengine/tax_benefit_models/us/model.py b/src/policyengine/tax_benefit_models/us/model.py index c517855f..51338b3e 100644 --- a/src/policyengine/tax_benefit_models/us/model.py +++ b/src/policyengine/tax_benefit_models/us/model.py @@ -16,7 +16,13 @@ output_dataset_filepath as _output_dataset_filepath, ) -from .datasets import PolicyEngineUSDataset, USYearData +from .datasets import PolicyEngineUSDataset, USYearData, _validate_entity_ids +from .spm import ( + SPMProvenance, + SPMSelection, + calculation_provenance, + resolve_spm_selection, +) if TYPE_CHECKING: from policyengine.core.simulation import Simulation @@ -157,6 +163,12 @@ def run(self, simulation: "Simulation") -> "Simulation": if dataset.data is None: dataset.load() + # Validate every entity before constructing either the reform or its + # baseline, so no set_input can suppress a canonical SPM formula. + from spm_calculator.policyengine_adapter import validate_policyengine_inputs + + validate_policyengine_inputs(dataset.data.entity_data) + # Apply regional scoping if specified if simulation.scoping_strategy: scoped_data = simulation.scoping_strategy.apply( @@ -190,7 +202,17 @@ def run(self, simulation: "Simulation") -> "Simulation": dynamic_reform = build_reform_dict(simulation.dynamic) reform_dict = merge_reform_dicts(policy_reform, dynamic_reform) - microsim = Microsimulation(reform=reform_dict) + class InputMicrosimulation(Microsimulation): + # The caller already supplied the entire dataset. The country + # default would load another population before we build this one. + default_dataset = None + + microsim = InputMicrosimulation( + situation={}, + reform=reform_dict, + spm=simulation.spm_config, + default_input_period=dataset.year, + ) # Use ``microsim.tax_benefit_system``, not the module-level # ``system``: ``Microsimulation.__init__`` applies structural # reforms (e.g. ``gov.contrib.ctc.*``) to its per-sim system but @@ -248,22 +270,33 @@ def run(self, simulation: "Simulation") -> "Simulation": # Person entity also needs person-level group ID columns so that # downstream joins (e.g. person->tax_unit) work. person_input_df = pd.DataFrame(dataset.data.person) - for col in person_input_df.columns: - if col.startswith("person_") and col.endswith("_id"): - target_col = col.replace("person_", "") - if target_col in id_columns: - data["person"][target_col] = person_input_df[col].values + for entity in US_GROUP_ENTITIES: + target_col = f"{entity}_id" + native_col = f"person_{target_col}" + source_col = ( + native_col if native_col in person_input_df.columns else target_col + ) + if source_col in person_input_df.columns: + data["person"][target_col] = person_input_df[source_col].values # Calculate non-ID, non-weight variables from simulation. # ``resolve_entity_variables`` merges bundled defaults with # caller-supplied ``simulation.extra_variables``; unknown # entity keys or variable names raise with close-match hints. for entity, variables in self.resolve_entity_variables(simulation).items(): + # Country populations use their own ID order. Restore each native + # table's order before attaching values to its IDs and weights. + input_ids = pd.DataFrame(getattr(dataset.data, entity))[f"{entity}_id"] + output_order = pd.Index(microsim.populations[entity].ids).get_indexer( + input_ids + ) + if (output_order < 0).any(): + raise ValueError(f"Missing {entity}_id in simulation output") for var in variables: if var not in id_columns and var not in weight_columns: data[entity][var] = microsim.calculate( var, period=simulation.dataset.year, map_to=entity - ).values + ).values[output_order] data["person"] = MicroDataFrame(data["person"], weights="person_weight") data["marital_unit"] = MicroDataFrame( @@ -283,6 +316,7 @@ def run(self, simulation: "Simulation") -> "Simulation": filepath=str(_output_dataset_filepath(simulation)), year=simulation.dataset.year, is_output_dataset=True, + metadata={"spm_config": dict(microsim.spm_config)}, data=USYearData( person=data["person"], marital_unit=data["marital_unit"], @@ -292,6 +326,10 @@ def run(self, simulation: "Simulation") -> "Simulation": household=data["household"], ), ) + simulation.spm_receipt = SPMProvenance.model_validate( + calculation_provenance(microsim) + ) + simulation.spm = SPMSelection.model_validate(microsim.spm_config) def _build_simulation_from_dataset(self, microsim, dataset, system): """Build a PolicyEngine Core simulation from dataset entity IDs. @@ -304,6 +342,10 @@ def _build_simulation_from_dataset(self, microsim, dataset, system): from policyengine_core.simulations.simulation_builder import ( SimulationBuilder, ) + from spm_calculator.policyengine_adapter import validate_policyengine_inputs + + validate_policyengine_inputs(dataset.data.entity_data) + _validate_entity_ids(dataset.data.entity_data) builder = SimulationBuilder() builder.populations = system.instantiate_entities() @@ -336,6 +378,24 @@ def _build_simulation_from_dataset(self, microsim, dataset, system): else "tax_unit_id" ) + # The builder requires membership IDs and declared population IDs to + # describe the same groups. Check explicitly before its positional + # membership mapping can silently attach a person to a different group. + for entity, link in { + "household": household_id_col, + "marital_unit": marital_unit_id_col, + "family": family_id_col, + "spm_unit": spm_unit_id_col, + "tax_unit": tax_unit_id_col, + }.items(): + if link not in person_data or person_data[link].isna().any(): + raise ValueError(f"Missing nonnull person {link} membership") + entity_ids = pd.DataFrame(getattr(dataset.data, entity))[f"{entity}_id"] + if set(entity_ids) != set(person_data[link]): + raise ValueError( + f"Native {entity}_id values must match person {link} memberships" + ) + builder.declare_person_entity("person", person_data["person_id"].values) builder.declare_entity( "household", np.unique(person_data[household_id_col].values) @@ -401,7 +461,8 @@ def _build_simulation_from_dataset(self, microsim, dataset, system): ("tax_unit", dataset.data.tax_unit), ("marital_unit", dataset.data.marital_unit), ]: - df = pd.DataFrame(entity_df) + df = pd.DataFrame(entity_df).set_index(f"{entity_name}_id", drop=False) + df = df.loc[microsim.populations[entity_name].ids] for column in df.columns: if column not in id_columns and column in system.variables: microsim.set_input(column, dataset.year, df[column].values) @@ -411,6 +472,7 @@ def managed_microsimulation( *, dataset: Optional[str] = None, allow_unmanaged: bool = False, + spm: Optional[SPMSelection] = None, **kwargs, ): """Construct a country-package Microsimulation pinned to this bundle. @@ -428,12 +490,13 @@ def managed_microsimulation( "**kwargs, so policyengine.py can enforce the release bundle." ) + selection = resolve_spm_selection(spm) source = materialize_dataset( "us", dataset, allow_unmanaged=allow_unmanaged, ) - microsim = Microsimulation(dataset=source.path, **kwargs) + microsim = Microsimulation(dataset=source.path, spm=selection, **kwargs) microsim.policyengine_bundle = dict(us_latest.release_bundle) microsim.policyengine_bundle.update( build_runtime_dataset_provenance( @@ -442,6 +505,7 @@ def managed_microsimulation( source.bundle_dataset, ) ) + microsim.policyengine_bundle["spm"] = dict(microsim.spm_config) return microsim diff --git a/src/policyengine/tax_benefit_models/us/spm.py b/src/policyengine/tax_benefit_models/us/spm.py new file mode 100644 index 00000000..568fa58f --- /dev/null +++ b/src/policyengine/tax_benefit_models/us/spm.py @@ -0,0 +1,49 @@ +"""Typed SPM selection and receipts for the certified US model bundle.""" + +from policyengine.core.spm import SPMProvenance, SPMSelection + +__all__ = [ + "SPMSelection", + "SPMProvenance", + "resolve_spm_selection", + "calculation_provenance", +] + + +def resolve_spm_selection(selection=None) -> dict: + """Resolve public options against an independently pinned bundle artifact.""" + from policyengine.bundle import get_current_bundle + + configured = get_current_bundle().get("measurements", {}).get("spm") + if not isinstance(configured, dict): + raise ValueError( + "The current bundle has no certified SPM measurement configuration" + ) + defaults = SPMSelection.model_validate(configured) + if defaults.forecast_content_sha256 is None or defaults.scenario is None: + raise ValueError( + "The bundle must pin the SPM artifact hash and default scenario" + ) + chosen = SPMSelection.model_validate({} if selection is None else selection) + if chosen.forecast_content_sha256 not in (None, defaults.forecast_content_sha256): + raise ValueError("SPM selection does not match this bundle's artifact hash") + # Serialization intentionally preserves omissions in a partial selection. + # Materialize the validated bundle defaults before applying that selection, + # so the returned config freezes every setting for later replay. + values = {name: getattr(defaults, name) for name in SPMSelection.model_fields} + values.update(chosen.model_dump(exclude_unset=True)) + if ( + chosen.geography_kind != defaults.geography_kind + and "geography_kind" in chosen.model_fields_set + ): + values["geography_id"] = chosen.geography_id + # Explicit null must not remove an independently expected artifact identity. + values["forecast_content_sha256"] = defaults.forecast_content_sha256 + values["scenario"] = chosen.scenario or defaults.scenario + return SPMSelection.model_validate(values).model_dump() + + +def calculation_provenance(simulation) -> dict: + return SPMProvenance.model_validate(simulation.spm_provenance()).model_dump( + mode="json" + ) diff --git a/tests/test_dataset_runtime.py b/tests/test_dataset_runtime.py index 9d97db0e..76cab79b 100644 --- a/tests/test_dataset_runtime.py +++ b/tests/test_dataset_runtime.py @@ -9,6 +9,7 @@ MaterializedDataset, _resolve_bundle_dataset, ) +from policyengine.tax_benefit_models.us.spm import resolve_spm_selection REPO_ROOT = Path(__file__).resolve().parents[1] @@ -70,7 +71,9 @@ def test_us_create_datasets_passes_verified_bundle_source_to_country_package( allow_unmanaged=False, data_dir=Path("./data"), ) - microsimulation.assert_called_once_with(dataset="/tmp/populace_us_2024.h5") + microsimulation.assert_called_once_with( + dataset="/tmp/populace_us_2024.h5", spm=resolve_spm_selection() + ) def test_uk_create_datasets_passes_verified_bundle_source_to_country_package( diff --git a/tests/test_household_calculator_snapshot.py b/tests/test_household_calculator_snapshot.py index 987d49d8..f75816d9 100644 --- a/tests/test_household_calculator_snapshot.py +++ b/tests/test_household_calculator_snapshot.py @@ -117,9 +117,13 @@ def test_us_household_snapshot(case_name: str) -> None: import policyengine as pe kwargs = US_CASES[case_name] - result = pe.us.calculate_household(**kwargs) + result = pe.us.calculate_household(**kwargs, spm={"geography_kind": "national"}) out: dict[str, float] = {} - _flatten("", result.to_dict(), out) + # Provenance is an additive receipt tested in test_spm_household.py; keep + # this historical snapshot focused on its existing numeric output contract. + values = result.to_dict() + values.pop("provenance", None) + _flatten("", values, out) _check_snapshot(case_name, out) diff --git a/tests/test_household_impact.py b/tests/test_household_impact.py index 584c74f2..56133b1a 100644 --- a/tests/test_household_impact.py +++ b/tests/test_household_impact.py @@ -4,6 +4,9 @@ ``pe.uk.calculate_household`` pair returning a dot-accessible :class:`HouseholdResult`. Input validation raises on unknown variable names; extra variables are a flat list dispatched by the library. + +US regression cases explicitly select national SPM geography because these +fixtures test the calculator interface without claiming a local SPM area. """ import pytest @@ -125,6 +128,7 @@ def test__axes__then_result_values_are_axis_series(self): class TestUSCalculateHousehold: def test__single_adult__then_returns_result_with_net_income(self): result = pe.us.calculate_household( + spm={"geography_kind": "national"}, people=[{"age": 30, "is_tax_unit_head": True}], year=2026, ) @@ -134,6 +138,7 @@ def test__single_adult__then_returns_result_with_net_income(self): def test__single_adult_with_income__then_tax_unit_income_tax_positive(self): result = pe.us.calculate_household( + spm={"geography_kind": "national"}, people=[{"age": 30, "employment_income": 50000, "is_tax_unit_head": True}], tax_unit={"filing_status": "SINGLE"}, year=2026, @@ -143,6 +148,7 @@ def test__single_adult_with_income__then_tax_unit_income_tax_positive(self): def test__reform_applied_through_dict__then_numbers_change(self): baseline = pe.us.calculate_household( + spm={"geography_kind": "national"}, people=[{"age": 35, "employment_income": 60000, "is_tax_unit_head": True}], tax_unit={"filing_status": "SINGLE"}, year=2026, @@ -150,6 +156,7 @@ def test__reform_applied_through_dict__then_numbers_change(self): # Halve the standard deduction — biggest tax number a reform dict # can move for a simple wage-earner test case. reformed = pe.us.calculate_household( + spm={"geography_kind": "national"}, people=[{"age": 35, "employment_income": 60000, "is_tax_unit_head": True}], tax_unit={"filing_status": "SINGLE"}, year=2026, @@ -159,6 +166,7 @@ def test__reform_applied_through_dict__then_numbers_change(self): def test__extra_variables_flat_list__then_values_appear_on_entity(self): result = pe.us.calculate_household( + spm={"geography_kind": "national"}, people=[{"age": 35, "employment_income": 60000, "is_tax_unit_head": True}], tax_unit={"filing_status": "SINGLE"}, year=2026, @@ -169,6 +177,7 @@ def test__extra_variables_flat_list__then_values_appear_on_entity(self): def test__axes__then_result_values_are_axis_series(self): result = pe.us.calculate_household( + spm={"geography_kind": "national"}, people=[ { "age": 35, @@ -202,6 +211,7 @@ def test__axes__then_result_values_are_axis_series(self): def test__nested_axes_shape__then_supported(self): result = pe.us.calculate_household( + spm={"geography_kind": "national"}, people=[ { "age": 35, @@ -227,6 +237,7 @@ def test__nested_axes_shape__then_supported(self): def test__axes_nonpositive_count__then_raises_before_calculation(self): with pytest.raises(ValueError, match="'count' must be a positive integer"): pe.us.calculate_household( + spm={"geography_kind": "national"}, people=[{"age": 35, "is_tax_unit_head": True}], year=2026, axes=[ @@ -242,6 +253,7 @@ def test__axes_nonpositive_count__then_raises_before_calculation(self): def test__axes_mismatched_group_counts__then_raises_before_calculation(self): with pytest.raises(ValueError, match="must share 'count'"): pe.us.calculate_household( + spm={"geography_kind": "national"}, people=[{"age": 35, "is_tax_unit_head": True}], year=2026, axes=[ @@ -262,6 +274,7 @@ def test__axes_mismatched_group_counts__then_raises_before_calculation(self): def test__reform_compiles_effective_date_form(self): result = pe.us.calculate_household( + spm={"geography_kind": "national"}, people=[{"age": 30, "is_tax_unit_head": True}], year=2026, reform={"gov.irs.credits.ctc.amount.adult_dependent": {"2026-01-01": 1000}}, @@ -271,6 +284,7 @@ def test__reform_compiles_effective_date_form(self): def test__monthly_year_period__then_raises_before_calculation(self): with pytest.raises(ValueError, match="Monthly periods are not supported"): pe.us.calculate_household( + spm={"geography_kind": "national"}, people=[{"age": 30, "is_tax_unit_head": True}], year="2026-01", ) @@ -281,6 +295,7 @@ def test__periodized_person_input__then_raises_before_calculation(self): match=r"Periodized household inputs.*people\[0\]\.employment_income", ): pe.us.calculate_household( + spm={"geography_kind": "national"}, people=[ { "age": 30, @@ -297,6 +312,7 @@ def test__periodized_group_input__then_raises_before_calculation(self): match=r"Periodized household inputs.*household\.state_code", ): pe.us.calculate_household( + spm={"geography_kind": "national"}, people=[{"age": 30, "is_tax_unit_head": True}], household={"state_code": {"2026-01": "CA"}}, year=2026, @@ -320,6 +336,7 @@ def test__non_annual_year__then_error_includes_received_year(self): def test__unknown_person_variable__then_raises_with_suggestion(self): with pytest.raises(ValueError, match="employment_incme"): pe.us.calculate_household( + spm={"geography_kind": "national"}, people=[{"age": 35, "employment_incme": 60000}], year=2026, ) @@ -329,6 +346,7 @@ def test__variable_on_wrong_entity__then_raises_with_entity_swap_hint(self): # point the caller at the correct entity kwarg. with pytest.raises(ValueError, match="belongs on tax_unit"): pe.us.calculate_household( + spm={"geography_kind": "national"}, people=[{"age": 35, "filing_status": "SINGLE"}], year=2026, ) @@ -340,6 +358,7 @@ def test__empty_people__then_raises(self): def test__unknown_extra_variable__then_raises(self): with pytest.raises(ValueError, match="not defined"): pe.us.calculate_household( + spm={"geography_kind": "national"}, people=[{"age": 35}], year=2026, extra_variables=["not_a_real_variable"], @@ -347,6 +366,7 @@ def test__unknown_extra_variable__then_raises(self): def test__unknown_dot_access__then_raises_with_extra_variables_hint(self): result = pe.us.calculate_household( + spm={"geography_kind": "national"}, people=[{"age": 35, "is_tax_unit_head": True}], year=2026, ) @@ -356,6 +376,7 @@ def test__unknown_dot_access__then_raises_with_extra_variables_hint(self): def test__unknown_reform_path__then_raises_with_close_match(self): with pytest.raises(ValueError, match="not defined"): pe.us.calculate_household( + spm={"geography_kind": "national"}, people=[{"age": 35, "is_tax_unit_head": True}], year=2026, reform={"gov.irs.not_a_real_parameter": 0}, @@ -364,6 +385,7 @@ def test__unknown_reform_path__then_raises_with_close_match(self): def test__unknown_axis_variable__then_raises_with_suggestion(self): with pytest.raises(ValueError, match="axis variable"): pe.us.calculate_household( + spm={"geography_kind": "national"}, people=[{"age": 35, "is_tax_unit_head": True}], year=2026, axes=[ @@ -386,6 +408,7 @@ def test__us_kwarg_on_uk__then_raises_with_uk_hint(self): def test__uk_kwarg_on_us__then_raises_with_us_hint(self): with pytest.raises(TypeError, match="UK-only"): pe.us.calculate_household( + spm={"geography_kind": "national"}, people=[{"age": 30, "is_tax_unit_head": True}], benunit={"foo": 1}, ) @@ -394,6 +417,7 @@ def test__uk_kwarg_on_us__then_raises_with_us_hint(self): class TestHouseholdResultSerialisation: def test__to_dict_produces_plain_dict_tree(self): result = pe.us.calculate_household( + spm={"geography_kind": "national"}, people=[{"age": 30, "is_tax_unit_head": True}], year=2026, ) @@ -405,6 +429,7 @@ def test__to_dict_produces_plain_dict_tree(self): def test__write_creates_json_file(self, tmp_path): result = pe.us.calculate_household( + spm={"geography_kind": "national"}, people=[{"age": 30, "is_tax_unit_head": True}], year=2026, ) diff --git a/tests/test_spm_household.py b/tests/test_spm_household.py new file mode 100644 index 00000000..2bf23600 --- /dev/null +++ b/tests/test_spm_household.py @@ -0,0 +1,292 @@ +"""Real household acceptance for the canonical SPM measurement contract. + +Development runs use the explicit local-wheel manifest pytest plugin; these +tests never replace the packaged production manifest or claim certification. +""" + +import json +import math +from copy import deepcopy + +import numpy as np +import pytest + +# The canonical calculator API is not published yet. Skip rather than weaken +# these cases: they must run against the real module, never a substitute. +pytest.importorskip("spm_calculator.errors") +pytest.importorskip("spm_calculator.release") +pytest.importorskip("spm_calculator.rolling_forecast") +pytest.importorskip("spm_calculator.policyengine_adapter") + +from spm_calculator.errors import SPMInputError # noqa: E402 +from spm_calculator.release import SPMUnit # noqa: E402 +from spm_calculator.rolling_forecast import load_forecast # noqa: E402 + +import policyengine as pe +from policyengine.tax_benefit_models.us.spm import resolve_spm_selection + +AMOUNT_VARIABLES = { + "spm_unit_reference_spm_threshold": "reference_threshold", + "spm_unit_unadjusted_spm_threshold": "unadjusted_threshold", + "spm_unit_geographic_adjustment": "geographic_factor", + "spm_unit_spm_threshold": "threshold", + "spm_unit_spm_threshold_housing_portion": "housing_portion", +} + +# Spell out the public boundary independently of the adapter's validation set +# so accidentally removing an owned output cannot also remove its test case. +FORMULA_OWNED_VARIABLES = [ + *AMOUNT_VARIABLES, + "spm_measurement_adults", + "spm_measurement_children", + "spm_unit_capped_housing_subsidy", + "spm_unit_net_income", + "spm_unit_benefits", + "spm_unit_is_in_spm_poverty", + "spm_unit_is_in_deep_spm_poverty", + "poverty_line", + "poverty_gap", +] + + +def household_inputs(**overrides): + """One adult with observed location, tenure and earnings, no baked outputs.""" + values = { + "people": [{"age": 40, "employment_income": 50_000}], + "tax_unit": {"filing_status": "SINGLE"}, + "spm_unit": {"spm_unit_tenure_type": "RENTER"}, + "household": {"state_code": "CA", "county_fips": "06037"}, + "year": 2026, + } + values.update(overrides) + return values + + +@pytest.mark.parametrize("year", [2024, 2026, 2035]) +def test_explicit_national_amounts_and_component_provenance(year, tmp_path): + result = pe.us.calculate_household( + **household_inputs(year=year), + spm={"geography_kind": "national"}, + extra_variables=list(AMOUNT_VARIABLES), + ) + forecast = load_forecast() + expected = forecast.calculate_unit( + SPMUnit( + unit_id="wrapper-acceptance", + year=year, + num_adults=1, + num_children=0, + tenure="renter", + geography_kind="national", + ), + scenario=resolve_spm_selection()["scenario"], + ) + # Each exported amount is the canonical final value cast once to the + # country's float32 storage, rather than recombined stored intermediates. + for variable, field in AMOUNT_VARIABLES.items(): + assert result.spm_unit[variable] == float(np.float32(expected[field])) + assert math.isfinite(result.household.household_net_income) + assert result.to_dict()["provenance"]["spm_config"] == resolve_spm_selection( + {"geography_kind": "national"} + ) + receipt = result.to_dict()["provenance"]["spm"] + assert ( + receipt["forecast_sha256"] == resolve_spm_selection()["forecast_content_sha256"] + ) + assert receipt["geography_kind"] == "national" + assert receipt["years"][str(year)] == forecast.entry( + year, scenario=receipt["scenario"] + ) + assert receipt["geographies"] + assert all(item["county_assignment"] is None for item in receipt["geographies"]) + assert receipt["runtime_versions"]["spm-calculator"] + assert json.loads(json.dumps(result.to_dict())) == result.to_dict() + path = result.write(tmp_path / f"household-{year}.json") + assert json.loads(path.read_text())["provenance"]["spm"] == receipt + + +def test_default_county_and_explicit_metro_resolve_the_same_area(): + inputs = household_inputs() + county = pe.us.calculate_household(**inputs, extra_variables=list(AMOUNT_VARIABLES)) + receipt = county.to_dict()["provenance"]["spm"] + assignment = receipt["geographies"][0]["county_assignment"] + expected_assignment = load_forecast().resolve_county( + 2026, "06037", county_vintage="2020", scenario=receipt["scenario"] + ) + assert assignment == expected_assignment + assert receipt["geography_kind"] == "county" + metro = pe.us.calculate_household( + **inputs, + spm={"geography_kind": "metro", "geography_id": assignment["area_id"]}, + extra_variables=list(AMOUNT_VARIABLES), + ) + for variable in AMOUNT_VARIABLES: + assert county.spm_unit[variable] == metro.spm_unit[variable] + assert inputs == household_inputs() + + +@pytest.mark.parametrize( + "location,settings,code", + [ + ({"state_code": "CA"}, {}, "SPM_GEOGRAPHY_REQUIRED"), + ( + {"state_code": "CA", "county_fips": "99999"}, + {}, + "SPM_GEOGRAPHY_UNAVAILABLE", + ), + ( + {"state_code": "CA"}, + {"geography_kind": "metro", "geography_id": "unavailable-area"}, + "SPM_GEOGRAPHY_UNAVAILABLE", + ), + ], +) +def test_default_resource_outputs_require_a_real_geography(location, settings, code): + with pytest.raises(SPMInputError) as caught: + pe.us.calculate_household(**household_inputs(household=location), spm=settings) + assert caught.value.code == code + assert caught.value.to_dict()["code"] == code + + +def test_state_only_tax_graph_succeeds_and_resource_graph_requires_geography( + monkeypatch, +): + from policyengine.tax_benefit_models.us.model import us_latest + + # Limit only this test's requested outputs. The public wrapper continues to + # include resources and SPM poverty by default. + monkeypatch.setattr(us_latest, "entity_variables", {"tax_unit": ["income_tax"]}) + inputs = household_inputs(household={"state_code": "CA"}) + tax = pe.us.calculate_household(**inputs, extra_variables=["housing_assistance"]) + assert tax.tax_unit.income_tax > 0 + assert tax.spm_unit.housing_assistance == 0 + assert tax.to_dict()["provenance"]["spm"]["years"] == {} + for variable in ( + "household_net_income", + "household_benefits", + "marginal_tax_rate", + "household_income_decile", + "equiv_household_net_income", + ): + with pytest.raises(SPMInputError) as caught: + pe.us.calculate_household(**inputs, extra_variables=[variable]) + assert caught.value.code == "SPM_GEOGRAPHY_REQUIRED" + for settings, located_inputs in ( + ({"geography_kind": "national"}, inputs), + ({"geography_kind": "county"}, household_inputs()), + ): + located = pe.us.calculate_household( + **located_inputs, + spm=settings, + extra_variables=["household_net_income", "marginal_tax_rate"], + ) + assert math.isfinite(located.household.household_net_income) + assert math.isfinite(located.person[0].marginal_tax_rate) + + +def test_adultless_measurement_has_a_structured_composition_error(): + with pytest.raises(SPMInputError) as caught: + pe.us.calculate_household(**household_inputs(people=[{"age": 8}])) + assert caught.value.code == "SPM_COMPOSITION_REQUIRED" + assert caught.value.to_dict()["code"] == "SPM_COMPOSITION_REQUIRED" + + +@pytest.mark.parametrize("variable", sorted(FORMULA_OWNED_VARIABLES)) +def test_formula_owned_household_inputs_are_rejected(variable): + with pytest.raises(ValueError, match="computed outputs supplied as inputs"): + pe.us.calculate_household( + **household_inputs(spm_unit={variable: 1}), + ) + + +@pytest.mark.parametrize("variable", sorted(FORMULA_OWNED_VARIABLES)) +def test_formula_owned_axes_are_rejected(variable): + with pytest.raises(ValueError, match="computed outputs supplied as inputs"): + pe.us.calculate_household( + **household_inputs(), + axes=[{"name": variable, "min": 0, "max": 1, "count": 2}], + ) + + +@pytest.mark.parametrize( + "adult", + [ + {"age": 40, "is_adult": False}, + {"age": 16, "is_adult": False, "is_spm_independent_minor_role": True}, + ], +) +def test_generic_adult_inputs_do_not_override_measurement_counts(adult): + result = pe.us.calculate_household( + **household_inputs( + people=[adult, {"age": 8}], + spm_unit={"spm_unit_count_adults": 7, "spm_unit_count_children": 9}, + ), + extra_variables=[ + "spm_unit_count_adults", + "spm_unit_count_children", + "spm_measurement_adults", + "spm_measurement_children", + ], + ) + assert result.person[0].is_adult == 0 + assert result.spm_unit.spm_unit_count_adults == 7 + assert result.spm_unit.spm_unit_count_children == 9 + assert result.spm_unit.spm_measurement_adults == 1 + assert result.spm_unit.spm_measurement_children == 1 + + +def test_axes_reforms_and_scenarios_keep_detached_receipts(): + inputs = household_inputs() + selection = {"geography_kind": "national", "scenario": "ce_trend"} + baseline = pe.us.calculate_household( + **inputs, spm=selection, extra_variables=["spm_unit_spm_threshold"] + ) + baseline_receipt = deepcopy(baseline.to_dict()["provenance"]["spm"]) + axes = pe.us.calculate_household( + **inputs, + spm=selection, + axes=[{"name": "employment_income", "min": 40_000, "max": 60_000, "count": 3}], + extra_variables=["spm_unit_spm_threshold"], + ) + assert axes.person[0].employment_income == [40_000, 50_000, 60_000] + assert ( + axes.spm_unit.spm_unit_spm_threshold + == [baseline.spm_unit.spm_unit_spm_threshold] * 3 + ) + assert ( + axes.household.household_net_income[1] + == baseline.household.household_net_income + ) + reform = pe.us.calculate_household( + **inputs, + spm=selection, + reform={"gov.irs.deductions.standard.amount.SINGLE": 5_000}, + extra_variables=["spm_unit_spm_threshold"], + ) + assert reform.tax_unit.income_tax > baseline.tax_unit.income_tax + assert ( + reform.spm_unit.spm_unit_spm_threshold + == baseline.spm_unit.spm_unit_spm_threshold + ) + sensitivity = pe.us.calculate_household( + **inputs, + spm={"geography_kind": "national", "scenario": "zero_real"}, + extra_variables=["spm_unit_spm_threshold"], + ) + assert ( + sensitivity.spm_unit.spm_unit_spm_threshold + != baseline.spm_unit.spm_unit_spm_threshold + ) + assert baseline.to_dict()["provenance"]["spm"] == baseline_receipt + exported = baseline.to_dict() + exported["provenance"]["spm"]["years"].clear() + assert baseline.to_dict()["provenance"]["spm"] == baseline_receipt + assert selection == {"geography_kind": "national", "scenario": "ce_trend"} + + +@pytest.mark.parametrize("year", [2021, 2036]) +def test_year_outside_canonical_horizon_is_not_silently_extrapolated(year): + with pytest.raises(ValueError, match="no entry|unavailable|not available"): + pe.us.calculate_household( + **household_inputs(year=year), spm={"geography_kind": "national"} + ) diff --git a/tests/test_spm_model.py b/tests/test_spm_model.py new file mode 100644 index 00000000..7ec2a618 --- /dev/null +++ b/tests/test_spm_model.py @@ -0,0 +1,245 @@ +"""Small real-country checks for the high-level canonical SPM contract.""" + +import json + +import pandas as pd +import pytest +from microdf import MicroDataFrame + +pytest.importorskip("spm_calculator.policyengine_adapter") +pytest.importorskip("policyengine_us") + +import policyengine as pe +from policyengine.core.simulation import Simulation, _cache +from policyengine.core.spm import SPMSelection +from policyengine.tax_benefit_models.us.datasets import ( + PolicyEngineUSDataset, + USYearData, +) + + +@pytest.fixture +def tiny_spm_dataset(tmp_path): + """One adult, one child, observed CA county and native group membership.""" + tables = {} + for entity in ( + "person", + "household", + "spm_unit", + "tax_unit", + "family", + "marital_unit", + ): + count = 2 if entity in {"person", "marital_unit"} else 1 + frame = pd.DataFrame( + {f"{entity}_id": range(1, count + 1), f"{entity}_weight": [1.0] * count} + ) + if entity == "person": + frame["age"] = [35, 8] + frame["employment_income"] = [35_000, 0] + for group in ("household", "spm_unit", "tax_unit", "family"): + frame[f"{group}_id"] = [1, 1] + frame["marital_unit_id"] = [1, 2] + elif entity == "household": + frame["state_code"] = ["CA"] + frame["county_fips"] = ["06037"] + tables[entity] = MicroDataFrame(frame, weights=f"{entity}_weight") + return PolicyEngineUSDataset( + name="synthetic-spm", + description="Uncertified two-person unit fixture", + filepath=str(tmp_path / "input.h5"), + year=2026, + data=USYearData(**tables), + ) + + +def test_high_level_selection_is_typed_and_rejects_provider_paths(): + sim = Simulation(spm={"geography_kind": "national"}) + assert isinstance(sim.spm, SPMSelection) + assert ( + json.loads(sim.model_dump_json(include={"spm"}))["spm"]["geography_kind"] + == "national" + ) + with pytest.raises(ValueError): + Simulation(spm={"path": "/tmp/forecast.json"}) + + +@pytest.mark.parametrize( + "reform", [None, {"gov.irs.credits.ctc.amount.base[0].amount": 3_000}] +) +@pytest.mark.parametrize( + "column", + ["spm_measurement_adults", "spm_unit_spm_threshold", "spm_unit_net_income"], +) +def test_computed_dataframe_inputs_rejected_before_any_set_input( + tiny_spm_dataset, monkeypatch, reform, column +): + from policyengine_us.spm import SPMSimulationMixin + + tiny_spm_dataset.data.spm_unit[column] = [1] + calls = [] + monkeypatch.setattr( + SPMSimulationMixin, "set_input", lambda *args: calls.append(args) + ) + sim = Simulation( + dataset=tiny_spm_dataset, tax_benefit_model_version=pe.us.model, policy=reform + ) + with pytest.raises(ValueError, match="computed outputs supplied as inputs"): + sim.run() + assert calls == [] + + +def test_real_high_level_provenance_cache_and_disk_round_trip( + tiny_spm_dataset, monkeypatch +): + _cache.clear() + county = Simulation( + id="spm-replay", dataset=tiny_spm_dataset, tax_benefit_model_version=pe.us.model + ) + county.ensure() + county_receipt = county.spm_provenance() + assert county_receipt["years"]["2026"] + assert county_receipt["geography_kind"] == "county" + assert county.spm_config["geography_kind"] == "county" + assert ( + county.spm.forecast_content_sha256 + == county.spm_config["forecast_content_sha256"] + ) + assert county.spm.scenario == "ce_trend" + assert county.release_bundle["spm"] == county.spm_provenance() + from policyengine import bundle + + original_bundle = bundle.get_current_bundle() + original_bundle["measurements"]["spm"]["scenario"] = "zero_real" + with monkeypatch.context() as context: + context.setattr(bundle, "get_current_bundle", lambda: original_bundle) + assert county.spm_config["scenario"] == "ce_trend" + county_receipt["years"].clear() + assert county.spm_provenance()["years"] + assert json.loads(county.model_dump_json(include={"spm", "spm_receipt"}))[ + "spm_receipt" + ]["years"] + + cached = Simulation( + id=county.id, dataset=tiny_spm_dataset, tax_benefit_model_version=pe.us.model + ) + cached.ensure() + assert cached.spm_provenance() == county.spm_provenance() + _cache.clear() + restored = Simulation( + id=county.id, dataset=tiny_spm_dataset, tax_benefit_model_version=pe.us.model + ) + restored.load() + assert restored.spm_provenance() == county.spm_provenance() + + county.spm = SPMSelection(geography_kind="national", scenario="zero_real") + with pytest.raises(ValueError, match="SPM settings changed"): + county.save() + county.ensure() + assert county.spm_provenance()["geography_kind"] == "national" + assert county.spm_provenance()["scenario"] == "zero_real" + assert county.output_dataset.filepath != restored.output_dataset.filepath + assert restored.spm_provenance()["geography_kind"] == "county" + + +def test_generic_composition_inputs_remain_allowed(tiny_spm_dataset): + tiny_spm_dataset.data.person["is_adult"] = [True, False] + tiny_spm_dataset.data.spm_unit["spm_unit_count_adults"] = [1] + tiny_spm_dataset.data.spm_unit["spm_unit_count_children"] = [1] + sim = Simulation(dataset=tiny_spm_dataset, tax_benefit_model_version=pe.us.model) + sim.run() + assert sim.output_dataset.data.person["is_adult"].tolist() == [True, False] + assert sim.spm_provenance()["years"]["2026"] + + +def test_state_only_high_level_default_requires_geography(tiny_spm_dataset): + from spm_calculator.errors import SPMInputError + + tiny_spm_dataset.data.household.drop(columns=["county_fips"], inplace=True) + sim = Simulation(dataset=tiny_spm_dataset, tax_benefit_model_version=pe.us.model) + with pytest.raises(SPMInputError) as error: + sim.run() + assert error.value.code == "SPM_GEOGRAPHY_REQUIRED" + sim.spm = SPMSelection(geography_kind="national") + sim.run() + assert sim.spm_provenance()["geography_kind"] == "national" + + +def test_run_record_binds_spm_settings_and_receipt(tiny_spm_dataset): + from policyengine.core.run_record import build_simulation_run_record_payloads + + tiny_spm_dataset.save() + sim = Simulation( + dataset=tiny_spm_dataset, + tax_benefit_model_version=pe.us.model, + spm={"geography_kind": "national"}, + ) + sim.run() + sim.save() + payloads = build_simulation_run_record_payloads(sim) + assert payloads["input"]["spm"] == sim.spm_config + assert payloads["results"]["spm"] == sim.spm_provenance() + sim.spm = SPMSelection(geography_kind="national", scenario="zero_real") + with pytest.raises(ValueError, match="SPM settings changed"): + build_simulation_run_record_payloads(sim) + + +def test_real_high_level_reform_and_baseline_share_selection( + tiny_spm_dataset, monkeypatch +): + from policyengine.tax_benefit_models.us.model import PolicyEngineUSLatest + + tiny_spm_dataset.data.household.drop(columns=["county_fips"], inplace=True) + configurations = [] + original = PolicyEngineUSLatest._build_simulation_from_dataset + + def capture(self, country_simulation, dataset, system): + configurations.append(country_simulation.spm_config) + return original(self, country_simulation, dataset, system) + + monkeypatch.setattr(PolicyEngineUSLatest, "_build_simulation_from_dataset", capture) + sim = Simulation( + dataset=tiny_spm_dataset, + tax_benefit_model_version=pe.us.model, + spm={"geography_kind": "national", "scenario": "zero_real"}, + policy={"gov.irs.credits.ctc.amount.base[0].amount": 3_000}, + ) + sim.run() + assert len(configurations) == 2 + assert configurations == [sim.spm_config, sim.spm_config] + assert sim.spm_provenance()["scenario"] == "zero_real" + + +def test_managed_native_h5_selection_and_clone_provenance(tiny_spm_dataset, tmp_path): + """The actual managed loader can consume a tiny native H5 in development.""" + import h5py + import numpy as np + + path = tmp_path / "synthetic-native.h5" + with h5py.File(path, "w") as stream: + for entity, frame in tiny_spm_dataset.data.entity_data.items(): + for name in frame.columns: + output_name = name + if entity == "person" and name.endswith("_id") and name != "person_id": + output_name = f"person_{name}" + values = frame[name].to_numpy() + if values.dtype.kind in {"O", "U"}: + values = np.asarray(values, dtype="S") + stream.create_dataset(f"{output_name}/2026", data=values) + managed = pe.us.managed_microsimulation( + dataset=str(path), + allow_unmanaged=True, + spm={"geography_kind": "national", "scenario": "zero_real"}, + ) + assert managed.policyengine_bundle["spm"] == managed.spm_config + managed.calculate("spm_unit_spm_threshold", 2026) + clone = managed.clone() + assert clone.spm_config == managed.spm_config + assert clone.spm_provenance() == managed.spm_provenance() + clone.calculate("spm_unit_spm_threshold", 2025) + assert "2025" in clone.spm_provenance()["years"] + assert "2025" not in managed.spm_provenance()["years"] + returned = managed.spm_provenance() + returned["years"].clear() + assert managed.spm_provenance()["years"] + json.dumps(managed.spm_provenance()) diff --git a/tests/test_spm_selection.py b/tests/test_spm_selection.py new file mode 100644 index 00000000..1b127c9a --- /dev/null +++ b/tests/test_spm_selection.py @@ -0,0 +1,247 @@ +"""The wrapper pins measurement provenance and accepts explicit locations.""" + +import json + +import pytest +from pydantic import ValidationError + +import policyengine as pe +from policyengine.tax_benefit_models.us.spm import SPMSelection, resolve_spm_selection + + +@pytest.fixture +def pinned_bundle(monkeypatch): + value = { + "measurements": { + "spm": { + "forecast_content_sha256": "a" * 64, + "scenario": "ce_trend", + "geography_kind": "county", + } + } + } + monkeypatch.setattr("policyengine.bundle.get_current_bundle", lambda: value) + return value + + +def test_default_selection_preserves_independent_bundle_hash(pinned_bundle): + config = resolve_spm_selection() + assert config["forecast_content_sha256"] == "a" * 64 + assert config["geography_kind"] == "county" + assert config["scenario"] == "ce_trend" + assert SPMSelection.model_validate_json(json.dumps(config)).model_dump() == config + + +def test_explicit_national_and_scenario_do_not_replace_artifact(pinned_bundle): + config = resolve_spm_selection( + SPMSelection(geography_kind="national", scenario="zero_real") + ) + assert config["forecast_content_sha256"] == "a" * 64 + assert config["geography_kind"] == "national" + assert config["scenario"] == "zero_real" + assert pinned_bundle["measurements"]["spm"]["scenario"] == "ce_trend" + + +def test_no_null_or_unmatched_digest_bypass(pinned_bundle): + assert ( + resolve_spm_selection({"forecast_content_sha256": None})[ + "forecast_content_sha256" + ] + == "a" * 64 + ) + with pytest.raises(ValueError, match="artifact hash"): + resolve_spm_selection({"forecast_content_sha256": "b" * 64}) + pinned_bundle["measurements"]["spm"].pop("forecast_content_sha256") + with pytest.raises(ValueError, match="must pin"): + resolve_spm_selection() + + +@pytest.mark.parametrize( + "value", + [ + {"geography_kind": "state"}, + {"geography_kind": "congressional_district"}, + {"geography_kind": "national", "geography_id": "A"}, + {"geography_kind": "metro"}, + {"year_policy": "pe_cpi_u"}, + {"allow_estimated": True}, + {"path": "/tmp/other.json"}, + ], +) +def test_old_fallback_policies_and_ambiguous_locations_rejected(value): + with pytest.raises(ValidationError): + SPMSelection.model_validate(value) + + +def test_missing_bundle_configuration_is_not_silently_inferred(monkeypatch): + monkeypatch.setattr("policyengine.bundle.get_current_bundle", lambda: {}) + with pytest.raises(ValueError, match="no certified SPM"): + resolve_spm_selection() + + +@pytest.mark.parametrize("value", [[], "", False, 0]) +def test_falsey_non_selection_is_rejected(pinned_bundle, value): + with pytest.raises(ValidationError): + resolve_spm_selection(value) + + +@pytest.mark.parametrize( + "value", + [ + {"scenario": ""}, + {"scenario": " "}, + {"county_vintage": ""}, + {"county_vintage": 2020}, + {"as_of": "2026-02-30"}, + {"as_of": "20260909"}, + {"as_of": "2026-09-09T00:00:00"}, + ], +) +def test_malformed_selection_values_rejected(value): + with pytest.raises(ValidationError): + SPMSelection.model_validate(value) + + +def test_switching_explicit_geography_drops_default_area(pinned_bundle): + pinned_bundle["measurements"]["spm"].update( + geography_kind="metro", geography_id="12345" + ) + config = resolve_spm_selection({"geography_kind": "national"}) + assert config["geography_id"] is None + assert config["geography_kind"] == "national" + + +@pytest.mark.parametrize("geography_kind", ["county", "national", "metro"]) +def test_partial_selection_preserves_omissions_in_public_round_trips( + pinned_bundle, geography_kind +): + pinned_bundle["measurements"]["spm"].update( + geography_kind=geography_kind, + geography_id="12345" if geography_kind == "metro" else None, + county_vintage="2010", + as_of="2026-06-01", + ) + selection = SPMSelection(scenario="zero_real") + envelope = pe.Simulation(spm=selection) + nested_json = envelope.model_dump_json(include={"spm"}) + assert json.loads(nested_json) == {"spm": {"scenario": "zero_real"}} + restored_selections = [ + SPMSelection.model_validate(selection.model_dump()), + SPMSelection.model_validate_json(selection.model_dump_json()), + pe.Simulation.model_validate_json(nested_json).spm, + ] + expected = resolve_spm_selection(selection) + for restored in restored_selections: + assert restored.model_fields_set == {"scenario"} + assert resolve_spm_selection(restored) == expected + assert resolve_spm_selection(restored)["geography_kind"] == geography_kind + assert resolve_spm_selection(restored)["county_vintage"] == "2010" + assert resolve_spm_selection(restored)["as_of"] == "2026-06-01" + + +@pytest.mark.parametrize("geography_kind", ["national", "metro"]) +def test_resolved_selection_retains_all_settings_after_public_json_round_trip( + pinned_bundle, geography_kind +): + defaults = pinned_bundle["measurements"]["spm"] + defaults.update( + geography_kind=geography_kind, + geography_id="12345" if geography_kind == "metro" else None, + ) + expected = resolve_spm_selection({"scenario": "zero_real"}) + resolved = SPMSelection.model_validate(expected) + assert set(json.loads(resolved.model_dump_json())) == set(SPMSelection.model_fields) + restored = pe.Simulation.model_validate_json( + pe.Simulation(spm=resolved).model_dump_json(include={"spm"}) + ).spm + defaults.update( + geography_kind="county", + geography_id=None, + scenario="ce_trend", + county_vintage="2010", + as_of="2026-06-01", + ) + assert restored.model_fields_set == set(SPMSelection.model_fields) + assert resolve_spm_selection(restored) == expected + + +@pytest.mark.parametrize("geography_kind", ["national", "metro"]) +def test_explicit_class_defaults_remain_overrides_after_json_round_trip( + pinned_bundle, geography_kind +): + pinned_bundle["measurements"]["spm"].update( + geography_kind=geography_kind, + geography_id="12345" if geography_kind == "metro" else None, + county_vintage="2010", + as_of="2026-06-01", + ) + selection = SPMSelection(geography_kind="county", county_vintage="2020", as_of=None) + restored = SPMSelection.model_validate_json(selection.model_dump_json()) + config = resolve_spm_selection(restored) + assert config["geography_kind"] == "county" + assert config["geography_id"] is None + assert config["county_vintage"] == "2020" + assert config["as_of"] is None + + +@pytest.mark.parametrize("geography_kind", ["national", "metro"]) +def test_partial_selection_json_round_trip_preserves_real_household_threshold( + monkeypatch, geography_kind +): + """State-only inputs must still use the inherited national or metro area.""" + pytest.importorskip("spm_calculator.policyengine_adapter") + pytest.importorskip("policyengine_us.spm") + import numpy as np + from spm_calculator.release import SPMUnit + from spm_calculator.rolling_forecast import load_forecast + + from policyengine import bundle + + configured = bundle.get_current_bundle() + forecast = load_forecast() + area_id = ( + forecast.resolve_county(2026, "06037", scenario="zero_real")["area_id"] + if geography_kind == "metro" + else None + ) + configured["measurements"]["spm"].update( + geography_kind=geography_kind, geography_id=area_id + ) + monkeypatch.setattr(bundle, "get_current_bundle", lambda: configured) + selection = SPMSelection(scenario="zero_real") + restored_selections = [ + selection, + SPMSelection.model_validate_json(selection.model_dump_json()), + pe.Simulation.model_validate_json( + pe.Simulation(spm=selection).model_dump_json(include={"spm"}) + ).spm, + ] + expected = forecast.calculate_unit( + SPMUnit( + unit_id="selection-round-trip", + year=2026, + num_adults=1, + num_children=0, + tenure="renter", + geography_kind=geography_kind, + geography_id=area_id, + ), + scenario="zero_real", + ) + for restored in restored_selections: + result = pe.us.calculate_household( + people=[{"age": 40, "employment_income": 50_000}], + tax_unit={"filing_status": "SINGLE"}, + spm_unit={"spm_unit_tenure_type": "RENTER"}, + household={"state_code": "CA"}, + year=2026, + spm=restored, + extra_variables=["spm_unit_spm_threshold"], + ) + assert result.spm_unit.spm_unit_spm_threshold == float( + np.float32(expected["threshold"]) + ) + assert result.to_dict()["provenance"]["spm_config"] == resolve_spm_selection( + selection + ) + assert result.to_dict()["provenance"]["spm"]["geography_kind"] == geography_kind diff --git a/tests/test_us_native_alignment.py b/tests/test_us_native_alignment.py new file mode 100644 index 00000000..ba1a813b --- /dev/null +++ b/tests/test_us_native_alignment.py @@ -0,0 +1,226 @@ +"""Real country calculations keep independent native entity orders aligned.""" + +import hashlib +import importlib + +import numpy as np +import pandas as pd +import pytest + +# The canonical calculator API is not published yet. Skip rather than weaken +# these cases: they must run against the real module, never a substitute. +pytest.importorskip("spm_calculator.release") +pytest.importorskip("spm_calculator.rolling_forecast") +pytest.importorskip("spm_calculator.policyengine_adapter") + +from spm_calculator.release import SPMUnit # noqa: E402 +from spm_calculator.rolling_forecast import load_forecast # noqa: E402 + +import policyengine as pe +from policyengine.tax_benefit_models.us.datasets import PolicyEngineUSDataset +from policyengine.tax_benefit_models.us.model import PolicyEngineUSLatest +from policyengine.tax_benefit_models.us.spm import resolve_spm_selection + + +@pytest.fixture +def independent_entity_frames(): + # Person order, group IDs, table orders, and indexes differ independently. + # The California unit also has a child, so aggregate outputs exercise real + # membership joins instead of only one-to-one relabeling. + person = pd.DataFrame( + { + "person_id": [300, 101, 200, 100], + "person_household_id": [30, 10, 20, 10], + "person_tax_unit_id": [3, 1, 2, 1], + "person_spm_unit_id": [330, 110, 220, 110], + "person_family_id": [3000, 1000, 2000, 1000], + "person_marital_unit_id": [33, 44, 22, 11], + "age": [50, 8, 40, 30], + "employment_income": [31_000.0, 1_000.0, 23_000.0, 13_000.0], + }, + index=[91, -4, 300, 17], + ) + return { + "person": person, + "household": pd.DataFrame( + { + "household_id": [20, 10, 30], + "household_weight": [2.5, 7.0, 4.0], + "county_fips": ["36061", "06037", "48201"], + "state_fips": [36, 6, 48], + }, + index=[77, 42, -9], + ), + "tax_unit": pd.DataFrame({"tax_unit_id": [3, 2, 1]}, index=[8, 12, -7]), + "spm_unit": pd.DataFrame( + { + "spm_unit_id": [220, 330, 110], + "spm_unit_tenure_type": [ + "OWNER_WITHOUT_MORTGAGE", + "OWNER_WITH_MORTGAGE", + "RENTER", + ], + }, + index=[13, -8, 50], + ), + "family": pd.DataFrame({"family_id": [1000, 3000, 2000]}, index=[9, -2, 17]), + "marital_unit": pd.DataFrame( + {"marital_unit_id": [22, 11, 44, 33]}, index=[87, -1, 11, 44] + ), + } + + +@pytest.mark.parametrize("format", ["fixed", "table"]) +@pytest.mark.parametrize("link_style", ["native", "plain"]) +def test_native_inputs_and_calculated_outputs_align_by_id( + tmp_path, independent_entity_frames, format, link_style, monkeypatch +): + frames = independent_entity_frames + if link_style == "plain": + frames["person"] = frames["person"].rename( + columns={ + f"person_{entity}_id": f"{entity}_id" + for entity in frames + if entity != "person" + } + ) + else: + # Conflicting plain aliases must not override native memberships at + # either input construction or output assembly. + for entity, frame in frames.items(): + if entity != "person": + frames["person"][f"{entity}_id"] = frame[f"{entity}_id"].iloc[0] + path = tmp_path / f"shuffled-{format}.h5" + with pd.HDFStore(path, mode="w") as store: + for entity, frame in frames.items(): + store.put(entity, frame, format=format) + source_sha256 = hashlib.sha256(path.read_bytes()).hexdigest() + dataset = PolicyEngineUSDataset( + name="independently shuffled native tables", + description="Uncertified four-person numeric alignment fixture", + filepath=str(path), + year=2026, + ) + inputs = { + entity: pd.DataFrame(frame).copy(deep=True) + for entity, frame in dataset.data.entity_data.items() + } + + # Keep the real high-level input builder, country calculations, result + # assembly and SPM receipt. Limit requested variables to this regression. + outputs = {entity: ["employment_income"] for entity in frames} + outputs["person"] += ["county_fips", "state_fips"] + outputs["household"] += ["county_fips", "state_fips"] + outputs["spm_unit"] += ["spm_unit_spm_threshold"] + monkeypatch.setattr( + PolicyEngineUSLatest, "resolve_entity_variables", lambda self, sim: outputs + ) + country_simulations = [] + build = PolicyEngineUSLatest._build_simulation_from_dataset + + def capture(self, country_simulation, dataset, system): + build(self, country_simulation, dataset, system) + country_simulations.append(country_simulation) + + monkeypatch.setattr(PolicyEngineUSLatest, "_build_simulation_from_dataset", capture) + simulation = pe.Simulation(dataset=dataset, tax_benefit_model_version=pe.us.model) + simulation.run() + output = simulation.output_dataset.data.entity_data + country = country_simulations[0] + + # Assert numeric geography at both boundaries, including the two CA people. + assert output["person"]["county_fips"].tolist() == [ + "48201", + "06037", + "36061", + "06037", + ] + assert output["person"]["state_fips"].tolist() == [48, 6, 36, 6] + assert output["household"]["county_fips"].tolist() == ["36061", "06037", "48201"] + assert output["household"]["state_fips"].tolist() == [36, 6, 48] + np.testing.assert_array_equal( + country.calculate("state_fips", 2026, map_to="person").values, + [48, 6, 36, 6], + ) + + incomes = frames["person"] + for entity, frame in inputs.items(): + entity_id = f"{entity}_id" + assert output[entity][entity_id].tolist() == frame[entity_id].tolist() + assert output[entity].weights.tolist() == frame[f"{entity}_weight"].tolist() + if entity == "person": + expected = incomes.set_index("person_id")["employment_income"] + else: + link = f"person_{entity_id}" if link_style == "native" else entity_id + expected = incomes.groupby(link)["employment_income"].sum() + assert output["person"][entity_id].tolist() == incomes[link].tolist() + np.testing.assert_array_equal( + output[entity]["employment_income"].to_numpy(), + expected.loc[frame[entity_id]].to_numpy(), + ) + # Each entity receives its native weight in the country's own ID order. + expected_weights = frame.set_index(entity_id)[f"{entity}_weight"] + np.testing.assert_array_equal( + country.calculate(f"{entity}_weight", 2026).values, + expected_weights.loc[country.populations[entity].ids].to_numpy(), + ) + pd.testing.assert_frame_equal( + pd.DataFrame(dataset.data.entity_data[entity]), frame + ) + assert output["spm_unit"]["employment_income"].sum() == 279_500.0 + + # Thresholds are compared with the real standalone calculator for each + # county/composition/tenure; merely retaining the IDs cannot pass this test. + forecast = load_forecast() + scenario = resolve_spm_selection()["scenario"] + expected_thresholds = [] + for spm_id, county, children, tenure in ( + (220, "36061", 0, "owner_without_mortgage"), + (330, "48201", 0, "owner_with_mortgage"), + (110, "06037", 1, "renter"), + ): + assignment = forecast.resolve_county( + 2026, county, county_vintage="2020", scenario=scenario + ) + result = forecast.calculate_unit( + SPMUnit( + unit_id=str(spm_id), + year=2026, + num_adults=1, + num_children=children, + tenure=tenure, + geography_kind="metro", + geography_id=assignment["area_id"], + ), + scenario=scenario, + ) + expected_thresholds.append(np.float32(result["threshold"])) + np.testing.assert_array_equal( + output["spm_unit"]["spm_unit_spm_threshold"].to_numpy(), expected_thresholds + ) + assert simulation.spm_provenance()["geography_kind"] == "county" + assert hashlib.sha256(path.read_bytes()).hexdigest() == source_sha256 + + +@pytest.mark.parametrize( + "selection", + [ + {"path": "/tmp/invalid.json"}, + {"geography_kind": "unsupported"}, + {"forecast_content_sha256": "0" * 64}, + ], +) +def test_invalid_managed_spm_rejected_before_dataset_materialization( + monkeypatch, selection +): + module = importlib.import_module("policyengine.tax_benefit_models.us.model") + calls = [] + + def sentinel(*args, **kwargs): + calls.append((args, kwargs)) + raise AssertionError("Invalid SPM must not materialize data") + + monkeypatch.setattr(module, "materialize_dataset", sentinel) + with pytest.raises(ValueError): + pe.us.managed_microsimulation(spm=selection) + assert calls == [] diff --git a/tests/test_us_native_hdf_weights.py b/tests/test_us_native_hdf_weights.py new file mode 100644 index 00000000..50f89412 --- /dev/null +++ b/tests/test_us_native_hdf_weights.py @@ -0,0 +1,244 @@ +"""Native pandas HDF weights are projected through IDs without editing the H5.""" + +import hashlib + +import numpy as np +import pandas as pd +import pytest + +from policyengine.tax_benefit_models.us.datasets import PolicyEngineUSDataset + + +@pytest.fixture +def native_frames(): + """Small BuildP-shaped source with only calibrated household weights. + + ID order and pandas indexes intentionally differ across every table. Three + people share one SPM unit; its weight must remain one household weight. + """ + return { + "person": pd.DataFrame( + { + "person_id": [101, 20, 888, 42, 5], + "person_household_id": [12, 900, 12, 900, 12], + "person_tax_unit_id": [21, 700, 21, 9, 21], + "person_spm_unit_id": [6, 8800, 6, 8800, 6], + "person_family_id": [8, 99, 8, 99, 8], + "person_marital_unit_id": [3, 67, 10, 25, 3], + "age": np.array([16, 40, 8, 33, 3], dtype=np.int16), + "is_spm_independent_minor_role": [True, True, False, True, False], + "employment_income": [0.0, 24_000.0, 0.0, 30_000.0, 0.0], + }, + index=pd.Index([19, -4, 60, 3, 100], name="source_row"), + ), + "household": pd.DataFrame( + { + "household_id": [900, 12], + "household_weight": [3.25, 8.5], + "county_fips": np.array(["06037", "48201"], dtype=object), + }, + index=pd.Index([44, -10], name="source_row"), + ), + "tax_unit": pd.DataFrame({"tax_unit_id": [700, 9, 21]}, index=[50, -2, 19]), + "spm_unit": pd.DataFrame( + {"spm_unit_id": [6, 8800], "source_audit_amount": [10.0, 100.0]}, + index=[81, 3], + ), + "family": pd.DataFrame({"family_id": [99, 8]}, index=[0, -80]), + "marital_unit": pd.DataFrame( + {"marital_unit_id": [25, 3, 67, 10]}, index=[7, 200, -12, 31] + ), + } + + +EXPECTED_WEIGHTS = { + "person": [8.5, 3.25, 8.5, 3.25, 8.5], + "household": [3.25, 8.5], + "tax_unit": [3.25, 3.25, 8.5], + "spm_unit": [8.5, 3.25], + "family": [3.25, 8.5], + "marital_unit": [3.25, 8.5, 3.25, 8.5], +} + + +def write_native_hdf(tmp_path, frames, format): + path = tmp_path / f"native-{format}.h5" + with pd.HDFStore(path, mode="w") as store: + for entity, frame in frames.items(): + store.put(entity, frame, format=format) + return path + + +def load_native_hdf(path): + return PolicyEngineUSDataset( + name="Synthetic native household-weight fixture", + description="Uncertified five-person loader regression", + filepath=str(path), + year=2024, + ) + + +@pytest.mark.parametrize("format", ["fixed", "table"]) +@pytest.mark.parametrize("link_style", ["native", "plain"]) +def test_native_hdf_derives_missing_weights_by_ids_without_changing_source( + tmp_path, native_frames, format, link_style +): + if link_style == "plain": + native_frames["person"] = native_frames["person"].rename( + columns={ + f"person_{entity}_id": f"{entity}_id" + for entity in native_frames + if entity != "person" + } + ) + path = write_native_hdf(tmp_path, native_frames, format) + before = hashlib.sha256(path.read_bytes()).hexdigest() + + dataset = load_native_hdf(path) + + for entity, original in native_frames.items(): + loaded = getattr(dataset.data, entity) + # Existing arrays, dtypes, county, primitive roles and native indexes + # survive unchanged; derived weight columns exist only in memory. + pd.testing.assert_frame_equal(pd.DataFrame(loaded)[original.columns], original) + expected = pd.Series( + EXPECTED_WEIGHTS[entity], index=original.index, name=f"{entity}_weight" + ) + pd.testing.assert_series_equal( + pd.DataFrame(loaded)[f"{entity}_weight"], expected + ) + pd.testing.assert_series_equal(loaded.weights, expected, check_names=False) + + assert dataset.data.person.employment_income.sum() == 175_500.0 + assert dataset.data.spm_unit.source_audit_amount.sum() == 410.0 + assert hashlib.sha256(path.read_bytes()).hexdigest() == before + with pd.HDFStore(path, mode="r") as store: + for entity, original in native_frames.items(): + pd.testing.assert_frame_equal(store[entity], original) + + +@pytest.mark.parametrize("format", ["fixed", "table"]) +def test_existing_person_and_entity_weights_are_preserved( + tmp_path, native_frames, format +): + # Deliberately distinct supplied weights catch both overwriting and using + # person weights as the source for missing group weights. + native_frames["person"]["person_weight"] = [11.0, 12.0, 13.0, 14.0, 15.0] + native_frames["tax_unit"]["tax_unit_weight"] = [23.0, 24.0, 25.0] + native_frames["family"]["family_weight"] = [31.0, 32.0] + path = write_native_hdf(tmp_path, native_frames, format) + before = hashlib.sha256(path.read_bytes()).hexdigest() + + dataset = load_native_hdf(path) + + for entity, original in native_frames.items(): + loaded = getattr(dataset.data, entity) + pd.testing.assert_frame_equal(pd.DataFrame(loaded)[original.columns], original) + expected = ( + original[f"{entity}_weight"].tolist() + if f"{entity}_weight" in original + else EXPECTED_WEIGHTS[entity] + ) + assert loaded.weights.tolist() == expected + assert pd.DataFrame(loaded)[f"{entity}_weight"].tolist() == expected + assert hashlib.sha256(path.read_bytes()).hexdigest() == before + + +def test_native_person_links_take_precedence_over_plain_aliases( + tmp_path, native_frames +): + for entity, frame in native_frames.items(): + if entity != "person": + native_frames["person"][f"{entity}_id"] = frame[f"{entity}_id"].iloc[0] + path = write_native_hdf(tmp_path, native_frames, "table") + before = hashlib.sha256(path.read_bytes()).hexdigest() + + dataset = load_native_hdf(path) + + for entity, original in native_frames.items(): + loaded = getattr(dataset.data, entity) + pd.testing.assert_frame_equal(pd.DataFrame(loaded)[original.columns], original) + assert loaded.weights.tolist() == EXPECTED_WEIGHTS[entity] + assert hashlib.sha256(path.read_bytes()).hexdigest() == before + + +def test_empty_native_entity_tables_keep_indexes_and_source_weight_dtype( + tmp_path, native_frames +): + frames = {entity: frame.iloc[:0].copy() for entity, frame in native_frames.items()} + path = write_native_hdf(tmp_path, frames, "fixed") + before = hashlib.sha256(path.read_bytes()).hexdigest() + + dataset = load_native_hdf(path) + + for entity, original in frames.items(): + loaded = pd.DataFrame(getattr(dataset.data, entity)) + pd.testing.assert_frame_equal(loaded[original.columns], original) + assert loaded[f"{entity}_weight"].empty + assert ( + loaded[f"{entity}_weight"].dtype + == frames["household"]["household_weight"].dtype + ) + assert hashlib.sha256(path.read_bytes()).hexdigest() == before + + +@pytest.mark.parametrize( + "problem", + [ + "missing_person_household_link", + "unknown_household_id", + "duplicate_household_id", + "orphan_spm_unit", + "spm_unit_spans_households", + ], +) +def test_missing_or_ambiguous_native_links_raise_without_mutating_hdf( + tmp_path, native_frames, problem +): + if problem == "missing_person_household_link": + native_frames["person"] = native_frames["person"].drop( + columns="person_household_id" + ) + elif problem == "unknown_household_id": + native_frames["person"].loc[19, "person_household_id"] = 404 + elif problem == "duplicate_household_id": + native_frames["household"].loc[-10, "household_id"] = 900 + elif problem == "orphan_spm_unit": + native_frames["spm_unit"].loc[81, "spm_unit_id"] = 404 + elif problem == "spm_unit_spans_households": + native_frames["person"].loc[-4, "person_spm_unit_id"] = 6 + path = write_native_hdf(tmp_path, native_frames, "table") + before = hashlib.sha256(path.read_bytes()).hexdigest() + + with pytest.raises(ValueError, match="weight|household|spm_unit"): + load_native_hdf(path) + + assert hashlib.sha256(path.read_bytes()).hexdigest() == before + + +@pytest.mark.parametrize("format", ["fixed", "table"]) +@pytest.mark.parametrize("entity", ["tax_unit", "spm_unit", "family", "marital_unit"]) +@pytest.mark.parametrize("problem", ["missing", "duplicate"]) +def test_invalid_group_ids_rejected_before_weight_mapping( + tmp_path, native_frames, format, entity, problem +): + entity_id = f"{entity}_id" + frame = native_frames[entity] + if problem == "missing": + # Matching null membership used to map successfully through pandas. + old_id = frame[entity_id].iloc[0] + frame.loc[frame.index[0], entity_id] = np.nan + link = f"person_{entity_id}" + person = native_frames["person"] + person.loc[person[link] == old_id, link] = np.nan + else: + # A repeated complete row used to duplicate its household weight and + # silently increase weighted totals (SPM audit total 410 -> 495). + native_frames[entity] = pd.concat([frame, frame.iloc[[0]]]) + path = write_native_hdf(tmp_path, native_frames, format) + before = hashlib.sha256(path.read_bytes()).hexdigest() + + with pytest.raises(ValueError, match=f"unique nonmissing {entity_id}"): + load_native_hdf(path) + + assert hashlib.sha256(path.read_bytes()).hexdigest() == before From dba5599d81c774db685d620e503e1334e2b4e97b Mon Sep 17 00:00:00 2001 From: Max Ghenis Date: Fri, 11 Sep 2026 12:30:24 -0400 Subject: [PATCH 05/46] Pin and validate canonical SPM settings in the release bundle The bundle manifest gains a measurements.spm block holding the forecast content hash, scenario, geography kind and county vintage that every US run resolves against. A stdlib-only validator enforces that shape wherever the manifest is read: the runtime bundle loader, the derived- artifact generator and the maintenance CLI all reject unknown fields, provider or path overrides, a metro selection without a geography id, and a bundle missing its hash or scenario. Because the generator runs in CI before dependencies are installed, it loads that validator by file path instead of importing the package. Two maintenance subcommands are added. set-spm stages the measurement settings and can pin the SPM calculator by reading authoritative PyPI metadata and hashing the actual wheel bytes. development-manifest writes an explicitly uncertified fixture from local wheel METADATA for pre- publication testing, with a pytest plugin that loads it; a companion test proves the fixture authenticates the code Python actually resolves rather than a checkout on sys.path. bundle check grows --published-spm for the release preflight. The runtime installer's component filter is corrected so a runtime dependency is included regardless of the requested countries. This pin is a measurement setting, not a data or package certification. Co-Authored-By: Claude Fable 5.1 --- changelog.d/canonical-spm-bundle.changed.md | 1 + scripts/bundle.py | 108 ++++++- scripts/generate_bundle_artifacts.py | 22 +- scripts/spm_bundle.py | 147 +++++++++ src/policyengine/bundle.py | 13 +- src/policyengine/data/bundle/manifest.json | 10 + .../provenance/bundle_validation.py | 152 +++++++++ .../provenance/spm_configuration.py | 26 ++ tests/fixtures/spm_development.py | 223 +++++++++++++ tests/test_spm_bundle_bootstrap.py | 125 ++++++++ tests/test_spm_bundle_tooling.py | 299 ++++++++++++++++++ tests/test_spm_development_authentication.py | 95 ++++++ 12 files changed, 1208 insertions(+), 13 deletions(-) create mode 100644 changelog.d/canonical-spm-bundle.changed.md create mode 100644 scripts/spm_bundle.py create mode 100644 src/policyengine/provenance/bundle_validation.py create mode 100644 src/policyengine/provenance/spm_configuration.py create mode 100644 tests/fixtures/spm_development.py create mode 100644 tests/test_spm_bundle_bootstrap.py create mode 100644 tests/test_spm_bundle_tooling.py create mode 100644 tests/test_spm_development_authentication.py diff --git a/changelog.d/canonical-spm-bundle.changed.md b/changelog.d/canonical-spm-bundle.changed.md new file mode 100644 index 00000000..decd5812 --- /dev/null +++ b/changelog.d/canonical-spm-bundle.changed.md @@ -0,0 +1 @@ +Validate independently pinned SPM bundle settings, verify published calculator wheel identities, and support explicitly uncertified local-wheel development fixtures while production promotion remains pending. diff --git a/scripts/bundle.py b/scripts/bundle.py index 8c2aa2ba..4c535e21 100644 --- a/scripts/bundle.py +++ b/scripts/bundle.py @@ -74,23 +74,69 @@ def _generate(args: argparse.Namespace) -> int: result = generate(check=False) if not args.include_tros: return result - return _generate_tros() + return _generate_tros(strict=args.strict_tros) def _check(args: argparse.Namespace) -> int: - from generate_bundle_artifacts import generate + from generate_bundle_artifacts import ( + generate, + load_bundle_manifest, + validate_bundle_measurements, + ) + validate_bundle_measurements( + load_bundle_manifest(), require_published_spm=args.published_spm + ) result = generate(check=True) if not args.include_tros: return result - return result or _check_tros() + return result or _check_tros(strict=args.strict_tros) + +def _set_spm(args: argparse.Namespace) -> int: + from generate_bundle_artifacts import ( + generate, + load_bundle_manifest, + write_bundle_manifest, + ) + from spm_bundle import configure_spm, published_calculator_component + + calculator = ( + published_calculator_component(args.calculator_version) + if args.calculator_version + else None + ) + settings = { + key: getattr(args, key) + for key in ( + "forecast_content_sha256", + "scenario", + "geography_kind", + "geography_id", + "county_vintage", + "as_of", + ) + } + write_bundle_manifest(configure_spm(load_bundle_manifest(), settings, calculator)) + return generate(check=False) + + +def _development_manifest(args: argparse.Namespace) -> int: + from generate_bundle_artifacts import load_bundle_manifest + from spm_bundle import write_development_manifest + + write_development_manifest( + load_bundle_manifest(), args.country_wheel, args.calculator_wheel, args.output + ) + print(f"Wrote uncertified local development fixture {args.output}") + return 0 -def _generate_tros() -> int: + +def _generate_tros(*, strict: bool = False) -> int: os.environ.setdefault("POLICYENGINE_SKIP_COUNTRY_IMPORTS", "1") from generate_trace_tros import regenerate_all - written, regressions = regenerate_all() + written, regressions = regenerate_all(strict=strict) for path in written: print(f"wrote {path}") for country_id, tro_path, reason in regressions: @@ -106,12 +152,12 @@ def _generate_tros() -> int: return 0 -def _check_tros() -> int: +def _check_tros(*, strict: bool = False) -> int: os.environ.setdefault("POLICYENGINE_SKIP_COUNTRY_IMPORTS", "1") from generate_trace_tros import generated_tros changed = False - for path, payload in generated_tros(): + for path, payload in generated_tros(strict=strict): if path.exists() and path.read_bytes() == payload: continue print( @@ -201,6 +247,32 @@ def _parser() -> argparse.ArgumentParser: ) packages.set_defaults(func=_update_packages) + spm = subparsers.add_parser( + "set-spm", help="Stage SPM defaults and optional verified PyPI calculator pin." + ) + spm.add_argument("--forecast-content-sha256", required=True) + spm.add_argument("--scenario", required=True) + spm.add_argument( + "--geography-kind", choices=["county", "metro", "national"], default="county" + ) + spm.add_argument("--geography-id") + spm.add_argument("--county-vintage", default="2020") + spm.add_argument("--as-of") + spm.add_argument( + "--calculator-version", + help="Verify actual published wheel bytes before adding the runtime dependency.", + ) + spm.set_defaults(func=_set_spm) + + development = subparsers.add_parser( + "development-manifest", + help="Write an uncertified fixture using actual local wheel metadata.", + ) + development.add_argument("--country-wheel", required=True, type=Path) + development.add_argument("--calculator-wheel", required=True, type=Path) + development.add_argument("--output", required=True, type=Path) + development.set_defaults(func=_development_manifest) + generate = subparsers.add_parser( "generate", help="Regenerate derived bundle artifacts.", @@ -219,6 +291,11 @@ def _parser() -> argparse.ArgumentParser: "check", help="Check derived bundle metadata.", ) + check.add_argument( + "--published-spm", + action="store_true", + help="Check SPM measurement pins include a published hashed calculator; does not certify country/data compatibility or release readiness.", + ) check.add_argument( "--include-tros", action="store_true", @@ -229,12 +306,25 @@ def _parser() -> argparse.ArgumentParser: ) check.set_defaults(func=_check) + for command in (generate, check): + command.add_argument( + "--strict-tros", + action="store_true", + help="Require full authenticated manifests matching reviewed TRO source pins; requires --include-tros.", + ) + return parser def main(argv: list[str] | None = None) -> int: - args = _parser().parse_args(argv) - return args.func(args) + parser = _parser() + args = parser.parse_args(argv) + if getattr(args, "strict_tros", False) and not args.include_tros: + parser.error("--strict-tros requires --include-tros") + try: + return args.func(args) + except ValueError as exc: + parser.error(str(exc)) if __name__ == "__main__": diff --git a/scripts/generate_bundle_artifacts.py b/scripts/generate_bundle_artifacts.py index d99b993d..cdbfaea7 100644 --- a/scripts/generate_bundle_artifacts.py +++ b/scripts/generate_bundle_artifacts.py @@ -4,7 +4,9 @@ import argparse import copy +import importlib.util import json +import os import re import sys from pathlib import Path @@ -20,6 +22,21 @@ BUNDLE_MANIFEST = ( REPO_ROOT / "src" / "policyengine" / "data" / "bundle" / "manifest.json" ) +# Other maintenance subcommands explicitly load runtime helpers after this +# module; retain their source-checkout and country-import behavior. +os.environ.setdefault("POLICYENGINE_SKIP_COUNTRY_IMPORTS", "1") +sys.path.insert(0, str(REPO_ROOT / "src")) + +# These commands run before dependencies are installed in release/PR CI. Load +# the same stdlib-only validator used at runtime without importing policyengine. +_VALIDATION_SPEC = importlib.util.spec_from_file_location( + "_policyengine_bundle_validation", + REPO_ROOT / "src" / "policyengine" / "provenance" / "bundle_validation.py", +) +assert _VALIDATION_SPEC is not None and _VALIDATION_SPEC.loader is not None +_VALIDATION_MODULE = importlib.util.module_from_spec(_VALIDATION_SPEC) +_VALIDATION_SPEC.loader.exec_module(_VALIDATION_MODULE) +validate_bundle_measurements = _VALIDATION_MODULE.validate_bundle_measurements OPTIONAL_DEPENDENCIES_HEADER = "[project.optional-dependencies]" NEXT_SECTION_PATTERN = re.compile(r"\n\[tool\.setuptools\]", re.MULTILINE) @@ -34,7 +51,9 @@ def load_bundle_manifest(path: Path = BUNDLE_MANIFEST) -> dict[str, Any]: - return json.loads(path.read_text()) + bundle = json.loads(path.read_text()) + validate_bundle_measurements(bundle) + return bundle def write_bundle_manifest( @@ -51,6 +70,7 @@ def normalized_manifest(bundle: Mapping[str, Any]) -> dict[str, Any]: so release assets and runtime commands can consume one stable shape. """ + validate_bundle_measurements(bundle) packages = { key: { **value, diff --git a/scripts/spm_bundle.py b/scripts/spm_bundle.py new file mode 100644 index 00000000..54e6c380 --- /dev/null +++ b/scripts/spm_bundle.py @@ -0,0 +1,147 @@ +"""SPM bundle pins and explicitly uncertified local-wheel development fixtures.""" + +from __future__ import annotations + +import copy +import hashlib +import json +import re +import zipfile +from email.parser import BytesParser +from pathlib import Path +from urllib.parse import urlparse + +import requests +from generate_bundle_artifacts import normalized_manifest +from packaging.version import Version + +from policyengine.provenance.bundle_validation import validate_bundle_measurements +from policyengine.provenance.spm_configuration import SPMMeasurementConfiguration + + +def _calculator_component(version: str, sha256: str, wheel_url: str) -> dict: + return { + "name": "spm-calculator", + "import_name": "spm_calculator", + "version": version, + "role": "runtime_dependency", + "country": "us", + "sha256": sha256, + "wheel_url": wheel_url, + } + + +def published_calculator_component(version: str) -> dict: + """Read authoritative registry metadata and verify the actual wheel bytes.""" + if str(Version(version)) != version: + raise ValueError("Pass an exact normalized calculator version") + response = requests.get( + f"https://pypi.org/pypi/spm-calculator/{version}/json", timeout=60 + ) + response.raise_for_status() + metadata = response.json() + if metadata["info"]["version"] != version: + raise ValueError("Registry returned a different calculator version") + wheels = [ + entry + for entry in metadata.get("urls", []) + if entry.get("packagetype") == "bdist_wheel" + and entry.get("filename", "").endswith("-py3-none-any.whl") + and not entry.get("yanked") + ] + if len(wheels) != 1: + raise ValueError("Expected exactly one non-yanked universal calculator wheel") + wheel = wheels[0] + url = urlparse(wheel["url"]) + if url.scheme != "https" or url.hostname != "files.pythonhosted.org": + raise ValueError("Calculator wheel must be published on files.pythonhosted.org") + expected = wheel.get("digests", {}).get("sha256", "") + if not re.fullmatch(r"[0-9a-f]{64}", expected): + raise ValueError("Registry wheel metadata has no valid sha256") + download = requests.get(wheel["url"], timeout=60) + download.raise_for_status() + if hashlib.sha256(download.content).hexdigest() != expected: + raise ValueError( + "Published calculator wheel bytes do not match registry sha256" + ) + return _calculator_component(version, expected, wheel["url"]) + + +def configure_spm(bundle: dict, configuration: dict, calculator: dict | None) -> dict: + """Stage measurement defaults and, when available, a verified runtime pin.""" + updated = copy.deepcopy(bundle) + updated.setdefault("measurements", {})["spm"] = ( + SPMMeasurementConfiguration.model_validate(configuration).model_dump( + mode="json" + ) + ) + if calculator is not None: + updated["packages"]["spm-calculator"] = calculator + for extra in ("us", "models"): + entries = updated["extras"][extra] + if "spm-calculator" not in entries: + entries.append("spm-calculator") + validate_bundle_measurements(updated) + return normalized_manifest(updated) + + +def local_wheel_component(path: Path, expected_name: str) -> dict: + """Extract actual wheel metadata and hash, without registry identity claims.""" + path = path.resolve(strict=True) + with zipfile.ZipFile(path) as archive: + metadata_paths = [ + name for name in archive.namelist() if name.endswith(".dist-info/METADATA") + ] + if len(metadata_paths) != 1: + raise ValueError("Wheel must contain exactly one METADATA file") + metadata = BytesParser().parsebytes(archive.read(metadata_paths[0])) + if metadata["Name"].lower().replace("_", "-") != expected_name: + raise ValueError(f"Expected {expected_name} wheel, got {metadata['Name']}") + version = str(Version(metadata["Version"])) + return { + "name": expected_name, + "version": version, + "sha256": hashlib.sha256(path.read_bytes()).hexdigest(), + "wheel_url": path.as_uri(), + } + + +def development_manifest( + bundle: dict, country_wheel: Path, calculator_wheel: Path +) -> dict: + """Build a local test identity, stripping the inherited US certification.""" + country = local_wheel_component(country_wheel, "policyengine-us") + calculator = local_wheel_component(calculator_wheel, "spm-calculator") + updated = copy.deepcopy(bundle) + updated["packages"]["policyengine-us"].update(country) + updated["data_releases"]["us"]["model_package"] = country + updated["data_releases"]["us"].pop("certification", None) + updated["data_releases"]["us"].pop("certified_data_artifact", None) + updated["development"] = { + "purpose": "local canonical SPM wrapper tests", + "data_certification": "not_certified", + "promotion_status": "pending", + "data_release_metadata": "inherited reference only; no compatibility claim", + "local_wheels": {"policyengine-us": country, "spm-calculator": calculator}, + } + return configure_spm( + updated, + bundle["measurements"]["spm"], + _calculator_component( + calculator["version"], calculator["sha256"], calculator["wheel_url"] + ), + ) + + +def write_development_manifest( + bundle: dict, country_wheel: Path, calculator_wheel: Path, output: Path +) -> dict: + """Write an explicitly local development fixture, never the packaged manifest.""" + from generate_bundle_artifacts import BUNDLE_MANIFEST + + if output.resolve() == BUNDLE_MANIFEST.resolve(): + raise ValueError("Development fixtures cannot overwrite the packaged manifest") + manifest = development_manifest(bundle, country_wheel, calculator_wheel) + output.parent.mkdir(parents=True, exist_ok=True) + output.write_text(json.dumps(manifest, indent=2, sort_keys=True) + "\n") + return manifest diff --git a/src/policyengine/bundle.py b/src/policyengine/bundle.py index c4518673..2831fa59 100644 --- a/src/policyengine/bundle.py +++ b/src/policyengine/bundle.py @@ -13,7 +13,7 @@ import shutil import subprocess import venv as venv_module -from datetime import datetime, timezone +from datetime import UTC, datetime from importlib import metadata from importlib.resources import files from pathlib import Path @@ -21,6 +21,7 @@ import requests +from policyengine.provenance.bundle_validation import validate_bundle_measurements from policyengine.provenance.dataset_materialization import ( DatasetMaterializationError, MaterializedDataset, @@ -71,6 +72,10 @@ def _normalise_manifest(manifest: Mapping[str, Any]) -> dict[str, Any]: payload.setdefault("packages", {}) payload.setdefault("extras", {}) payload.setdefault("data_releases", _data_releases_from_countries(payload)) + try: + validate_bundle_measurements(payload) + except ValueError as exc: + raise BundleError(str(exc)) from exc return payload @@ -183,10 +188,12 @@ def _include_component( return False role = component.get("role") country = component.get("country") - if role in {"bundle_carrier", "runtime_dependency"}: + if role == "bundle_carrier": return True if isinstance(country, str): return country in countries + if role == "runtime_dependency": + return True return key == "policyengine" @@ -352,7 +359,7 @@ def write_receipt( ) -> Path: receipt = { "schema_version": 1, - "installed_at": datetime.now(timezone.utc).isoformat(), + "installed_at": datetime.now(UTC).isoformat(), "bundle_version": manifest["bundle_version"], "policyengine_version": manifest["policyengine_version"], "countries": list(countries), diff --git a/src/policyengine/data/bundle/manifest.json b/src/policyengine/data/bundle/manifest.json index 1018ce8c..8b787240 100644 --- a/src/policyengine/data/bundle/manifest.json +++ b/src/policyengine/data/bundle/manifest.json @@ -253,6 +253,16 @@ "spm-calculator" ] }, + "measurements": { + "spm": { + "as_of": null, + "county_vintage": "2020", + "forecast_content_sha256": "3d86d5c4c0423480e6b69b75d222ffa4a7a2639e4094df5ba2504af01be17173", + "geography_id": null, + "geography_kind": "county", + "scenario": "ce_trend" + } + }, "packages": { "policyengine": { "import_name": "policyengine", diff --git a/src/policyengine/provenance/bundle_validation.py b/src/policyengine/provenance/bundle_validation.py new file mode 100644 index 00000000..51324de8 --- /dev/null +++ b/src/policyengine/provenance/bundle_validation.py @@ -0,0 +1,152 @@ +"""Validate independently pinned measurement configuration in bundle manifests.""" + +import re +from datetime import date +from typing import Any, Mapping +from urllib.parse import urlparse + +# Canonical PEP 440 spellings only: no aliases, leading zeroes, or implicit +# pre/post/dev numbers. Keep bootstrap validation independent of packaging. +_INTEGER = r"(?:0|[1-9][0-9]*)" +_NORMALIZED_VERSION = re.compile( + rf"(?:[1-9][0-9]*!)?{_INTEGER}(?:\.{_INTEGER})*" + rf"(?:(?:a|b|rc){_INTEGER})?(?:\.post{_INTEGER})?(?:\.dev{_INTEGER})?" + r"(?:\+[a-z0-9]+(?:\.[a-z0-9]+)*)?" +) + + +def _is_normalized_version(value: Any) -> bool: + if not isinstance(value, str) or not _NORMALIZED_VERSION.fullmatch(value): + return False + local = value.partition("+")[2] + return all( + not part.isdigit() or part == str(int(part)) for part in local.split(".") + ) + + +def validate_spm_configuration(configuration: Any) -> None: + """Validate JSON defaults using only the standard library. + + Both runtime loading and pre-install bundle maintenance call this function. + Keep it importable by file path without executing the wrapper package. + """ + if not isinstance(configuration, Mapping): + raise ValueError("SPM measurement configuration must be an object") + allowed = { + "forecast_content_sha256", + "scenario", + "geography_kind", + "geography_id", + "county_vintage", + "as_of", + } + if set(configuration) - allowed: + raise ValueError("SPM measurement configuration contains unknown fields") + sha256 = configuration.get("forecast_content_sha256") + if not isinstance(sha256, str) or not re.fullmatch(r"[0-9a-f]{64}", sha256): + raise ValueError("SPM forecast_content_sha256 must be a lowercase SHA256") + scenario = configuration.get("scenario") + if not isinstance(scenario, str) or not re.fullmatch(r"\S+", scenario): + raise ValueError("SPM scenario must be nonempty without whitespace") + kind = configuration.get("geography_kind", "county") + if kind not in ("county", "metro", "national"): + raise ValueError("SPM geography_kind must be county, metro, or national") + geography_id = configuration.get("geography_id") + if geography_id is not None and ( + not isinstance(geography_id, str) + or not geography_id.strip() + or geography_id != geography_id.strip() + ): + raise ValueError("SPM geography_id must be nonempty without outer whitespace") + if (kind == "metro") != (geography_id is not None): + raise ValueError("Only metro SPM selection requires geography_id") + vintage = configuration.get("county_vintage", "2020") + if not isinstance(vintage, str) or not re.fullmatch(r"[0-9]{4}", vintage): + raise ValueError("SPM county_vintage must be a four-digit year") + as_of = configuration.get("as_of") + if as_of is not None: + if not isinstance(as_of, str) or not re.fullmatch( + r"[0-9]{4}-[0-9]{2}-[0-9]{2}", as_of + ): + raise ValueError("SPM as_of must be an ISO YYYY-MM-DD date") + if date.fromisoformat(as_of).isoformat() != as_of: + raise ValueError("SPM as_of must be an ISO YYYY-MM-DD date") + + +def validate_bundle_measurements( + bundle: Mapping[str, Any], *, require_published_spm: bool = False +) -> None: + """Reject malformed SPM pins, retaining support for historical bundles. + + Drafts may stage a content pin before registry publication. The stricter + measurement-pin check requires the calculator's published wheel identity as + well. It does not establish country/data compatibility or release readiness. + """ + measurements = bundle.get("measurements", {}) + if not isinstance(measurements, Mapping): + raise ValueError("Bundle measurements must be an object") + if "spm" not in measurements: + if require_published_spm: + raise ValueError("Published SPM pin check requires measurements.spm") + return + validate_spm_configuration(measurements["spm"]) + packages = bundle.get("packages", {}) + if not isinstance(packages, Mapping): + raise ValueError("Bundle packages must be an object") + calculator = packages.get("spm-calculator") + if calculator is None: + if require_published_spm: + raise ValueError("SPM promotion pending: no published spm-calculator pin") + return + if not isinstance(calculator, Mapping): + raise ValueError("spm-calculator package pin must be an object") + role = calculator.get("role") + if calculator.get("name") != "spm-calculator" or role not in ( + "runtime_dependency", + "country_dependency", + ): + raise ValueError("spm-calculator must be an explicit runtime_dependency") + # A protective `country_dependency` pin declares a compatible version only. + # A `runtime_dependency` pin additionally carries the registry-verified + # wheel identity that the promotion gate below requires. + verified = role == "runtime_dependency" + if calculator.get("country") != "us": + raise ValueError("spm-calculator must be scoped to country us") + if calculator.get("installable", True) is not True or calculator.get("markers"): + raise ValueError("spm-calculator must be installed unconditionally for US") + sha256 = calculator.get("sha256") + if verified and ( + not isinstance(sha256, str) or not re.fullmatch(r"[0-9a-f]{64}", sha256) + ): + raise ValueError("spm-calculator requires its wheel sha256") + version = calculator.get("version") + if not _is_normalized_version(version): + raise ValueError("spm-calculator requires an exact normalized version") + requirement = calculator.get("install_requirement") + expected_requirement = f"spm-calculator=={version}" + if calculator.get("markers"): + expected_requirement += f"; {calculator['markers']}" + if requirement is not None and requirement != expected_requirement: + raise ValueError( + "spm-calculator install_requirement must match its exact version" + ) + extras = bundle.get("extras", {}) + if not isinstance(extras, Mapping): + raise ValueError("Bundle extras must be an object") + for extra in ("us", "models"): + entries = extras.get(extra, []) + if not isinstance(entries, list) or "spm-calculator" not in entries: + raise ValueError(f"spm-calculator must be included in the {extra} extra") + if require_published_spm: + if not verified: + raise ValueError("SPM promotion pending: no published spm-calculator pin") + if bundle.get("development"): + raise ValueError( + "Development fixtures are not certified production bundles" + ) + wheel_url = calculator.get("wheel_url") + if not isinstance(wheel_url, str): + raise ValueError("Published SPM pin check requires a published PyPI wheel") + url = urlparse(wheel_url) + if url.scheme != "https" or url.hostname != "files.pythonhosted.org": + raise ValueError("Published SPM pin check requires a published PyPI wheel") diff --git a/src/policyengine/provenance/spm_configuration.py b/src/policyengine/provenance/spm_configuration.py new file mode 100644 index 00000000..277a4493 --- /dev/null +++ b/src/policyengine/provenance/spm_configuration.py @@ -0,0 +1,26 @@ +"""Typed operator input for SPM bundle configuration.""" + +from typing import Literal, Optional + +from pydantic import BaseModel, ConfigDict, Field, model_validator + +from policyengine.provenance.bundle_validation import validate_spm_configuration + + +class SPMMeasurementConfiguration(BaseModel): + """Canonical SPM defaults; this pin is not a data certification claim.""" + + model_config = ConfigDict(extra="forbid", strict=True) + + forecast_content_sha256: str = Field(pattern=r"^[0-9a-f]{64}$") + scenario: str = Field(min_length=1, pattern=r"^\S+$") + geography_kind: Literal["county", "metro", "national"] = "county" + geography_id: Optional[str] = None + county_vintage: str = Field(default="2020", pattern=r"^[0-9]{4}$") + as_of: Optional[str] = None + + @model_validator(mode="before") + @classmethod + def canonical_defaults(cls, value): + validate_spm_configuration(value) + return value diff --git a/tests/fixtures/spm_development.py b/tests/fixtures/spm_development.py new file mode 100644 index 00000000..6ed81414 --- /dev/null +++ b/tests/fixtures/spm_development.py @@ -0,0 +1,223 @@ +"""Explicit test-only bootstrap for unpublished country/calculator wheels. + +Load with ``-p tests.fixtures.spm_development --spm-development-manifest PATH``. +This bypass is never imported by production code or enabled by default pytest. +It supplies an UNVERIFIED receipt, not fabricated data compatibility evidence. +""" + +import copy +import hashlib +import importlib +import json +import os +import sys +import zipfile +from importlib import metadata +from pathlib import Path +from urllib.parse import unquote, urlparse + +import pytest + +_DEVELOPMENT_MANIFEST = None + + +def _verify_import_sources(packages): + """Authenticate import resolution and already loaded modules, not just metadata. + + Distribution metadata can describe the right wheel while PYTHONPATH or a + previously imported module supplies different code. Check without importing + the country packages, then repeat after the explicit bootstrap imports them. + """ + for import_name, (root, members) in packages.items(): + spec = importlib.util.find_spec(import_name) + if spec is None or spec.origin is None: + raise ValueError(f"Cannot authenticate import source for {import_name}") + expected_init = root / "__init__.py" + if Path(spec.origin).resolve() != expected_init: + raise ValueError( + f"Unexpected import source for {import_name}: {spec.origin}" + ) + if {Path(p).resolve() for p in spec.submodule_search_locations or ()} != {root}: + raise ValueError(f"Unexpected package search path for {import_name}") + for name, module in tuple(sys.modules.items()): + if name != import_name and not name.startswith(import_name + "."): + continue + if module is None: + continue + source = getattr(module, "__file__", None) + module_spec = getattr(module, "__spec__", None) + if ( + source is None + and module_spec is not None + and module_spec.origin is None + and module_spec.submodule_search_locations + ): + # Country parameter directories are legitimate namespace + # packages. Every namespace search directory must still belong + # to the authenticated installation and wheel inventory. + locations = { + Path(p).resolve() for p in module_spec.submodule_search_locations + } + if locations != { + Path(p).resolve() for p in getattr(module, "__path__", ()) + }: + raise ValueError( + f"Unexpected loaded package search path for {name}" + ) + for location in locations: + try: + prefix = location.relative_to(root).as_posix() + "/" + except ValueError as exc: + raise ValueError( + f"Unexpected loaded import source for {name}" + ) from exc + if members is not None and not any( + member.startswith(prefix) for member in members + ): + raise ValueError(f"Unexpected loaded import source for {name}") + continue + if not source or module_spec is None or module_spec.origin is None: + raise ValueError(f"Cannot authenticate loaded import source for {name}") + source = Path(source).resolve() + try: + relative = source.relative_to(root).as_posix() + except ValueError as exc: + raise ValueError(f"Unexpected loaded import source for {name}") from exc + if Path(module_spec.origin).resolve() != source or ( + members is not None and relative not in members + ): + raise ValueError(f"Unexpected loaded import source for {name}") + if hasattr(module, "__path__") and { + Path(p).resolve() for p in module.__path__ + } != {source.parent}: + raise ValueError(f"Unexpected loaded package search path for {name}") + + +def pytest_addoption(parser): + parser.addoption("--spm-development-manifest", type=Path) + + +def pytest_load_initial_conftests(early_config, parser, args): + path = early_config.known_args_namespace.spm_development_manifest + if path is None: + raise pytest.UsageError( + "The SPM development plugin requires an explicit manifest" + ) + try: + activate_spm_development_manifest(path) + except ValueError as exc: + raise pytest.UsageError(str(exc)) from exc + + +def _verify_local_wheel_installations(payload): + """Require both fixture wheel identity and installed source byte identity.""" + packages = {} + for name, import_name in ( + ("policyengine-us", "policyengine_us"), + ("spm-calculator", "spm_calculator"), + ): + component = payload["packages"][name] + if component["import_name"] != import_name: + raise ValueError(f"Unexpected import name for {name}") + uri = urlparse(component["wheel_url"]) + if uri.scheme != "file" or uri.netloc: + raise ValueError("Development fixtures require explicit local wheel files") + path = Path(unquote(uri.path)) + if hashlib.sha256(path.read_bytes()).hexdigest() != component["sha256"]: + raise ValueError(f"Local {name} wheel does not match the fixture sha256") + distribution = metadata.distribution(name) + members = set() + with zipfile.ZipFile(path) as archive: + for member in archive.namelist(): + if not member.startswith(import_name + "/") or member.endswith("/"): + continue + installed = distribution.locate_file(member) + if not installed.is_file() or installed.read_bytes() != archive.read( + member + ): + raise ValueError( + f"Installed {name} file differs from wheel: {member}" + ) + members.add(member[len(import_name) + 1 :]) + if not members: + raise ValueError(f"Local {name} wheel contains no package files") + packages[import_name] = ( + Path(distribution.locate_file(import_name)).resolve(), + members, + ) + # Core's version is pinned in the fixture; its wheel is authenticated by the + # separate four-wheel producer probe. Still reject a shadowed core import. + packages["policyengine_core"] = ( + Path( + metadata.distribution("policyengine-core").locate_file("policyengine_core") + ).resolve(), + None, + ) + _verify_import_sources(packages) + return packages + + +def activate_spm_development_manifest(path): + """Explicitly bootstrap an unpublished local runtime before country imports. + + This test helper is also available to bounded development scripts. It never + certifies the inherited data and refuses a mismatched installed wheel. + """ + global _DEVELOPMENT_MANIFEST + + if "policyengine.tax_benefit_models.us.model" in sys.modules: + raise ValueError( + "Activate the development fixture before importing country models" + ) + path = Path(path) + payload = json.loads(path.read_text()) + if payload.get("development", {}).get("data_certification") != "not_certified": + raise ValueError("The SPM plugin only accepts uncertified development fixtures") + for name in ("policyengine-us", "policyengine-core", "spm-calculator"): + if metadata.version(name) != payload["packages"][name]["version"]: + raise ValueError(f"Installed {name} does not match the development fixture") + packages = _verify_local_wheel_installations(payload) + os.environ["POLICYENGINE_SKIP_COUNTRY_IMPORTS"] = "1" + import policyengine + from policyengine import bundle + from policyengine.provenance import manifest + + _DEVELOPMENT_MANIFEST = payload + original_release = manifest.get_release_manifest + original_certify = manifest.certify_data_release_compatibility + + def development_release(country_id): + if country_id == "us": + return manifest.CountryReleaseManifest.model_validate( + copy.deepcopy(payload["data_releases"]["us"]) + ) + return original_release(country_id) + + def unavailable_data_release(country_id): + raise manifest.DataReleaseManifestUnavailableError( + "Explicit offline local-wheel development fixture; data certification pending" + ) + + def unverified_development(country_id, runtime_model_version, **kwargs): + if country_id == "us": + return manifest.DataCertification( + compatibility_basis="unverified_development_fixture", + certified_for_model_version=runtime_model_version, + ) + return original_certify(country_id, runtime_model_version, **kwargs) + + manifest.get_release_manifest = development_release + manifest.get_data_release_manifest = unavailable_data_release + manifest.certify_data_release_compatibility = unverified_development + bundle.get_current_bundle = lambda: copy.deepcopy(payload) + policyengine.us = importlib.import_module("policyengine.tax_benefit_models.us") + if importlib.util.find_spec("policyengine_uk") is not None: + policyengine.uk = importlib.import_module("policyengine.tax_benefit_models.uk") + _verify_import_sources(packages) + return copy.deepcopy(payload) + + +@pytest.fixture +def spm_development_bundle(): + """Expose the explicitly enabled, unverified fixture for test assertions.""" + return copy.deepcopy(_DEVELOPMENT_MANIFEST) diff --git a/tests/test_spm_bundle_bootstrap.py b/tests/test_spm_bundle_bootstrap.py new file mode 100644 index 00000000..bb17c402 --- /dev/null +++ b/tests/test_spm_bundle_bootstrap.py @@ -0,0 +1,125 @@ +"""Exercise the bundle commands as CI runs them before runtime installation.""" + +import json +import os +import shutil +import subprocess +import sys +from pathlib import Path + +import pytest +import yaml + +REPO_ROOT = Path(__file__).resolve().parents[1] + + +@pytest.fixture +def bootstrap_checkout(tmp_path): + paths = ( + "scripts/bundle.py", + "scripts/generate_bundle_artifacts.py", + "src/policyengine/provenance/bundle_validation.py", + "src/policyengine/data/bundle/manifest.json", + "pyproject.toml", + ) + for path in paths: + destination = tmp_path / path + destination.parent.mkdir(parents=True, exist_ok=True) + shutil.copy2(REPO_ROOT / path, destination) + return tmp_path + + +def _bootstrap(checkout, *arguments): + env = os.environ.copy() + env.pop("PYTHONPATH", None) + env.pop("PYTHONHOME", None) + return subprocess.run( + [sys.executable, "-S", *arguments], + cwd=checkout, + env=env, + capture_output=True, + text=True, + timeout=30, + check=False, + ) + + +@pytest.mark.parametrize( + "arguments", + [ + ("scripts/bundle.py", "check"), + ("scripts/bundle.py", "generate"), + ("scripts/generate_bundle_artifacts.py", "--check"), + ("scripts/generate_bundle_artifacts.py",), + ], +) +def test_bundle_commands_work_without_site_packages(bootstrap_checkout, arguments): + manifest = bootstrap_checkout / "src/policyengine/data/bundle/manifest.json" + pyproject = bootstrap_checkout / "pyproject.toml" + before = (manifest.read_bytes(), pyproject.read_bytes()) + result = _bootstrap(bootstrap_checkout, *arguments) + assert result.returncode == 0, result.stdout + result.stderr + assert (manifest.read_bytes(), pyproject.read_bytes()) == before + + +def test_dependency_free_check_keeps_published_spm_gate(bootstrap_checkout): + manifest = bootstrap_checkout / "src/policyengine/data/bundle/manifest.json" + staged = json.loads(manifest.read_text()) + staged["packages"].pop("spm-calculator", None) + manifest.write_text(json.dumps(staged)) + result = _bootstrap( + bootstrap_checkout, "scripts/bundle.py", "check", "--published-spm" + ) + assert result.returncode == 2 + assert "no published spm-calculator pin" in result.stderr + assert "ModuleNotFoundError" not in result.stderr + + +@pytest.mark.parametrize( + "change", + [ + {"forecast_content_sha256": "unverified"}, + {"scenario": ""}, + {"geography_kind": "metro", "geography_id": None}, + {"as_of": "2026-02-30"}, + {"provider": "/tmp/unsupported.json"}, + ], +) +def test_dependency_free_generation_validates_before_writing( + bootstrap_checkout, change +): + manifest = bootstrap_checkout / "src/policyengine/data/bundle/manifest.json" + invalid = json.loads(manifest.read_text()) + invalid["measurements"]["spm"].update(change) + manifest.write_text(json.dumps(invalid)) + before = manifest.read_bytes() + result = _bootstrap(bootstrap_checkout, "scripts/bundle.py", "generate") + assert result.returncode == 2 + assert "ModuleNotFoundError" not in result.stderr + assert manifest.read_bytes() == before + + +def test_release_checks_published_spm_before_publication_and_after_pypi_visibility(): + workflow = yaml.safe_load((REPO_ROOT / ".github/workflows/push.yaml").read_text()) + publish_steps = workflow["jobs"]["Publish"]["steps"] + # Publish installs the package first, so its gate is the full bundle check + # rather than the dependency-light one NotifyConsumers has to use. + prepublication_gate = next( + index + for index, step in enumerate(publish_steps) + if "bundle.py check --published-spm" in step.get("run", "") + ) + publication = next( + index + for index, step in enumerate(publish_steps) + if step.get("uses", "").startswith("pypa/gh-action-pypi-publish@") + ) + assert prepublication_gate < publication + notify = workflow["jobs"]["NotifyConsumers"] + assert "Publish" in notify["needs"] + steps = notify["steps"] + runs = [step.get("run", "") for step in steps] + wait = runs.index("bash .github/wait-for-pypi.sh") + strict_gate = runs.index("python -S scripts/bundle.py check --published-spm") + dispatch = runs.index("bash .github/dispatch-policyengine-release.sh") + assert wait < strict_gate < dispatch diff --git a/tests/test_spm_bundle_tooling.py b/tests/test_spm_bundle_tooling.py new file mode 100644 index 00000000..ea93dbf7 --- /dev/null +++ b/tests/test_spm_bundle_tooling.py @@ -0,0 +1,299 @@ +"""Measurement pins are independent of data certification and registry publication.""" + +import copy +import hashlib +import sys +import zipfile +from pathlib import Path +from types import SimpleNamespace + +import pytest +from packaging.version import InvalidVersion, Version + +from policyengine import bundle +from policyengine.provenance.bundle_validation import ( + _is_normalized_version, + validate_bundle_measurements, +) +from policyengine.provenance.spm_configuration import SPMMeasurementConfiguration + +sys.path.insert(0, str(Path(__file__).resolve().parents[1] / "scripts")) +import bundle as maintenance # noqa: E402 +from generate_bundle_artifacts import ( # noqa: E402 + load_bundle_manifest, + normalized_manifest, +) +from spm_bundle import ( # noqa: E402 + configure_spm, + development_manifest, + published_calculator_component, + write_development_manifest, +) + + +@pytest.fixture +def canonical_bundle(): + # Read the packaged JSON even when an explicitly enabled development plugin + # replaces runtime get_current_bundle for the household test process. + return load_bundle_manifest() + + +@pytest.mark.parametrize( + "change", + [ + {"forecast_content_sha256": None}, + {"forecast_content_sha256": "a" * 63}, + {"forecast_content_sha256": "A" * 64}, + {"scenario": None}, + {"scenario": " "}, + {"scenario": "ce_trend "}, + {"scenario": "ce trend"}, + {"geography_kind": "state"}, + {"geography_kind": "metro", "geography_id": None}, + {"geography_kind": "metro", "geography_id": ""}, + {"geography_kind": "national", "geography_id": "31080"}, + {"county_vintage": 2020}, + {"as_of": "2026-02-30"}, + {"as_of": "20260909"}, + {"forecast_path": "/tmp/provider.json"}, + {"provider": "fallback"}, + ], +) +def test_malformed_measurement_defaults_fail_before_bundle_use( + canonical_bundle, change +): + canonical_bundle["measurements"]["spm"].update(change) + with pytest.raises(ValueError): + SPMMeasurementConfiguration.model_validate( + canonical_bundle["measurements"]["spm"] + ) + with pytest.raises(ValueError): + normalized_manifest(canonical_bundle) + with pytest.raises(bundle.BundleError): + bundle._normalise_manifest(canonical_bundle) + + +@pytest.mark.parametrize("measurements", [None, [], "spm", {"spm": None}]) +def test_measurements_require_an_object(canonical_bundle, measurements): + canonical_bundle["measurements"] = measurements + with pytest.raises(ValueError): + validate_bundle_measurements(canonical_bundle) + + +def test_historical_bundle_without_measurements_still_loads(canonical_bundle): + canonical_bundle.pop("measurements") + validate_bundle_measurements(canonical_bundle) + assert ( + bundle._normalise_manifest(canonical_bundle)["bundle_version"] + == canonical_bundle["bundle_version"] + ) + + +def test_staged_config_preserves_certification_and_fails_release_gate(canonical_bundle): + before = copy.deepcopy(canonical_bundle) + updated = configure_spm( + canonical_bundle, canonical_bundle["measurements"]["spm"], None + ) + assert updated["data_releases"] == before["data_releases"] + assert updated["packages"] == before["packages"] + assert updated["packages"]["policyengine-us"]["version"] == "1.764.6" + validate_bundle_measurements(updated) + with pytest.raises(ValueError, match="promotion pending"): + validate_bundle_measurements(updated, require_published_spm=True) + + +def _calculator_pin(): + # Synthetic registry metadata is a unit fixture, not publication evidence. + return { + "name": "spm-calculator", + "version": "1.0.0", + "role": "runtime_dependency", + "country": "us", + "sha256": "a" * 64, + "wheel_url": "https://files.pythonhosted.org/unit-test/spm.whl", + } + + +def test_published_runtime_dependency_includes_us_and_preserves_uk(canonical_bundle): + updated = configure_spm( + canonical_bundle, canonical_bundle["measurements"]["spm"], _calculator_pin() + ) + assert "spm-calculator==1.0.0" in bundle.bundle_install_requirements( + updated, countries=["us"] + ) + assert "spm-calculator==1.0.0" not in bundle.bundle_install_requirements( + updated, countries=["uk"] + ) + assert updated["extras"]["uk"] == canonical_bundle["extras"]["uk"] + validate_bundle_measurements(updated, require_published_spm=True) + + +@pytest.mark.parametrize( + "change", + [ + {"sha256": None}, + {"role": "country_model"}, + {"country": "uk"}, + {"version": ">=1.0"}, + {"version": None}, + {"install_requirement": "spm-calculator>=1"}, + {"installable": False}, + {"markers": "python_version < '0'"}, + ], +) +def test_calculator_requires_scoped_runtime_identity(canonical_bundle, change): + component = {**_calculator_pin(), **change} + with pytest.raises(ValueError): + configure_spm( + canonical_bundle, canonical_bundle["measurements"]["spm"], component + ) + + +def _registry_responses(monkeypatch, wheel_bytes, download_bytes): + import spm_bundle + + expected = hashlib.sha256(wheel_bytes).hexdigest() + wheel_url = ( + "https://files.pythonhosted.org/unit-test/spm_calculator-1.0.0-py3-none-any.whl" + ) + payload = { + "info": {"version": "1.0.0"}, + "urls": [ + { + "packagetype": "bdist_wheel", + "filename": "spm_calculator-1.0.0-py3-none-any.whl", + "url": wheel_url, + "digests": {"sha256": expected}, + } + ], + } + calls = [] + + def get(url, **kwargs): + calls.append(url) + return SimpleNamespace( + json=lambda: payload, content=download_bytes, raise_for_status=lambda: None + ) + + monkeypatch.setattr(spm_bundle.requests, "get", get) + return expected, calls, wheel_url + + +def test_registry_pin_hashes_downloaded_bytes(monkeypatch): + expected, calls, url = _registry_responses( + monkeypatch, b"test-wheel", b"test-wheel" + ) + component = published_calculator_component("1.0.0") + assert component["sha256"] == expected + assert component["role"] == "runtime_dependency" + assert calls == ["https://pypi.org/pypi/spm-calculator/1.0.0/json", url] + + +def test_registry_pin_rejects_changed_wheel_bytes(monkeypatch): + _registry_responses(monkeypatch, b"expected-wheel", b"changed-wheel") + with pytest.raises(ValueError, match="do not match"): + published_calculator_component("1.0.0") + + +def _write_wheel(tmp_path, name, version): + path = tmp_path / f"{name}-{version}-py3-none-any.whl" + with zipfile.ZipFile(path, "w") as archive: + archive.writestr( + f"{name}-{version}.dist-info/METADATA", + f"Name: {name}\nVersion: {version}\n", + ) + return path + + +def test_development_fixture_uses_actual_wheel_pins_without_certification( + canonical_bundle, tmp_path +): + country = _write_wheel(tmp_path, "policyengine-us", "1.824.7") + calculator = _write_wheel(tmp_path, "spm-calculator", "1.0.0") + before = copy.deepcopy(canonical_bundle) + development = development_manifest(canonical_bundle, country, calculator) + assert development["development"]["data_certification"] == "not_certified" + assert development["development"]["promotion_status"] == "pending" + assert development["packages"]["policyengine-us"]["version"] == "1.824.7" + assert ( + development["packages"]["spm-calculator"]["sha256"] + == hashlib.sha256(calculator.read_bytes()).hexdigest() + ) + assert "certification" not in development["data_releases"]["us"] + assert "certified_data_artifact" not in development["data_releases"]["us"] + assert development["data_releases"]["uk"] == before["data_releases"]["uk"] + assert canonical_bundle == before + with pytest.raises(ValueError, match="Development fixtures"): + validate_bundle_measurements(development, require_published_spm=True) + + +def test_development_fixture_cannot_replace_packaged_manifest( + canonical_bundle, tmp_path +): + from generate_bundle_artifacts import BUNDLE_MANIFEST + + with pytest.raises(ValueError, match="cannot overwrite"): + write_development_manifest( + canonical_bundle, + tmp_path / "missing", + tmp_path / "missing", + BUNDLE_MANIFEST, + ) + + +def test_cli_rejects_invalid_configuration_without_writing( + monkeypatch, canonical_bundle +): + import generate_bundle_artifacts + + monkeypatch.setattr( + generate_bundle_artifacts, "load_bundle_manifest", lambda: canonical_bundle + ) + monkeypatch.setattr( + generate_bundle_artifacts, + "write_bundle_manifest", + lambda _: pytest.fail("Invalid configuration wrote a manifest"), + ) + with pytest.raises(SystemExit) as error: + maintenance.main( + [ + "set-spm", + "--forecast-content-sha256", + "invalid", + "--scenario", + "ce_trend", + ] + ) + assert error.value.code == 2 + + +@pytest.mark.parametrize( + "version", + [ + "1", + "1.0.0", + "1.0rc1", + "1.0.post0.dev1", + "2!1.0+linux.x86.1", + "1.0+01", + "1.0+Linux", + "01.0", + "1.01", + "0!1.0", + "v1.0", + "1.0RC1", + "1.0-1", + "1.0.post", + "1.0.dev", + "1.*", + ">=1.0", + "", + None, + ], +) +def test_dependency_free_exact_version_validation_matches_packaging(version): + try: + expected = isinstance(version, str) and str(Version(version)) == version + except InvalidVersion: + expected = False + assert _is_normalized_version(version) is expected diff --git a/tests/test_spm_development_authentication.py b/tests/test_spm_development_authentication.py new file mode 100644 index 00000000..05da8c7b --- /dev/null +++ b/tests/test_spm_development_authentication.py @@ -0,0 +1,95 @@ +"""Candidate metadata must authenticate the code Python actually resolves.""" + +import json +import os +import subprocess +import sys +from pathlib import Path + +import pytest + + +def _probe(tmp_path, payload, setup): + manifest = tmp_path / "development.json" + manifest.write_text(json.dumps(payload)) + env = os.environ.copy() + env.pop("PYTHONPATH", None) + env["PYTHONDONTWRITEBYTECODE"] = "1" + script = "\n".join( + [ + "import importlib.util, json, sys, types", + "from pathlib import Path", + "from tests.fixtures.spm_development import _verify_local_wheel_installations", + f"payload = json.loads(Path({str(manifest)!r}).read_text())", + setup, + "_verify_local_wheel_installations(payload)", + "assert 'policyengine_us' not in sys.modules", + "assert 'spm_calculator' not in sys.modules", + "print('Candidate import sources authenticated before country imports')", + ] + ) + return subprocess.run( + [sys.executable, "-c", script], + cwd=Path(__file__).resolve().parents[1], + env=env, + capture_output=True, + text=True, + timeout=60, + ) + + +@pytest.fixture +def candidate_manifest(): + from tests.fixtures.spm_development import _DEVELOPMENT_MANIFEST + + if _DEVELOPMENT_MANIFEST is None: + pytest.skip("Requires the explicit local-wheel development manifest") + return _DEVELOPMENT_MANIFEST + + +def test_authenticates_real_candidate_before_import(tmp_path, candidate_manifest): + result = _probe(tmp_path, candidate_manifest, "") + assert result.returncode == 0, result.stderr + assert "authenticated before country imports" in result.stdout + + +@pytest.mark.parametrize( + "import_name", ["policyengine_us", "spm_calculator", "policyengine_core"] +) +def test_rejects_pythonpath_shadow_before_executing_it( + tmp_path, candidate_manifest, import_name +): + shadow = tmp_path / "shadow" + package = shadow / import_name + package.mkdir(parents=True) + (package / "__init__.py").write_text( + "raise AssertionError('Shadow package was executed')\n" + ) + result = _probe( + tmp_path, candidate_manifest, f"sys.path.insert(0, {str(shadow)!r})" + ) + assert result.returncode != 0 + assert f"Unexpected import source for {import_name}" in result.stderr + assert "Shadow package was executed" not in result.stderr + + +def test_rejects_preloaded_submodule_outside_authenticated_wheel( + tmp_path, candidate_manifest +): + source = tmp_path / "foreign_spm.py" + source.write_text("# Foreign module, never executed\n") + result = _probe( + tmp_path, + candidate_manifest, + "\n".join( + [ + "module = types.ModuleType('policyengine_us.spm')", + f"module.__file__ = {str(source)!r}", + "module.__spec__ = importlib.util.spec_from_file_location(" + "module.__name__, module.__file__)", + "sys.modules[module.__name__] = module", + ] + ), + ) + assert result.returncode != 0 + assert "Unexpected loaded import source for policyengine_us.spm" in result.stderr From d896f1d4e94a90c9c27bcba410743d6ce1ea8c07 Mon Sep 17 00:00:00 2001 From: Max Ghenis Date: Fri, 11 Sep 2026 12:30:24 -0400 Subject: [PATCH 06/46] Accept the repository-bundle TRACE emission context The repository generator has always emitted pe:emittedIn as "repository- bundle", but that value was missing from the packaged TRO schema's enum, so any payload it produced failed validation against the schema shipped alongside it. Adding the value closes that gap. The UK and US bundle sidecars are regenerated so their bundle-manifest artifact hash and composition fingerprint match the manifest now carrying the SPM measurement pin. Co-Authored-By: Claude Fable 5.1 --- changelog.d/repository-tro-schema.fixed.md | 1 + src/policyengine/data/bundle/uk.trace.tro.jsonld | 4 ++-- src/policyengine/data/bundle/us.trace.tro.jsonld | 4 ++-- src/policyengine/data/schemas/trace_tro.schema.json | 2 +- 4 files changed, 6 insertions(+), 5 deletions(-) create mode 100644 changelog.d/repository-tro-schema.fixed.md diff --git a/changelog.d/repository-tro-schema.fixed.md b/changelog.d/repository-tro-schema.fixed.md new file mode 100644 index 00000000..0b0354e6 --- /dev/null +++ b/changelog.d/repository-tro-schema.fixed.md @@ -0,0 +1 @@ +Accept the repository-bundle TRACE emission context and validate all generated country TROs against the packaged schema before writing bundle sidecars. diff --git a/src/policyengine/data/bundle/uk.trace.tro.jsonld b/src/policyengine/data/bundle/uk.trace.tro.jsonld index ecea2927..4e03f86f 100644 --- a/src/policyengine/data/bundle/uk.trace.tro.jsonld +++ b/src/policyengine/data/bundle/uk.trace.tro.jsonld @@ -75,7 +75,7 @@ "@type": "trov:ResearchArtifact", "schema:name": "policyengine.py bundle manifest for uk", "trov:mimeType": "application/json", - "trov:sha256": "75a9dab38c12dd00051f363532f2f45315557fecaf41d5827615bac4180de358" + "trov:sha256": "d430b8e561d76e5836a17bf506b06b9cd16560eb77f15718dd46f4e78513de85" }, { "@id": "composition/1/artifact/data_release_manifest", @@ -102,7 +102,7 @@ "trov:hasFingerprint": { "@id": "composition/1/fingerprint", "@type": "trov:CompositionFingerprint", - "trov:sha256": "0f664a8f0d9a4adaff2b7a04cac2dcb9d813770e395b6d77170a8078360e3733" + "trov:sha256": "ae802a52d5b8d5d5d388ab7b815c78c6d56bd82c3483de25eb93e4aa42b8f5fe" } }, "trov:hasPerformance": { diff --git a/src/policyengine/data/bundle/us.trace.tro.jsonld b/src/policyengine/data/bundle/us.trace.tro.jsonld index f579283a..735cca73 100644 --- a/src/policyengine/data/bundle/us.trace.tro.jsonld +++ b/src/policyengine/data/bundle/us.trace.tro.jsonld @@ -75,7 +75,7 @@ "@type": "trov:ResearchArtifact", "schema:name": "policyengine.py bundle manifest for us", "trov:mimeType": "application/json", - "trov:sha256": "75a9dab38c12dd00051f363532f2f45315557fecaf41d5827615bac4180de358" + "trov:sha256": "d430b8e561d76e5836a17bf506b06b9cd16560eb77f15718dd46f4e78513de85" }, { "@id": "composition/1/artifact/data_release_manifest", @@ -102,7 +102,7 @@ "trov:hasFingerprint": { "@id": "composition/1/fingerprint", "@type": "trov:CompositionFingerprint", - "trov:sha256": "1a3131f53ec77aa7eebf243f01bff2969848e8979dc40d23d94f28dff03aa5de" + "trov:sha256": "7c5f0d14060114a68d236c44cc0da748224ec02c4f714914f8b8b1f82aaf0c51" } }, "trov:hasPerformance": { diff --git a/src/policyengine/data/schemas/trace_tro.schema.json b/src/policyengine/data/schemas/trace_tro.schema.json index f7b902ca..6046caf6 100644 --- a/src/policyengine/data/schemas/trace_tro.schema.json +++ b/src/policyengine/data/schemas/trace_tro.schema.json @@ -174,7 +174,7 @@ }, "pe:emittedIn": { "type": "string", - "enum": ["local", "github-actions", "policyengine-api"] + "enum": ["local", "github-actions", "policyengine-api", "repository-bundle"] } } } From 98dfc0d727398221015809d7c9e878fe8da9ab04 Mon Sep 17 00:00:00 2001 From: Max Ghenis Date: Fri, 11 Sep 2026 12:30:24 -0400 Subject: [PATCH 07/46] Gate release publication on authenticated bundle provenance Adds three release gates and wires them into CI. release_lock.py checks that the reviewed uv.lock resolves entirely from PyPI, that every distribution has an HTTPS files.pythonhosted.org artifact URL and a valid SHA256, and that versioning changes nothing but the root package version, restoring the original lock on any other graph change; --refresh performs that single-version bump. check_release_credentials.py authenticates the release job's Hugging Face token through whoami-v2 and requires read-only scope, failing closed on missing metadata, write scope or an unrecognized permission schema, and logging no token or account detail. TRO generation gains a strict mode for public releases. It requires a full release manifest for every bundled country from a pinned revision, checks the certified dataset hash and the pinned model wheel hash and URL, and verifies that the fetched data-release manifest's bytes and URI match the hash and location already recorded in the reviewed sidecar, so versioning can change wrapper versions but never substitute remote bytes. It also ties the certification to the model the lock actually installs, and every serialized payload is validated against the packaged schema before any file is written. The workflows run the registry-lock check on trusted pull requests and the credential, published-measurement and strict-TRO checks in the release jobs, where the sidecars and wheels are produced immediately beforehand; bundle verification is skipped for forked pull requests, which cannot hold the private manifest credential. The CI Python matrices also move to the 3.11 through 3.14 range the project now requires, and the smoke-import job no longer needs its h5py workaround. Test fixtures vendor published US inputs copied byte for byte from the prior release, documented in a README; the private UK manifest stays out of the repository and is authenticated in CI. Co-Authored-By: Claude Fable 5.1 --- .github/workflows/pr_code_changes.yaml | 21 +- .github/workflows/push.yaml | 42 +- .../canonical-spm-release-validation.fixed.md | 1 + scripts/check_release_credentials.py | 51 +++ scripts/generate_trace_tros.py | 158 +++++++- scripts/release_lock.py | 198 ++++++++++ tests/fixtures/release_tros/README.md | 23 ++ .../release_tros/country-models.lock.toml | 36 ++ tests/fixtures/release_tros/manifest.json | 288 ++++++++++++++ .../release_tros/us-release_manifest.json | 364 ++++++++++++++++++ .../fixtures/release_tros/us.trace.tro.jsonld | 134 +++++++ tests/test_generate_trace_tros.py | 120 ++++++ tests/test_release_credentials.py | 88 +++++ tests/test_release_lock.py | 229 +++++++++++ tests/test_release_tro_generation.py | 320 +++++++++++++++ 15 files changed, 2056 insertions(+), 17 deletions(-) create mode 100644 changelog.d/canonical-spm-release-validation.fixed.md create mode 100644 scripts/check_release_credentials.py create mode 100644 scripts/release_lock.py create mode 100644 tests/fixtures/release_tros/README.md create mode 100644 tests/fixtures/release_tros/country-models.lock.toml create mode 100644 tests/fixtures/release_tros/manifest.json create mode 100644 tests/fixtures/release_tros/us-release_manifest.json create mode 100644 tests/fixtures/release_tros/us.trace.tro.jsonld create mode 100644 tests/test_generate_trace_tros.py create mode 100644 tests/test_release_credentials.py create mode 100644 tests/test_release_lock.py create mode 100644 tests/test_release_tro_generation.py diff --git a/.github/workflows/pr_code_changes.yaml b/.github/workflows/pr_code_changes.yaml index 78711dbf..d64d5424 100644 --- a/.github/workflows/pr_code_changes.yaml +++ b/.github/workflows/pr_code_changes.yaml @@ -10,6 +10,7 @@ on: - .github/** - changelog.d/** - pyproject.toml + - uv.lock - src/policyengine/data/bundle/manifest.json workflow_dispatch: @@ -57,7 +58,7 @@ jobs: strategy: fail-fast: false matrix: - python-version: ["3.9", "3.10", "3.11", "3.12", "3.13", "3.14"] + python-version: ["3.11", "3.12", "3.13", "3.14"] steps: - uses: actions/checkout@v6 - name: Set up Python ${{ matrix.python-version }} @@ -68,16 +69,13 @@ jobs: - name: Install uv uses: astral-sh/setup-uv@v8.1.0 - name: Install package (no country-model extras) - # `h5py` is used transitively by policyengine.core.scoping_strategy - # but is normally supplied via the [us]/[uk] extras (through - # policyengine-core). Install it directly so the smoke import can - # exercise the wrapper without the country models, which pin - # versions that don't support 3.9/3.10 yet. - run: uv pip install --system . h5py + run: uv pip install --system . - name: Smoke-import core modules run: python -c "import policyengine; from policyengine.core import Dataset, Policy, Simulation; from policyengine.outputs import aggregate, poverty, inequality; print('import OK')" BundleVerification: name: Verify bundle metadata + # Private manifest credentials are available only to trusted repository PRs. + if: github.event_name != 'pull_request' || github.event.pull_request.head.repo.full_name == github.repository runs-on: ubuntu-latest env: POLICYENGINE_SKIP_COUNTRY_IMPORTS: "1" @@ -113,6 +111,13 @@ jobs: PY - name: Check installed package consistency run: python -m pip check + # Only the reviewed registry lock is a pull-request concern. The + # read-only credential, published-measurement and TRACE sidecar gates + # are release gates: the release workflow regenerates the sidecars and + # publishes the wheels immediately before checking them, so they cannot + # be satisfied from a pull request. They run in push.yaml instead. + - name: Check the reviewed registry lock + run: python scripts/release_lock.py - name: Verify bundle packages run: policyengine bundle verify --country us --country uk --packages-only --json Test: @@ -120,7 +125,7 @@ jobs: strategy: fail-fast: false matrix: - python-version: ['3.13', '3.14'] + python-version: ['3.11', '3.12', '3.13', '3.14'] steps: - name: Checkout repo uses: actions/checkout@v6 diff --git a/.github/workflows/push.yaml b/.github/workflows/push.yaml index d7ba2b12..fa367e7b 100644 --- a/.github/workflows/push.yaml +++ b/.github/workflows/push.yaml @@ -43,7 +43,7 @@ jobs: strategy: fail-fast: false matrix: - python-version: ['3.13', '3.14'] + python-version: ['3.11', '3.12', '3.13', '3.14'] steps: - name: Checkout repo uses: actions/checkout@v6 @@ -114,22 +114,39 @@ jobs: fetch-depth: 0 - name: Fetch tags run: git fetch --tags --force + - name: Install uv + uses: astral-sh/setup-uv@v8.1.0 - name: Setup Python uses: actions/setup-python@v6 with: python-version: '3.13' + - name: Install package for TRO verification and regeneration + run: uv pip install -e . h5py --system + - name: Check reviewed release inputs before versioning + env: + HUGGING_FACE_TOKEN: ${{ secrets.HUGGING_FACE_TOKEN }} + run: | + if [[ -z "${HUGGING_FACE_TOKEN:-}" ]]; then + echo "::error::Full private release verification requires HUGGING_FACE_TOKEN" + exit 1 + fi + python scripts/check_release_credentials.py + python scripts/bundle.py check --published-spm --include-tros --strict-tros + python scripts/release_lock.py - name: Build changelog run: pip install yaml-changelog towncrier && make changelog - name: Generate derived bundle metadata run: python scripts/bundle.py generate + - name: Refresh only the release version in the registry lock + run: python scripts/release_lock.py --refresh - name: Preview changelog update run: ".github/get-changelog-diff.sh" - - name: Install package for TRO regeneration - run: pip install -e . h5py - name: Regenerate bundled TRACE TROs env: HUGGING_FACE_TOKEN: ${{ secrets.HUGGING_FACE_TOKEN }} - run: python scripts/bundle.py generate --include-tros + run: | + python scripts/bundle.py generate --include-tros --strict-tros + python scripts/bundle.py check --published-spm --include-tros --strict-tros - name: Update changelog, bundle metadata, and TROs uses: EndBug/add-and-commit@v9 with: @@ -160,6 +177,17 @@ jobs: - name: Capture published version id: version run: bash .github/capture-version.sh + - name: Validate complete bundle and registry lock before publication + env: + HUGGING_FACE_TOKEN: ${{ secrets.HUGGING_FACE_TOKEN }} + run: | + if [[ -z "${HUGGING_FACE_TOKEN:-}" ]]; then + echo "::error::Full private release verification requires HUGGING_FACE_TOKEN" + exit 1 + fi + python scripts/check_release_credentials.py + python scripts/bundle.py check --published-spm --include-tros --strict-tros + python scripts/release_lock.py - name: Publish a git tag run: ".github/publish-git-tag.sh" - name: Build package @@ -200,10 +228,16 @@ jobs: steps: - name: Checkout repo uses: actions/checkout@v6 + - name: Set up Python + uses: actions/setup-python@v6 + with: + python-version: '3.13' - name: Wait for release to be visible on PyPI env: VERSION: ${{ needs.Publish.outputs.version }} run: bash .github/wait-for-pypi.sh + - name: Validate published SPM bundle before consumer updates + run: python -S scripts/bundle.py check --published-spm - name: Generate GitHub App token id: app-token uses: actions/create-github-app-token@v1 diff --git a/changelog.d/canonical-spm-release-validation.fixed.md b/changelog.d/canonical-spm-release-validation.fixed.md new file mode 100644 index 00000000..0eb0cbb8 --- /dev/null +++ b/changelog.d/canonical-spm-release-validation.fixed.md @@ -0,0 +1 @@ +Require complete authenticated bundle provenance and a registry-only, version-consistent lockfile before automatic release publication. diff --git a/scripts/check_release_credentials.py b/scripts/check_release_credentials.py new file mode 100644 index 00000000..0f4546bb --- /dev/null +++ b/scripts/check_release_credentials.py @@ -0,0 +1,51 @@ +"""Verify the trusted release job's Hugging Face credential has read-only scope.""" + +from __future__ import annotations + +import os +import sys + +import requests + + +def check_read_only_token(token: str | None) -> None: + if not token or not token.strip(): + raise ValueError( + "Full private release verification requires HUGGING_FACE_TOKEN" + ) + try: + response = requests.get( + "https://huggingface.co/api/whoami-v2", + headers={"Authorization": f"Bearer {token}"}, + timeout=30, + allow_redirects=False, + ) + if response.status_code != 200: + raise ValueError + payload = response.json() + auth = payload.get("auth", {}) + if ( + auth.get("type") != "access_token" + or auth.get("accessToken", {}).get("role") != "read" + ): + raise ValueError + except (requests.RequestException, ValueError, TypeError, AttributeError): + # Never include the response, account, token, or request exception in CI + # output. Unknown and fine-grained permission schemas fail closed. + raise ValueError( + "Release verification requires an authenticated Hugging Face access token with role read" + ) from None + + +def main() -> int: + try: + check_read_only_token(os.environ.get("HUGGING_FACE_TOKEN")) + except ValueError as exc: + print(str(exc), file=sys.stderr) + return 1 + print("Verified read-only Hugging Face release credential") + return 0 + + +if __name__ == "__main__": + raise SystemExit(main()) diff --git a/scripts/generate_trace_tros.py b/scripts/generate_trace_tros.py index bc7d5d26..a9541b46 100644 --- a/scripts/generate_trace_tros.py +++ b/scripts/generate_trace_tros.py @@ -6,18 +6,30 @@ matches the pinned bundle. The richer data release manifest is included when available; otherwise the TRO still binds the certified dataset sha256 and URI pinned in the bundle manifest. + +Public release workflows use ``strict=True``: complete manifests must match +the hash and URI in the reviewed sidecars, and all builder inputs must already +be pinned. Preparing a newly certified bundle remains a separate local step. """ from __future__ import annotations import json import sys +from importlib.resources import files from pathlib import Path +from jsonschema import Draft202012Validator +from release_lock import PYPI, load_toml, validate_registry_artifacts + +from policyengine.provenance.certification import validate_release_manifest from policyengine.provenance.manifest import ( + CountryReleaseManifest, + DataReleaseManifest, DataReleaseManifestUnavailableError, get_data_release_manifest, get_release_manifest, + https_release_manifest_uri, ) from policyengine.provenance.trace import ( build_trace_tro_from_release_bundle, @@ -29,32 +41,166 @@ REPO_ROOT / "src" / "policyengine" / "data" / "bundle" / "manifest.json" ) BUNDLE_TRO_DIR = REPO_ROOT / "src" / "policyengine" / "data" / "bundle" +BUNDLE_LOCK = REPO_ROOT / "uv.lock" -def regenerate_all() -> tuple[list[Path], list[tuple[str, Path, str]]]: +def regenerate_all( + *, strict: bool = False +) -> tuple[list[Path], list[tuple[str, Path, str]]]: written: list[Path] = [] regressions: list[tuple[str, Path, str]] = [] - for tro_path, payload in generated_tros(): + payloads = generated_tros(strict=strict) + BUNDLE_TRO_DIR.mkdir(parents=True, exist_ok=True) + for tro_path, payload in payloads: tro_path.write_bytes(payload) written.append(tro_path) return written, regressions -def generated_tros() -> list[tuple[Path, bytes]]: +def _validate_reviewed_inputs( + country: CountryReleaseManifest, + data: DataReleaseManifest, + tro_path: Path, +) -> None: + """Authenticate full release inputs without downloading the dataset again.""" + prefix = f"{country.country_id}: strict TRACE release" + revision = country.data_package.release_manifest_revision + if not revision or revision.lower() in {"main", "master", "latest"}: + raise ValueError(f"{prefix} requires a pinned data release manifest revision") + if not country.model_package.sha256 or not country.model_package.wheel_url: + raise ValueError(f"{prefix} requires a pinned model wheel hash and URL") + if ( + country.certification is None + or country.certification.certified_for_model_version + != country.model_package.version + ): + raise ValueError(f"{prefix} requires certification for the pinned model") + validate_release_manifest( + data, country.model_package.name, country.model_package.version + ) + certified = country.certified_data_artifact + artifact = data.artifacts.get(certified.dataset) if certified else None + if ( + certified is None + or artifact is None + or not certified.sha256 + or artifact.sha256 != certified.sha256 + ): + raise ValueError( + f"{prefix} requires the certified dataset and matching hash in the full data release manifest" + ) + + # The committed TRO is the reviewed source pin. A changed data release must + # first be certified and its full sidecar prepared/reviewed before release; + # Versioning may change wrapper versions, but never substitute remote bytes. + artifact_id = "composition/1/artifact/data_release_manifest" + try: + tro = json.loads(tro_path.read_bytes())["@graph"][0] + pins = [ + item["trov:sha256"] + for item in tro["trov:hasComposition"]["trov:hasArtifact"] + if item["@id"] == artifact_id + ] + locations = [ + item["trov:hasLocation"] + for arrangement in tro["trov:hasArrangement"] + for item in arrangement["trov:hasArtifactLocation"] + if item["trov:hasArtifact"]["@id"] == artifact_id + ] + except (OSError, ValueError, KeyError, IndexError, TypeError) as exc: + raise ValueError( + f"{prefix} requires a reviewed full sidecar at {tro_path}" + ) from exc + if not data.source_sha256 or pins != [data.source_sha256]: + raise ValueError( + f"{prefix}: reviewed data release manifest hash does not match fetched bytes" + ) + if locations != [https_release_manifest_uri(country.data_package)]: + raise ValueError( + f"{prefix}: reviewed data release manifest URI does not match the bundle" + ) + + +def _validate_runtime_model(bundle: dict, country: CountryReleaseManifest) -> None: + """Connect the certification to the package and wheel installed by the lock.""" + key = f"policyengine-{country.country_id}" + component = bundle.get("packages", {}).get(key, {}) + model = country.model_package + prefix = f"{country.country_id}: strict TRACE release" + if ( + model.name != key + or component.get("name") != model.name + or component.get("version") != model.version + or any( + field in component and component[field] != getattr(model, field) + for field in ("sha256", "wheel_url") + ) + ): + raise ValueError( + f"{prefix}: runtime country model differs from the certified model" + ) + try: + locked = [ + package + for package in load_toml(BUNDLE_LOCK).get("package", []) + if package["name"] == model.name + ] + except (OSError, ValueError) as exc: + raise ValueError(f"{prefix} requires the reviewed registry lock") from exc + if ( + len(locked) != 1 + or locked[0]["version"] != model.version + or locked[0].get("source") != {"registry": PYPI} + ): + raise ValueError( + f"{prefix}: registry lock differs from the certified runtime model" + ) + validate_registry_artifacts(locked[0]) + if {(wheel["url"], wheel["hash"]) for wheel in locked[0].get("wheels", [])} != { + (model.wheel_url, f"sha256:{model.sha256}") + }: + raise ValueError( + f"{prefix}: locked runtime wheel differs from the certified model wheel" + ) + + +def generated_tros(*, strict: bool = False) -> list[tuple[Path, bytes]]: + """Prepare and schema-validate every serialized country TRO without writes.""" payloads: list[tuple[Path, bytes]] = [] bundle = json.loads(BUNDLE_MANIFEST.read_text()) - BUNDLE_TRO_DIR.mkdir(parents=True, exist_ok=True) + schema = json.loads( + files("policyengine") + .joinpath("data", "schemas", "trace_tro.schema.json") + .read_text() + ) + validator = Draft202012Validator(schema) + if strict: + expected = { + country + for country in ("us", "uk") + if f"policyengine-{country}" in bundle.get("packages", {}) + } + if not expected or expected - bundle.get("data_releases", {}).keys(): + raise ValueError( + "Strict TRACE release requires a data release manifest for every bundled country model" + ) for country_id in sorted(bundle.get("data_releases", {})): tro_path = BUNDLE_TRO_DIR / f"{country_id}.trace.tro.jsonld" country_manifest = get_release_manifest(country_id) + if strict: + _validate_runtime_model(bundle, country_manifest) try: data_release_manifest = get_data_release_manifest(country_id) except DataReleaseManifestUnavailableError as exc: + if strict: + raise data_release_manifest = None print( f"warning: {country_id}: {exc}; writing limited TRO", file=sys.stderr, ) + if strict: + _validate_reviewed_inputs(country_manifest, data_release_manifest, tro_path) tro = build_trace_tro_from_release_bundle( country_manifest, data_release_manifest, @@ -63,7 +209,9 @@ def generated_tros() -> list[tuple[Path, bytes]]: model_wheel_url=country_manifest.model_package.wheel_url, emission_context={"pe:emittedIn": "repository-bundle"}, ) - payloads.append((tro_path, serialize_trace_tro(tro))) + payload = serialize_trace_tro(tro) + validator.validate(json.loads(payload)) + payloads.append((tro_path, payload)) return payloads diff --git a/scripts/release_lock.py b/scripts/release_lock.py new file mode 100644 index 00000000..93fab4db --- /dev/null +++ b/scripts/release_lock.py @@ -0,0 +1,198 @@ +"""Check the release lock, or refresh only its post-bump root version. + +Resolve reviewed dependency changes with ordinary ``uv lock`` before the PR is +reviewed. Versioning must not introduce a new dependency graph after review. +""" + +from __future__ import annotations + +import argparse +import copy +import os +import re +import subprocess +import sys +import tomllib +from pathlib import Path +from urllib.parse import urlsplit + +REPO_ROOT = Path(__file__).resolve().parents[1] +PYPI = "https://pypi.org/simple" + + +def validate_registry_artifacts(package: dict) -> None: + """PyPI's source label alone does not constrain uv's download locations.""" + artifacts = [*package.get("wheels", [])] + if package.get("sdist"): + artifacts.append(package["sdist"]) + if not artifacts: + raise ValueError( + f"Release lock dependency {package['name']} has no registry artifacts" + ) + for artifact in artifacts: + url = urlsplit(artifact.get("url", "")) + if ( + url.scheme != "https" + or url.hostname != "files.pythonhosted.org" + or url.username is not None + or url.password is not None + or url.port not in (None, 443) + or not url.path.startswith("/packages/") + or url.query + or url.fragment + ): + raise ValueError( + f"Release lock dependency {package['name']} has a non-PyPI artifact URL" + ) + if re.fullmatch(r"sha256:[0-9a-f]{64}", artifact.get("hash", "")) is None: + raise ValueError( + f"Release lock dependency {package['name']} has an invalid artifact SHA256" + ) + + +def load_toml(path: Path) -> dict: + return tomllib.loads(path.read_text()) + + +def validate_registry_project(project: dict) -> None: + """Reject development sources in public package and resolver metadata.""" + uv = project.get("tool", {}).get("uv", {}) + if uv.get("sources") or uv.get("index") or uv.get("workspace"): + raise ValueError( + "Release metadata must use registry dependencies without uv source/index/workspace overrides" + ) + + def reject_urls(value): + if isinstance(value, str) and "@" in value: + raise ValueError( + "Release requirements must be registry dependencies, not direct URLs or local paths" + ) + if isinstance(value, list): + for item in value: + reject_urls(item) + if isinstance(value, dict): + for item in value.values(): + reject_urls(item) + + metadata = project.get("project", {}) + for value in ( + metadata.get("dependencies", []), + metadata.get("optional-dependencies", {}), + project.get("dependency-groups", {}), + project.get("build-system", {}).get("requires", []), + uv.get("constraint-dependencies", []), + uv.get("override-dependencies", []), + ): + reject_urls(value) + + +def _normalized_python_range(value: object) -> str: + """Compare the reviewed range by content, not by spelling. + + ``uv lock`` writes a canonically spaced specifier set, so an unchanged + range can differ from ``pyproject.toml`` by whitespace alone. Any real + change to a clause still fails the comparison. + """ + if not isinstance(value, str): + return "" + return ",".join(clause.strip() for clause in value.split(",")) + + +def validate_registry_lock( + project: dict, lock: dict, *, allow_previous_version: bool = False +) -> None: + """The checkout itself is the only permitted non-registry package.""" + metadata = project["project"] + if _normalized_python_range(lock.get("requires-python")) != ( + _normalized_python_range(metadata["requires-python"]) + ): + raise ValueError("Release lock Python range differs from pyproject.toml") + root_name = metadata["name"].lower().replace("_", "-") + roots = [] + for package in lock.get("package", []): + if package["name"] == root_name: + roots.append(package) + if package.get("source") != {"editable": "."}: + raise ValueError( + "Release lock root must be the current checkout; dependencies must use the registry" + ) + elif package.get("source") != {"registry": PYPI}: + raise ValueError( + f"Release lock dependency {package['name']} is not from the PyPI registry" + ) + else: + validate_registry_artifacts(package) + if len(roots) != 1: + raise ValueError("Release lock must contain exactly one root package") + if not allow_previous_version and roots[0]["version"] != metadata["version"]: + raise ValueError("Release lock root version differs from pyproject.toml") + + +def without_root_version(lock: dict, root_name: str) -> dict: + result = copy.deepcopy(lock) + for package in result["package"]: + if package["name"] == root_name: + package.pop("version") + return result + + +def resolver_environment() -> dict[str, str]: + # Do not let an inherited development index, find-links directory, project + # path, frozen mode, or resolver option change this publication boundary. + env = {key: value for key, value in os.environ.items() if not key.startswith("UV_")} + env["UV_FROZEN"] = "0" + return env + + +def check_release_lock(root: Path = REPO_ROOT, *, refresh: bool = False) -> None: + project = load_toml(root / "pyproject.toml") + validate_registry_project(project) + lock_path = root / "uv.lock" + before_bytes = lock_path.read_bytes() + before = tomllib.loads(before_bytes.decode()) + validate_registry_lock(project, before, allow_previous_version=refresh) + command = [ + "uv", + "lock", + "--no-config", + "--no-sources", + "--default-index", + PYPI, + "--python", + sys.executable, + ] + kwargs = {"cwd": root, "env": resolver_environment(), "check": True} + try: + if refresh: + subprocess.run(command, **kwargs) + after = load_toml(lock_path) + validate_registry_lock(project, after) + name = project["project"]["name"].lower().replace("_", "-") + if without_root_version(before, name) != without_root_version(after, name): + raise ValueError( + "Versioning changed the reviewed dependency graph; prepare and review the lock before release" + ) + subprocess.run([*command, "--check"], **kwargs) + except (ValueError, OSError, subprocess.CalledProcessError): + if refresh: + lock_path.write_bytes(before_bytes) + raise + + +def main() -> int: + parser = argparse.ArgumentParser(description=__doc__) + parser.add_argument( + "--refresh", + action="store_true", + help="Refresh only the root package version after the automatic bump", + ) + args = parser.parse_args() + try: + check_release_lock(refresh=args.refresh) + except (ValueError, OSError, subprocess.CalledProcessError) as exc: + parser.error(str(exc)) + return 0 + + +if __name__ == "__main__": + raise SystemExit(main()) diff --git a/tests/fixtures/release_tros/README.md b/tests/fixtures/release_tros/README.md new file mode 100644 index 00000000..f64ee4bb --- /dev/null +++ b/tests/fixtures/release_tros/README.md @@ -0,0 +1,23 @@ +# Historical public TRACE inputs + +These are existing published inputs used to exercise the real repository TRO +generator. They do not certify a new canonical SPM release. + +- `manifest.json` and `us.trace.tro.jsonld`: copied byte for byte from + PolicyEngine/policyengine.py commit + `3c3b4f6442f4a5adc47274734d71a6ca10103b43`, under + `src/policyengine/data/bundle/`. +- `us-release_manifest.json`: published Populace release + `populace-us-2024-buildp-sparse-rmloss100-cae8640-20260728T011454Z`, at + `https://huggingface.co/datasets/policyengine/populace-us/resolve/populace-us-2024-buildp-sparse-rmloss100-cae8640-20260728T011454Z/releases/populace-us-2024-buildp-sparse-rmloss100-cae8640-20260728T011454Z/release_manifest.json`. + Its raw SHA256 is + `dd949ba3c4c7a56aff8442c6db5a031d2c84c3da59f5d14832f468c358604506`, + matching the reviewed sidecar's data-release-manifest artifact. +- `country-models.lock.toml`: the complete, unchanged `policyengine-us` and + `policyengine-uk` package blocks extracted from `uv.lock` at the same wrapper + commit. These bind the certified model names, versions, wheel URLs and hashes + to the actual reviewed installation artifacts. This excerpt is used only by + the TRO tests; the separate lock tests resolve a complete project with uv. + +The UK private data manifest is not vendored here. Release CI authenticates it +from the pinned Hugging Face revision using the repository's existing secret. diff --git a/tests/fixtures/release_tros/country-models.lock.toml b/tests/fixtures/release_tros/country-models.lock.toml new file mode 100644 index 00000000..e3936a3c --- /dev/null +++ b/tests/fixtures/release_tros/country-models.lock.toml @@ -0,0 +1,36 @@ +[[package]] +name = "policyengine-uk" +version = "2.90.2" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "microdf-python" }, + { name = "policyengine-core" }, + { name = "pydantic" }, + { name = "tables", version = "3.9.2", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version < '3.10'" }, + { name = "tables", version = "3.10.1", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version == '3.10.*'" }, + { name = "tables", version = "3.11.1", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version >= '3.11'" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/40/e1/6652fd4ff8806d81b7c846ce2baeef4b9a8457ed380bb0853419d3cf36d9/policyengine_uk-2.90.2.tar.gz", hash = "sha256:c9baeaded1bb6d6f5ca5fcd962b80a198509513737c194aa867b0ef6d13d6a09", size = 1237478, upload-time = "2026-08-13T05:30:05.961Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/ec/da/658bff2df6094bd7bf2b0f561ff60fd41508adbd0a3af29b4553c83bc678/policyengine_uk-2.90.2-py3-none-any.whl", hash = "sha256:f100ef97e20b29b84138078f1dbc5b83f8857e6bc74b745fc6abef7ea1eeadf2", size = 2034933, upload-time = "2026-08-13T05:30:03.884Z" }, +] + +[[package]] +name = "policyengine-us" +version = "1.764.6" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "microdf-python" }, + { name = "pandas" }, + { name = "policyengine-core" }, + { name = "spm-calculator" }, + { name = "tables", version = "3.9.2", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version < '3.10'" }, + { name = "tables", version = "3.10.1", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version == '3.10.*'" }, + { name = "tables", version = "3.11.1", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version >= '3.11'" }, + { name = "tqdm" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/6e/cc/9e65a9586069beab1f02102a23f7a0e6332f1a9c2d590cb9f0e68f9ac987/policyengine_us-1.764.6.tar.gz", hash = "sha256:9817d48abc5b7d690b8abe33047005e5cf40b71654a85a2534bb5e3bcf921f79", size = 11009555, upload-time = "2026-07-06T13:13:07.254Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/e2/5a/0566666f30ad06e416f0cf25b24bdee73acd71897fb2de3c0ac43b59e3e3/policyengine_us-1.764.6-py3-none-any.whl", hash = "sha256:2b2c5c02c26ab212270948db1f30129728b625fc6109fc9ca0c604b714546909", size = 13117921, upload-time = "2026-07-06T13:13:03.476Z" }, +] + diff --git a/tests/fixtures/release_tros/manifest.json b/tests/fixtures/release_tros/manifest.json new file mode 100644 index 00000000..335d0b02 --- /dev/null +++ b/tests/fixtures/release_tros/manifest.json @@ -0,0 +1,288 @@ +{ + "bundle_version": "5.3.0", + "citation": { + "publisher": "PolicyEngine", + "title": "PolicyEngine bundle 5.3.0", + "type": "software-bundle", + "version": "5.3.0" + }, + "countries": { + "uk": { + "model_package": "policyengine-uk" + }, + "us": { + "model_package": "policyengine-us" + } + }, + "data_releases": { + "uk": { + "build_id": "policyengine-uk-data-1.56.16", + "bundle_id": "uk-5.3.0", + "certification": { + "built_with_model_git_sha": "12a1e028afeef08d8b2d74ee03fd9de3a78b2dd3", + "built_with_model_version": "2.89.2", + "certified_by": "policyengine.py bundle certification", + "certified_for_model_version": "2.90.2", + "compatibility_basis": "legacy_compatible_model_package", + "data_build_fingerprint": "sha256:6bfff835b504094c05342d2b4d770d2d94c8c1adcb15c4468931f53b21a89ea9", + "data_build_id": "policyengine-uk-data-1.56.16" + }, + "certified_data_artifact": { + "build_id": "policyengine-uk-data-1.56.16", + "data_package": { + "name": "policyengine-uk-data", + "version": "1.56.16" + }, + "dataset": "enhanced_frs_2024_25", + "sha256": "e433e532b17bd8ce76030156285816e33d44e93edabd2204adbef71d19a68712", + "uri": "hf://policyengine/policyengine-uk-data-private/enhanced_frs_2024_25.h5@1.56.16" + }, + "country_id": "uk", + "data_package": { + "name": "policyengine-uk-data", + "release_manifest_path": "releases/1.56.16/release_manifest.json", + "release_manifest_revision": "2966541fca275ab5ccb6cfb659b010b1db37ad49", + "repo_id": "policyengine/policyengine-uk-data-private", + "repo_type": "model", + "version": "1.56.16" + }, + "data_producer": "populace", + "datasets": { + "enhanced_frs_2024_25": { + "path": "enhanced_frs_2024_25.h5", + "repo_id": "policyengine/policyengine-uk-data-private", + "revision": "1.56.16", + "sha256": "e433e532b17bd8ce76030156285816e33d44e93edabd2204adbef71d19a68712" + }, + "enhanced_frs_2024_25_tiny": { + "path": "enhanced_frs_2024_25_tiny.h5", + "repo_id": "policyengine/policyengine-uk-data-private", + "revision": "1.56.16", + "sha256": "6cbbda7eef1b0b16d62e24a3099dc7959906287d659868d31aa7fc82af598871" + }, + "frs_2024_25": { + "path": "frs_2024_25.h5", + "repo_id": "policyengine/policyengine-uk-data-private", + "revision": "1.56.16", + "sha256": "d4d29901d7ea0c1f636060d4177a411a39c1977fa9fd23a61af3bb0f030769bd" + }, + "frs_2024_25_tiny": { + "path": "frs_2024_25_tiny.h5", + "repo_id": "policyengine/policyengine-uk-data-private", + "revision": "1.56.16", + "sha256": "74ca086be959b2942ecbf7755034d94dc6a817f4d9d54f2585a1c6df849d9588" + }, + "local_authority_weights": { + "path": "local_authority_weights.h5", + "repo_id": "policyengine/policyengine-uk-data-private", + "revision": "1.56.16", + "sha256": "6d634d69fb472b80cc17ffe696e028c54d7b739166bb7d9a57860f9d73537aa5" + }, + "local_geography_weights": { + "path": "local_geography_weights.csv.gz", + "repo_id": "policyengine/policyengine-uk-data-private", + "revision": "1.56.16", + "sha256": "6f623e277089d06c2c50ec3d8ab766635f695c8579d360410e3dcb4214ea9709" + }, + "parliamentary_constituency_weights": { + "path": "parliamentary_constituency_weights.h5", + "repo_id": "policyengine/policyengine-uk-data-private", + "revision": "1.56.16", + "sha256": "6870c36072a0c34d2e6ce3ebf58664b7932ca46a9e3808d387988773e63b66d5" + } + }, + "default_dataset": "enhanced_frs_2024_25", + "default_dataset_uri": "hf://policyengine/policyengine-uk-data-private/enhanced_frs_2024_25.h5@1.56.16", + "model_package": { + "name": "policyengine-uk", + "sha256": "f100ef97e20b29b84138078f1dbc5b83f8857e6bc74b745fc6abef7ea1eeadf2", + "version": "2.90.2", + "wheel_url": "https://files.pythonhosted.org/packages/ec/da/658bff2df6094bd7bf2b0f561ff60fd41508adbd0a3af29b4553c83bc678/policyengine_uk-2.90.2-py3-none-any.whl" + }, + "policyengine_version": "5.3.0", + "region_datasets": { + "national": { + "path_template": "enhanced_frs_2024_25.h5" + } + }, + "release_manifest_uri": "https://huggingface.co/policyengine/policyengine-uk-data-private/resolve/2966541fca275ab5ccb6cfb659b010b1db37ad49/releases/1.56.16/release_manifest.json", + "schema_version": 1, + "source_manifest_uri": "hf://model/policyengine/policyengine-uk-data-private@2966541fca275ab5ccb6cfb659b010b1db37ad49/releases/1.56.16/release_manifest.json", + "version": "policyengine-uk-data-1.56.16" + }, + "us": { + "build_id": "populace-us-2024-buildp-sparse-rmloss100-cae8640-20260728T011454Z", + "bundle_id": "us-5.3.0", + "certification": { + "built_with_model_version": "1.764.6", + "certified_by": "policyengine.py bundle certification", + "certified_for_model_version": "1.764.6", + "compatibility_basis": "built_with_model_package", + "data_build_id": "populace-us-2024-buildp-sparse-rmloss100-cae8640-20260728T011454Z" + }, + "certified_data_artifact": { + "build_id": "populace-us-2024-buildp-sparse-rmloss100-cae8640-20260728T011454Z", + "data_package": { + "name": "populace-data", + "version": "0.1.0" + }, + "dataset": "populace_us_2024", + "sha256": "48b9d479fb4fd1c3537f9383ce4697d130b6f618658409d74f6233c43b994c7e", + "uri": "hf://policyengine/populace-us/populace_us_2024.h5@populace-us-2024-buildp-sparse-rmloss100-cae8640-20260728T011454Z" + }, + "country_id": "us", + "data_package": { + "name": "populace-data", + "release_manifest_path": "releases/populace-us-2024-buildp-sparse-rmloss100-cae8640-20260728T011454Z/release_manifest.json", + "release_manifest_revision": "populace-us-2024-buildp-sparse-rmloss100-cae8640-20260728T011454Z", + "repo_id": "policyengine/populace-us", + "repo_type": "dataset", + "version": "0.1.0" + }, + "data_producer": "populace", + "datasets": { + "calibration_diagnostics": { + "path": "releases/populace-us-2024-buildp-sparse-rmloss100-cae8640-20260728T011454Z/calibration_diagnostics.json", + "repo_id": "policyengine/populace-us", + "revision": "populace-us-2024-buildp-sparse-rmloss100-cae8640-20260728T011454Z", + "sha256": "870449b44e86b13b25bcea1a57f0e7af37f4d4db18be815eea3acdf9fe6eb40e" + }, + "demographics": { + "path": "releases/populace-us-2024-buildp-sparse-rmloss100-cae8640-20260728T011454Z/demographics.json", + "repo_id": "policyengine/populace-us", + "revision": "populace-us-2024-buildp-sparse-rmloss100-cae8640-20260728T011454Z", + "sha256": "50c39dea345fa4e21f32272304b376979b9572ca17383dd13061b752d9a81112" + }, + "populace_us_2024": { + "path": "populace_us_2024.h5", + "repo_id": "policyengine/populace-us", + "revision": "populace-us-2024-buildp-sparse-rmloss100-cae8640-20260728T011454Z", + "sha256": "48b9d479fb4fd1c3537f9383ce4697d130b6f618658409d74f6233c43b994c7e" + }, + "populace_us_2024_calibration": { + "path": "populace_us_2024_calibration.npz", + "repo_id": "policyengine/populace-us", + "revision": "populace-us-2024-buildp-sparse-rmloss100-cae8640-20260728T011454Z", + "sha256": "b6e9c0678de5e79e8ddbd7a2a2f17c357bd6b368153cea7b5119c299a280a1b5" + }, + "us_source_coverage": { + "path": "releases/populace-us-2024-buildp-sparse-rmloss100-cae8640-20260728T011454Z/us_source_coverage.json", + "repo_id": "policyengine/populace-us", + "revision": "populace-us-2024-buildp-sparse-rmloss100-cae8640-20260728T011454Z", + "sha256": "6406c8686c292015a5bc7265a42402f89935f9395a8b63510efdd4d9562e7ff5" + }, + "us_ssi_take_up": { + "path": "releases/populace-us-2024-buildp-sparse-rmloss100-cae8640-20260728T011454Z/us_ssi_take_up.json", + "repo_id": "policyengine/populace-us", + "revision": "populace-us-2024-buildp-sparse-rmloss100-cae8640-20260728T011454Z", + "sha256": "ce631a37e2fe2ed22927fa232d41150d3b6cf328b38d3e5b38018fc4eec1b44d" + } + }, + "default_dataset": "populace_us_2024", + "default_dataset_uri": "hf://policyengine/populace-us/populace_us_2024.h5@populace-us-2024-buildp-sparse-rmloss100-cae8640-20260728T011454Z", + "model_package": { + "name": "policyengine-us", + "sha256": "2b2c5c02c26ab212270948db1f30129728b625fc6109fc9ca0c604b714546909", + "version": "1.764.6", + "wheel_url": "https://files.pythonhosted.org/packages/e2/5a/0566666f30ad06e416f0cf25b24bdee73acd71897fb2de3c0ac43b59e3e3/policyengine_us-1.764.6-py3-none-any.whl" + }, + "policyengine_version": "5.3.0", + "region_datasets": { + "national": { + "path_template": "populace_us_2024.h5" + } + }, + "release_manifest_uri": "https://huggingface.co/datasets/policyengine/populace-us/resolve/populace-us-2024-buildp-sparse-rmloss100-cae8640-20260728T011454Z/releases/populace-us-2024-buildp-sparse-rmloss100-cae8640-20260728T011454Z/release_manifest.json", + "schema_version": 1, + "source_manifest_uri": "hf://dataset/policyengine/populace-us@populace-us-2024-buildp-sparse-rmloss100-cae8640-20260728T011454Z/releases/populace-us-2024-buildp-sparse-rmloss100-cae8640-20260728T011454Z/release_manifest.json", + "version": "populace-us-2024-buildp-sparse-rmloss100-cae8640-20260728T011454Z" + } + }, + "dataset_overlays": { + "uk": { + "enhanced_frs_2023_24": { + "data_package_name": "policyengine-uk-data", + "path": "enhanced_frs_2023_24.h5", + "repo_id": "policyengine/policyengine-uk-data-private", + "repo_type": "model", + "revision": "655dd07e4bb9c777b00dac044949611f1feb824f", + "sha256": "584ae33d80ca0431254610a3f8254d132da73477d31966d6446282861ecae50d" + }, + "frs_2023_24": { + "data_package_name": "policyengine-uk-data", + "path": "frs_2023_24.h5", + "repo_id": "policyengine/policyengine-uk-data-private", + "repo_type": "model", + "revision": "655dd07e4bb9c777b00dac044949611f1feb824f", + "sha256": "df26d4d7af9d164aa2d064181b39290292d2f62bb26fee6126fc095fc06da292" + }, + "populace_uk_2023": { + "data_package_name": "populace-data", + "path": "populace_uk_2023.h5", + "repo_id": "policyengine/populace-uk-private", + "repo_type": "dataset", + "revision": "populace-uk-2023-dd68c73-4aa4b14-20260619T023711Z", + "sha256": "f17306ccb2aad7ff0130be3589b560afb2e2a12a943570911cd0c77f07934833" + } + }, + "us": { + "populace_us_2024_acs_local": { + "data_package_name": "populace-data", + "path": "populace_us_2024_acs_local.h5", + "repo_id": "policyengine/populace-us", + "repo_type": "dataset", + "revision": "populace-us-2024-buildo-acs-local-77e2061-20260724T110908Z", + "sha256": "71763290ded993789af0ad818fd833a6aabb9ca7ff7d14f78315340d8617c6f4" + } + } + }, + "extras": { + "models": [ + "policyengine-core", + "policyengine-us", + "policyengine-uk" + ], + "uk": [ + "policyengine-core", + "policyengine-uk" + ], + "us": [ + "policyengine-core", + "policyengine-us" + ] + }, + "packages": { + "policyengine": { + "import_name": "policyengine", + "install_requirement": "policyengine==5.3.0", + "name": "policyengine", + "role": "bundle_carrier", + "version": "5.3.0" + }, + "policyengine-core": { + "import_name": "policyengine_core", + "install_requirement": "policyengine-core==3.30.1", + "name": "policyengine-core", + "role": "runtime_dependency", + "version": "3.30.1" + }, + "policyengine-uk": { + "country": "uk", + "import_name": "policyengine_uk", + "install_requirement": "policyengine-uk==2.90.2", + "name": "policyengine-uk", + "role": "country_model", + "version": "2.90.2" + }, + "policyengine-us": { + "country": "us", + "import_name": "policyengine_us", + "install_requirement": "policyengine-us==1.764.6", + "name": "policyengine-us", + "role": "country_model", + "version": "1.764.6" + } + }, + "policyengine_version": "5.3.0", + "schema_version": 2 +} diff --git a/tests/fixtures/release_tros/us-release_manifest.json b/tests/fixtures/release_tros/us-release_manifest.json new file mode 100644 index 00000000..3a71d3b0 --- /dev/null +++ b/tests/fixtures/release_tros/us-release_manifest.json @@ -0,0 +1,364 @@ +{ + "schema_version": 1, + "data_package": { + "name": "populace-data", + "version": "0.1.0" + }, + "default_datasets": { + "national": "populace_us_2024" + }, + "build": { + "build_id": "populace-us-2024-buildp-sparse-rmloss100-cae8640-20260728T011454Z", + "built_at": "2026-07-28T03:56:03.415544+00:00", + "built_with_core_package": { + "name": "policyengine-core", + "version": "3.26.11" + }, + "built_with_model_package": { + "name": "policyengine-us", + "version": "1.764.6" + }, + "timing": { + "target_compilation_seconds": 1132.080194915994, + "calibration_seconds": 1072.4242046249856, + "elapsed_through_calibration_seconds": 2800.574625542009, + "total_build_seconds": 8161.056042167009 + }, + "ledger_artifact": { + "path_name": "consumer_facts_buildn_v9_4.jsonl", + "fact_row_count": 37405, + "facts_sha256": "b3c0835631a446eb96aa84d86f3ee962d15ca356174c7114db52974f1cacc080", + "schema_version": null, + "manifest_sha256": null + }, + "warm_start_calibration": { + "enabled": false + }, + "selection_source": { + "mode": "frozen_support", + "join_key": [ + "source_year", + "source_household_id", + "household_support_channel", + "household_support_clone_index" + ], + "source": { + "kind": "reselection", + "method": "buildm-keogh-carrier-protect-swap", + "issue": "PolicyEngine/populace#434", + "base_h5_sha256": "5d8a31622e6f4772188944385c5a6d4bd0e589f846043e6fd56634c9f6ef7d61", + "parent_selection_identities_sha256": "152baca3afeedfe93fbef49e95978b8f3804eec38c32eb8e5c3a1216a925340d", + "parent_selection": "buildi_rmloss100_selection_source (152baca3)", + "n_add": 9, + "swap_rule": "ADD the 9 keogh_distributions carrier households absent from the parent (13 carriers total in base-m; 4 already selected); REMOVE the 9 lowest-base-weight non-carrier asec-channel households (deterministic tiebreak on the join key). Channel composition preserved. Motivated by the keogh_distribution_neutralization reform-coverage smoke probe: the parent selection predates the keogh restoration and carried ~$0.1M unweighted of the $148.97M weighted source mass." + }, + "n_source": 57240, + "n_base_candidates": 337704, + "n_selected": 57240, + "n_unmapped": 0, + "n_ambiguous": 0 + }, + "default_dataset": { + "method": "dense_no_l0", + "sparse": false, + "n_candidate_households": 57240, + "n_exported_households": 57240, + "epochs": 6000, + "l2_lambda": 0.0, + "final_loss": 0.01768391632283638 + }, + "base_population_scale": { + "passed": true, + "details": { + "measure": "person_weight", + "population": 334200000.0, + "benchmark": 334200000.0, + "relative_error": 0.0, + "max_abs_relative_error": 0.25, + "calibration_mass_policy": "conserve", + "mass_repair": { + "method": "rescale_household_weights_to_census_person_population", + "applied": true, + "reason": "US fiscal refresh rescaled base household weights to the Census 2024 national person-population benchmark before mass='conserve' calibration.", + "initial_population": 62697558.345378846, + "benchmark": 334200000.0, + "factor": 5.330351114456635, + "initial_relative_error": -0.8123950977098179, + "repaired_population": 334200000.0, + "repaired_relative_error": 0.0, + "mass_change": { + "entity": "household", + "old_total": 22988559.15013668, + "new_total": 122537091.8856833, + "declared_factor": 5.330351114456635, + "reason": "US fiscal refresh rescaled base household weights to the Census 2024 national person-population benchmark before mass='conserve' calibration." + } + } + } + }, + "immigration_composition": { + "passed": true, + "details": { + "summary": { + "person_population": 334200000.0, + "ssn_card_type": { + "population": { + "CITIZEN": 308582144.34344304, + "NONE": 13090234.740726216, + "NON_CITIZEN_VALID_EAD": 4011891.027315071, + "OTHER_NON_CITIZEN": 8515729.8885157 + }, + "share": { + "CITIZEN": 0.9233457341216129, + "NONE": 0.03916886517272955, + "NON_CITIZEN_VALID_EAD": 0.012004461482091774, + "OTHER_NON_CITIZEN": 0.025480939223565827 + } + }, + "immigration_status_str": { + "population": { + "CITIZEN": 308582144.34344304, + "CUBAN_HAITIAN_ENTRANT": 518697.3384474666, + "DACA": 398079.4611381175, + "LEGAL_PERMANENT_RESIDENT": 11610844.116245188, + "UNDOCUMENTED": 13090234.740726216 + }, + "share": { + "CITIZEN": 0.9233457341216129, + "CUBAN_HAITIAN_ENTRANT": 0.0015520566680055853, + "DACA": 0.0011911414157334454, + "LEGAL_PERMANENT_RESIDENT": 0.03474220262191857, + "UNDOCUMENTED": 0.03916886517272955 + } + } + }, + "controls": { + "undocumented_workers": 8300000.0, + "undocumented_students": 408000.0, + "undocumented_population_anchor": 11000000.0, + "sources": { + "undocumented_workers": "https://www.pewresearch.org/short-reads/2024/07/22/what-we-know-about-unauthorized-immigrants-living-in-the-us/", + "undocumented_students": "https://www.higheredimmigrationportal.org/research/undocumented-students-in-higher-education-updated-march-2021/", + "undocumented_population_anchor": "https://www.pewresearch.org/short-reads/2024/07/22/what-we-know-about-unauthorized-immigrants-living-in-the-us/" + } + }, + "non_citizen_share_band": [ + 0.03, + 0.12 + ], + "undocumented_anchor_relative_band": [ + 0.5, + 1.6 + ] + } + }, + "ecps_parity": { + "passed": true, + "details": { + "reference_populated_layers": 158, + "gaps": 0, + "exempted": [ + "employer_sponsored_insurance_premiums", + "financial_assistance", + "is_unmarried_partner_of_household_head", + "is_wic_at_nutritional_risk", + "survivor_benefits", + "takes_up_dc_ptc", + "takes_up_early_head_start_if_eligible" + ], + "stale_exemptions": [], + "dormant_exemptions": [], + "reference": { + "repo_id": "policyengine/policyengine-us-data", + "repo_type": "model", + "filename": "enhanced_cps_2024.h5", + "revision": "21280dca5995e978d706740a8a4b9b7860cfd7b6", + "sha256": "0a6b961ad363a421bde99f2c8e5d8f20370bcba45fd303050537a25bdd805b14", + "vintage": "2024", + "period": "2024" + }, + "candidate_populated_layers": 172, + "known_gaps": { + "employer_sponsored_insurance_premiums": { + "reason": "SOURCE UNAVAILABILITY WITH EVIDENCE: archived commit 42ed5d45c56df80d754fbe24cce21cfeb8d05cbe datasets/cps/cps.py lines 197-271 and 1575-1581 requires NOW_OWNGRP, NOW_HIPAID, NOW_GRPFTYP, and PHIP_VAL; datasets/cps/census_cps.py lines 13-55 declares the three NOW_* fields optional. All three SHA-recorded 2022-2024 HDF inputs consumed by the hermetic --asec-h5 build contain PHIP_VAL but omit all three required NOW_* fields, so the retired owner/payment/plan-type derivation cannot execute. has_esi and NOW_GRP are not semantically valid substitutes.", + "issue": "PolicyEngine/populace#32" + }, + "financial_assistance": { + "reason": "SOURCE UNAVAILABILITY WITH EVIDENCE: archived commit 42ed5d45c56df80d754fbe24cce21cfeb8d05cbe datasets/cps/cps.py lines 1493-1496 derives person-level financial_assistance directly from FIN_VAL; datasets/cps/census_cps.py lines 39-58 and 306-359 make FIN_VAL a required person source. All three SHA-recorded 2022-2024 ASEC H5 inputs consumed by the hermetic --asec-h5 build omit FIN_VAL, FIN_YN, and I_FINVAL from their person tables. HFINVAL and FFINVAL are household/family aggregates and cannot recover the person recipient without synthetic allocation. The retired PUF-clone QRF at datasets/cps/extended_cps.py lines 140-194 and 639-745 trains financial_assistance from CPS person observations, so PUF provides no independent replacement source.", + "issue": "PolicyEngine/populace#38" + }, + "is_unmarried_partner_of_household_head": { + "reason": "SOURCE UNAVAILABILITY WITH EVIDENCE: archived commit 42ed5d45c56df80d754fbe24cce21cfeb8d05cbe datasets/cps/cps.py lines 1214-1221 derives is_unmarried_partner_of_household_head exclusively from person PERRP; datasets/cps/census_cps.py lines 306-318 declares that person source. All three SHA-recorded 2022-2024 ASEC HDF inputs consumed by the hermetic build omit PERRP. The 2022 and 2023 inputs also omit PECOHAB, A_EXPRRP, and A_FAMREL; only 2024 carries those alternatives, and its A_EXPRRP code 13 conflates partner with roommate. Populace's older-year relationship fallback uses only line, spouse, sex, and parent pointers and never identifies a partner. Using the 2024 pointer for one-third of the pool while treating the other two-thirds as false, or assigning partner status from the combined partner/roommate code, would synthesize statuses the locked sources do not report.", + "issue": "PolicyEngine/populace#38" + }, + "is_wic_at_nutritional_risk": { + "reason": "SOURCE UNAVAILABILITY WITH EVIDENCE: archived commit 42ed5d45c56df80d754fbe24cce21cfeb8d05cbe datasets/cps/cps.py lines 684-701 does not map a measured nutritional-risk assessment; it loads category priors, makes a seeded Bernoulli draw, and ORs that synthetic draw with reported WIC receipt. parameters/take_up/wic_nutritional_risk.yaml lines 1-13 gives every fixed category prior a 1980 effective key (the infant factor was historically adopted in 1991), while parameters/__init__.py lines 38-50 merely carries the latest value forward. datasets/cps/census_cps.py lines 293-365 loads only WIC receipt/value fields, and cps.py line 1558 maps WICYN solely to receives_wic. All three SHA-recorded 2022-2024 ASEC H5 inputs consumed by the hermetic build contain only SPM_WICVAL/WICYN on person and HRNUMWIC/HRWICYN on household; none contains an individual nutritional-risk assessment. The Census questionnaire gives WICYN an adult-female universe and allows receipt reporting for the respondent or on behalf of a child, so a yes does not identify the assessed person and code 0 is not-in-universe rather than a negative assessment. USDA requires a competent professional to assess and document risk in separate WIC certification records; FNS adopted an all-category 1.0 risk adjustment while producing its CY2020 estimates and applied it to the revised CY2016-2021 series. Assigning risk to a WICYN row, assigning False to nonrecipients, or recreating the retired random False outcomes would synthesize person-level assessments absent from the locked sources, while assigning True to all unknowns is the PolicyEngine-US default and carries no non-default signal.", + "issue": "PolicyEngine/populace#312" + }, + "survivor_benefits": { + "reason": "SOURCE UNAVAILABILITY WITH EVIDENCE: archived commit 42ed5d45c56df80d754fbe24cce21cfeb8d05cbe datasets/cps/cps.py line 1495 derives person-level survivor_benefits exclusively from SRVS_VAL; datasets/cps/census_cps.py lines 39-58 and 306-359 make SRVS_VAL a required person source. All three SHA-recorded 2022-2024 ASEC H5 inputs consumed by the hermetic --asec-h5 build omit SRVS_VAL from their person tables. Their FSURVAL/HSURVAL fields are only family/household aggregates: each household value exactly equals summed family values, and 657/647/610 positive families have multiple people, so the recipient cannot be recovered. The retired PUF-clone QRF at datasets/cps/extended_cps.py lines 140-194 and 639-745 trains survivor_benefits from CPS person observations, so PUF provides no independent replacement source. Restoring a nonzero person-year amount without SRVS_VAL would synthesize data.", + "issue": "PolicyEngine/populace#38" + }, + "takes_up_dc_ptc": { + "reason": "SOURCE UNAVAILABILITY WITH EVIDENCE: archived commit 42ed5d45c56df80d754fbe24cce21cfeb8d05cbe datasets/cps/cps.py lines 565-599 does not carry an observed DC property-tax-credit claim; it loads a scalar rate and assigns takes_up_dc_ptc by an independent seeded Bernoulli draw. parameters/take_up/dc_ptc.yaml lines 1-11 divides an aggregate 37,133-claim count by a 131,791,388 PolicyEngine value estimate, which is neither dimensionally a participation denominator nor mathematically 0.32; utils/randomness.py lines 5-28 confirms the output is generated from a variable-name-seeded random stream. Archived census_cps.py lines 251-262 exposes only generic state-tax liability amounts, not a Schedule H claim; archived puf.py lines 704-719 maps other_credits from federal P08000, and the locked PUF has no state geography. All three SHA-recorded 2022-2024 ASEC H5 inputs and the SHA-recorded processed PUF 1.8.0 input consumed by the hermetic build omit takes_up_dc_ptc and any DC Schedule H claim indicator. The aggregate claim count cannot identify claiming tax units, so replaying the retired draw or selecting modeled-eligible units would synthesize claimant identities absent from every locked source.", + "issue": "PolicyEngine/populace#312" + }, + "takes_up_early_head_start_if_eligible": { + "reason": "SOURCE UNAVAILABILITY WITH EVIDENCE: archived commit 42ed5d45c56df80d754fbe24cce21cfeb8d05cbe datasets/cps/cps.py lines 582-583 and 640-648 does not carry an observed Early Head Start enrollment indicator; it loads a scalar rate and assigns takes_up_early_head_start_if_eligible by an unanchored person-level draw. parameters/take_up/early_head_start.yaml lines 1-9 supplies 0.09 from a NIEER research chart, not an ACF administrative participation fact, and utils/randomness.py lines 5-28 confirms variable-name-seeded randomness. PolicyEngine-US 1.764.6 applies the input to people under age 3 or pregnant. All three SHA-recorded 2022-2024 ASEC H5 inputs and the processed PUF consumed by the hermetic build omit both Head Start take-up flags and any individual Head Start/Early Head Start enrollment indicator. The independently SHA-pinned full 2023 SIPP observes standard Head Start only through a nursery/preschool education item with no reported positives under age 3; its child-care fields cover only ages 2-7, contain no Early Head Start distinction or pregnancy branch, and have a Census-documented universe inconsistency. ACF's exact EHS cumulative-enrollment total includes every participant served during the year and turnover replacements, while its funded enrollment is capacity; neither identifies point-in-time person recipients. Assigning either aggregate to modeled-eligible infants, toddlers, or pregnant people, mapping ambiguous age-2 SIPP Head Start responses to EHS, or replaying the retired 0.09 draw would synthesize the missing person values.", + "issue": "PolicyEngine/populace#312" + } + } + } + }, + "hours_worked_signal": { + "passed": true, + "details": { + "worked_share": 0.5296885704999547, + "mean_weekly_hours_workers": 37.56500077416023, + "worked_share_band": [ + 0.35, + 0.62 + ], + "mean_weekly_hours_band": [ + 30.0, + 45.0 + ], + "unique_counts": { + "weekly_hours_worked_before_lsr": 94, + "hours_worked_last_week": 99, + "weeks_worked": 53 + } + } + }, + "snap_take_up_signal": { + "passed": true, + "details": { + "take_up_share": 0.81845299430267, + "take_up_share_band": [ + 0.7, + 0.95 + ], + "unique_count": 2, + "reported_share": 0.09658006751385996, + "reporters_not_taking_up": 0 + } + }, + "eligibility_inputs_signal": { + "passed": true, + "details": { + "disabled_share": 0.09470379050475294, + "full_time_college_student_share": 0.014372059621570291, + "parent_share": 0.30573658099536954, + "veterans_benefits_share": 0.014050680524622211, + "disabled_share_band": [ + 0.05, + 0.2 + ], + "full_time_student_share_band": [ + 0.01, + 0.08 + ], + "parent_share_band": [ + 0.12, + 0.4 + ], + "veterans_benefits_share_band": [ + 0.002, + 0.04 + ], + "unique_counts": { + "is_disabled": 2, + "is_blind": 2, + "is_full_time_college_student": 2, + "own_children_in_household": 14, + "veterans_benefits": 568 + } + } + }, + "pregnancy_signal": { + "passed": true, + "details": { + "pregnant_share": 0.008977563210398776, + "pregnant_share_band": [ + 0.002, + 0.02 + ], + "unique_count": 2 + } + }, + "snap_discretionary_exemption_signal": { + "passed": true, + "details": { + "exempt_share": 0.046463120829380554, + "exempt_share_band": [ + 0.015, + 0.09 + ], + "unique_count": 2 + } + }, + "documented_absent_inputs": { + "is_homeless": "No ASEC source: the CPS samples the housed population, so the pre-HR1 SNAP ABAWD homeless exemption cannot fire (PolicyEngine/populace#351).", + "was_in_foster_care": "No ASEC item for foster-care history, so the pre-HR1 former-foster-youth ABAWD exemption (7 CFR 273.24(c)(9)) cannot fire (PolicyEngine/populace#351).", + "is_snap_work_program_participant": "No ASEC item measures SNAP E&T or qualifying work-program participation, so compliance via program participation never fires; USDA reports E&T reaches a small minority of participants, so the always-False default understates compliance only modestly (PolicyEngine/populace#249).", + "weekly_snap_work_program_hours": "No ASEC item measures qualifying work-program hours, so combined work-plus-program hours toward the 20-hour ABAWD test omit program hours (PolicyEngine/populace#249).", + "is_snap_workfare_participant": "No ASEC item measures workfare participation under 7 CFR 273.7(m), which satisfies the ABAWD requirement regardless of hours; defaults False (PolicyEngine/populace#249)." + } + }, + "compatible_core_packages": [ + { + "name": "policyengine-core", + "specifier": "==3.26.11" + } + ], + "compatible_model_packages": [ + { + "name": "policyengine-us", + "specifier": "==1.764.6" + } + ], + "artifacts": { + "populace_us_2024": { + "kind": "microdata", + "path": "populace_us_2024.h5", + "repo_id": "policyengine/populace-us", + "revision": "populace-us-2024-buildp-sparse-rmloss100-cae8640-20260728T011454Z", + "sha256": "48b9d479fb4fd1c3537f9383ce4697d130b6f618658409d74f6233c43b994c7e" + }, + "populace_us_2024_calibration": { + "kind": "calibration", + "path": "populace_us_2024_calibration.npz", + "repo_id": "policyengine/populace-us", + "revision": "populace-us-2024-buildp-sparse-rmloss100-cae8640-20260728T011454Z", + "sha256": "b6e9c0678de5e79e8ddbd7a2a2f17c357bd6b368153cea7b5119c299a280a1b5" + }, + "calibration_diagnostics": { + "kind": "diagnostics", + "path": "calibration_diagnostics.json", + "repo_id": "policyengine/populace-us", + "revision": "populace-us-2024-buildp-sparse-rmloss100-cae8640-20260728T011454Z", + "sha256": "870449b44e86b13b25bcea1a57f0e7af37f4d4db18be815eea3acdf9fe6eb40e" + }, + "us_source_coverage": { + "kind": "diagnostics", + "path": "us_source_coverage.json", + "repo_id": "policyengine/populace-us", + "revision": "populace-us-2024-buildp-sparse-rmloss100-cae8640-20260728T011454Z", + "sha256": "6406c8686c292015a5bc7265a42402f89935f9395a8b63510efdd4d9562e7ff5" + }, + "us_ssi_take_up": { + "kind": "diagnostics", + "path": "us_ssi_take_up.json", + "repo_id": "policyengine/populace-us", + "revision": "populace-us-2024-buildp-sparse-rmloss100-cae8640-20260728T011454Z", + "sha256": "ce631a37e2fe2ed22927fa232d41150d3b6cf328b38d3e5b38018fc4eec1b44d" + }, + "demographics": { + "kind": "diagnostics", + "path": "demographics.json", + "repo_id": "policyengine/populace-us", + "revision": "populace-us-2024-buildp-sparse-rmloss100-cae8640-20260728T011454Z", + "sha256": "50c39dea345fa4e21f32272304b376979b9572ca17383dd13061b752d9a81112" + } + } +} \ No newline at end of file diff --git a/tests/fixtures/release_tros/us.trace.tro.jsonld b/tests/fixtures/release_tros/us.trace.tro.jsonld new file mode 100644 index 00000000..1d21864e --- /dev/null +++ b/tests/fixtures/release_tros/us.trace.tro.jsonld @@ -0,0 +1,134 @@ +{ + "@context": [ + { + "pe": "https://policyengine.org/trace/0.1#", + "rdf": "http://www.w3.org/1999/02/22-rdf-syntax-ns#", + "rdfs": "http://www.w3.org/2000/01/rdf-schema#", + "schema": "https://schema.org/", + "trov": "https://w3id.org/trace/trov/0.1#" + } + ], + "@graph": [ + { + "@id": "tro", + "@type": "trov:TransparentResearchObject", + "schema:creator": { + "@type": "schema:Organization", + "schema:name": "PolicyEngine", + "schema:url": "https://policyengine.org" + }, + "schema:dateCreated": "2026-07-28T03:56:03.415544+00:00", + "schema:description": "TRACE TRO for certified runtime bundle us-5.3.0 covering the bundle manifest, the certified dataset artifact, the country model wheel, and the country data release manifest when it is available.", + "schema:name": "policyengine us certified bundle TRO", + "trov:createdWith": { + "@type": "schema:SoftwareApplication", + "schema:name": "policyengine", + "schema:softwareVersion": "5.3.0" + }, + "trov:hasArrangement": [ + { + "@id": "arrangement/1", + "@type": "trov:ArtifactArrangement", + "rdfs:comment": "Certified arrangement for bundle us-5.3.0.", + "trov:hasArtifactLocation": [ + { + "@id": "arrangement/1/location/bundle_manifest", + "@type": "trov:ArtifactLocation", + "trov:hasArtifact": { + "@id": "composition/1/artifact/bundle_manifest" + }, + "trov:hasLocation": "data/bundle/manifest.json" + }, + { + "@id": "arrangement/1/location/data_release_manifest", + "@type": "trov:ArtifactLocation", + "trov:hasArtifact": { + "@id": "composition/1/artifact/data_release_manifest" + }, + "trov:hasLocation": "https://huggingface.co/datasets/policyengine/populace-us/resolve/populace-us-2024-buildp-sparse-rmloss100-cae8640-20260728T011454Z/releases/populace-us-2024-buildp-sparse-rmloss100-cae8640-20260728T011454Z/release_manifest.json" + }, + { + "@id": "arrangement/1/location/dataset", + "@type": "trov:ArtifactLocation", + "trov:hasArtifact": { + "@id": "composition/1/artifact/dataset" + }, + "trov:hasLocation": "https://huggingface.co/datasets/policyengine/populace-us/resolve/populace-us-2024-buildp-sparse-rmloss100-cae8640-20260728T011454Z/populace_us_2024.h5" + }, + { + "@id": "arrangement/1/location/model_wheel", + "@type": "trov:ArtifactLocation", + "trov:hasArtifact": { + "@id": "composition/1/artifact/model_wheel" + }, + "trov:hasLocation": "https://files.pythonhosted.org/packages/e2/5a/0566666f30ad06e416f0cf25b24bdee73acd71897fb2de3c0ac43b59e3e3/policyengine_us-1.764.6-py3-none-any.whl" + } + ] + } + ], + "trov:hasComposition": { + "@id": "composition/1", + "@type": "trov:ArtifactComposition", + "trov:hasArtifact": [ + { + "@id": "composition/1/artifact/bundle_manifest", + "@type": "trov:ResearchArtifact", + "schema:name": "policyengine.py bundle manifest for us", + "trov:mimeType": "application/json", + "trov:sha256": "57c6fe9c5c370ea2b9460120656004b1a583db06cabb6c774551effc3c125695" + }, + { + "@id": "composition/1/artifact/data_release_manifest", + "@type": "trov:ResearchArtifact", + "schema:name": "populace-data release manifest 0.1.0", + "trov:mimeType": "application/json", + "trov:sha256": "dd949ba3c4c7a56aff8442c6db5a031d2c84c3da59f5d14832f468c358604506" + }, + { + "@id": "composition/1/artifact/dataset", + "@type": "trov:ResearchArtifact", + "schema:name": "populace_us_2024", + "trov:mimeType": "application/x-hdf5", + "trov:sha256": "48b9d479fb4fd1c3537f9383ce4697d130b6f618658409d74f6233c43b994c7e" + }, + { + "@id": "composition/1/artifact/model_wheel", + "@type": "trov:ResearchArtifact", + "schema:name": "policyengine-us==1.764.6 wheel", + "trov:mimeType": "application/zip", + "trov:sha256": "2b2c5c02c26ab212270948db1f30129728b625fc6109fc9ca0c604b714546909" + } + ], + "trov:hasFingerprint": { + "@id": "composition/1/fingerprint", + "@type": "trov:CompositionFingerprint", + "trov:sha256": "c9806c4dbc8cb664ca2ed431af72f8540c08171fea5ce02d6e579f2d6866cf21" + } + }, + "trov:hasPerformance": { + "@id": "trp/1", + "@type": "trov:TransparentResearchPerformance", + "pe:builtWithModelVersion": "1.764.6", + "pe:certifiedBy": "policyengine.py bundle certification", + "pe:certifiedForModelVersion": "1.764.6", + "pe:compatibilityBasis": "built_with_model_package", + "pe:dataBuildId": "populace-us-2024-buildp-sparse-rmloss100-cae8640-20260728T011454Z", + "pe:emittedIn": "repository-bundle", + "rdfs:comment": "Certification of build populace-us-2024-buildp-sparse-rmloss100-cae8640-20260728T011454Z for policyengine-us 1.764.6.", + "trov:accessedArrangement": { + "@id": "arrangement/1" + }, + "trov:startedAtTime": "2026-07-28T03:56:03.415544+00:00", + "trov:wasConductedBy": { + "@id": "trs" + } + }, + "trov:wasAssembledBy": { + "@id": "trs", + "@type": "trov:TransparentResearchSystem", + "rdfs:comment": "PolicyEngine certification workflow that pins a country model version, a country data release, and a specific dataset artifact.", + "schema:name": "PolicyEngine release pipeline" + } + } + ] +} diff --git a/tests/test_generate_trace_tros.py b/tests/test_generate_trace_tros.py new file mode 100644 index 00000000..012b0e3d --- /dev/null +++ b/tests/test_generate_trace_tros.py @@ -0,0 +1,120 @@ +"""Repository TRO generation must validate complete payloads before any writes.""" + +import json +import sys +from importlib.resources import files +from pathlib import Path + +import pytest +from jsonschema import Draft202012Validator, ValidationError + +from policyengine.provenance.manifest import DataReleaseManifestUnavailableError + +sys.path.insert(0, str(Path(__file__).resolve().parents[1] / "scripts")) +import generate_trace_tros as generator # noqa: E402 + + +@pytest.fixture +def schema(): + return json.loads( + files("policyengine") + .joinpath("data", "schemas", "trace_tro.schema.json") + .read_text() + ) + + +@pytest.fixture +def offline_generator(monkeypatch): + """Use actual bundled US/UK pins, exercising the supported offline fallback.""" + + def unavailable(country): + raise DataReleaseManifestUnavailableError("No network in this test") + + monkeypatch.setattr(generator, "get_data_release_manifest", unavailable) + return generator + + +def test_real_generated_country_payloads_validate(offline_generator, schema): + payloads = offline_generator.generated_tros() + assert {path.name for path, _ in payloads} == { + "us.trace.tro.jsonld", + "uk.trace.tro.jsonld", + } + for _, payload in payloads: + tro = json.loads(payload) + assert ( + tro["@graph"][0]["trov:hasPerformance"]["pe:emittedIn"] + == "repository-bundle" + ) + Draft202012Validator(schema).validate(tro) + + +@pytest.mark.parametrize("country", ["us", "uk"]) +def test_packaged_repository_tros_validate(country, schema): + payload = json.loads( + files("policyengine") + .joinpath("data", "bundle", f"{country}.trace.tro.jsonld") + .read_text() + ) + Draft202012Validator(schema).validate(payload) + + +def test_unknown_emission_still_rejected(offline_generator, schema): + payload = json.loads(offline_generator.generated_tros()[0][1]) + payload["@graph"][0]["trov:hasPerformance"]["pe:emittedIn"] = "unknown-emission" + with pytest.raises(ValidationError, match="unknown-emission"): + Draft202012Validator(schema).validate(payload) + + +@pytest.mark.parametrize("existing", [False, True]) +@pytest.mark.parametrize("defect", ["emission", "composition"]) +def test_last_serialized_country_failure_preserves_all_outputs( + offline_generator, monkeypatch, tmp_path, existing, defect +): + output = tmp_path / "bundle" + before = {} + if existing: + output.mkdir() + for country in ["uk", "us"]: + path = output / f"{country}.trace.tro.jsonld" + path.write_bytes(f"preserve original {country}".encode()) + before[path.name] = path.read_bytes() + monkeypatch.setattr(offline_generator, "BUNDLE_TRO_DIR", output) + serialize = offline_generator.serialize_trace_tro + serialized = [] + + def corrupt_last(tro): + # Corrupt the serialized US output after the valid UK payload is prepared. + payload = json.loads(serialize(tro)) + serialized.append(payload) + if len(serialized) == 2: + node = payload["@graph"][0] + if defect == "emission": + node["trov:hasPerformance"]["pe:emittedIn"] = "unknown-emission" + else: + del node["trov:hasComposition"] + return json.dumps(payload).encode() + + monkeypatch.setattr(offline_generator, "serialize_trace_tro", corrupt_last) + with pytest.raises(ValidationError): + offline_generator.regenerate_all() + assert len(serialized) == 2 + if existing: + assert {p.name: p.read_bytes() for p in output.iterdir()} == before + else: + assert not output.exists() + + +def test_dry_generation_has_no_writes_then_regeneration_writes_all( + offline_generator, monkeypatch, tmp_path, schema +): + output = tmp_path / "new-bundle" + monkeypatch.setattr(offline_generator, "BUNDLE_TRO_DIR", output) + expected = dict(offline_generator.generated_tros()) + assert not output.exists() + written, regressions = offline_generator.regenerate_all() + assert set(written) == set(expected) + assert regressions == [] + for path in written: + assert path.read_bytes() == expected[path] + Draft202012Validator(schema).validate(json.loads(path.read_bytes())) diff --git a/tests/test_release_credentials.py b/tests/test_release_credentials.py new file mode 100644 index 00000000..c0b40934 --- /dev/null +++ b/tests/test_release_credentials.py @@ -0,0 +1,88 @@ +"""Credential verification fails closed without exposing account or token data.""" + +import importlib.util +from pathlib import Path + +import pytest +import requests + +ROOT = Path(__file__).resolve().parents[1] +spec = importlib.util.spec_from_file_location( + "release_credentials", ROOT / "scripts/check_release_credentials.py" +) +credentials = importlib.util.module_from_spec(spec) +spec.loader.exec_module(credentials) + + +@pytest.mark.parametrize("role", ["read", "write", "fineGrained", "admin", "", None]) +def test_requires_authenticated_read_role(monkeypatch, capsys, role): + token = "fixture-secret-must-not-appear" + monkeypatch.setenv("HUGGING_FACE_TOKEN", token) + response = requests.Response() + response.status_code = 200 + payload = { + "name": "fixture-private-account", + "auth": {"type": "access_token", "accessToken": {"role": role}}, + } + + def whoami(url, **kwargs): + assert url == "https://huggingface.co/api/whoami-v2" + assert kwargs["headers"] == {"Authorization": f"Bearer {token}"} + assert kwargs["allow_redirects"] is False + assert kwargs["timeout"] == 30 + response.json = lambda: payload + return response + + monkeypatch.setattr(credentials.requests, "get", whoami) + assert credentials.main() == (0 if role == "read" else 1) + output = capsys.readouterr() + assert token not in output.out + output.err + assert payload["name"] not in output.out + output.err + + +@pytest.mark.parametrize( + "payload", + [ + {}, + {"auth": None}, + {"auth": {"type": "oauth", "accessToken": {"role": "read"}}}, + {"auth": {"type": "access_token"}}, + [], + ], +) +def test_missing_or_unknown_auth_metadata_is_rejected(monkeypatch, payload): + response = requests.Response() + response.status_code = 200 + response.json = lambda: payload + monkeypatch.setattr(credentials.requests, "get", lambda *a, **kw: response) + with pytest.raises(ValueError, match="role read"): + credentials.check_read_only_token("fixture-token") + + +@pytest.mark.parametrize("failure", ["timeout", "invalid-json", 301, 401, 403, 503]) +def test_fetch_failures_never_log_sensitive_response(monkeypatch, capsys, failure): + secret = "fixture-sensitive-detail" + monkeypatch.setenv("HUGGING_FACE_TOKEN", secret) + + def fetch(*args, **kwargs): + if failure == "timeout": + raise requests.Timeout(secret) + response = requests.Response() + response.status_code = failure if isinstance(failure, int) else 200 + response._content = secret.encode() + return response + + monkeypatch.setattr(credentials.requests, "get", fetch) + assert credentials.main() == 1 + output = capsys.readouterr() + assert secret not in output.out + output.err + + +@pytest.mark.parametrize("token", [None, "", " "]) +def test_absent_credentials_fail_before_network(monkeypatch, token): + def unexpected(*args, **kwargs): + pytest.fail("No request may be sent without a credential") + + monkeypatch.setattr(credentials.requests, "get", unexpected) + with pytest.raises(ValueError, match="requires HUGGING_FACE_TOKEN"): + credentials.check_read_only_token(token) diff --git a/tests/test_release_lock.py b/tests/test_release_lock.py new file mode 100644 index 00000000..5c5c68c9 --- /dev/null +++ b/tests/test_release_lock.py @@ -0,0 +1,229 @@ +"""Release versioning preserves the reviewed registry dependency graph.""" + +import copy +import importlib.util +import shutil +import subprocess +from pathlib import Path + +import pytest + +ROOT = Path(__file__).resolve().parents[1] +spec = importlib.util.spec_from_file_location( + "release_lock", ROOT / "scripts/release_lock.py" +) +release_lock = importlib.util.module_from_spec(spec) +spec.loader.exec_module(release_lock) + + +def test_registry_lock_rejects_other_sources(): + project = { + "project": { + "name": "policyengine", + "version": "6.0.0", + "requires-python": ">=3.11", + } + } + lock = { + "requires-python": ">=3.11", + "package": [ + {"name": "policyengine", "version": "6.0.0", "source": {"editable": "."}}, + { + "name": "example", + "version": "1.0.0", + "source": {"registry": "https://pypi.org/simple"}, + "wheels": [ + { + "url": "https://files.pythonhosted.org/packages/example.whl", + "hash": "sha256:" + "a" * 64, + } + ], + }, + ], + } + release_lock.validate_registry_lock(project, lock) + for source in ( + {"directory": "../example"}, + {"git": "https://example.com/repo"}, + {"url": "file:///tmp/example.whl"}, + {"registry": "https://other.example/simple"}, + ): + changed = copy.deepcopy(lock) + changed["package"][1]["source"] = source + with pytest.raises(ValueError, match="registry"): + release_lock.validate_registry_lock(project, changed) + + +def test_registry_lock_rejects_version_and_python_drift(): + project = { + "project": { + "name": "policyengine", + "version": "6.0.0", + "requires-python": ">=3.11", + } + } + lock = { + "requires-python": ">=3.11", + "package": [ + {"name": "policyengine", "version": "5.3.0", "source": {"editable": "."}} + ], + } + with pytest.raises(ValueError, match="version"): + release_lock.validate_registry_lock(project, lock) + release_lock.validate_registry_lock(project, lock, allow_previous_version=True) + lock["requires-python"] = ">=3.9" + with pytest.raises(ValueError, match="Python"): + release_lock.validate_registry_lock(project, lock, allow_previous_version=True) + + +@pytest.mark.parametrize( + "config", + [ + {"project": {"dependencies": ["example @ file:///tmp/example.whl"]}}, + { + "project": { + "optional-dependencies": {"us": ["example @ https://example.com/x.whl"]} + } + }, + {"tool": {"uv": {"sources": {"example": {"path": "../example"}}}}}, + {"tool": {"uv": {"index": [{"url": "https://other.example/simple"}]}}}, + ], +) +def test_registry_project_rejects_development_overrides(config): + with pytest.raises(ValueError, match="registry"): + release_lock.validate_registry_project(config) + + +def test_actual_uv_refresh_updates_only_root_version(tmp_path): + if shutil.which("uv") is None: + pytest.skip("uv is required for the release workflow") + project = tmp_path / "pyproject.toml" + project.write_text( + '[project]\nname = "policyengine"\nversion = "5.3.0"\nrequires-python = ">=3.11"\n[build-system]\nrequires = ["setuptools"]\nbuild-backend = "setuptools.build_meta"\n' + ) + subprocess.run( + ["uv", "lock", "--offline", "--no-config"], + cwd=tmp_path, + check=True, + env=release_lock.resolver_environment(), + ) + before = release_lock.load_toml(tmp_path / "uv.lock") + project.write_text(project.read_text().replace('"5.3.0"', '"6.0.0"')) + release_lock.check_release_lock(tmp_path, refresh=True) + after = release_lock.load_toml(tmp_path / "uv.lock") + assert after["package"][0]["version"] == "6.0.0" + assert release_lock.without_root_version( + before, "policyengine" + ) == release_lock.without_root_version(after, "policyengine") + release_lock.check_release_lock(tmp_path) + + +def test_refresh_restores_reviewed_lock_on_dependency_drift(tmp_path, monkeypatch): + (tmp_path / "pyproject.toml").write_text( + '[project]\nname="policyengine"\nversion="6.0.0"\nrequires-python=">=3.11"\n' + ) + lock_path = tmp_path / "uv.lock" + before = b'requires-python=">=3.11"\n[[package]]\nname="policyengine"\nversion="5.3.0"\nsource={editable="."}\n' + lock_path.write_bytes(before) + + def changed(*args, **kwargs): + lock_path.write_bytes( + before.replace(b"5.3.0", b"6.0.0") + + b'[[package]]\nname="unexpected"\nversion="1"\nsource={registry="https://pypi.org/simple"}\nwheels=[{url="https://files.pythonhosted.org/packages/example.whl",hash="sha256:' + + b"a" * 64 + + b'"}]\n' + ) + + monkeypatch.setattr(release_lock.subprocess, "run", changed) + with pytest.raises(ValueError, match="dependency graph"): + release_lock.check_release_lock(tmp_path, refresh=True) + assert lock_path.read_bytes() == before + + +def test_resolver_environment_disables_inherited_overrides(monkeypatch): + monkeypatch.setenv("UV_FROZEN", "1") + monkeypatch.setenv("UV_FIND_LINKS", "/tmp/candidate-wheels") + monkeypatch.setenv("UV_INDEX", "https://other.example/simple") + env = release_lock.resolver_environment() + assert env["UV_FROZEN"] == "0" + assert "UV_FIND_LINKS" not in env + assert "UV_INDEX" not in env + + +@pytest.fixture(scope="module") +def actual_registry_project(tmp_path_factory): + root = tmp_path_factory.mktemp("registry-artifact-lock") + (root / "pyproject.toml").write_text( + '[project]\nname="policyengine"\nversion="6.0.0"\nrequires-python=">=3.11"\n' + 'dependencies=["packaging==26.2"]\n[build-system]\nrequires=["setuptools"]\n' + 'build-backend="setuptools.build_meta"\n' + ) + subprocess.run( + [ + "uv", + "lock", + "--no-config", + "--no-sources", + "--default-index", + release_lock.PYPI, + ], + cwd=root, + check=True, + env=release_lock.resolver_environment(), + ) + release_lock.check_release_lock(root) + return root + + +@pytest.mark.parametrize("kind", ["sdist", "wheel"]) +@pytest.mark.parametrize( + "url", + [ + "file:///tmp/unpublished-candidate.whl", + "https://example.com/unpublished-candidate.whl", + "http://files.pythonhosted.org/packages/candidate.whl", + "https://files.pythonhosted.org.evil.example/packages/candidate.whl", + "https://user@files.pythonhosted.org/packages/candidate.whl", + "https://files.pythonhosted.org:8443/packages/candidate.whl", + ], +) +def test_actual_uv_lock_rejects_artifact_locations_without_mutating( + actual_registry_project, tmp_path, kind, url +): + for name in ("pyproject.toml", "uv.lock"): + shutil.copyfile(actual_registry_project / name, tmp_path / name) + lock_path = tmp_path / "uv.lock" + package = next( + p + for p in release_lock.load_toml(lock_path)["package"] + if p["name"] == "packaging" + ) + artifact = package["sdist"] if kind == "sdist" else package["wheels"][0] + lock_path.write_text(lock_path.read_text().replace(artifact["url"], url)) + before = lock_path.read_bytes() + for refresh in (False, True): + with pytest.raises(ValueError, match="artifact"): + release_lock.check_release_lock(tmp_path, refresh=refresh) + assert lock_path.read_bytes() == before + + +@pytest.mark.parametrize( + "digest", ["sha256:short", "sha256:" + "g" * 64, "md5:" + "a" * 64, ""] +) +def test_actual_uv_lock_rejects_malformed_hash_without_mutating( + actual_registry_project, tmp_path, digest +): + for name in ("pyproject.toml", "uv.lock"): + shutil.copyfile(actual_registry_project / name, tmp_path / name) + lock_path = tmp_path / "uv.lock" + package = next( + p + for p in release_lock.load_toml(lock_path)["package"] + if p["name"] == "packaging" + ) + before_text = lock_path.read_text() + lock_path.write_text(before_text.replace(package["wheels"][0]["hash"], digest)) + before = lock_path.read_bytes() + with pytest.raises(ValueError, match="artifact"): + release_lock.check_release_lock(tmp_path) + assert lock_path.read_bytes() == before diff --git a/tests/test_release_tro_generation.py b/tests/test_release_tro_generation.py new file mode 100644 index 00000000..05f8277e --- /dev/null +++ b/tests/test_release_tro_generation.py @@ -0,0 +1,320 @@ +"""Release regeneration authenticates the reviewed, complete TRACE inputs.""" + +import argparse +import copy +import hashlib +import importlib.util +import json +import shutil +import sys +from pathlib import Path + +import pytest +import requests + +from policyengine.provenance import manifest + +ROOT = Path(__file__).resolve().parents[1] +FIXTURES = ROOT / "tests" / "fixtures" / "release_tros" +sys.path.insert(0, str(ROOT / "scripts")) +import bundle as maintenance # noqa: E402 +import generate_trace_tros as generator # noqa: E402 + + +@pytest.fixture +def release(monkeypatch, tmp_path): + package = tmp_path / "policyengine" + bundle_dir = package / "data" / "bundle" + bundle_dir.mkdir(parents=True) + for name in ("manifest.json", "us.trace.tro.jsonld"): + shutil.copyfile(FIXTURES / name, bundle_dir / name) + # Only the public US snapshot is a repository test fixture. UK is verified + # against its authenticated private release in the release preflight. + source = json.loads((bundle_dir / "manifest.json").read_bytes()) + source["data_releases"].pop("uk") + source["packages"].pop("policyengine-uk") + (bundle_dir / "manifest.json").write_text(json.dumps(source)) + monkeypatch.setattr(generator, "BUNDLE_MANIFEST", bundle_dir / "manifest.json") + monkeypatch.setattr(generator, "BUNDLE_TRO_DIR", bundle_dir) + lock_path = tmp_path / "uv.lock" + shutil.copyfile(FIXTURES / "country-models.lock.toml", lock_path) + monkeypatch.setattr(generator, "BUNDLE_LOCK", lock_path) + monkeypatch.setattr(manifest, "files", lambda _: package) + response = requests.Response() + response.status_code = 200 + response._content = (FIXTURES / "us-release_manifest.json").read_bytes() + monkeypatch.setattr(manifest.requests, "get", lambda *a, **kw: response) + manifest.get_release_manifest.cache_clear() + manifest.get_data_release_manifest.cache_clear() + yield bundle_dir, response + manifest.get_release_manifest.cache_clear() + manifest.get_data_release_manifest.cache_clear() + + +def test_strict_generation_is_complete_and_deterministic(release): + first = generator.generated_tros(strict=True) + assert first == generator.generated_tros(strict=True) + artifacts = json.loads(first[0][1])["@graph"][0]["trov:hasComposition"][ + "trov:hasArtifact" + ] + assert len(artifacts) == 4 + assert any( + item["trov:sha256"] + == hashlib.sha256( + (FIXTURES / "us-release_manifest.json").read_bytes() + ).hexdigest() + for item in artifacts + ) + + +def test_runtime_pin_update_requires_matching_certified_model(release): + from prepare_package_bundle_update import update_package_pins + + path = release[0] / "manifest.json" + original = path.read_bytes() + tro_path = release[0] / "us.trace.tro.jsonld" + reviewed = tro_path.read_bytes() + updated = update_package_pins( + json.loads(original), argparse.Namespace(core=None, us="1.824.1", uk=None) + ) + path.write_text(json.dumps(updated)) + manifest.get_release_manifest.cache_clear() + manifest.get_data_release_manifest.cache_clear() + for action in (generator.regenerate_all, maintenance._check_tros): + with pytest.raises(ValueError, match="runtime.*model"): + action(strict=True) + assert tro_path.read_bytes() == reviewed + # Restoring the actual certified runtime pin admits the same real inputs. + path.write_bytes(original) + manifest.get_release_manifest.cache_clear() + manifest.get_data_release_manifest.cache_clear() + generator.regenerate_all(strict=True) + assert maintenance._check_tros(strict=True) == 0 + + +@pytest.mark.parametrize("field", ["name", "version", "sha256", "wheel_url"]) +def test_runtime_model_identity_must_match_certification(release, field): + path = release[0] / "manifest.json" + payload = json.loads(path.read_bytes()) + payload["packages"]["policyengine-us"][field] = "different" + path.write_text(json.dumps(payload)) + with pytest.raises(ValueError, match="runtime.*model"): + generator.regenerate_all(strict=True) + + +@pytest.mark.parametrize( + "change", ["missing", "version", "url", "hash", "registry", "extra-wheel"] +) +def test_certified_model_must_match_locked_wheel(release, change): + lock_path = generator.BUNDLE_LOCK + text = lock_path.read_text() + model = manifest.get_release_manifest("us").model_package + if change == "missing": + lock_path.unlink() + elif change == "extra-wheel": + record = next( + line for line in text.splitlines() if f'url = "{model.wheel_url}"' in line + ) + lock_path.write_text( + text.replace(record, record + "\n" + record.replace("/e2/", "/aa/")) + ) + else: + old, new = { + "version": ('version = "1.764.6"', 'version = "1.824.1"'), + "url": (model.wheel_url, model.wheel_url.replace("/e2/", "/aa/")), + "hash": (model.sha256, "a" * 64), + "registry": ("https://pypi.org/simple", "https://example.com/simple"), + }[change] + assert old in text + lock_path.write_text(text.replace(old, new)) + path = release[0] / "us.trace.tro.jsonld" + before = path.read_bytes() + with pytest.raises(ValueError, match="lock|registry"): + generator.regenerate_all(strict=True) + assert path.read_bytes() == before + + +@pytest.mark.parametrize("status", [401, 403, 404, 503]) +def test_strict_fetch_failure_preserves_reviewed_tro(release, status): + bundle_dir, response = release + path = bundle_dir / "us.trace.tro.jsonld" + before = path.read_bytes() + response.status_code = status + with pytest.raises(ValueError, match="data release manifest"): + generator.regenerate_all(strict=True) + assert path.read_bytes() == before + + +def test_local_generation_retains_explicit_limited_mode(release, capsys): + release[1].status_code = 404 + assert generator.generated_tros() + assert "writing limited TRO" in capsys.readouterr().err + + +def test_strict_rejects_changed_remote_manifest_bytes(release): + # Equivalent JSON with changed bytes must not silently replace reviewed pins. + release[1]._content += b"\n" + with pytest.raises(ValueError, match="reviewed.*hash"): + generator.generated_tros(strict=True) + + +def test_strict_network_failure_never_writes_limited_output( + release, monkeypatch, capsys +): + path = release[0] / "us.trace.tro.jsonld" + before = path.read_bytes() + + def unavailable(*args, **kwargs): + raise requests.Timeout("fixture timeout") + + monkeypatch.setattr(manifest.requests, "get", unavailable) + with pytest.raises(manifest.DataReleaseManifestUnavailableError): + generator.regenerate_all(strict=True) + assert path.read_bytes() == before + assert "limited TRO" not in capsys.readouterr().err + + +def test_strict_missing_remote_dataset_never_writes_limited_output(release, capsys): + path = release[0] / "us.trace.tro.jsonld" + before = path.read_bytes() + data = json.loads(release[1].content) + del data["artifacts"][data["default_datasets"]["national"]] + release[1]._content = json.dumps(data).encode() + with pytest.raises(ValueError, match="Default dataset"): + generator.regenerate_all(strict=True) + assert path.read_bytes() == before + assert "limited TRO" not in capsys.readouterr().err + + +@pytest.mark.parametrize("remove_models", [False, True]) +def test_strict_cannot_skip_missing_country_declarations(release, remove_models): + path = release[0] / "manifest.json" + payload = json.loads(path.read_bytes()) + payload["data_releases"] = {} + if remove_models: + payload["packages"] = {} + path.write_text(json.dumps(payload)) + with pytest.raises(ValueError, match="every bundled country"): + generator.generated_tros(strict=True) + + +@pytest.mark.parametrize( + "change", ["dataset", "hash", "wheel", "certification", "revision"] +) +def test_strict_rejects_incomplete_or_drifted_certification( + release, monkeypatch, change +): + country = copy.deepcopy(manifest.get_release_manifest("us")) + if change == "dataset": + country.certified_data_artifact.dataset = "missing" + elif change == "hash": + country.certified_data_artifact.sha256 = "a" * 64 + elif change == "wheel": + country.model_package.sha256 = None + elif change == "certification": + country.certification.certified_for_model_version = "0.0.0" + else: + country.data_package.release_manifest_revision = "main" + monkeypatch.setattr(generator, "get_release_manifest", lambda _: country) + with pytest.raises(ValueError): + generator.generated_tros(strict=True) + + +@pytest.mark.parametrize("change", ["missing", "limited", "location"]) +def test_strict_requires_prepared_reviewed_sidecar(release, change): + path = release[0] / "us.trace.tro.jsonld" + if change == "missing": + path.unlink() + elif change == "limited": + release[1].status_code = 404 + path.write_bytes(generator.generated_tros()[0][1]) + release[1].status_code = 200 + else: + path.write_text(path.read_text().replace("resolve/populace-", "resolve/other-")) + with pytest.raises(ValueError, match="reviewed"): + generator.generated_tros(strict=True) + + +def test_strict_all_countries_validate_before_any_write(release, monkeypatch): + path = release[0] / "us.trace.tro.jsonld" + before = path.read_bytes() + bundle_path = release[0] / "manifest.json" + bundle = json.loads(bundle_path.read_bytes()) + bundle["data_releases"]["zz"] = bundle["data_releases"]["us"] + bundle_path.write_text(json.dumps(bundle)) + with pytest.raises(ValueError): + generator.regenerate_all(strict=True) + assert path.read_bytes() == before + + +def test_reviewed_sidecar_allows_only_version_regeneration(release): + spec = importlib.util.spec_from_file_location( + "release_bump", ROOT / ".github/bump_version.py" + ) + bump = importlib.util.module_from_spec(spec) + spec.loader.exec_module(bump) + before = generator.generated_tros(strict=True)[0][1] + bump.sync_bundle_versions(release[0] / "manifest.json", "6.0.0") + manifest.get_release_manifest.cache_clear() + manifest.get_data_release_manifest.cache_clear() + after = generator.generated_tros(strict=True)[0][1] + assert before != after + assert b'"schema:softwareVersion": "6.0.0"' in after + assert b'"pe:emittedIn": "repository-bundle"' in after + # New version bytes are allowed, but regenerate/check must agree exactly. + generator.regenerate_all(strict=True) + assert generator.generated_tros(strict=True)[0][1] == after + + +def test_strict_cli_requires_tros_and_checks_exact_bytes(release, monkeypatch): + import generate_bundle_artifacts + + with pytest.raises(SystemExit) as exc: + maintenance.main(["check", "--strict-tros"]) + assert exc.value.code == 2 + monkeypatch.setattr(generate_bundle_artifacts, "generate", lambda **kw: 0) + monkeypatch.setattr(maintenance, "REPO_ROOT", release[0].parents[2]) + assert maintenance.main(["check", "--include-tros", "--strict-tros"]) == 1 + assert maintenance.main(["generate", "--include-tros", "--strict-tros"]) == 0 + assert maintenance.main(["check", "--include-tros", "--strict-tros"]) == 0 + + +def test_release_workflows_gate_complete_inputs_and_lock(): + import yaml + + pr = yaml.safe_load((ROOT / ".github/workflows/pr_code_changes.yaml").read_text()) + push = yaml.safe_load((ROOT / ".github/workflows/push.yaml").read_text()) + assert "pull_request_target" not in pr.get("on", pr.get(True, {})) + assert ( + "github.event.pull_request.head.repo.full_name == github.repository" + in pr["jobs"]["BundleVerification"]["if"] + ) + for job in ( + push["jobs"]["Versioning"], + push["jobs"]["Publish"], + ): + commands = "\n".join(step.get("run", "") for step in job["steps"]) + assert "check --published-spm --include-tros --strict-tros" in commands + assert 'if [[ -z "${HUGGING_FACE_TOKEN:-}" ]]' in commands + assert "python scripts/release_lock.py" in commands + assert commands.index( + "python scripts/check_release_credentials.py" + ) < commands.index("check --published-spm") + # The pull-request job checks only the reviewed registry lock. The + # read-only credential, published-measurement and TRACE sidecar gates + # depend on the release workflow regenerating sidecars and publishing + # wheels first, so a pull request can never satisfy them. + pr_commands = "\n".join( + step.get("run", "") for step in pr["jobs"]["BundleVerification"]["steps"] + ) + assert "python scripts/release_lock.py" in pr_commands + assert "--published-spm" not in pr_commands + commands = "\n".join( + step.get("run", "") for step in push["jobs"]["Versioning"]["steps"] + ) + assert commands.index("check --published-spm") < commands.index("make changelog") + assert ( + commands.index("make changelog") + < commands.index("release_lock.py --refresh") + < commands.index("generate --include-tros --strict-tros") + ) From 7fdf1142e9a60d6d14e5812881aa1451b73ce935 Mon Sep 17 00:00:00 2001 From: Max Ghenis Date: Fri, 11 Sep 2026 12:30:25 -0400 Subject: [PATCH 08/46] Document the canonical SPM contract and release checks Documents the user-visible consequences of the change set. The household guide describes the exact selection keys, the artifact horizon and the explicit-geography errors; the microsimulation guide covers selection on Simulation and calculate_decile_impacts, the serialization and provenance surface, and the native HDF loader's weight projection and ID alignment. The release-bundle document records the preparation and checking commands, what strict generation authenticates, and what versioning may and may not change. README, the index, getting started and the countries page state the 3.11 to 3.14 requirement and use an observed county FIPS in their US examples; the two US examples do the same, with one showing an explicit national selection and both dropping stale household keys. A new engineering runbook records that the integration is pending promotion, which gates remain, and how to reproduce the local development fixture. Co-Authored-By: Claude Fable 5.1 --- README.md | 8 +- docs/countries.md | 6 +- .../runbooks/canonical-spm-development.md | 193 ++++++++++++++++++ docs/getting-started.md | 7 +- docs/households.md | 75 ++++++- docs/index.md | 2 +- docs/microsim.md | 76 +++++++ docs/release-bundles.md | 52 +++++ examples/employment_income_variation_us.py | 3 +- examples/household_impact_example.py | 15 +- 10 files changed, 421 insertions(+), 16 deletions(-) create mode 100644 docs/engineering/runbooks/canonical-spm-development.md diff --git a/README.md b/README.md index 112fa503..d26eaac4 100644 --- a/README.md +++ b/README.md @@ -4,6 +4,8 @@ A Python package for tax-benefit microsimulation analysis. Run policy simulations, analyse distributional impacts, and visualise results across the UK and US. +Requires Python 3.11–3.14. + > **Results are estimates.** PolicyEngine simulates a large, evolving body of tax-benefit law (the US model alone encodes more than 95,000 parameters across 5,500+ variables) over survey microdata calibrated to administrative targets. Treat outputs as estimates, and validate them against the policies relevant to your analysis, the scope of the rules engine, and external or back-of-the-envelope calculations. You can inspect the certified US dataset's calibration at https://calibration-diagnostics.vercel.app/populace. @@ -26,13 +28,17 @@ print(uk.household.hbai_household_net_income) # net income us = pe.us.calculate_household( people=[{"age": 35, "employment_income": 60_000}], tax_unit={"filing_status": "SINGLE"}, - household={"state_code": "CA"}, + household={"state_code": "CA", "county_fips": "06037"}, year=2026, reform={"gov.irs.credits.ctc.amount.adult_dependent": 1000}, ) print(us.tax_unit.income_tax, us.household.household_net_income) ``` +US default outputs require observed county FIPS or an explicit national/SPM-area +choice. See the [SPM household contract](docs/households.md#spm-geography-and-measurement-selection) +for settings, provenance and the pending coordinated bundle release. + ### Population analysis ```python diff --git a/docs/countries.md b/docs/countries.md index 26553627..4e2227a5 100644 --- a/docs/countries.md +++ b/docs/countries.md @@ -38,7 +38,9 @@ Override in any output with `income_variable=`. ## State / regional breakdown US: Populace row scoping uses `state_fips` and `congressional_district_geoid`. -`state_code` remains the human-readable input for custom households. +`state_code` remains the human-readable state input for custom households. +US resource and poverty calculations also require observed `county_fips` or an +explicit national or SPM-area choice; see [Households](households.md#spm-geography-and-measurement-selection). UK: constituency code and local authority code on every household where available. @@ -72,7 +74,7 @@ Most analysis patterns are identical — swap `pe.us` for `pe.uk`: pe.us.calculate_household( people=[{"age": 35, "employment_income": 60_000}], tax_unit={"filing_status": "SINGLE"}, - household={"state_code": "CA"}, + household={"state_code": "CA", "county_fips": "06037"}, year=2026, ) diff --git a/docs/engineering/runbooks/canonical-spm-development.md b/docs/engineering/runbooks/canonical-spm-development.md new file mode 100644 index 00000000..4e094bc5 --- /dev/null +++ b/docs/engineering/runbooks/canonical-spm-development.md @@ -0,0 +1,193 @@ +# Canonical SPM wrapper development and remaining release gates + +The canonical SPM wrapper integration is **pending promotion**. The packaged +5.3.0 manifest still pins US 1.764.6 and its existing certified Build P release. +Only `measurements.spm` has been staged: forecast content SHA-256 +`3d86d5c4c0423480e6b69b75d222ffa4a7a2639e4094df5ba2504af01be17173`, scenario +`ce_trend`, county geography, vintage `2020`. This measurement pin does not +certify the new country package or the enriched data. + +The configuration accepts exactly `forecast_content_sha256`, `scenario`, +`geography_kind`, `geography_id`, `county_vintage` and `as_of`. The hash and +scenario are mandatory in a bundle. County and national modes omit +`geography_id`; metro requires it. `as_of`, when present, is an ISO calendar +date. Unknown fields, paths, provider overrides and fallback settings fail +validation. Country runtime validation resolves scenario, year and location +against the pinned artifact when the calculation needs SPM. + +## Reproduce the local development fixture + +Use a fresh isolated environment containing the exact qualified local country, +calculator and Core wheels. Preserve all earlier qualification environments. Set +`SPM_PYTHON` to that environment's interpreter and `SPM_COUNTRY_WHEEL` and +`SPM_CALCULATOR_WHEEL` to the authenticated wheel paths. The following command +reads wheel `METADATA` and hashes the bytes; it never substitutes an invented +registry release or changes the packaged manifest. + +```bash +UV_CACHE_DIR=/tmp/wrapper-spm-uv-cache \ + uv run --python "$SPM_PYTHON" --no-sync python scripts/bundle.py \ + development-manifest \ + --country-wheel "$SPM_COUNTRY_WHEEL" \ + --calculator-wheel "$SPM_CALCULATOR_WHEEL" \ + --output /tmp/wrapper-spm-development-manifest.json +``` + +The most recently qualified development country wheel is version 1.824.7, SHA-256 +`7644819916a4f8ca2aa37a4a9d992fa834bfa66ab2d441326a9686baa5c1a688`. +The observed local calculator wheel is version 1.0.0, SHA-256 +`c49c41da5fd482e563eaea956e205a3ba6841cadd4dd32bef4c0c3dbce17ffba`. +These are local development artifacts, not published package identities. +The calculator wheel uses the canonical source commit +`fd7de4b3b670a10acd72567904ccea800252c9a5`. Core remains 3.30.1 and UK 2.90.2. + +The fixture records `development.data_certification: not_certified` and +`development.promotion_status: pending`. It removes the inherited US +`certification` and `certified_data_artifact`; inherited data metadata is +reference-only. It records the real local wheel names, versions, file URIs and +SHA-256 hashes. UK release metadata is retained unchanged. It refuses to +overwrite the packaged manifest. + +Country model initialization requires a data compatibility receipt even for +household-only tests. An explicitly enabled pytest plugin supplies the +`unverified_development_fixture` receipt and disables remote data-manifest +fetches for that test process. It does not add a compatibility claim, certify a +dataset, or install a production environment-variable override. Its existing +receipt field `certified_for_model_version` names the tested runtime; the +`unverified_development_fixture` basis explicitly denies certification. + +```bash +UV_CACHE_DIR=/tmp/wrapper-spm-uv-cache \ + uv run --python "$SPM_PYTHON" --no-sync python -m pytest \ + -p tests.fixtures.spm_development \ + --spm-development-manifest /tmp/wrapper-spm-development-manifest.json \ + tests/test_spm_selection.py tests/test_spm_model.py \ + tests/test_spm_household.py -q +``` + +The plugin requires explicit opt-in and checks installed US/Core/calculator +versions, both local wheel hashes, and every installed country/calculator +package file against those wheel bytes before country imports. It authenticates +the actual US/Core/calculator import origins and package search paths, rejects +shadowing from `PYTHONPATH` or previously loaded modules, and repeats these +checks after bootstrap. Core wheel bytes are checked by the separate four-wheel +native probe. Only focused household +and synthetic small-dataset tests belong in this command. The final acceptance +worker owns the heavy population process. Local enriched H5 experiments require +an explicit path with `allow_unmanaged=True` and remain development runs while +certification is pending. + +An explicitly labeled development script can use the same bootstrap before +importing country models. This helper lives under `tests/fixtures` and is not a +production environment override: + +```python +from tests.fixtures.spm_development import activate_spm_development_manifest + +development = activate_spm_development_manifest( + "/tmp/wrapper-spm-development-manifest.json" +) +import policyengine as pe + +assert development["development"]["promotion_status"] == "pending" +assert pe.us.model.data_certification.compatibility_basis == ( + "unverified_development_fixture" +) +``` + +Run such a script from the wrapper worktree with the isolated interpreter and +`uv run --python "$SPM_PYTHON" --no-sync`. The final acceptance worker can +then call `pe.us.managed_microsimulation(dataset=LOCAL_H5, +allow_unmanaged=True, spm={...})`, reporting the explicit development receipt. +Only that worker is authorized for the heavy population process. + +Bundle-only tests avoid the country bootstrap entirely: + +```bash +UV_CACHE_DIR=/tmp/wrapper-spm-uv-cache POLICYENGINE_SKIP_COUNTRY_IMPORTS=1 \ + uv run --python "$SPM_PYTHON" --no-sync python -m pytest --noconftest \ + tests/test_spm_bundle_tooling.py tests/test_bundle_metadata.py \ + tests/test_bundle.py -q +``` + +## Stage and later verify published measurement pins + +The operator command validates all settings before writing. Omitting +`--calculator-version` stages only configuration and preserves package pins +and data certification: + +```bash +uv run --python "$SPM_PYTHON" --no-sync python scripts/bundle.py set-spm \ + --forecast-content-sha256 3d86d5c4c0423480e6b69b75d222ffa4a7a2639e4094df5ba2504af01be17173 \ + --scenario ce_trend +uv run --python "$SPM_PYTHON" --no-sync python scripts/bundle.py check +``` + +After the calculator's actual registry release is authorized and published, +repeat `set-spm` with `--calculator-version "$PUBLISHED_CALCULATOR_VERSION"`. +The command fetches that exact version's authoritative PyPI metadata, requires +one non-yanked universal wheel on `files.pythonhosted.org`, downloads its bytes, +and checks the registry SHA-256 before writing. It adds `spm-calculator` as a +hashed `runtime_dependency` scoped to the US, adds it to US/models/dev extras, +and preserves UK requirements. The version must come from the real release; +local wheel metadata is not proof of registry publication. + +`scripts/bundle.py check --published-spm` checks the measurement pin and +published calculator dependency metadata. **It does not establish country/data +compatibility or whole-release readiness.** It rejects development fixtures and +currently exits 2 with `SPM promotion pending: no published spm-calculator pin`. +Ordinary `check` passes for the staged draft; this is metadata consistency only. + +## Exact work remaining before a production bundle + +1. Complete the coordinator's canonical numerical acceptance and source/review + gates. Deploy the separately owned protections for old bundle images before + calculator publication. Record the reviewed source and actual published + calculator wheel identity and import/content verification. +2. Authenticate the producer's clean reviewed source and build a new immutable + native source-enrichment release. The completed producer handoff records a + local candidate at `/private/tmp/microcosm-spm-native-role-final-20260909`, + whose H5 SHA-256 is + `6496cc4393d4d3c6574f76eca231de5898c803b9067645591fd5c4d3e65aee84`. + The candidate is not published or certified. The new person primitive is + `is_spm_independent_minor_role`; all prior fields, values, dtypes, membership, + county inputs and weights are preserved. This is source enrichment, not + recalibration. The inherited diagnostics remain schema 5, SHA-256 + `870449b44e86b13b25bcea1a57f0e7af37f4d4db18be815eea3acdf9fe6eb40e`; + relabeling those diagnostics as schema 6 is invalid. +3. In the producer checkout, run its real `python -m + microcosm.data.source_enrichment --certify` contract with the new candidate, + exact parent H5 and actual country/Core/wrapper compatibility wheels. That + contract verifies native loaders, registration, input precedence and wheel + source identities without a full population simulation. Run the actual + publisher's `--preflight-only` contract. The producer handoff contains the + full commands. Commit authentication and compatibility evidence are still + required; no edited compatibility ranges can replace them. +4. Publish the reviewed immutable enrichment release only after its real gates + pass. Resolve the actual country/wrapper publication sequence with the + coordinator; do not invent future versions to break that dependency. + Refresh country and wrapper locks against real registry bytes. +5. Certify the published data release through `scripts/bundle.py certify-data + --country us --data-producer populace --model-version + "$PUBLISHED_COUNTRY_VERSION" --manifest-uri "$PUBLISHED_RELEASE_MANIFEST_URI"`. + The URI must identify the actual immutable producer release. This command + updates the country pin and data metadata from real compatibility evidence. + Regenerate bundle artifacts and TRACE sidecars, inspect full-manifest hash + bindings, and run the required exact model/data tests. Staging an SPM hash + changes the manifest bytes; old sidecar bindings cannot establish provenance + for those new bytes. +6. Finish the coordinator-owned final population, simulation beta, API staging, + browser/client and production version-route acceptance. The focused local + tests above do not establish those deployment gates. + +No release, publication, deployment, full population run or data certification +was performed by this wrapper tooling work. + +The bundled TRACE sidecars are regenerated against the current manifest, so +`TestVendoredSidecarBinding` passes: both `us.trace.tro.jsonld` and +`uk.trace.tro.jsonld` bind manifest SHA-256 +`d430b8e561d76e5836a17bf506b06b9cd16560eb77f15718dd46f4e78513de85`, which is +the hash of `src/policyengine/data/bundle/manifest.json` in this tree. That +binding covers the staged measurement bytes only; it is metadata provenance, +not data certification, and the sidecars must be regenerated and reviewed again +at the real bundle promotion step against published artifacts. diff --git a/docs/getting-started.md b/docs/getting-started.md index 6dd489b1..bbd9afa5 100644 --- a/docs/getting-started.md +++ b/docs/getting-started.md @@ -4,6 +4,8 @@ title: "Getting started" ## Install +Use Python 3.11–3.14, the supported range for the certified country models. + ```bash pip install policyengine ``` @@ -26,7 +28,7 @@ import policyengine as pe result = pe.us.calculate_household( people=[{"age": 35, "employment_income": 60_000}], tax_unit={"filing_status": "SINGLE"}, - household={"state_code": "CA"}, + household={"state_code": "CA", "county_fips": "06037"}, year=2026, ) @@ -35,7 +37,7 @@ result.tax_unit.eitc result.household.household_net_income ``` -Each `.*` lookup is a regular Python scalar. The result is a typed `HouseholdResult` with entity sections (`person[i]`, `tax_unit`, `spm_unit`, `household`) populated from every variable in the country model. +Each `.*` lookup is a regular Python scalar. The result is a typed `HouseholdResult` with entity sections (`person[i]`, `tax_unit`, `spm_unit`, `household`) populated from the default output catalog. Additional outputs can be requested with `extra_variables`. US defaults include SPM resources and poverty, so provide observed county FIPS or explicitly select national measurement with `spm={"geography_kind": "national"}`; see [Households](households.md#spm-geography-and-measurement-selection). ## Apply a reform @@ -45,6 +47,7 @@ Reforms are parameter-path → value dicts: reformed = pe.us.calculate_household( people=[{"age": 35, "employment_income": 60_000}], tax_unit={"filing_status": "SINGLE"}, + household={"state_code": "CA", "county_fips": "06037"}, year=2026, reform={"gov.irs.credits.ctc.amount.adult_dependent": 1_000}, ) diff --git a/docs/households.md b/docs/households.md index bd6f8973..7d9e6e78 100644 --- a/docs/households.md +++ b/docs/households.md @@ -2,7 +2,7 @@ title: "Households" --- -`pe.us.calculate_household` and `pe.uk.calculate_household` compute every variable in the country model for a single household. Same keyword arguments, different entity structures. +`pe.us.calculate_household` and `pe.uk.calculate_household` compute a default set of tax, benefit and income outputs for a single household. Use `extra_variables` to request additional outputs. ## US @@ -15,7 +15,7 @@ result = pe.us.calculate_household( {"age": 5}, ], tax_unit={"filing_status": "JOINT"}, - household={"state_code": "TX"}, + household={"state_code": "TX", "county_fips": "48201"}, year=2026, ) ``` @@ -27,12 +27,77 @@ result = pe.us.calculate_household( | `people` | List of person dicts. Keys are any person-level variable on the model. | | `tax_unit` | Tax-unit inputs (e.g. `filing_status`). | | `spm_unit` | SPM-unit inputs. | -| `household` | Household inputs. `state_code` is essentially always needed. | +| `household` | Household inputs, including `state_code` and observed five-digit `county_fips` for the default SPM geography selection. | | `family` | Family-level inputs. | | `marital_unit` | Marital-unit inputs. | All adults default to one shared tax unit and household. For separate tax units (e.g. two adult roommates), construct the `Simulation` directly and set the entity-membership arrays. +### SPM geography and measurement selection + +US household results include SPM resources and poverty by default. Provide the +household's county FIPS, as above, or explicitly select national measurement: + +```python +result = pe.us.calculate_household( + people=[{"age": 40, "employment_income": 50_000}], + tax_unit={"filing_status": "SINGLE"}, + household={"state_code": "CA"}, + year=2026, + spm={"geography_kind": "national"}, +) +receipt = result.to_dict()["provenance"]["spm"] +result.write("household-result.json") # Includes the JSON-compatible receipt. +``` + +County FIPS assigns a household to the selected year's Census SPM estimation +area; it does not select a separately estimated county rent factor. National +measurement is a conscious analytical choice and is recorded in provenance. +A fixed SPM area can instead be selected with +`spm={"geography_kind": "metro", "geography_id": area_id}`, using an area ID +available in the pinned artifact for the requested year. + +The `spm` argument accepts a dictionary or `pe.us.SPMSelection`. Its complete +set of keys is: + +| Key | Meaning | +|---|---| +| `forecast_content_sha256` | Optional assertion of the bundle's independently pinned artifact content hash. A different hash is rejected. | +| `scenario` | Scenario within that artifact: `ce_trend` by default or the `zero_real` sensitivity. | +| `geography_kind` | `county` by default; `national` or `metro` require an explicit choice. | +| `geography_id` | Required only for a fixed `metro` SPM area. | +| `county_vintage` | County assignment vintage, `"2020"` by default. | +| `as_of` | Optional information-date cutoff accepted by the pinned artifact. | + +Forecast paths, provider objects and fallback policies are not settings. The +canonical artifact covers 2022–2035. Its receipt distinguishes published +national inputs from forecast components and research geography; an unsupported +year fails instead of being extrapolated by the wrapper. Scenario forecasts +are conditional research estimates, not agency forecasts or uncertainty bounds. + +State alone is insufficient for SPM and raises `SPM_GEOGRAPHY_REQUIRED`. +Unknown counties or selected areas raise `SPM_GEOGRAPHY_UNAVAILABLE`; a measured +unit with no classified adult raises `SPM_COMPOSITION_REQUIRED`. These are +calculator `SPMInputError` exceptions with `code` and `to_dict()` attributes. +Geography is checked when an SPM-dependent formula runs. Net income, benefits, +MTR, income decile and equivalized net income all evaluate the housing cap, +including households receiving zero housing assistance, so each requires this +choice. A genuinely independent tax-only country-model calculation can use +state alone; the wrapper's default outputs include SPM-dependent results. + +Supply observed inputs such as age, tenure, county and the source-backed +`is_spm_independent_minor_role`. Computed SPM thresholds, geographic factors, +resources, poverty outputs and `spm_measurement_adults` / +`spm_measurement_children` cannot be supplied as household inputs or axes. +Generic `is_adult`, `spm_unit_count_adults` and `spm_unit_count_children` remain +separate benefit inputs; they do not override SPM measurement composition. + +These changes require the coordinated canonical country/calculator bundle. +The packaged 5.3.0 production manifest still pins `policyengine-us==1.764.6` +and does not certify this integration. Local-wheel development manifest fixtures +are explicitly uncertified. The measurement receipt identifies a calculation; +it does not certify a population dataset or establish publication readiness. + ## UK ```python @@ -94,7 +159,7 @@ The year determines which parameter values apply. For multi-year analysis, call ## Extra variables -The result exposes every variable in the model by default. To surface variables that aren't in the default catalog explicitly: +The result exposes a default output catalog. To request additional variables: ```python result = pe.us.calculate_household( @@ -120,7 +185,7 @@ result = pe.us.calculate_household( } ], tax_unit={"filing_status": "SINGLE"}, - household={"state_code": "CA"}, + household={"state_code": "CA", "county_fips": "06037"}, year=2026, axes=[ { diff --git a/docs/index.md b/docs/index.md index ec4b6a49..45b5eff6 100644 --- a/docs/index.md +++ b/docs/index.md @@ -20,7 +20,7 @@ import policyengine as pe result = pe.us.calculate_household( people=[{"age": 35, "employment_income": 60_000}], tax_unit={"filing_status": "SINGLE"}, - household={"state_code": "CA"}, + household={"state_code": "CA", "county_fips": "06037"}, year=2026, ) diff --git a/docs/microsim.md b/docs/microsim.md index 0e3d73e1..91795062 100644 --- a/docs/microsim.md +++ b/docs/microsim.md @@ -4,6 +4,10 @@ title: "Microsimulation" For population-level estimates — budget cost, winners and losers, poverty impact — run a microsimulation over calibrated microdata. +For US decile analysis, `calculate_decile_impacts(dataset=..., spm=...)` passes +the selection to both constructed simulations. When supplying existing +baseline and reform simulations, set `spm` on each simulation directly. + ## Quick example ```python @@ -28,6 +32,78 @@ total_snap.result `Simulation.ensure()` loads a cached result if one exists, or runs and caches on miss. Call `Simulation.run()` explicitly if you want to bypass the cache. +### Canonical US SPM settings and provenance + +The coordinated canonical SPM integration resolves measurement settings from +the bundle. Its default uses each household's observed `county_fips` and native +SPM membership. To make an explicit national sensitivity calculation: + +```python +simulation = pe.Simulation( + dataset=dataset, + tax_benefit_model_version=pe.us.model, + spm={"geography_kind": "national", "scenario": "zero_real"}, +) +simulation.run() +settings = simulation.spm_config +receipt = simulation.spm_provenance() +serialized = simulation.model_dump_json(include={"spm", "spm_receipt"}) +``` + +`spm_config` exposes all six resolved settings; `spm_provenance()` returns a +detached JSON-compatible measurement receipt after calculation. The stored +`spm_receipt` is a Pydantic model. Before calculation, serializing a partial +selection preserves omitted options so they continue to inherit the bundle's +defaults after JSON restoration; successful calculation freezes all six settings. +Selection participates in simulation identity +and cached results. The example serializes the measurement settings and receipt; +the existing full simulation model graph contains circular model/variable +references and cannot currently be exported with an unrestricted +`model_dump_json()`. Use simulation persistence and run records for saved runs. +The same `spm=` selection is accepted by +`pe.us.managed_microsimulation`; its returned country simulation exposes +`spm_config` and `spm_provenance()` too. See the [household SPM +contract](households.md#spm-geography-and-measurement-selection) for exact keys, +the 2022–2035 artifact horizon and explicit geography errors. No provider or +forecast-file path can replace the bundle's pinned artifact. + +For a small local development dataset, explicitly opt out of managed data +selection: + +```python +simulation = pe.us.managed_microsimulation( + dataset=str(local_h5_path), + allow_unmanaged=True, + spm={"geography_kind": "county"}, +) +``` + +This does not certify the local file. The canonical population release must +preserve the prior native arrays, memberships and weights while adding the +source-backed `is_spm_independent_minor_role` input. Formula-owned measurement +counts, thresholds, geographic factors, SPM resources and poverty outputs must +not be present as input columns; the wrapper validates the input DataFrames +before passing them to the country model. Generic adult/child counts remain +separate. The native source enrichment is not recalibration, and inherited +schema-5 calibration diagnostics cannot be relabeled as schema 6. + +The wrapper's native pandas HDF loader accepts files that store only calibrated +`household_weight`. It maps missing person and group weight columns through +native membership IDs in memory, retaining supplied weight columns and the +original row order. Inputs are aligned to country populations by native entity ID, +and calculated outputs are aligned back to those IDs before attaching weights. +Independently shuffled entity tables therefore retain the correct geography and +results. Null or duplicate entity IDs are rejected before weight mapping. +Group weights are not sums of person weights. The file is +opened read-only; missing or ambiguous links raise an error instead of creating +unweighted rows. Core variable/period H5 files use the same mapping. + +The packaged 5.3.0 production manifest retains `policyengine-us==1.764.6` and +does not yet certify this integration. Local-wheel tests use an explicitly +uncertified development manifest. Registry publication, a producer-issued data +compatibility certification and final bundle promotion remain separate gates; +measurement provenance alone does not satisfy them. + ## Datasets Microdata is stored as HDF5 on Hugging Face. `ensure_datasets` downloads, caches, and uprates: diff --git a/docs/release-bundles.md b/docs/release-bundles.md index e7e2469b..63026246 100644 --- a/docs/release-bundles.md +++ b/docs/release-bundles.md @@ -12,6 +12,58 @@ This document defines the intended reproducibility boundary for `policyengine.py`. +## Preparing and checking release artifacts + +After certifying a new country/data pair, prepare its complete sidecars with +`python scripts/bundle.py generate --include-tros`. Review the generated manifest, +TRO source hashes and locations, and the resolved `uv.lock` together. Ordinary +local generation can emit limited provenance when a remote manifest is unavailable; +that output cannot pass the public release checks. + +Trusted same-repository PRs, Versioning, and Publish require: + +```bash +python scripts/check_release_credentials.py +python scripts/bundle.py check --published-spm --include-tros --strict-tros +python scripts/release_lock.py +``` + +Strict generation requires full US/UK release manifests from pinned revisions, +matching the raw hashes and URLs in the reviewed sidecars. It checks the existing +publisher compatibility contract and certified dataset hash, and requires pinned +model wheel hashes and URLs. The certified model must match the corresponding +runtime package name/version, any explicit component wheel pins, and the exact +wheel URL and SHA256 in the reviewed `uv.lock`. Missing credentials, fetch failures, missing datasets, +or changed source bytes stop the workflow. These metadata checks do not replace +native dataset certification or the model test suite. Supply a read-only Hugging +Face credential through the repository's `HUGGING_FACE_TOKEN` secret; PR verification +uses `pull_request` only for same-repository heads, never `pull_request_target`. +Each trusted verification job authenticates its actual credential with Hugging +Face's `whoami-v2` endpoint and requires access-token role `read`. Missing metadata, +write scope, and unrecognized or fine-grained permission schemas fail closed; +the check logs no token, account details, or response body. The workflow does not +change existing credential permissions. + +Versioning checks those reviewed inputs before the bump, generates the versioned +bundle metadata, and runs `python scripts/release_lock.py --refresh`. The helper +clears inherited uv resolver overrides, explicitly disables frozen mode, resolves +from PyPI without local sources, and permits only the root package version to change +in the reviewed lock. Every locked distribution must use an HTTPS artifact URL +on PyPI's `files.pythonhosted.org` host and a valid SHA256 digest; a registry source +label alone is insufficient. Any dependency graph change fails and restores the original +lock; stage such changes in a reviewed PR instead. The helper then runs the actual +`uv lock --check`. Final unpublished dependency pins must wait for registry +publication before these checks can pass. + +The bump changes the wrapper version, bundle identifiers, derived requirements and +citation fields, root lock version, and the TRO bundle-manifest hash/composition +fingerprint and wrapper-version fields. Remote data and model hashes, certification +fields, dataset identities, and fixed repository emission context remain unchanged. +TRO timestamps come from the pinned manifest, not the current clock. The changelog +uses Towncrier's release date and consumes the reviewed fragments. Run the actual +version helper, generator, strict checks, and lock refresh in a staging checkout +before merge, and compare the eventual published wheel against that receipt. + The key design decision is: - country `*-data` repos build and stage immutable data artifacts diff --git a/examples/employment_income_variation_us.py b/examples/employment_income_variation_us.py index 9ac2891f..b56c4e8a 100644 --- a/examples/employment_income_variation_us.py +++ b/examples/employment_income_variation_us.py @@ -101,7 +101,8 @@ def create_dataset_with_varied_employment_income( # Create household data household_data = { "household_id": list(range(n_households)), - "state_name": ["CA"] * n_households, # California + "state_code": ["CA"] * n_households, + "county_fips": ["06037"] * n_households, # Los Angeles County "household_weight": [1000.0] * n_households, } diff --git a/examples/household_impact_example.py b/examples/household_impact_example.py index 4b96cd96..0133dce9 100644 --- a/examples/household_impact_example.py +++ b/examples/household_impact_example.py @@ -61,11 +61,11 @@ def us_example() -> None: print("US household calculator") print("=" * 60) - # Single adult earning $50,000 in California. + # Single adult earning $50,000 in Los Angeles County, California. single = pe.us.calculate_household( people=[{"age": 35, "employment_income": 50_000}], tax_unit={"filing_status": "SINGLE"}, - household={"state_code_str": "CA"}, + household={"state_code": "CA", "county_fips": "06037"}, year=2026, ) print("\nSingle adult, $50k income (California):") @@ -73,7 +73,9 @@ def us_example() -> None: print(f" Income tax: ${single.tax_unit.income_tax:,.0f}") print(f" Payroll tax: ${single.tax_unit.employee_payroll_tax:,.0f}") - # Married couple with two kids, Texas, lower income. + # Married couple with two kids, Texas, lower income. Explicit national + # measurement is recorded in the result; state alone does not select an + # SPM area. Use an observed county_fips for local SPM measurement instead. family = pe.us.calculate_household( people=[ {"age": 35, "employment_income": 40_000}, @@ -82,8 +84,9 @@ def us_example() -> None: {"age": 5}, ], tax_unit={"filing_status": "JOINT"}, - household={"state_code_str": "TX"}, + household={"state_code": "TX"}, year=2026, + spm={"geography_kind": "national"}, ) print("\nMarried couple with 2 children, $40k income (Texas):") print(f" Net income: ${family.household.household_net_income:,.0f}") @@ -91,6 +94,10 @@ def us_example() -> None: print(f" EITC: ${family.tax_unit.eitc:,.0f}") print(f" Child tax credit: ${family.tax_unit.ctc:,.0f}") print(f" SNAP: ${family.spm_unit.snap:,.0f}") + receipt = family.to_dict()["provenance"]["spm"] + print(f" SPM geography: {receipt['geography_kind']}") + print(f" SPM scenario: {receipt['scenario']}") + print(f" SPM artifact hash: {receipt['forecast_sha256']}") def main() -> None: From f07048d750ee2a53c7c7dde2fbed99205897fc8a Mon Sep 17 00:00:00 2001 From: Max Ghenis Date: Fri, 11 Sep 2026 12:44:15 -0400 Subject: [PATCH 09/46] Record the opened draft PR and its CI results Co-Authored-By: Claude Fable 5.1 --- PROGRESS.md | 77 ++++++++++++++++++++++++++++++++--------------------- 1 file changed, 47 insertions(+), 30 deletions(-) diff --git a/PROGRESS.md b/PROGRESS.md index e7a13479..c5ca5070 100644 --- a/PROGRESS.md +++ b/PROGRESS.md @@ -2,46 +2,63 @@ ## State -Turning the canonical SPM wrapper worktree -(`max/spm-canonical-wrapper-release-20260910`, base `6a56ced4` = `origin/main`) -into a reviewable draft PR against `main` with hosted CI. +**Complete.** Draft PR https://github.com/PolicyEngine/policyengine.py/pull/515 +is open against `main` from `max/spm-canonical-wrapper-release-20260910`, with +eight commits and hosted CI run. #512 is left open and untouched, referenced as +superseded. -Verified at start: `git rev-parse origin/main` = `6a56ced4f959ce9a1f3b794389a4b42699de8abc` -= local HEAD. `git ls-remote origin refs/heads/max/spm-canonical-wrapper-release-20260910` -is empty, so the branch does not yet exist on origin. +Hosted CI: nine checks pass (changelog, Lint, Mypy, Verify bundle metadata, +Install + smoke-import on 3.11/3.12/3.13/3.14, docs build); the four `Test` +jobs fail, each with `39 failed, 1005 passed, 14 skipped` — identical to the +local run, one root cause. ## Done - Read background: production checkout handoff/output/progress/native-loader reports and `rollout/wrapper-loader-candidate/FINAL-REPORT.md`. -- Step 1 file-set comparison. release-final carries 72 changed/untracked paths; - production carries 70. Release-final adds exactly six: the TRO schema fix - (`src/policyengine/data/schemas/trace_tro.schema.json`, regenerated - `uk`/`us.trace.tro.jsonld`, `tests/test_generate_trace_tros.py`, - `changelog.d/repository-tro-schema.fixed.md`) plus a regenerated `uv.lock`. - Production holds only four extra paths, all working reports that must not be - committed. 55 of the 66 common paths are byte-identical; 11 differ. +- Step 1 file-set comparison. release-final carries 72 changed/untracked paths, + production 70. release-final adds exactly six (the TRO schema fix plus a + regenerated `uv.lock`); production holds only four extra working reports, + which were not committed. 55 of 66 common paths are byte-identical. + Eleven differ; after adversarial verification, four differ beyond the rebase, + the TRO schema fix and the Python floor bump — all a deliberate, internally + consistent relocation of release gates out of PR CI into `push.yaml`, plus + extra `importorskip` gating release-final has and production lacks. - Verified the TRO sidecars rebind to the working-tree manifest hash `d430b8e561d76e5836a17bf506b06b9cd16560eb77f15718dd46f4e78513de85`. -- Step 2 safety audit: all 36 untracked paths are text, 195 KB total. No wheels, - venv, `__pycache__`, `.h5` or binaries; no secret-shaped strings; no - `/Users/...` paths. Every `/tmp/...` occurrence is a runbook example or a - rejection sentinel in a test, never a default in committed logic. -- Step 4 local checks (see the report for exit codes). Green: ruff format, ruff - check, `uv pip check`, `scripts/bundle.py check`, `scripts/release_lock.py`, - `policyengine bundle verify`, the Python-Compat smoke import. -- `make test` FAILS: 39 failed, 1005 passed, 14 skipped, exit 1. All 39 share - one cause: `ModuleNotFoundError: No module named - 'spm_calculator.policyengine_adapter'`, from the new lazy validation imports - in `us/model.py` and `us/household.py`. -- Established why no pin bump fixes that. `spm-calculator 1.0.0` reached PyPI +- Step 2 safety audit: all 36 untracked paths are text, 195 KB total. No + wheels, venv, `__pycache__`, `.h5` or binaries; no secret-shaped strings; no + `/Users/...` paths. Excluded the four production report files, and `.coverage` + produced by my own test run. +- Corrected a provably false paragraph in the new runbook: it claimed + `TestVendoredSidecarBinding` "currently fails" and cited superseded hashes. + Verified the test now passes and rewrote the paragraph around the real + binding. +- Step 3: seven logical commits, sentence-case subjects, each ending with the + `Co-Authored-By: Claude Fable 5.1` trailer. All 72 paths land in exactly one + commit; working tree clean afterwards. +- Step 4 local checks. Green: `make install`, ruff format, ruff check, + `uv pip check`, `check-changelog.sh`, `bundle.py check`, `release_lock.py`, + `policyengine bundle verify`, smoke import, `--collect-only`. `make test` + exits 1: 39 failed, 1005 passed, 14 skipped. `mypy` exits 2 but its CI step + is non-blocking and fails the same way on `main`. +- Established why no pin bump fixes the 39. `spm-calculator 1.0.0` reached PyPI today (2026-09-11T15:45:10Z), but `policyengine-us 2.0.0` has not (latest 1.825.0). Probed in a throwaway venv: with calculator 1.0.0, country model - 1.764.6 cannot even load its variables (`spm_unit_geographic_adjustment` - imports `spm_calculator.geoadj`, removed in 1.0.0). So the canonical tuple - needs policyengine-us 2.0.0, which is unpublished. + 1.764.6 cannot load its own variables, because + `spm_unit_geographic_adjustment` imports `spm_calculator.geoadj`, removed in + 1.0.0. Publishing policyengine-us 2.0.0 is the gate. +- Steps 5-7: pushed the branch (it did not exist on origin), opened the draft + PR, and watched CI to completion. ## Next -- Step 3: logical commits with the Fable 5.1 co-author trailer. -- Step 5: push branch. Step 6: draft PR. Step 7: watch CI and report. +Not this lane's work, recorded for the coordinator: + +- Publish `policyengine-us 2.0.0` (and then wrapper 6.0.0), or make the US SPM + path conditional on canonical availability — the `spm=` Simulation keyword as + well as the adapter import. Until then the four `Test` jobs stay red. +- Data certification, the producer's immutable source-enrichment release, and + population/API acceptance remain separate gates. The five historical US + snapshot differences under country 1.824.7, including January-only annual + SNAP, remain a country-owner investigation. From 223bce477c59fe04ec40fce918fcb722f2d143ae Mon Sep 17 00:00:00 2001 From: Max Ghenis Date: Fri, 11 Sep 2026 13:30:19 -0400 Subject: [PATCH 10/46] Justify the SPM receipt dataset by its real reason The receipt is stored as a UTF-8 JSON dataset rather than an HDF5 attribute. The comment justified that by saying receipts can exceed HDF5's attribute-size limit, and the pull request body repeated it. That limit is an HDF5 1.6-era compact-storage threshold, lifted in 1.8. On the supported stack (h5py 3.16.0, libhdf5 2.0.0) attributes of 65KB, 110KB, 282KB and 5MB all write and read back byte-identical from a file created by pandas' HDFStore, with the frames still readable afterwards. Keep the dataset, which is the better choice on its own merits, and state the reason that actually holds. No behaviour change. Co-Authored-By: Claude Opus 5 --- PROGRESS.md | 37 +++++++++++++++++-- changelog.d/spm-receipt-comment.fixed.md | 1 + .../common/model_version.py | 5 ++- 3 files changed, 37 insertions(+), 6 deletions(-) create mode 100644 changelog.d/spm-receipt-comment.fixed.md diff --git a/PROGRESS.md b/PROGRESS.md index c5ca5070..03ac9c63 100644 --- a/PROGRESS.md +++ b/PROGRESS.md @@ -2,10 +2,15 @@ ## State -**Complete.** Draft PR https://github.com/PolicyEngine/policyengine.py/pull/515 -is open against `main` from `max/spm-canonical-wrapper-release-20260910`, with -eight commits and hosted CI run. #512 is left open and untouched, referenced as -superseded. +**Complete, and independently re-verified.** Draft PR +https://github.com/PolicyEngine/policyengine.py/pull/515 is open against `main` +from `max/spm-canonical-wrapper-release-20260910`, with ten commits and hosted CI +run. #512 is left open and untouched, referenced as superseded. + +The lane was cut off by a rate-limit re-pick after opening the PR and before +writing its report, leaving the deliverable at 0 bytes. The resumed lane re-ran +every local check from scratch, re-derived the file-set comparison, and put the +PR body's mechanism claims through a six-way adversarial audit. Hosted CI: nine checks pass (changelog, Lint, Mypy, Verify bundle metadata, Install + smoke-import on 3.11/3.12/3.13/3.14, docs build); the four `Test` @@ -50,6 +55,30 @@ local run, one root cause. 1.0.0. Publishing policyengine-us 2.0.0 is the gate. - Steps 5-7: pushed the branch (it did not exist on origin), opened the draft PR, and watched CI to completion. +- Re-verification pass. Corrected the file-set numbers: 73 release-final paths + (not 72), 67 common, 54 byte-identical, 13 differing (not 11), and production + holds three exclusive reports, not four — `PROGRESS.md` exists in both + checkouts under the same name. Of the 13, two are pure rebase artifacts + (`origin/main` already carries 5.3.1 and the `spm-calculator` + `country_dependency` pin), three are consequences of the rebase, two are the + TRO schema fix and the Python floor, four are deliberate divergences, and two + are non-code. +- Six-cluster adversarial audit of the PR body against source. Of ten claims an + auditor flagged, nine were refuted and stand as written. One survived: the + body and the source comment both justified storing the SPM receipt as an H5 + dataset by saying receipts "exceed HDF5's attribute-size limit". That limit + was an HDF5 1.6-era compact-storage threshold lifted in 1.8. Confirmed by + direct test on the pinned stack (h5py 3.16.0, libhdf5 2.0.0): attributes of + 65KB, 110KB, 282KB and 5MB all write and round-trip byte-identical into a + `pd.HDFStore(mode="w")` file, with pandas still reading the frames after. + The storage choice is sound and correctly described; only the stated + necessity was false. Rewrote the comment and the PR body around the real + reason. No behaviour change. +- Independently confirmed the integrity-critical claim: nothing is stubbed, + mocked, faked or vendored for the canonical tuple. `src/policyengine/` + contains no `ModuleType` or `sys.modules[...]` assignment at all, and the one + `types.ModuleType('policyengine_us.spm')` in the tree is an adversarial test + that plants a foreign module and asserts the loader rejects it. ## Next diff --git a/changelog.d/spm-receipt-comment.fixed.md b/changelog.d/spm-receipt-comment.fixed.md new file mode 100644 index 00000000..6e03aeea --- /dev/null +++ b/changelog.d/spm-receipt-comment.fixed.md @@ -0,0 +1 @@ +Corrected the SPM receipt storage comment, which justified the H5 dataset by an HDF5 attribute-size limit that the supported h5py range does not have. diff --git a/src/policyengine/tax_benefit_models/common/model_version.py b/src/policyengine/tax_benefit_models/common/model_version.py index 54466728..1d6982b9 100644 --- a/src/policyengine/tax_benefit_models/common/model_version.py +++ b/src/policyengine/tax_benefit_models/common/model_version.py @@ -376,8 +376,9 @@ def save(self, simulation: Simulation) -> None: ) simulation.output_dataset.save() if serialized_spm is not None: - # Receipts can exceed HDF5's attribute-size limit. Store UTF-8 - # JSON in a dataset, keeping the result file self-contained. + # Store UTF-8 JSON in a dataset rather than an attribute: the + # payload size stays unbounded and the receipt is kept out of + # the object header, keeping the result file self-contained. with h5py.File(simulation.output_dataset.filepath, "a") as stream: stream.create_dataset( "policyengine_spm", From e49e5e5948c8562bf3a182c1e4867649a49f98aa Mon Sep 17 00:00:00 2001 From: Max Ghenis Date: Fri, 11 Sep 2026 21:18:06 -0400 Subject: [PATCH 11/46] Record verified prerequisites for the canonical release Co-Authored-By: Claude Fable 5.1 --- PROGRESS.md | 135 ++++++++++++++++++++-------------------------------- 1 file changed, 52 insertions(+), 83 deletions(-) diff --git a/PROGRESS.md b/PROGRESS.md index 03ac9c63..67638374 100644 --- a/PROGRESS.md +++ b/PROGRESS.md @@ -1,93 +1,62 @@ -# Canonical SPM wrapper release PR +# Bind the canonical wrapper to published country 2.0.0; prepare the 6.0.0 release commit ## State -**Complete, and independently re-verified.** Draft PR -https://github.com/PolicyEngine/policyengine.py/pull/515 is open against `main` -from `max/spm-canonical-wrapper-release-20260910`, with ten commits and hosted CI -run. #512 is left open and untouched, referenced as superseded. +**In progress.** Prerequisites all verified; repinning the bundle manifest to the +canonical tuple next. Branch `max/spm-canonical-wrapper-release-20260910`, PR #515 +(draft, open, mergeable), starting head `223bce47`, base `origin/main` `6a56ced4`. + +## Prerequisites (all verified 2026-09-11) + +| Item | Value | +|---|---| +| policyengine-us 2.0.0 on PyPI | HTTP 200; wheel sha256 `4ca187509efc2f7d3ace5035c27de88c744d0f112fdc7da027372f3ae07c540a`, uploaded 2026-09-12T01:11:24Z | +| policyengine-us 2.0.0 deps | `policyengine-core>=3.30.1`, `spm-calculator==1.0.0`, requires-python `<3.15,>=3.11` | +| spm-calculator 1.0.0 on PyPI | HTTP 200; wheel sha256 `e354937a5e1a4045d4966ed594a528d8b02866fabaac9bb5672017004b627305` (matches brief) | +| policyengine-core 3.32.5 on PyPI | HTTP 200; wheel sha256 `9d8c162d5fe5c784ae16c885da3ffbfc39a536add0e111133144d8edd555935f` | +| Core 3.32.5 was the acceptance runtime | `rollout/final-core-matrix-20260911-r2/FINAL-REPORT.md` records Core 3.32.5 `9d8c162d…` — byte-identical to the published wheel. Both gate conditions met. | +| HF tag `populace-us-2024-spm-20260909` | exists on `policyengine/populace-us`; tag commit `9a814a3b3b53c0ecd6e1737b6ec862c31300ef6f` | +| H5 it resolves to | `populace_us_2024.h5` LFS sha256 `6496cc4393d4d3c6574f76eca231de5898c803b9067645591fd5c4d3e65aee84` (matches brief) | +| Certified release manifest | `releases/populace-us-2024-spm-20260909/release_manifest.json`, sha256 `6c39eb1e61afc1629f2abe57032dced9c0750c60e42b9acad812c7b3476a2989` — the local certified file at `rollout/h5-producer-20260911/certified/…` is byte-identical to the Hub-published one | +| PR #515 | draft, open, MERGEABLE, head `223bce47` == local HEAD == origin branch | +| PR #515 CI at dispatch | 9 pass, 4 fail (`Test 3.11/3.12/3.13/3.14`) | + +## Mechanism established by reading source (not assumed) + +- `pyproject.toml`'s `[project.optional-dependencies]` is **generated** from the bundle + manifest — `scripts/generate_bundle_artifacts.py:97-143` rewrites the whole block from + `bundle["extras"]` × `bundle["packages"]`. Pins are therefore edited in + `src/policyengine/data/bundle/manifest.json`, never by hand in pyproject. +- The wrapper **version** is owned by CI, not by this commit: `.github/workflows/push.yaml:136-137` + Versioning runs `make changelog` → `.github/bump_version.py`, whose `infer_bump` + (`bump_version.py:70-88`) returns `major` when any `.breaking.` fragment exists, and + `bump_version.py:91-98` maps 5.3.1 → 6.0.0. So `pyproject` `version` stays 5.3.1 in this PR. +- `bump_version.sync_bundle_versions` (`:120-147`) also owns the manifest's own + `bundle_version` / `policyengine_version` / `packages.policyengine.version` / + per-country `bundle_id`. Those stay 5.3.1 here too. -The lane was cut off by a rate-limit re-pick after opening the PR and before -writing its report, leaving the deliverable at 0 bytes. The resumed lane re-ran -every local check from scratch, re-derived the file-set comparison, and put the -PR body's mechanism claims through a six-way adversarial audit. +## Done -Hosted CI: nine checks pass (changelog, Lint, Mypy, Verify bundle metadata, -Install + smoke-import on 3.11/3.12/3.13/3.14, docs build); the four `Test` -jobs fail, each with `39 failed, 1005 passed, 14 skipped` — identical to the -local run, one root cause. +- Verified every prerequisite above from the authoritative source (PyPI JSON API, HF Hub + API, `gh pr view`), not from a prior lane's report. +- Read the release machinery: `scripts/bundle.py`, `generate_bundle_artifacts.py`, + `prepare_package_bundle_update.py`, `certify_data_release.py`, `spm_bundle.py`, + `Makefile`, `.github/workflows/push.yaml`, `.github/bump_version.py`. +- Confirmed `changelog.d/` already holds two `.breaking.md` fragments + (`canonical-spm-python.breaking.md`, `canonical-spm-wrapper.breaking.md`), so the + automatic bump is already major. -## Done +## Open question being investigated -- Read background: production checkout handoff/output/progress/native-loader - reports and `rollout/wrapper-loader-candidate/FINAL-REPORT.md`. -- Step 1 file-set comparison. release-final carries 72 changed/untracked paths, - production 70. release-final adds exactly six (the TRO schema fix plus a - regenerated `uv.lock`); production holds only four extra working reports, - which were not committed. 55 of 66 common paths are byte-identical. - Eleven differ; after adversarial verification, four differ beyond the rebase, - the TRO schema fix and the Python floor bump — all a deliberate, internally - consistent relocation of release gates out of PR CI into `push.yaml`, plus - extra `importorskip` gating release-final has and production lacks. -- Verified the TRO sidecars rebind to the working-tree manifest hash - `d430b8e561d76e5836a17bf506b06b9cd16560eb77f15718dd46f4e78513de85`. -- Step 2 safety audit: all 36 untracked paths are text, 195 KB total. No - wheels, venv, `__pycache__`, `.h5` or binaries; no secret-shaped strings; no - `/Users/...` paths. Excluded the four production report files, and `.coverage` - produced by my own test run. -- Corrected a provably false paragraph in the new runbook: it claimed - `TestVendoredSidecarBinding` "currently fails" and cited superseded hashes. - Verified the test now passes and rewrote the paragraph around the real - binding. -- Step 3: seven logical commits, sentence-case subjects, each ending with the - `Co-Authored-By: Claude Fable 5.1` trailer. All 72 paths land in exactly one - commit; working tree clean afterwards. -- Step 4 local checks. Green: `make install`, ruff format, ruff check, - `uv pip check`, `check-changelog.sh`, `bundle.py check`, `release_lock.py`, - `policyengine bundle verify`, smoke import, `--collect-only`. `make test` - exits 1: 39 failed, 1005 passed, 14 skipped. `mypy` exits 2 but its CI step - is non-blocking and fails the same way on `main`. -- Established why no pin bump fixes the 39. `spm-calculator 1.0.0` reached PyPI - today (2026-09-11T15:45:10Z), but `policyengine-us 2.0.0` has not (latest - 1.825.0). Probed in a throwaway venv: with calculator 1.0.0, country model - 1.764.6 cannot load its own variables, because - `spm_unit_geographic_adjustment` imports `spm_calculator.geoadj`, removed in - 1.0.0. Publishing policyengine-us 2.0.0 is the gate. -- Steps 5-7: pushed the branch (it did not exist on origin), opened the draft - PR, and watched CI to completion. -- Re-verification pass. Corrected the file-set numbers: 73 release-final paths - (not 72), 67 common, 54 byte-identical, 13 differing (not 11), and production - holds three exclusive reports, not four — `PROGRESS.md` exists in both - checkouts under the same name. Of the 13, two are pure rebase artifacts - (`origin/main` already carries 5.3.1 and the `spm-calculator` - `country_dependency` pin), three are consequences of the rebase, two are the - TRO schema fix and the Python floor, four are deliberate divergences, and two - are non-code. -- Six-cluster adversarial audit of the PR body against source. Of ten claims an - auditor flagged, nine were refuted and stand as written. One survived: the - body and the source comment both justified storing the SPM receipt as an H5 - dataset by saying receipts "exceed HDF5's attribute-size limit". That limit - was an HDF5 1.6-era compact-storage threshold lifted in 1.8. Confirmed by - direct test on the pinned stack (h5py 3.16.0, libhdf5 2.0.0): attributes of - 65KB, 110KB, 282KB and 5MB all write and round-trip byte-identical into a - `pd.HDFStore(mode="w")` file, with pandas still reading the frames after. - The storage choice is sound and correctly described; only the stated - necessity was false. Rewrote the comment and the PR body around the real - reason. No behaviour change. -- Independently confirmed the integrity-critical claim: nothing is stubbed, - mocked, faked or vendored for the canonical tuple. `src/policyengine/` - contains no `ModuleType` or `sys.modules[...]` assignment at all, and the one - `types.ModuleType('policyengine_us.spm')` in the tree is an adversarial test - that plants a foreign module and asserts the loader rejects it. +Three different sha256 values for a "policyengine-us 2.0.0" wheel exist across today's +lanes: published `4ca18750…`, acceptance-matrix `4208998a…`, loader-candidate `05c729ae…`. +The first is the only one on PyPI. Determining whether the matrix's numerical acceptance +transfers to the published wheel bytes. ## Next -Not this lane's work, recorded for the coordinator: - -- Publish `policyengine-us 2.0.0` (and then wrapper 6.0.0), or make the US SPM - path conditional on canonical availability — the `spm=` Simulation keyword as - well as the adapter import. Until then the four `Test` jobs stay red. -- Data certification, the producer's immutable source-enrichment release, and - population/API acceptance remain separate gates. The five historical US - snapshot differences under country 1.824.7, including January-only annual - SNAP, remain a country-owner investigation. +1. Repin the bundle manifest to the canonical tuple and regenerate pyproject. +2. Certify the US data release at the tag above into the manifest. +3. Refresh `uv.lock` (uv only, no local wheel links) and run the lock/release guards. +4. Run the full suite as CI does; the SPM tests must run, not skip. +5. Commit, force-with-lease push against `223bce47`, watch CI. Leave draft. From a70801927719f2c022281613ed1e0fecc4b68b3a Mon Sep 17 00:00:00 2001 From: Max Ghenis Date: Fri, 11 Sep 2026 21:23:04 -0400 Subject: [PATCH 12/46] Keep Populace rules armed for source-enrichment releases The Populace US producer publishes source-enrichment releases under the data package name microcosm-data and labels release-directory sidecars with the evidence kind. Four certification rules matched the literal populace-data and the release-directory path rewrite matched only the diagnostics kind, so a source-enrichment release silently bypassed the per-artifact reachability check and pinned its sidecars at repo-root paths. Certifying populace-us-2024-spm-20260909 under the old rules produced a bundle in which eight of ten certified artifact URIs returned 404; with both names and both kinds recognised, all ten resolve. Co-Authored-By: Claude Fable 5.1 --- src/policyengine/provenance/certification.py | 17 ++-- tests/test_certify_data_release.py | 91 ++++++++++++++++++++ 2 files changed, 103 insertions(+), 5 deletions(-) diff --git a/src/policyengine/provenance/certification.py b/src/policyengine/provenance/certification.py index 73e4f1db..19d08ff0 100644 --- a/src/policyengine/provenance/certification.py +++ b/src/policyengine/provenance/certification.py @@ -70,6 +70,13 @@ # check, so a claims-certified bundle stays importable without network access. BASIS_PUBLISHER_CLAIM = "legacy_compatible_model_package" POPULACE_US_SOURCE_COVERAGE_FILE = "us_source_coverage.json" +# The Populace US producer publishes under `microcosm-data` since the +# source-enrichment releases; `populace-data` is its earlier name. Both denote +# the same producer, so every Populace-specific rule below must accept either. +POPULACE_DATA_PACKAGES = frozenset({"populace-data", "microcosm-data"}) +# Artifact kinds published beside the release manifest rather than at the repo +# root, so their manifest paths are relative to the release directory. +RELEASE_SCOPED_ARTIFACT_KINDS = frozenset({"diagnostics", "evidence"}) US_STATE_CODES = ( "AL", "AK", @@ -322,7 +329,7 @@ def artifact_path_for_country_manifest(artifact, uri_parts: dict) -> str: path = artifact.path release_dir = release_manifest_dir(uri_parts) if ( - artifact.kind == "diagnostics" + artifact.kind in RELEASE_SCOPED_ARTIFACT_KINDS and artifact.repo_id == uri_parts["repo_id"] and artifact.revision == uri_parts["revision"] and not path.startswith(f"{release_dir}/") @@ -370,7 +377,7 @@ def required_supplemental_release_files( ) -> tuple[str, ...]: if ( country == "us" - and manifest.data_package.name == "populace-data" + and manifest.data_package.name in POPULACE_DATA_PACKAGES and uri_parts["repo_id"] == "policyengine/populace-us" ): return (POPULACE_US_SOURCE_COVERAGE_FILE,) @@ -384,7 +391,7 @@ def should_validate_vendored_artifacts( ) -> bool: return ( country == "us" - and manifest.data_package.name == "populace-data" + and manifest.data_package.name in POPULACE_DATA_PACKAGES and uri_parts["repo_id"] == "policyengine/populace-us" ) @@ -489,7 +496,7 @@ def build_country_manifest_payload( for name, artifact in manifest.artifacts.items(): if ( country == "us" - and manifest.data_package.name == "populace-data" + and manifest.data_package.name in POPULACE_DATA_PACKAGES and artifact.path.endswith(".h5") and ( artifact.path.startswith("states/") @@ -513,7 +520,7 @@ def build_country_manifest_payload( for region, template in sorted(raw_regions.items()): if ( country == "us" - and manifest.data_package.name == "populace-data" + and manifest.data_package.name in POPULACE_DATA_PACKAGES and region in {"state", "congressional_district"} ): continue diff --git a/tests/test_certify_data_release.py b/tests/test_certify_data_release.py index ebc8a9fe..9b25a1db 100644 --- a/tests/test_certify_data_release.py +++ b/tests/test_certify_data_release.py @@ -16,6 +16,8 @@ certify_data_release, merge_us_state_release_manifest, parse_manifest_uri, + required_supplemental_release_files, + should_validate_vendored_artifacts, validate_release_manifest, ) from policyengine.provenance.manifest import DataReleaseManifest @@ -741,3 +743,92 @@ def test__given_vendored_bundle_manifest__then_tro_sidecar_binds_it(self): assert bundle_manifest["trov:sha256"] == expected performance = tro["@graph"][0]["trov:hasPerformance"] assert performance["pe:emittedIn"] == "repository-bundle" + + +def _source_enrichment_manifest_payload() -> dict: + """A `microcosm-data` release whose sidecars use the `evidence` kind. + + This is the shape the Populace US producer publishes for source-enrichment + releases: the data package is named `microcosm-data` rather than + `populace-data`, and release-directory sidecars are labelled `evidence` + rather than `diagnostics`. + """ + payload = _release_manifest_payload() + payload["data_package"]["name"] = "microcosm-data" + payload["artifacts"]["us_source_coverage"]["kind"] = "evidence" + payload["artifacts"]["build_manifest"] = { + "kind": "evidence", + "path": "build_manifest.json", + "repo_id": "policyengine/populace-us", + "revision": TAG, + "sha256": "1" * 64, + "size_bytes": 1, + } + return payload + + +class TestSourceEnrichmentProducerRelease: + """The `microcosm-data` producer name must keep every Populace rule armed. + + A release published under the newer producer name previously fell through + every `populace-data` guard, which both skipped the per-artifact + reachability check and left release-scoped sidecars pinned at repo-root + paths that do not resolve. + """ + + def _payload(self) -> dict: + return build_country_manifest_payload( + country="us", + manifest=DataReleaseManifest.model_validate( + _source_enrichment_manifest_payload() + ), + uri_parts=parse_manifest_uri(MANIFEST_URI), + policyengine_version="9.9.9", + model_package="policyengine-us", + model_version="1.723.0", + model_wheel={"sha256": "d" * 64, "url": "https://example/wheel"}, + ) + + def test__given_evidence_kind_sidecar__then_rewrites_to_release_path(self): + datasets = self._payload()["datasets"] + + assert datasets["us_source_coverage"]["path"] == ( + f"releases/{TAG}/us_source_coverage.json" + ) + assert datasets["build_manifest"]["path"] == ( + f"releases/{TAG}/build_manifest.json" + ) + + def test__given_microdata_kind__then_keeps_repo_root_path(self): + datasets = self._payload()["datasets"] + + assert datasets["populace_us_2024"]["path"] == "populace_us_2024.h5" + assert ( + datasets["populace_us_2024_calibration"]["path"] + == "populace_us_2024_calibration.npz" + ) + + def test__given_microcosm_producer__then_validates_vendored_artifacts(self): + assert should_validate_vendored_artifacts( + "us", + DataReleaseManifest.model_validate(_source_enrichment_manifest_payload()), + parse_manifest_uri(MANIFEST_URI), + ) + + def test__given_microcosm_producer__then_requires_source_coverage_file(self): + assert required_supplemental_release_files( + "us", + DataReleaseManifest.model_validate(_source_enrichment_manifest_payload()), + parse_manifest_uri(MANIFEST_URI), + ) == ("us_source_coverage.json",) + + def test__given_microcosm_producer__then_still_omits_area_h5_artifacts(self): + datasets = self._payload()["datasets"] + + assert "states/AK" not in datasets + assert "districts/CA-01" not in datasets + + def test__given_microcosm_producer__then_still_drops_area_region_templates(self): + assert self._payload()["region_datasets"] == { + "national": {"path_template": "populace_us_2024.h5"} + } From e14c7d2643e35f226f31e88525aea683e699fdb1 Mon Sep 17 00:00:00 2001 From: Max Ghenis Date: Fri, 11 Sep 2026 21:23:16 -0400 Subject: [PATCH 13/46] Bind the wrapper to the published canonical tuple Pin policyengine-us 2.0.0, policyengine-core 3.32.5 and spm-calculator 1.0.0 across the us, models and dev extras, and certify the US data release populace-us-2024-spm-20260909 into the bundle manifest. UK pins are unchanged: policyengine-uk 2.90.2 requires policyengine-core>=3.30.1, which 3.32.5 satisfies. The extras are generated from the bundle manifest, so the pins were moved through the repository tooling: bundle.py update-packages for the country and core versions, bundle.py set-spm --calculator-version to promote spm-calculator from a protective country_dependency pin to a registry-verified runtime_dependency carrying the published wheel sha256, and bundle.py certify-data for the dataset. The wrapper version stays 5.3.1 here because push.yaml's Versioning job owns it. Co-Authored-By: Claude Fable 5.1 --- .../canonical-certification-guards.fixed.md | 1 + changelog.d/canonical-tuple-pins.changed.md | 1 + ...s-populace-us-2024-spm-20260909.changed.md | 1 + pyproject.toml | 20 +-- src/policyengine/data/bundle/manifest.json | 116 +++++++++++------- 5 files changed, 84 insertions(+), 55 deletions(-) create mode 100644 changelog.d/canonical-certification-guards.fixed.md create mode 100644 changelog.d/canonical-tuple-pins.changed.md create mode 100644 changelog.d/certify-us-populace-us-2024-spm-20260909.changed.md diff --git a/changelog.d/canonical-certification-guards.fixed.md b/changelog.d/canonical-certification-guards.fixed.md new file mode 100644 index 00000000..08ba107f --- /dev/null +++ b/changelog.d/canonical-certification-guards.fixed.md @@ -0,0 +1 @@ +Recognise the `microcosm-data` producer name and the `evidence` artifact kind when certifying a US data release, so source-enrichment releases keep the per-artifact reachability check and pin their release-directory sidecars at paths that resolve. diff --git a/changelog.d/canonical-tuple-pins.changed.md b/changelog.d/canonical-tuple-pins.changed.md new file mode 100644 index 00000000..2702de27 --- /dev/null +++ b/changelog.d/canonical-tuple-pins.changed.md @@ -0,0 +1 @@ +Pin the canonical PolicyEngine tuple: policyengine-us 2.0.0, policyengine-core 3.32.5 and spm-calculator 1.0.0. diff --git a/changelog.d/certify-us-populace-us-2024-spm-20260909.changed.md b/changelog.d/certify-us-populace-us-2024-spm-20260909.changed.md new file mode 100644 index 00000000..3a021af1 --- /dev/null +++ b/changelog.d/certify-us-populace-us-2024-spm-20260909.changed.md @@ -0,0 +1 @@ +Certify the US populace data release `populace-us-2024-spm-20260909` (populace_us_2024, policyengine-us 2.0.0) into the PolicyEngine bundle manifest. diff --git a/pyproject.toml b/pyproject.toml index 22c29404..7324d429 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -40,19 +40,19 @@ graph = [ "networkx>=3.0", ] models = [ - "policyengine-core==3.30.1", - "policyengine-us==1.764.6", + "policyengine-core==3.32.5", + "policyengine-us==2.0.0", "policyengine-uk==2.90.2", - "spm-calculator==0.3.1", + "spm-calculator==1.0.0", ] uk = [ - "policyengine-core==3.30.1", + "policyengine-core==3.32.5", "policyengine-uk==2.90.2", ] us = [ - "policyengine-core==3.30.1", - "policyengine-us==1.764.6", - "spm-calculator==0.3.1", + "policyengine-core==3.32.5", + "policyengine-us==2.0.0", + "spm-calculator==1.0.0", ] dev = [ "pytest", @@ -68,10 +68,10 @@ dev = [ "towncrier>=24.8.0", "mypy>=1.11.0", "pytest-cov>=5.0.0", - "policyengine-core==3.30.1", - "policyengine-us==1.764.6", + "policyengine-core==3.32.5", + "policyengine-us==2.0.0", "policyengine-uk==2.90.2", - "spm-calculator==0.3.1", + "spm-calculator==1.0.0", ] [tool.setuptools] diff --git a/src/policyengine/data/bundle/manifest.json b/src/policyengine/data/bundle/manifest.json index 8b787240..10f1d422 100644 --- a/src/policyengine/data/bundle/manifest.json +++ b/src/policyengine/data/bundle/manifest.json @@ -111,80 +111,104 @@ "version": "policyengine-uk-data-1.56.16" }, "us": { - "build_id": "populace-us-2024-buildp-sparse-rmloss100-cae8640-20260728T011454Z", + "build_id": "populace-us-2024-spm-20260909", "bundle_id": "us-5.3.1", "certification": { - "built_with_model_version": "1.764.6", + "built_with_model_version": "2.0.0", "certified_by": "policyengine.py bundle certification", - "certified_for_model_version": "1.764.6", + "certified_for_model_version": "2.0.0", "compatibility_basis": "built_with_model_package", - "data_build_id": "populace-us-2024-buildp-sparse-rmloss100-cae8640-20260728T011454Z" + "data_build_id": "populace-us-2024-spm-20260909" }, "certified_data_artifact": { - "build_id": "populace-us-2024-buildp-sparse-rmloss100-cae8640-20260728T011454Z", + "build_id": "populace-us-2024-spm-20260909", "data_package": { - "name": "populace-data", + "name": "microcosm-data", "version": "0.1.0" }, "dataset": "populace_us_2024", - "sha256": "48b9d479fb4fd1c3537f9383ce4697d130b6f618658409d74f6233c43b994c7e", - "uri": "hf://policyengine/populace-us/populace_us_2024.h5@populace-us-2024-buildp-sparse-rmloss100-cae8640-20260728T011454Z" + "sha256": "6496cc4393d4d3c6574f76eca231de5898c803b9067645591fd5c4d3e65aee84", + "uri": "hf://policyengine/populace-us/populace_us_2024.h5@populace-us-2024-spm-20260909" }, "country_id": "us", "data_package": { - "name": "populace-data", - "release_manifest_path": "releases/populace-us-2024-buildp-sparse-rmloss100-cae8640-20260728T011454Z/release_manifest.json", - "release_manifest_revision": "populace-us-2024-buildp-sparse-rmloss100-cae8640-20260728T011454Z", + "name": "microcosm-data", + "release_manifest_path": "releases/populace-us-2024-spm-20260909/release_manifest.json", + "release_manifest_revision": "populace-us-2024-spm-20260909", "repo_id": "policyengine/populace-us", "repo_type": "dataset", "version": "0.1.0" }, "data_producer": "populace", "datasets": { + "build_manifest": { + "path": "releases/populace-us-2024-spm-20260909/build_manifest.json", + "repo_id": "policyengine/populace-us", + "revision": "populace-us-2024-spm-20260909", + "sha256": "73497b656d66eafab042f88ae60d1405aa14570984f8da51a968a676afafca3b" + }, "calibration_diagnostics": { - "path": "releases/populace-us-2024-buildp-sparse-rmloss100-cae8640-20260728T011454Z/calibration_diagnostics.json", + "path": "releases/populace-us-2024-spm-20260909/calibration_diagnostics.json", "repo_id": "policyengine/populace-us", - "revision": "populace-us-2024-buildp-sparse-rmloss100-cae8640-20260728T011454Z", + "revision": "populace-us-2024-spm-20260909", "sha256": "870449b44e86b13b25bcea1a57f0e7af37f4d4db18be815eea3acdf9fe6eb40e" }, - "demographics": { - "path": "releases/populace-us-2024-buildp-sparse-rmloss100-cae8640-20260728T011454Z/demographics.json", + "parent_build_manifest": { + "path": "releases/populace-us-2024-spm-20260909/parent_build_manifest.json", + "repo_id": "policyengine/populace-us", + "revision": "populace-us-2024-spm-20260909", + "sha256": "1990e8fd37ce66f499b8e6600c5896701bb07015be132a12de64e321a2edc67e" + }, + "parent_release_manifest": { + "path": "releases/populace-us-2024-spm-20260909/parent_release_manifest.json", "repo_id": "policyengine/populace-us", - "revision": "populace-us-2024-buildp-sparse-rmloss100-cae8640-20260728T011454Z", - "sha256": "50c39dea345fa4e21f32272304b376979b9572ca17383dd13061b752d9a81112" + "revision": "populace-us-2024-spm-20260909", + "sha256": "dd949ba3c4c7a56aff8442c6db5a031d2c84c3da59f5d14832f468c358604506" }, "populace_us_2024": { "path": "populace_us_2024.h5", "repo_id": "policyengine/populace-us", - "revision": "populace-us-2024-buildp-sparse-rmloss100-cae8640-20260728T011454Z", - "sha256": "48b9d479fb4fd1c3537f9383ce4697d130b6f618658409d74f6233c43b994c7e" + "revision": "populace-us-2024-spm-20260909", + "sha256": "6496cc4393d4d3c6574f76eca231de5898c803b9067645591fd5c4d3e65aee84" }, - "populace_us_2024_calibration": { - "path": "populace_us_2024_calibration.npz", + "source_enrichment": { + "path": "releases/populace-us-2024-spm-20260909/source_enrichment.json", "repo_id": "policyengine/populace-us", - "revision": "populace-us-2024-buildp-sparse-rmloss100-cae8640-20260728T011454Z", - "sha256": "b6e9c0678de5e79e8ddbd7a2a2f17c357bd6b368153cea7b5119c299a280a1b5" + "revision": "populace-us-2024-spm-20260909", + "sha256": "a6142b74d9bbdbda484f21532d699badb85ea5a572934898d55e5e134abe7af5" }, - "us_source_coverage": { - "path": "releases/populace-us-2024-buildp-sparse-rmloss100-cae8640-20260728T011454Z/us_source_coverage.json", + "source_enrichment_compatibility": { + "path": "releases/populace-us-2024-spm-20260909/source_enrichment_compatibility.json", "repo_id": "policyengine/populace-us", - "revision": "populace-us-2024-buildp-sparse-rmloss100-cae8640-20260728T011454Z", - "sha256": "6406c8686c292015a5bc7265a42402f89935f9395a8b63510efdd4d9562e7ff5" + "revision": "populace-us-2024-spm-20260909", + "sha256": "319035464dbe14c47c7bee81494355c84d116f1606bb0f20e628a3ff2a32e574" }, - "us_ssi_take_up": { - "path": "releases/populace-us-2024-buildp-sparse-rmloss100-cae8640-20260728T011454Z/us_ssi_take_up.json", + "source_spm_independence_provenance": { + "path": "releases/populace-us-2024-spm-20260909/source_spm_independence_provenance.json", "repo_id": "policyengine/populace-us", - "revision": "populace-us-2024-buildp-sparse-rmloss100-cae8640-20260728T011454Z", - "sha256": "ce631a37e2fe2ed22927fa232d41150d3b6cf328b38d3e5b38018fc4eec1b44d" + "revision": "populace-us-2024-spm-20260909", + "sha256": "265e844178ed3856d68c1638820f3089e0e1e1363aa4677e33bebb37cbd5bff3" + }, + "source_spm_person_independence": { + "path": "releases/populace-us-2024-spm-20260909/source_spm_person_independence.csv", + "repo_id": "policyengine/populace-us", + "revision": "populace-us-2024-spm-20260909", + "sha256": "22b5968d90fecfeef7614583e493fe10cc16bda8b5be82e6f49a5bc2102d3ce5" + }, + "us_source_coverage": { + "path": "releases/populace-us-2024-spm-20260909/us_source_coverage.json", + "repo_id": "policyengine/populace-us", + "revision": "populace-us-2024-spm-20260909", + "sha256": "6406c8686c292015a5bc7265a42402f89935f9395a8b63510efdd4d9562e7ff5" } }, "default_dataset": "populace_us_2024", - "default_dataset_uri": "hf://policyengine/populace-us/populace_us_2024.h5@populace-us-2024-buildp-sparse-rmloss100-cae8640-20260728T011454Z", + "default_dataset_uri": "hf://policyengine/populace-us/populace_us_2024.h5@populace-us-2024-spm-20260909", "model_package": { "name": "policyengine-us", - "sha256": "2b2c5c02c26ab212270948db1f30129728b625fc6109fc9ca0c604b714546909", - "version": "1.764.6", - "wheel_url": "https://files.pythonhosted.org/packages/e2/5a/0566666f30ad06e416f0cf25b24bdee73acd71897fb2de3c0ac43b59e3e3/policyengine_us-1.764.6-py3-none-any.whl" + "sha256": "4ca187509efc2f7d3ace5035c27de88c744d0f112fdc7da027372f3ae07c540a", + "version": "2.0.0", + "wheel_url": "https://files.pythonhosted.org/packages/fc/fa/b94e315f2cd67919afdc40bd136fc338ec629119b4d655bc5c03a0880874/policyengine_us-2.0.0-py3-none-any.whl" }, "policyengine_version": "5.3.1", "region_datasets": { @@ -192,10 +216,10 @@ "path_template": "populace_us_2024.h5" } }, - "release_manifest_uri": "https://huggingface.co/datasets/policyengine/populace-us/resolve/populace-us-2024-buildp-sparse-rmloss100-cae8640-20260728T011454Z/releases/populace-us-2024-buildp-sparse-rmloss100-cae8640-20260728T011454Z/release_manifest.json", + "release_manifest_uri": "https://huggingface.co/datasets/policyengine/populace-us/resolve/populace-us-2024-spm-20260909/releases/populace-us-2024-spm-20260909/release_manifest.json", "schema_version": 1, - "source_manifest_uri": "hf://dataset/policyengine/populace-us@populace-us-2024-buildp-sparse-rmloss100-cae8640-20260728T011454Z/releases/populace-us-2024-buildp-sparse-rmloss100-cae8640-20260728T011454Z/release_manifest.json", - "version": "populace-us-2024-buildp-sparse-rmloss100-cae8640-20260728T011454Z" + "source_manifest_uri": "hf://dataset/policyengine/populace-us@populace-us-2024-spm-20260909/releases/populace-us-2024-spm-20260909/release_manifest.json", + "version": "populace-us-2024-spm-20260909" } }, "dataset_overlays": { @@ -273,10 +297,10 @@ }, "policyengine-core": { "import_name": "policyengine_core", - "install_requirement": "policyengine-core==3.30.1", + "install_requirement": "policyengine-core==3.32.5", "name": "policyengine-core", "role": "runtime_dependency", - "version": "3.30.1" + "version": "3.32.5" }, "policyengine-uk": { "country": "uk", @@ -289,18 +313,20 @@ "policyengine-us": { "country": "us", "import_name": "policyengine_us", - "install_requirement": "policyengine-us==1.764.6", + "install_requirement": "policyengine-us==2.0.0", "name": "policyengine-us", "role": "country_model", - "version": "1.764.6" + "version": "2.0.0" }, "spm-calculator": { "country": "us", "import_name": "spm_calculator", - "install_requirement": "spm-calculator==0.3.1", + "install_requirement": "spm-calculator==1.0.0", "name": "spm-calculator", - "role": "country_dependency", - "version": "0.3.1" + "role": "runtime_dependency", + "sha256": "e354937a5e1a4045d4966ed594a528d8b02866fabaac9bb5672017004b627305", + "version": "1.0.0", + "wheel_url": "https://files.pythonhosted.org/packages/19/a0/c484f69a0ebf88a9b9fd0ac28176f8df66550f46714f1b0c5ec0b360824e/spm_calculator-1.0.0-py3-none-any.whl" } }, "policyengine_version": "5.3.1", From bccb603739020c35d18a24454ed9064b58ec7ca9 Mon Sep 17 00:00:00 2001 From: Max Ghenis Date: Fri, 11 Sep 2026 21:24:03 -0400 Subject: [PATCH 14/46] Relock on the canonical tuple uv lock only, registry sources throughout. policyengine-us 2.0.0 requires pandas>=3.0, which moves pandas 2.3.2 to 3.0.5 and splits numpy into 2.1.3 and 2.5.3 resolution branches. Co-Authored-By: Claude Fable 5.1 --- uv.lock | 221 ++++++++++++++++++++++++++++++++++++++++---------------- 1 file changed, 157 insertions(+), 64 deletions(-) diff --git a/uv.lock b/uv.lock index ac5c8db2..d5655d84 100644 --- a/uv.lock +++ b/uv.lock @@ -2,8 +2,15 @@ version = 1 revision = 3 requires-python = ">=3.11, <3.15" resolution-markers = [ - "python_full_version >= '3.12'", - "python_full_version < '3.12'", + "python_full_version >= '3.14' and sys_platform == 'win32'", + "python_full_version >= '3.14' and sys_platform == 'emscripten'", + "python_full_version >= '3.14' and sys_platform != 'emscripten' and sys_platform != 'win32'", + "python_full_version >= '3.12' and python_full_version < '3.14' and sys_platform == 'win32'", + "python_full_version >= '3.12' and python_full_version < '3.14' and sys_platform == 'emscripten'", + "python_full_version >= '3.12' and python_full_version < '3.14' and sys_platform != 'emscripten' and sys_platform != 'win32'", + "python_full_version < '3.12' and sys_platform == 'win32'", + "python_full_version < '3.12' and sys_platform == 'emscripten'", + "python_full_version < '3.12' and sys_platform != 'emscripten' and sys_platform != 'win32'", ] [[package]] @@ -115,7 +122,8 @@ dependencies = [ { name = "msgpack" }, { name = "ndindex" }, { name = "numexpr", marker = "platform_machine != 'wasm32'" }, - { name = "numpy" }, + { name = "numpy", version = "2.1.3", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version < '3.14'" }, + { name = "numpy", version = "2.5.3", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version >= '3.14'" }, { name = "platformdirs" }, { name = "py-cpuinfo", marker = "platform_machine != 'wasm32'" }, { name = "requests" }, @@ -614,7 +622,8 @@ name = "h5py" version = "3.16.0" source = { registry = "https://pypi.org/simple" } dependencies = [ - { name = "numpy" }, + { name = "numpy", version = "2.1.3", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version < '3.14'" }, + { name = "numpy", version = "2.5.3", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version >= '3.14'" }, ] sdist = { url = "https://files.pythonhosted.org/packages/db/33/acd0ce6863b6c0d7735007df01815403f5589a21ff8c2e1ee2587a38f548/h5py-3.16.0.tar.gz", hash = "sha256:a0dbaad796840ccaa67a4c144a0d0c8080073c34c76d5a6941d6818678ef2738", size = 446526, upload-time = "2026-03-06T13:49:08.07Z" } wheels = [ @@ -784,7 +793,8 @@ version = "2.5.2" source = { registry = "https://pypi.org/simple" } dependencies = [ { name = "ipython" }, - { name = "numpy" }, + { name = "numpy", version = "2.1.3", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version < '3.14'" }, + { name = "numpy", version = "2.5.3", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version >= '3.14'" }, { name = "pandas" }, ] sdist = { url = "https://files.pythonhosted.org/packages/02/a2/4652db589b5767ead6d1dd8016e94e6adc5ec9e9552ccd17cf1886900b04/itables-2.5.2.tar.gz", hash = "sha256:ec34bbacfbf4305570ea75b36970de442f924126f3701c323a5a46018de84c8a", size = 2356416, upload-time = "2025-09-02T20:14:41.722Z" } @@ -1178,7 +1188,8 @@ name = "microdf-python" version = "1.3.0" source = { registry = "https://pypi.org/simple" } dependencies = [ - { name = "numpy" }, + { name = "numpy", version = "2.1.3", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version < '3.14'" }, + { name = "numpy", version = "2.5.3", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version >= '3.14'" }, { name = "pandas" }, ] sdist = { url = "https://files.pythonhosted.org/packages/a3/1b/a4b6a53c8e6668c190e1928ebf9f8c67d6a4995ff4a4b0e3403ded30d4ab/microdf_python-1.3.0.tar.gz", hash = "sha256:f352fe32c25a4fd6d6188ef6aaca0214b53b29a8a7913aca6af0362dd9c922bd", size = 28141, upload-time = "2026-04-17T23:20:57.018Z" } @@ -1417,7 +1428,8 @@ name = "numexpr" version = "2.11.0" source = { registry = "https://pypi.org/simple" } dependencies = [ - { name = "numpy" }, + { name = "numpy", version = "2.1.3", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version < '3.14'" }, + { name = "numpy", version = "2.5.3", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version >= '3.14'" }, ] sdist = { url = "https://files.pythonhosted.org/packages/d2/8f/2cc977e91adbfbcdb6b49fdb9147e1d1c7566eb2c0c1e737e9a47020b5ca/numexpr-2.11.0.tar.gz", hash = "sha256:75b2c01a4eda2e7c357bc67a3f5c3dd76506c15b5fd4dc42845ef2e182181bad", size = 108960, upload-time = "2025-06-09T11:05:56.79Z" } wheels = [ @@ -1451,6 +1463,14 @@ wheels = [ name = "numpy" version = "2.1.3" source = { registry = "https://pypi.org/simple" } +resolution-markers = [ + "python_full_version >= '3.12' and python_full_version < '3.14' and sys_platform == 'win32'", + "python_full_version >= '3.12' and python_full_version < '3.14' and sys_platform == 'emscripten'", + "python_full_version >= '3.12' and python_full_version < '3.14' and sys_platform != 'emscripten' and sys_platform != 'win32'", + "python_full_version < '3.12' and sys_platform == 'win32'", + "python_full_version < '3.12' and sys_platform == 'emscripten'", + "python_full_version < '3.12' and sys_platform != 'emscripten' and sys_platform != 'win32'", +] sdist = { url = "https://files.pythonhosted.org/packages/25/ca/1166b75c21abd1da445b97bf1fa2f14f423c6cfb4fc7c4ef31dccf9f6a94/numpy-2.1.3.tar.gz", hash = "sha256:aa08e04e08aaf974d4458def539dece0d28146d866a39da5639596f4921fd761", size = 20166090, upload-time = "2024-11-02T17:48:55.832Z" } wheels = [ { url = "https://files.pythonhosted.org/packages/ad/81/c8167192eba5247593cd9d305ac236847c2912ff39e11402e72ae28a4985/numpy-2.1.3-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:4d1167c53b93f1f5d8a139a742b3c6f4d429b54e74e6b57d0eff40045187b15d", size = 21156252, upload-time = "2024-11-02T17:34:01.372Z" }, @@ -1495,6 +1515,62 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/86/09/a5ab407bd7f5f5599e6a9261f964ace03a73e7c6928de906981c31c38082/numpy-2.1.3-cp313-cp313t-win_amd64.whl", hash = "sha256:2564fbdf2b99b3f815f2107c1bbc93e2de8ee655a69c261363a1172a79a257d4", size = 12644098, upload-time = "2024-11-02T17:46:07.941Z" }, ] +[[package]] +name = "numpy" +version = "2.5.3" +source = { registry = "https://pypi.org/simple" } +resolution-markers = [ + "python_full_version >= '3.14' and sys_platform == 'win32'", + "python_full_version >= '3.14' and sys_platform == 'emscripten'", + "python_full_version >= '3.14' and sys_platform != 'emscripten' and sys_platform != 'win32'", +] +sdist = { url = "https://files.pythonhosted.org/packages/13/01/11703282db468b85f6f7b8c7f22d058de5970d5c7e60a3a8aaa313c3de36/numpy-2.5.3.tar.gz", hash = "sha256:df2d5874ff183595a4ba404edd04f6bd9b5505c1d7708573f6a6c17489a67563", size = 20791231, upload-time = "2026-09-06T16:27:47.073Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/d6/50/8fdbb16af64895706a45f06a4068e29db732ec180f3c1375f14123359138/numpy-2.5.3-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:cb189f09db39283b26bfd061ec16189e14f71c6755207f72a0f7540867afe5b9", size = 16994982, upload-time = "2026-09-06T16:24:29.244Z" }, + { url = "https://files.pythonhosted.org/packages/60/39/789131c1188c078dcb3a1692e72e1e050c68b88ffe72c9ccaac9bcd7a9cd/numpy-2.5.3-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:f59a878c33d6b88122d80d239bb3b845d58708750b0cb06a09aebb9b18ec696c", size = 12009327, upload-time = "2026-09-06T16:24:32.491Z" }, + { url = "https://files.pythonhosted.org/packages/9c/59/a312e95696e5f601914dd8b6dd844692ba61670807417e24b68e337b5c70/numpy-2.5.3-cp312-cp312-macosx_14_0_arm64.whl", hash = "sha256:a72f874bc9e10e4b8f80426fb49716d5141f64442a0c8418065093ec8017fbb0", size = 5445405, upload-time = "2026-09-06T16:24:35.071Z" }, + { url = "https://files.pythonhosted.org/packages/30/d0/5623a1707ed4fe16e3909fe3cf5ee3da004ae677ad23d83bbf3adf1a6faf/numpy-2.5.3-cp312-cp312-macosx_14_0_x86_64.whl", hash = "sha256:fc36dc566135b5eceec4cf89758fcb719266a019ef07dae1754ae7c9f617ef3e", size = 6783213, upload-time = "2026-09-06T16:24:37.253Z" }, + { url = "https://files.pythonhosted.org/packages/f1/32/84146fc020ad3c25f805f70ab60da46fe3c540a21369754a7e4369754b6f/numpy-2.5.3-cp312-cp312-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:76c2c1e6bfa5c84adc6434dfbf013aa92096a7985221762c8f11fedfd20fff58", size = 15687872, upload-time = "2026-09-06T16:24:39.751Z" }, + { url = "https://files.pythonhosted.org/packages/65/af/aa78d1a88805456e212b65461354cd943197fb9acecc4c90fd12295123a3/numpy-2.5.3-cp312-cp312-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:b7e18c623bb5c95acb3b3328861272816ba199fb531921c5d6d0b675f1fde9e3", size = 16717410, upload-time = "2026-09-06T16:24:42.745Z" }, + { url = "https://files.pythonhosted.org/packages/3b/24/faa79d865e69a97ba17473b23a1b74094b2259c03e820c70297293b9ea49/numpy-2.5.3-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:4f8929ee6c96bfbd7b4ed2032e0c03af86fe1826740ab61ddabf9072d06e57ff", size = 17040975, upload-time = "2026-09-06T16:24:45.961Z" }, + { url = "https://files.pythonhosted.org/packages/62/4a/8877e629445a7176297dffcaf9c485faa96a95d81728a62521ad55bd4c0f/numpy-2.5.3-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:b5d93cf48f687479941d12b69c873ad2cc76bbd487f0091c2200636497f34034", size = 18476479, upload-time = "2026-09-06T16:24:49.35Z" }, + { url = "https://files.pythonhosted.org/packages/c8/db/35e1c2d38b04cbd5b731f9d71495e055e813197669d22b612f11748d2ff9/numpy-2.5.3-cp312-cp312-win32.whl", hash = "sha256:bf63afbe037eb5d2fe87fbcc7778e61da53ebaf21d938a4515aa73b62532a5d4", size = 6133378, upload-time = "2026-09-06T16:24:51.915Z" }, + { url = "https://files.pythonhosted.org/packages/3c/a1/accf6d4f0c80c5d9ba9735d6b1550e444180599f34dec69ca01360f717ad/numpy-2.5.3-cp312-cp312-win_amd64.whl", hash = "sha256:0a59a421a32580a009e8a1751345bf829631b990dc1794b80514ab722b435def", size = 12567828, upload-time = "2026-09-06T16:24:54.255Z" }, + { url = "https://files.pythonhosted.org/packages/22/43/1764aff32e4652526ae2f71fa8b3efd8d25c8a3d6926914454e47138ed1e/numpy-2.5.3-cp312-cp312-win_arm64.whl", hash = "sha256:ccb32e0525d29e8b0572eb84c9a57af0e7a4e615726927506f55063c62414034", size = 10485432, upload-time = "2026-09-06T16:24:57.278Z" }, + { url = "https://files.pythonhosted.org/packages/79/e5/8fb89cd46d14e35699d13bf943a5f5f441ecee8667120a1f6105ab89e349/numpy-2.5.3-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:66a78fe4556c60aceda5916f9eacd638b18e9e681016ec302dcb4682d6d4d034", size = 16991061, upload-time = "2026-09-06T16:25:00.411Z" }, + { url = "https://files.pythonhosted.org/packages/2f/06/9dc9e48b5e5e941c8b10350c5ff2d721da42a20517d911d15544246775ff/numpy-2.5.3-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:92f30e89b8ee0ecf363033576c422b2f58fed6a80bed0aa48dff6d14c654663e", size = 12003676, upload-time = "2026-09-06T16:25:03.475Z" }, + { url = "https://files.pythonhosted.org/packages/ab/2a/98282aa5b8f58b1157d440bb6282eed47e3632a5de53a714fbab17e659fe/numpy-2.5.3-cp313-cp313-macosx_14_0_arm64.whl", hash = "sha256:f9a2353b37a1a9e78fd82b27ad7e2a32a2d036604d18f02b05e3136c62ca3b09", size = 5439695, upload-time = "2026-09-06T16:25:05.978Z" }, + { url = "https://files.pythonhosted.org/packages/a1/f9/b6533d777be9d6ffd29dc1be0867e563e6e8cc9a220ff1b716adc317f060/numpy-2.5.3-cp313-cp313-macosx_14_0_x86_64.whl", hash = "sha256:ccbc4665079665c3cf3bab4db9f6b095370cd6437d66be549b6c2a1fd19e1958", size = 6779395, upload-time = "2026-09-06T16:25:08.599Z" }, + { url = "https://files.pythonhosted.org/packages/73/85/735720d04ec197c5dcfacdfc9922667c7f1f5f496a279b7ba4d7c74c4cc7/numpy-2.5.3-cp313-cp313-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:c76d5dde9f445058f83d0c02af00557a4db91de9a9a57c0df87d1535001d654b", size = 15681750, upload-time = "2026-09-06T16:25:11.173Z" }, + { url = "https://files.pythonhosted.org/packages/3a/1b/3b16a9bc514a440a7a0883684111dcb1ef1aee960af2ca95da8fc775f124/numpy-2.5.3-cp313-cp313-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:a5fa86b80fd24bcd1aff83ad23be44ea323de3f787be8f8b15d4a65621e25321", size = 16708577, upload-time = "2026-09-06T16:25:14.171Z" }, + { url = "https://files.pythonhosted.org/packages/69/c4/386f397831b07328b639c96c5b62719346cf4baf07c68d927239752b1534/numpy-2.5.3-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:bd4cb9ad3c7889b9b3fe0a9a9fb5d2ed26f9879bff2608d9f01aed147a20d231", size = 17042047, upload-time = "2026-09-06T16:25:17.582Z" }, + { url = "https://files.pythonhosted.org/packages/5f/3e/a700ecbf36e85ae8328fd3b0e12eeddc22ed6358a64cb2bd913e0d195d65/numpy-2.5.3-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:1302b90c0e52281681b2975adfe8a860cb7b12216a27b4b0b4207c44bf7bccf0", size = 18465724, upload-time = "2026-09-06T16:25:20.949Z" }, + { url = "https://files.pythonhosted.org/packages/41/ee/38e785e88a4045f6ad1d1f2808dcdfafdca48c760260c0587bf171e29fc9/numpy-2.5.3-cp313-cp313-win32.whl", hash = "sha256:1c80eabb4035ecf4ca9cd49cde8a9fdd69a729e63e6474887d1523ade7aa277f", size = 6129003, upload-time = "2026-09-06T16:25:23.664Z" }, + { url = "https://files.pythonhosted.org/packages/f3/ec/100f2b1794ede74a9b3d7ec6b9736927f56713414c1dfe19ab6c383494bf/numpy-2.5.3-cp313-cp313-win_amd64.whl", hash = "sha256:71cad2b2a7451ab79d8f5e71b453485b6775963d5cf794179144a7463fe6e8ec", size = 12560965, upload-time = "2026-09-06T16:25:26.602Z" }, + { url = "https://files.pythonhosted.org/packages/80/b1/7dc825ca94c12acebbce4c37caa5e198695eb31424bc579679f32b1bb49d/numpy-2.5.3-cp313-cp313-win_arm64.whl", hash = "sha256:8e4dd766076855b5ff7ea52fa5f07ce26286726e0f8bff446b7739d02e6ea204", size = 10482343, upload-time = "2026-09-06T16:25:29.772Z" }, + { url = "https://files.pythonhosted.org/packages/70/78/cf416f15dc29375a229d9dfebf8db6e313f291580b39fa1a568b6052bb07/numpy-2.5.3-cp314-cp314-macosx_10_15_x86_64.whl", hash = "sha256:350ba9783ce969cf9f7ce6e6a9a58e1a6e2a19ca025b7ee448c4db727706212a", size = 16998686, upload-time = "2026-09-06T16:25:33.171Z" }, + { url = "https://files.pythonhosted.org/packages/9e/59/abcc2d8def4fd60eec7d87f92d27c13448ffd9ab14339bcc63a0d7a2fdea/numpy-2.5.3-cp314-cp314-macosx_11_0_arm64.whl", hash = "sha256:012e66aca395d795496446e52aeeb5866312a5d4d3f27da270e5a0b43f70dc5c", size = 12013862, upload-time = "2026-09-06T16:25:36.748Z" }, + { url = "https://files.pythonhosted.org/packages/94/75/4640d2d6e4b64a049e48425a82728a41ef4adb61332d2cba68055774878b/numpy-2.5.3-cp314-cp314-macosx_14_0_arm64.whl", hash = "sha256:adc1ada2662f8a5f960b8a10d9986897e7499ef07e06d4cfe7197f8cce923c07", size = 5449793, upload-time = "2026-09-06T16:25:39.476Z" }, + { url = "https://files.pythonhosted.org/packages/96/cd/625b57ae33d4ca560f32cc0b47b4a5922146d9beb998ddf773900d440a73/numpy-2.5.3-cp314-cp314-macosx_14_0_x86_64.whl", hash = "sha256:54a115e5a73b8fc44f0cebef486365a1894b5c9760685d4558b72b7c3eb846e0", size = 6785176, upload-time = "2026-09-06T16:25:42.069Z" }, + { url = "https://files.pythonhosted.org/packages/9c/72/12918652e7912ef9751e8694c88820fcd1908e0618cb23f5f3caa6004b7b/numpy-2.5.3-cp314-cp314-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:be5a8381859b6da607c84f4f7d6847725f1cf1853ef8a2c9e115b7d58bef47dc", size = 15703377, upload-time = "2026-09-06T16:25:45.135Z" }, + { url = "https://files.pythonhosted.org/packages/45/8f/9beacf79ca7c650688ad0baa80931adb988fe6e6e5d5903c23cc3dbd70eb/numpy-2.5.3-cp314-cp314-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:b0521d0f4aebb6e06189451025fa17a913287b13c03d5fe05c017333b654ea5b", size = 16711928, upload-time = "2026-09-06T16:25:48.461Z" }, + { url = "https://files.pythonhosted.org/packages/09/8d/41d0a56e1ac4c87495c897a211b1368691b7237aadabec8b3b8f3a74d48f/numpy-2.5.3-cp314-cp314-musllinux_1_2_aarch64.whl", hash = "sha256:9deb49575e5b0b94ed72c8a64ec4d033381adc27e9060ae842971f697ba96104", size = 17059507, upload-time = "2026-09-06T16:25:51.873Z" }, + { url = "https://files.pythonhosted.org/packages/08/1e/0dfbc5cc251d54e2af790f254d24ec38637fa97ec7d5d11de7ffed787098/numpy-2.5.3-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:b00eefbcf0f292945c4b4dec2ae845389ef5bcdcd596e6e4328051db5b5ba694", size = 18471002, upload-time = "2026-09-06T16:25:55.233Z" }, + { url = "https://files.pythonhosted.org/packages/b5/2c/dfa40f6991f8185c8c30ffd023dfcbb11888e823cfab9557b920f3bb7bed/numpy-2.5.3-cp314-cp314-win32.whl", hash = "sha256:c2381f82999704f818e2c987a865050e285ec3621262c66d40f5a96c8f899f8e", size = 6180485, upload-time = "2026-09-06T16:25:58.157Z" }, + { url = "https://files.pythonhosted.org/packages/a4/73/d2c08231e4fde7e415501fd02c715d96e98599b2d8384445933944152984/numpy-2.5.3-cp314-cp314-win_amd64.whl", hash = "sha256:2c25dfa72943e4336ddb6b0ee4277b47a0c85bede0807530ec68103bf58e2c10", size = 12698179, upload-time = "2026-09-06T16:26:00.789Z" }, + { url = "https://files.pythonhosted.org/packages/5c/e9/dcdcc9b95cf5f49815055573aee1b11cfbf5299f38a180e437ded050810f/numpy-2.5.3-cp314-cp314-win_arm64.whl", hash = "sha256:15aa985ac73a8db02db7663381aa109510449d3819d37206caed27b33a65a8a6", size = 10769383, upload-time = "2026-09-06T16:26:04.011Z" }, + { url = "https://files.pythonhosted.org/packages/49/c4/af8bc08a7ef4e1529a7c0cf24969accce316b783999802089a581ec99272/numpy-2.5.3-cp314-cp314t-macosx_11_0_arm64.whl", hash = "sha256:ac7bb1c52d445bd4f8f7f97fefe6abc3a084dc4d63df50d79b17fa2b78e89297", size = 12132668, upload-time = "2026-09-06T16:26:07.138Z" }, + { url = "https://files.pythonhosted.org/packages/c5/ae/0f15eb56d4ec5e13c1f7ff04ff407f997d1acbadb45d3e1f2e2645a8f43c/numpy-2.5.3-cp314-cp314t-macosx_14_0_arm64.whl", hash = "sha256:e6ab667ba76450084eb64013762c438ea76d9d29cc676dcd6c2e9892ba37f841", size = 5568580, upload-time = "2026-09-06T16:26:09.828Z" }, + { url = "https://files.pythonhosted.org/packages/23/fb/c72a8f25d4b6e96c354e7ab45ace3b27dc11e5d6a13b6c7d0cd6b08bf112/numpy-2.5.3-cp314-cp314t-macosx_14_0_x86_64.whl", hash = "sha256:f7fabeb6cea87d65f3b926de33d03fb016cfdc29314c90974383b5582ae72891", size = 6882634, upload-time = "2026-09-06T16:26:12.524Z" }, + { url = "https://files.pythonhosted.org/packages/07/a9/968c90ed2ab15060c338e8137f1215b5a60756ae07328e0a60d1c6734df4/numpy-2.5.3-cp314-cp314t-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:1fb6f8fb9ff0b3a69f52c66ce397b0246583e9f28616231b0e32ca49259a5fa6", size = 15748923, upload-time = "2026-09-06T16:26:15.092Z" }, + { url = "https://files.pythonhosted.org/packages/59/08/9df04103947b95e3b6b1f2ed1a70521f325647a31b82da6a2aae3a485508/numpy-2.5.3-cp314-cp314t-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:93e1f5447e2b1e479d7bd74701e84746b86450cff1fc368b132d195e2b8f8211", size = 16746748, upload-time = "2026-09-06T16:26:18.43Z" }, + { url = "https://files.pythonhosted.org/packages/41/a0/14c8d5fe5b53a334aabb653deb391c0fef49558f491880ea300ed6785224/numpy-2.5.3-cp314-cp314t-musllinux_1_2_aarch64.whl", hash = "sha256:c00abe94c1a69d75d827dcf1c025b25c8a45d230b3bcd77a9020883a1b047653", size = 17111561, upload-time = "2026-09-06T16:26:22.113Z" }, + { url = "https://files.pythonhosted.org/packages/c4/a6/d7e96e42f01522e154c32489640f16dfc4f6181d165d05fc3bec8c2c4999/numpy-2.5.3-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:536f963710a4e63934d80ac0dc4f478804a83e9a84b6828018f25d09953ada33", size = 18513945, upload-time = "2026-09-06T16:26:25.401Z" }, + { url = "https://files.pythonhosted.org/packages/25/39/3453afb7119d0449ef11c886874120ff180e2c337760e0e2d88f70f1a945/numpy-2.5.3-cp314-cp314t-win32.whl", hash = "sha256:4c8a6d2ebce6305fd82fbefca827775437147052a976ee7c94b36a0c1b52ac6c", size = 6335421, upload-time = "2026-09-06T16:26:28.175Z" }, + { url = "https://files.pythonhosted.org/packages/99/01/22815d2b19a1a746b1d45205cffebb3fe511a18acb75fba6c88491fc9894/numpy-2.5.3-cp314-cp314t-win_amd64.whl", hash = "sha256:9a37475425b431b4d060f23b4f52cd2f3aef6bc7c654bd760adf0040eec9d435", size = 12896420, upload-time = "2026-09-06T16:26:31.265Z" }, + { url = "https://files.pythonhosted.org/packages/fa/ee/a7cbba67eeaff038dc29ca8b98a88396c8b0cc9c89d4924f4a27a5c9150b/numpy-2.5.3-cp314-cp314t-win_arm64.whl", hash = "sha256:2d8240cb4c16fd831074aa2b2cf9fc54664d826341d61c372245b96a74a49a9a", size = 10857177, upload-time = "2026-09-06T16:26:34.167Z" }, +] + [[package]] name = "openpyxl" version = "3.1.5" @@ -1518,43 +1594,57 @@ wheels = [ [[package]] name = "pandas" -version = "2.3.2" +version = "3.0.5" source = { registry = "https://pypi.org/simple" } dependencies = [ - { name = "numpy" }, + { name = "numpy", version = "2.1.3", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version < '3.14'" }, + { name = "numpy", version = "2.5.3", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version >= '3.14'" }, { name = "python-dateutil" }, - { name = "pytz" }, - { name = "tzdata" }, -] -sdist = { url = "https://files.pythonhosted.org/packages/79/8e/0e90233ac205ad182bd6b422532695d2b9414944a280488105d598c70023/pandas-2.3.2.tar.gz", hash = "sha256:ab7b58f8f82706890924ccdfb5f48002b83d2b5a3845976a9fb705d36c34dcdb", size = 4488684, upload-time = "2025-08-21T10:28:29.257Z" } -wheels = [ - { url = "https://files.pythonhosted.org/packages/7a/59/f3e010879f118c2d400902d2d871c2226cef29b08c09fb8dc41111730400/pandas-2.3.2-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:1333e9c299adcbb68ee89a9bb568fc3f20f9cbb419f1dd5225071e6cddb2a743", size = 11563308, upload-time = "2025-08-21T10:26:56.656Z" }, - { url = "https://files.pythonhosted.org/packages/38/18/48f10f1cc5c397af59571d638d211f494dba481f449c19adbd282aa8f4ca/pandas-2.3.2-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:76972bcbd7de8e91ad5f0ca884a9f2c477a2125354af624e022c49e5bd0dfff4", size = 10820319, upload-time = "2025-08-21T10:26:59.162Z" }, - { url = "https://files.pythonhosted.org/packages/95/3b/1e9b69632898b048e223834cd9702052bcf06b15e1ae716eda3196fb972e/pandas-2.3.2-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:b98bdd7c456a05eef7cd21fd6b29e3ca243591fe531c62be94a2cc987efb5ac2", size = 11790097, upload-time = "2025-08-21T10:27:02.204Z" }, - { url = "https://files.pythonhosted.org/packages/8b/ef/0e2ffb30b1f7fbc9a588bd01e3c14a0d96854d09a887e15e30cc19961227/pandas-2.3.2-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:1d81573b3f7db40d020983f78721e9bfc425f411e616ef019a10ebf597aedb2e", size = 12397958, upload-time = "2025-08-21T10:27:05.409Z" }, - { url = "https://files.pythonhosted.org/packages/23/82/e6b85f0d92e9afb0e7f705a51d1399b79c7380c19687bfbf3d2837743249/pandas-2.3.2-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:e190b738675a73b581736cc8ec71ae113d6c3768d0bd18bffa5b9a0927b0b6ea", size = 13225600, upload-time = "2025-08-21T10:27:07.791Z" }, - { url = "https://files.pythonhosted.org/packages/e8/f1/f682015893d9ed51611948bd83683670842286a8edd4f68c2c1c3b231eef/pandas-2.3.2-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:c253828cb08f47488d60f43c5fc95114c771bbfff085da54bfc79cb4f9e3a372", size = 13879433, upload-time = "2025-08-21T10:27:10.347Z" }, - { url = "https://files.pythonhosted.org/packages/a7/e7/ae86261695b6c8a36d6a4c8d5f9b9ede8248510d689a2f379a18354b37d7/pandas-2.3.2-cp311-cp311-win_amd64.whl", hash = "sha256:9467697b8083f9667b212633ad6aa4ab32436dcbaf4cd57325debb0ddef2012f", size = 11336557, upload-time = "2025-08-21T10:27:12.983Z" }, - { url = "https://files.pythonhosted.org/packages/ec/db/614c20fb7a85a14828edd23f1c02db58a30abf3ce76f38806155d160313c/pandas-2.3.2-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:3fbb977f802156e7a3f829e9d1d5398f6192375a3e2d1a9ee0803e35fe70a2b9", size = 11587652, upload-time = "2025-08-21T10:27:15.888Z" }, - { url = "https://files.pythonhosted.org/packages/99/b0/756e52f6582cade5e746f19bad0517ff27ba9c73404607c0306585c201b3/pandas-2.3.2-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:1b9b52693123dd234b7c985c68b709b0b009f4521000d0525f2b95c22f15944b", size = 10717686, upload-time = "2025-08-21T10:27:18.486Z" }, - { url = "https://files.pythonhosted.org/packages/37/4c/dd5ccc1e357abfeee8353123282de17997f90ff67855f86154e5a13b81e5/pandas-2.3.2-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:0bd281310d4f412733f319a5bc552f86d62cddc5f51d2e392c8787335c994175", size = 11278722, upload-time = "2025-08-21T10:27:21.149Z" }, - { url = "https://files.pythonhosted.org/packages/d3/a4/f7edcfa47e0a88cda0be8b068a5bae710bf264f867edfdf7b71584ace362/pandas-2.3.2-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:96d31a6b4354e3b9b8a2c848af75d31da390657e3ac6f30c05c82068b9ed79b9", size = 11987803, upload-time = "2025-08-21T10:27:23.767Z" }, - { url = "https://files.pythonhosted.org/packages/f6/61/1bce4129f93ab66f1c68b7ed1c12bac6a70b1b56c5dab359c6bbcd480b52/pandas-2.3.2-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:df4df0b9d02bb873a106971bb85d448378ef14b86ba96f035f50bbd3688456b4", size = 12766345, upload-time = "2025-08-21T10:27:26.6Z" }, - { url = "https://files.pythonhosted.org/packages/8e/46/80d53de70fee835531da3a1dae827a1e76e77a43ad22a8cd0f8142b61587/pandas-2.3.2-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:213a5adf93d020b74327cb2c1b842884dbdd37f895f42dcc2f09d451d949f811", size = 13439314, upload-time = "2025-08-21T10:27:29.213Z" }, - { url = "https://files.pythonhosted.org/packages/28/30/8114832daff7489f179971dbc1d854109b7f4365a546e3ea75b6516cea95/pandas-2.3.2-cp312-cp312-win_amd64.whl", hash = "sha256:8c13b81a9347eb8c7548f53fd9a4f08d4dfe996836543f805c987bafa03317ae", size = 10983326, upload-time = "2025-08-21T10:27:31.901Z" }, - { url = "https://files.pythonhosted.org/packages/27/64/a2f7bf678af502e16b472527735d168b22b7824e45a4d7e96a4fbb634b59/pandas-2.3.2-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:0c6ecbac99a354a051ef21c5307601093cb9e0f4b1855984a084bfec9302699e", size = 11531061, upload-time = "2025-08-21T10:27:34.647Z" }, - { url = "https://files.pythonhosted.org/packages/54/4c/c3d21b2b7769ef2f4c2b9299fcadd601efa6729f1357a8dbce8dd949ed70/pandas-2.3.2-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:c6f048aa0fd080d6a06cc7e7537c09b53be6642d330ac6f54a600c3ace857ee9", size = 10668666, upload-time = "2025-08-21T10:27:37.203Z" }, - { url = "https://files.pythonhosted.org/packages/50/e2/f775ba76ecfb3424d7f5862620841cf0edb592e9abd2d2a5387d305fe7a8/pandas-2.3.2-cp313-cp313-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:0064187b80a5be6f2f9c9d6bdde29372468751dfa89f4211a3c5871854cfbf7a", size = 11332835, upload-time = "2025-08-21T10:27:40.188Z" }, - { url = "https://files.pythonhosted.org/packages/8f/52/0634adaace9be2d8cac9ef78f05c47f3a675882e068438b9d7ec7ef0c13f/pandas-2.3.2-cp313-cp313-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:4ac8c320bded4718b298281339c1a50fb00a6ba78cb2a63521c39bec95b0209b", size = 12057211, upload-time = "2025-08-21T10:27:43.117Z" }, - { url = "https://files.pythonhosted.org/packages/0b/9d/2df913f14b2deb9c748975fdb2491da1a78773debb25abbc7cbc67c6b549/pandas-2.3.2-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:114c2fe4f4328cf98ce5716d1532f3ab79c5919f95a9cfee81d9140064a2e4d6", size = 12749277, upload-time = "2025-08-21T10:27:45.474Z" }, - { url = "https://files.pythonhosted.org/packages/87/af/da1a2417026bd14d98c236dba88e39837182459d29dcfcea510b2ac9e8a1/pandas-2.3.2-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:48fa91c4dfb3b2b9bfdb5c24cd3567575f4e13f9636810462ffed8925352be5a", size = 13415256, upload-time = "2025-08-21T10:27:49.885Z" }, - { url = "https://files.pythonhosted.org/packages/22/3c/f2af1ce8840ef648584a6156489636b5692c162771918aa95707c165ad2b/pandas-2.3.2-cp313-cp313-win_amd64.whl", hash = "sha256:12d039facec710f7ba305786837d0225a3444af7bbd9c15c32ca2d40d157ed8b", size = 10982579, upload-time = "2025-08-21T10:28:08.435Z" }, - { url = "https://files.pythonhosted.org/packages/f3/98/8df69c4097a6719e357dc249bf437b8efbde808038268e584421696cbddf/pandas-2.3.2-cp313-cp313t-macosx_10_13_x86_64.whl", hash = "sha256:c624b615ce97864eb588779ed4046186f967374185c047070545253a52ab2d57", size = 12028163, upload-time = "2025-08-21T10:27:52.232Z" }, - { url = "https://files.pythonhosted.org/packages/0e/23/f95cbcbea319f349e10ff90db488b905c6883f03cbabd34f6b03cbc3c044/pandas-2.3.2-cp313-cp313t-macosx_11_0_arm64.whl", hash = "sha256:0cee69d583b9b128823d9514171cabb6861e09409af805b54459bd0c821a35c2", size = 11391860, upload-time = "2025-08-21T10:27:54.673Z" }, - { url = "https://files.pythonhosted.org/packages/ad/1b/6a984e98c4abee22058aa75bfb8eb90dce58cf8d7296f8bc56c14bc330b0/pandas-2.3.2-cp313-cp313t-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:2319656ed81124982900b4c37f0e0c58c015af9a7bbc62342ba5ad07ace82ba9", size = 11309830, upload-time = "2025-08-21T10:27:56.957Z" }, - { url = "https://files.pythonhosted.org/packages/15/d5/f0486090eb18dd8710bf60afeaf638ba6817047c0c8ae5c6a25598665609/pandas-2.3.2-cp313-cp313t-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:b37205ad6f00d52f16b6d09f406434ba928c1a1966e2771006a9033c736d30d2", size = 11883216, upload-time = "2025-08-21T10:27:59.302Z" }, - { url = "https://files.pythonhosted.org/packages/10/86/692050c119696da19e20245bbd650d8dfca6ceb577da027c3a73c62a047e/pandas-2.3.2-cp313-cp313t-musllinux_1_2_aarch64.whl", hash = "sha256:837248b4fc3a9b83b9c6214699a13f069dc13510a6a6d7f9ba33145d2841a012", size = 12699743, upload-time = "2025-08-21T10:28:02.447Z" }, - { url = "https://files.pythonhosted.org/packages/cd/d7/612123674d7b17cf345aad0a10289b2a384bff404e0463a83c4a3a59d205/pandas-2.3.2-cp313-cp313t-musllinux_1_2_x86_64.whl", hash = "sha256:d2c3554bd31b731cd6490d94a28f3abb8dd770634a9e06eb6d2911b9827db370", size = 13186141, upload-time = "2025-08-21T10:28:05.377Z" }, + { name = "tzdata", marker = "sys_platform == 'emscripten' or sys_platform == 'win32'" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/be/4f/5f3422a2afec5ffc46308b79e53291365a93748b498ac2e58bead0197916/pandas-3.0.5.tar.gz", hash = "sha256:dca3734d6ab7c906e6730f0788b0a1dbb9f2467731f9711f77995c8e9d62d712", size = 4658219, upload-time = "2026-07-22T22:19:28.819Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/48/ef/f1fd7431d635bf20015489bf0bd69c17fff1018de773540f651455a3916b/pandas-3.0.5-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:2946e77e4a53cd248cbde631a12f0e51c8324ce354c3eba4d20147c1ad6f4282", size = 10397178, upload-time = "2026-07-22T22:17:48.274Z" }, + { url = "https://files.pythonhosted.org/packages/31/b4/0eafac990a431561187694126de01f9b12559549b4d86360c0c4bd870fde/pandas-3.0.5-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:71ecc8fb7ed1a7aa4392316b5309a6347e8e7f832f38fd897846b3a1457a9298", size = 9990736, upload-time = "2026-07-22T22:17:52.388Z" }, + { url = "https://files.pythonhosted.org/packages/de/21/359880af3ea9b7cb23bea5b51e8e70ef3866c03be09da9a2787e18e330a8/pandas-3.0.5-cp311-cp311-manylinux_2_24_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:b173f5951ff6b8b0ec7675e20dff3c97b7e7a57dfcce387c2d7c5afe87cb7899", size = 10814438, upload-time = "2026-07-22T22:17:54.708Z" }, + { url = "https://files.pythonhosted.org/packages/d1/50/d6cc4d7e508bbccf5d6027314a8312bc7ac73d0ec7f195f53838daafab40/pandas-3.0.5-cp311-cp311-manylinux_2_24_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:2c0cf1dd9b55a22d105fc46c1b489af3bd42264fcba7c66297bf47a9a1d9c78a", size = 11323634, upload-time = "2026-07-22T22:17:56.858Z" }, + { url = "https://files.pythonhosted.org/packages/70/2b/d5f0a8c90dd0ae04e64ba53b871afb796ec026b615086d382ddc2ade729b/pandas-3.0.5-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:0fac0010c75e4efb6b99e249c183a8993ce0dc95c240f9b120a5e67c727b7928", size = 11850860, upload-time = "2026-07-22T22:17:59.1Z" }, + { url = "https://files.pythonhosted.org/packages/5c/30/183aec2e19adf778a98d29b5729a0a68f4cc4ebf9b9c3b70d0297355bcb1/pandas-3.0.5-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:08d24fe11a17dc33bd6e937dc9c665f9cba08fbdc9f657f405713515febe300d", size = 12411100, upload-time = "2026-07-22T22:18:01.485Z" }, + { url = "https://files.pythonhosted.org/packages/fa/9a/31f4983f191af51ab2a8f2d0c7b33dff3a84da26533f982fff02c2f9e28b/pandas-3.0.5-cp311-cp311-win_amd64.whl", hash = "sha256:b1261758dfb6cf12c3cff8300e21cefad30e7ec709abb4c24ac7318e6a52462a", size = 9968804, upload-time = "2026-07-22T22:18:03.903Z" }, + { url = "https://files.pythonhosted.org/packages/49/97/7886c89a39045c69ad82cbceaf3343810480c8ef49a216319ce8183860a6/pandas-3.0.5-cp311-cp311-win_arm64.whl", hash = "sha256:679f4e85b30ddb1515458ab1e788d3e260eae369b1f78da7a3aa4cac8ebf4a2a", size = 9205447, upload-time = "2026-07-22T22:18:06.134Z" }, + { url = "https://files.pythonhosted.org/packages/1c/54/1dc810ea558d1320b597aa140a514f2fdf1d2ea09c38cf556f13ea712ec9/pandas-3.0.5-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:fa290c16964d4963fbfbc358928239cf3bd755b20e988ce944877def2f44471d", size = 10411717, upload-time = "2026-07-22T22:18:08.307Z" }, + { url = "https://files.pythonhosted.org/packages/68/56/fbe81c09195924d8b7b8d4461a20458fe80a6a5ed6b24f0314da684277e1/pandas-3.0.5-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:c2e26bb46934b8a2ca0c3de1d3d606fc5f6746584791b2db264d58cf370e08dc", size = 9957095, upload-time = "2026-07-22T22:18:10.6Z" }, + { url = "https://files.pythonhosted.org/packages/e0/51/fac252f4a913ed5eabf3c11b880a9e8d5a6c10f0b2129d0462212d238b4d/pandas-3.0.5-cp312-cp312-manylinux_2_24_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:73fa87b08a7ef706f8aafda39ddaccf2a99047bea62d8c88a0361bcafb2237bc", size = 10485458, upload-time = "2026-07-22T22:18:12.834Z" }, + { url = "https://files.pythonhosted.org/packages/12/98/e976540c1addf70442be7842a18cf70884a964abbf69442504f4d2939989/pandas-3.0.5-cp312-cp312-manylinux_2_24_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:d373ce03ffd84010ed9839fa73672a9c8256990532e158440c0085db7d914b34", size = 10998091, upload-time = "2026-07-22T22:18:15.209Z" }, + { url = "https://files.pythonhosted.org/packages/a4/8c/1f29b5be8d3fc47dd7567eb167fabba2085879b31e0287ce7cba6d3d2ff4/pandas-3.0.5-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:2a29c53d85ea98c5e792c59ef82ee9fbe6ca902c0d0adb6b23f45ef894cd7bf6", size = 11499501, upload-time = "2026-07-22T22:18:17.689Z" }, + { url = "https://files.pythonhosted.org/packages/9d/e2/bd9c98ad2df7b38bde002adde4cdf353519da51881634323b126c55997f9/pandas-3.0.5-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:a5ad3b02ed6bc7d7ae9b70804b2c6aa31827489d150f8e623ce82491b82085d7", size = 12060559, upload-time = "2026-07-22T22:18:20.147Z" }, + { url = "https://files.pythonhosted.org/packages/f3/9a/ffbd852d58bd74a617fe2f8ee6a58a96982271ce41cf981eab22190b4a4b/pandas-3.0.5-cp312-cp312-pyemscripten_2024_0_wasm32.whl", hash = "sha256:b2acb4650527eec6822c3dadb2b771277b65e7dae7a267d4bccf65fd1bb3fbce", size = 7197652, upload-time = "2026-07-22T22:18:22.502Z" }, + { url = "https://files.pythonhosted.org/packages/70/b5/d2d3e9ae73362ba4229651b0ee1455cf78073a1ce585f6ff693782ce263e/pandas-3.0.5-cp312-cp312-win_amd64.whl", hash = "sha256:80a611068e8a3ac23f7398c6c14eb46dc974e5cc9997f653e2dcfd1da74edd41", size = 9831691, upload-time = "2026-07-22T22:18:24.534Z" }, + { url = "https://files.pythonhosted.org/packages/52/51/dea1e89d6a6796b9c43f85a09b484ee03edb8a4c4842e73e200a8c11301c/pandas-3.0.5-cp312-cp312-win_arm64.whl", hash = "sha256:25ff585b972a18ef1fe9ffa3ac6544d9950508aa76832e5147640b6022821e49", size = 9105796, upload-time = "2026-07-22T22:18:27.064Z" }, + { url = "https://files.pythonhosted.org/packages/bf/09/7b95c4a0025227d6f118c4039b423412ac6a982db02864166185d812fbc7/pandas-3.0.5-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:c1c05a767fe8e5b4fe9e1c29806829c582052eaedb9120a3da83ba3f69e24a5b", size = 10385742, upload-time = "2026-07-22T22:18:29.346Z" }, + { url = "https://files.pythonhosted.org/packages/8d/0c/dc78fd8c4da477b4b5e8ad37295af352190d21ef63a9ee1bc071753074cc/pandas-3.0.5-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:b86765f268b56f7e665b93bce9d5df69dee7f99e595cf8fb839483ab315942a3", size = 9932067, upload-time = "2026-07-22T22:18:31.833Z" }, + { url = "https://files.pythonhosted.org/packages/3e/71/3592c055cf44df9808550f9368ceda80ff2b224d355ef73fe251dcda1802/pandas-3.0.5-cp313-cp313-manylinux_2_24_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:c597ecf5616b5c420372c1d4d4c00dbbfba7398bea857dcc984347e1ea48417b", size = 10466756, upload-time = "2026-07-22T22:18:34.195Z" }, + { url = "https://files.pythonhosted.org/packages/e3/70/4363150359f95b4cb4bcbb34ca23572bb5495749a621a8f3d5a1ddfd293c/pandas-3.0.5-cp313-cp313-manylinux_2_24_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:4b11c36e218331d0387cbe3a0a5f75162357a1d92d57b2b08a336ff94b19b2be", size = 10938525, upload-time = "2026-07-22T22:18:36.81Z" }, + { url = "https://files.pythonhosted.org/packages/f7/d0/317e7a0c67c0e69fa905a0161409397a7dc2d46ff611f6ca4803352c042b/pandas-3.0.5-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:cf52e1f61d229496da17dc7ab54acdee627357e7008fd4fecba3d0ba2937fa58", size = 11489303, upload-time = "2026-07-22T22:18:39.287Z" }, + { url = "https://files.pythonhosted.org/packages/f1/8d/36dade89b49e4f9d5cbdbe863772581f98c0c6d78fc39ad4c557f6f2e17e/pandas-3.0.5-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:db172144bb56422bd157812f3b021eacc255451470b31e2c633c349490a1cfee", size = 11989004, upload-time = "2026-07-22T22:18:42.208Z" }, + { url = "https://files.pythonhosted.org/packages/9c/ba/18c4ec8a746e177da05a9e7a7963781d8ea195780724f854601b6ebd6b78/pandas-3.0.5-cp313-cp313-win_amd64.whl", hash = "sha256:0d298e951f23016ce4699951d044ae6418dbc91bf68cefca0f77666fcbb4e5c6", size = 9826896, upload-time = "2026-07-22T22:18:44.539Z" }, + { url = "https://files.pythonhosted.org/packages/de/ec/28a57266b753799a87b8bc79e7887ac6fd981b8c6d2978a0b7e7b6bd708c/pandas-3.0.5-cp313-cp313-win_arm64.whl", hash = "sha256:66266d3442a5e8b3c90274c2b8b230bee42dd1c286bc822cc2f9f2c7e12b883e", size = 9094790, upload-time = "2026-07-22T22:18:47.468Z" }, + { url = "https://files.pythonhosted.org/packages/51/2f/cf6aae281264f4463f0875bcbb15fd2bb6d291cc535187dad1732475e4a9/pandas-3.0.5-cp314-cp314-macosx_10_15_x86_64.whl", hash = "sha256:2f264fc46911cc8131a7322a16199bbf8e353d27c10bb211f5bd0c814324dc36", size = 10390034, upload-time = "2026-07-22T22:18:49.818Z" }, + { url = "https://files.pythonhosted.org/packages/06/ec/5189518c7a7659c4bdcc6b1eb32c46c6f3c86b0661ffd84143d1112c7732/pandas-3.0.5-cp314-cp314-macosx_11_0_arm64.whl", hash = "sha256:53730687fcd161883b24e10411c06d6a4c0f2275d2faf3bb2bc25deb4ba8007c", size = 9980065, upload-time = "2026-07-22T22:18:52.249Z" }, + { url = "https://files.pythonhosted.org/packages/ea/f1/598503ce8d7e3c35601e0747ba288c7864baae66380725bc12f13f884dfe/pandas-3.0.5-cp314-cp314-manylinux_2_24_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:960d3ebcf249f75206899fcd2c6de53f736b7265759ced0d3e559df0b8b709b0", size = 10545532, upload-time = "2026-07-22T22:18:54.813Z" }, + { url = "https://files.pythonhosted.org/packages/fa/de/ceae2adf7034e07e9910299fe412e1819c4f0dd520700a888bcb03625448/pandas-3.0.5-cp314-cp314-manylinux_2_24_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:9e94c2c5ca43bd3ca32bf64d32308887b65e5f9bfd8023ea52755107a999f93b", size = 10963120, upload-time = "2026-07-22T22:18:57.42Z" }, + { url = "https://files.pythonhosted.org/packages/66/25/86e0f4451874eb79e688deeebe3c451fec4557f8952005818d800ee8ac7e/pandas-3.0.5-cp314-cp314-musllinux_1_2_aarch64.whl", hash = "sha256:e819dd5f62966b481a8cb649d3299ebd886a1ea91ed5a99bf7ce77c98d18ab94", size = 11563178, upload-time = "2026-07-22T22:18:59.729Z" }, + { url = "https://files.pythonhosted.org/packages/f3/45/8643daa3b4147e433adfcccefdd0380d3aad79d86b15d8999730fe1944d5/pandas-3.0.5-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:3c5ed2e7c06e91d340dfd091d7934f9bc82e4a36b95f647f090b9d1c9ac649da", size = 12028708, upload-time = "2026-07-22T22:19:02.164Z" }, + { url = "https://files.pythonhosted.org/packages/96/58/ad979ae617615576e8aafd569c9d4b62f1191d896e38f51d66ba06f3b89a/pandas-3.0.5-cp314-cp314-win_amd64.whl", hash = "sha256:cd8f7c6dc98527058ee6264219343f5392240a6f1bfa654fc5d79023020d0c92", size = 9951806, upload-time = "2026-07-22T22:19:04.596Z" }, + { url = "https://files.pythonhosted.org/packages/69/32/7ac03886b304049a9d2625ee88f59af760d8a93bd30ed9239bce7b9869a8/pandas-3.0.5-cp314-cp314-win_arm64.whl", hash = "sha256:5183427f5a8156d480f30333777bc978be93650a49a7c01db26adffe95b31e85", size = 9238297, upload-time = "2026-07-22T22:19:06.836Z" }, + { url = "https://files.pythonhosted.org/packages/be/ed/1d1f2ee5547d5167face2376d11c8b2a4c7bfff5a416ee7a9046891fab1e/pandas-3.0.5-cp314-cp314t-macosx_10_15_x86_64.whl", hash = "sha256:303da736987d481074ca720ada325f8bd80c64ebc2d45ed79b29df3aaa4a26ca", size = 10849690, upload-time = "2026-07-22T22:19:09.391Z" }, + { url = "https://files.pythonhosted.org/packages/57/55/17e17152e98fbb0c4b1e562bc65387a2f20a80db0f4a86bf8d3a0e4248d4/pandas-3.0.5-cp314-cp314t-macosx_11_0_arm64.whl", hash = "sha256:3b2801bbb049d0136f6c213eae02b5fca969384fc2064dd728d8620552aa49da", size = 10509945, upload-time = "2026-07-22T22:19:11.773Z" }, + { url = "https://files.pythonhosted.org/packages/88/90/817d44dbf83facf9556f33576d9af0a241981e7bb5c00606c0bcb5df8dda/pandas-3.0.5-cp314-cp314t-manylinux_2_24_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:cce3a9d11d2b1f82c69a27ec1f4948a170e2c403c4bbfa8cca62e3fdebe2ef3a", size = 10392197, upload-time = "2026-07-22T22:19:14.024Z" }, + { url = "https://files.pythonhosted.org/packages/f1/da/889f00c0a6f5aa1545add70abbf01502dff87ab577adb855bd631c54d2f2/pandas-3.0.5-cp314-cp314t-manylinux_2_24_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:ef01af4d8dc6cd2c8d6c7736f149574ef93fe043811eeb5e445f2647154b5040", size = 10862726, upload-time = "2026-07-22T22:19:16.351Z" }, + { url = "https://files.pythonhosted.org/packages/bc/98/f1e934fb3c98fce859c6147c6785816c7b5b9ab7821115c5d8c4de9842b9/pandas-3.0.5-cp314-cp314t-musllinux_1_2_aarch64.whl", hash = "sha256:e2759e890db96dfcffdbd9b86c3c2cb6afaf58def482820317e06163ec1066cd", size = 11414864, upload-time = "2026-07-22T22:19:18.981Z" }, + { url = "https://files.pythonhosted.org/packages/fe/be/d448af7d657d82e1888dd8551f79c6d6fb161080b5b9752d84d910ec2319/pandas-3.0.5-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:b58b1b39d46a5862e3fb18f50d1a201398619d16a0f9f73f57eea5583cf0e63c", size = 11925105, upload-time = "2026-07-22T22:19:21.515Z" }, + { url = "https://files.pythonhosted.org/packages/29/c1/ccb4238212c8c4f496c584f3044d94e0c030ed8e1d68999db46c91c2242f/pandas-3.0.5-cp314-cp314t-win_amd64.whl", hash = "sha256:1c10461f6eeb35d8f05b6184c65c8b9991663b66c46b1d559b682cb34ae7c6ea", size = 10387612, upload-time = "2026-07-22T22:19:24.257Z" }, + { url = "https://files.pythonhosted.org/packages/d2/cf/6a51b2c38980e04c279fd2fa908a1b0982064e860444acfca4ec2e2c8359/pandas-3.0.5-cp314-cp314t-win_arm64.whl", hash = "sha256:3c5015fd1730fbf883647e88068176c839c102cea883ba1769a6f4593bfc1f8c", size = 9509776, upload-time = "2026-07-22T22:19:26.694Z" }, ] [[package]] @@ -1589,7 +1679,7 @@ name = "pexpect" version = "4.9.0" source = { registry = "https://pypi.org/simple" } dependencies = [ - { name = "ptyprocess" }, + { name = "ptyprocess", marker = "sys_platform != 'emscripten' and sys_platform != 'win32'" }, ] sdist = { url = "https://files.pythonhosted.org/packages/42/92/cc564bf6381ff43ce1f4d06852fc19a2f11d180f23dc32d9588bee2f149d/pexpect-4.9.0.tar.gz", hash = "sha256:ee7d41123f3c9911050ea2c2dac107568dc43b2d3b0c7557a33212c398ead30f", size = 166450, upload-time = "2023-11-25T09:07:26.339Z" } wheels = [ @@ -1700,16 +1790,16 @@ requires-dist = [ { name = "pandas", specifier = ">=2.0.0" }, { name = "plotly", marker = "extra == 'dev'", specifier = ">=5.0.0" }, { name = "plotly", marker = "extra == 'plotting'", specifier = ">=5.0.0" }, - { name = "policyengine-core", marker = "extra == 'dev'", specifier = "==3.30.1" }, - { name = "policyengine-core", marker = "extra == 'models'", specifier = "==3.30.1" }, - { name = "policyengine-core", marker = "extra == 'uk'", specifier = "==3.30.1" }, - { name = "policyengine-core", marker = "extra == 'us'", specifier = "==3.30.1" }, + { name = "policyengine-core", marker = "extra == 'dev'", specifier = "==3.32.5" }, + { name = "policyengine-core", marker = "extra == 'models'", specifier = "==3.32.5" }, + { name = "policyengine-core", marker = "extra == 'uk'", specifier = "==3.32.5" }, + { name = "policyengine-core", marker = "extra == 'us'", specifier = "==3.32.5" }, { name = "policyengine-uk", marker = "extra == 'dev'", specifier = "==2.90.2" }, { name = "policyengine-uk", marker = "extra == 'models'", specifier = "==2.90.2" }, { name = "policyengine-uk", marker = "extra == 'uk'", specifier = "==2.90.2" }, - { name = "policyengine-us", marker = "extra == 'dev'", specifier = "==1.764.6" }, - { name = "policyengine-us", marker = "extra == 'models'", specifier = "==1.764.6" }, - { name = "policyengine-us", marker = "extra == 'us'", specifier = "==1.764.6" }, + { name = "policyengine-us", marker = "extra == 'dev'", specifier = "==2.0.0" }, + { name = "policyengine-us", marker = "extra == 'models'", specifier = "==2.0.0" }, + { name = "policyengine-us", marker = "extra == 'us'", specifier = "==2.0.0" }, { name = "psutil", specifier = ">=5.9.0" }, { name = "pydantic", specifier = ">=2.0.0" }, { name = "pytest", marker = "extra == 'dev'" }, @@ -1717,9 +1807,9 @@ requires-dist = [ { name = "pytest-cov", marker = "extra == 'dev'", specifier = ">=5.0.0" }, { name = "requests", specifier = ">=2.31.0" }, { name = "ruff", marker = "extra == 'dev'", specifier = ">=0.9.0" }, - { name = "spm-calculator", marker = "extra == 'dev'", specifier = "==0.3.1" }, - { name = "spm-calculator", marker = "extra == 'models'", specifier = "==0.3.1" }, - { name = "spm-calculator", marker = "extra == 'us'", specifier = "==0.3.1" }, + { name = "spm-calculator", marker = "extra == 'dev'", specifier = "==1.0.0" }, + { name = "spm-calculator", marker = "extra == 'models'", specifier = "==1.0.0" }, + { name = "spm-calculator", marker = "extra == 'us'", specifier = "==1.0.0" }, { name = "towncrier", marker = "extra == 'dev'", specifier = ">=24.8.0" }, { name = "yaml-changelog", marker = "extra == 'dev'", specifier = ">=0.1.7" }, ] @@ -1727,7 +1817,7 @@ provides-extras = ["plotting", "graph", "models", "uk", "us", "dev"] [[package]] name = "policyengine-core" -version = "3.30.1" +version = "3.32.5" source = { registry = "https://pypi.org/simple" } dependencies = [ { name = "dpath" }, @@ -1736,7 +1826,8 @@ dependencies = [ { name = "ipython" }, { name = "microdf-python" }, { name = "numexpr" }, - { name = "numpy" }, + { name = "numpy", version = "2.1.3", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version < '3.14'" }, + { name = "numpy", version = "2.5.3", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version >= '3.14'" }, { name = "pandas" }, { name = "plotly" }, { name = "psutil" }, @@ -1747,9 +1838,9 @@ dependencies = [ { name = "standard-imghdr" }, { name = "wheel" }, ] -sdist = { url = "https://files.pythonhosted.org/packages/a1/9d/f71da3e348ddc52e058c76b72bbc4b2f20abcf0e7389ecd5aca5d550fd14/policyengine_core-3.30.1.tar.gz", hash = "sha256:a16f29fe51ec01a7be2171386f8bd26b9ba55ffc338adb70adcadafe33d9c55f", size = 500419, upload-time = "2026-07-19T15:50:41.288Z" } +sdist = { url = "https://files.pythonhosted.org/packages/12/54/cfd2138584de9cddaa4459bba7fdc381101ca8b8ffd38356a1b256850d5f/policyengine_core-3.32.5.tar.gz", hash = "sha256:f051c269b538fe77ee7868f7cfbc0138551960b1b677a0ad6fdc1a93f1660460", size = 420112, upload-time = "2026-09-10T04:16:48.889Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/b9/7e/07624d8add8889af1b4af796d36c38b2e2af494b1d78a064fab29e9bea00/policyengine_core-3.30.1-py3-none-any.whl", hash = "sha256:2dbcf5f590a0199a7b7c77fcbbda2ff6bc289f6c6169ca0af3beace35d8b3e63", size = 244846, upload-time = "2026-07-19T15:50:39.651Z" }, + { url = "https://files.pythonhosted.org/packages/c6/d9/72c3707465e9fe77e6f7a55ea64b49a287cbf875b78f6e862d945df26975/policyengine_core-3.32.5-py3-none-any.whl", hash = "sha256:9d8c162d5fe5c784ae16c885da3ffbfc39a536add0e111133144d8edd555935f", size = 245925, upload-time = "2026-09-10T04:16:47.153Z" }, ] [[package]] @@ -1769,7 +1860,7 @@ wheels = [ [[package]] name = "policyengine-us" -version = "1.764.6" +version = "2.0.0" source = { registry = "https://pypi.org/simple" } dependencies = [ { name = "microdf-python" }, @@ -1779,9 +1870,9 @@ dependencies = [ { name = "tables" }, { name = "tqdm" }, ] -sdist = { url = "https://files.pythonhosted.org/packages/6e/cc/9e65a9586069beab1f02102a23f7a0e6332f1a9c2d590cb9f0e68f9ac987/policyengine_us-1.764.6.tar.gz", hash = "sha256:9817d48abc5b7d690b8abe33047005e5cf40b71654a85a2534bb5e3bcf921f79", size = 11009555, upload-time = "2026-07-06T13:13:07.254Z" } +sdist = { url = "https://files.pythonhosted.org/packages/bf/c0/631546d94507cf2506731607e32354d99b9b9415382720e5cb1d02997ad9/policyengine_us-2.0.0.tar.gz", hash = "sha256:bffa42c408dbd657541b14bdced99dbb1251cf0663c66b578155e7d92b4d6099", size = 11875984, upload-time = "2026-09-12T01:11:26.472Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/e2/5a/0566666f30ad06e416f0cf25b24bdee73acd71897fb2de3c0ac43b59e3e3/policyengine_us-1.764.6-py3-none-any.whl", hash = "sha256:2b2c5c02c26ab212270948db1f30129728b625fc6109fc9ca0c604b714546909", size = 13117921, upload-time = "2026-07-06T13:13:03.476Z" }, + { url = "https://files.pythonhosted.org/packages/fc/fa/b94e315f2cd67919afdc40bd136fc338ec629119b4d655bc5c03a0880874/policyengine_us-2.0.0-py3-none-any.whl", hash = "sha256:4ca187509efc2f7d3ace5035c27de88c744d0f112fdc7da027372f3ae07c540a", size = 14806288, upload-time = "2026-09-12T01:11:24.028Z" }, ] [[package]] @@ -2667,19 +2758,20 @@ wheels = [ [[package]] name = "spm-calculator" -version = "0.3.1" +version = "1.0.0" source = { registry = "https://pypi.org/simple" } dependencies = [ { name = "census" }, - { name = "numpy" }, + { name = "numpy", version = "2.1.3", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version < '3.14'" }, + { name = "numpy", version = "2.5.3", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version >= '3.14'" }, { name = "openpyxl" }, { name = "pandas" }, { name = "requests" }, { name = "us" }, ] -sdist = { url = "https://files.pythonhosted.org/packages/54/3b/b805c7e3e18c5b5c00f61b60112f9690d084c910e2481bc020f35390d8fd/spm_calculator-0.3.1.tar.gz", hash = "sha256:41f2f4d00d8c03422a7d57b800052e7760b88e463a5884802f83ed58d35c18c1", size = 75945, upload-time = "2026-04-17T19:52:39.707Z" } +sdist = { url = "https://files.pythonhosted.org/packages/c2/5e/3f28b212c401795990250bb3daec741f699a8c7dc2a23a3db7005df6feb6/spm_calculator-1.0.0.tar.gz", hash = "sha256:a99aac8c2c0bf81a9455105bbf872366f1ea9066a99cd793714881cd3db846dc", size = 7150825, upload-time = "2026-09-11T15:45:12.732Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/8e/1b/29f705f8a96fc7f55f2c07dfcddbbae78efdc6f174d25d4a0560fc3f5cf9/spm_calculator-0.3.1-py3-none-any.whl", hash = "sha256:52c57ecc5a240ec941b0f2b0d93bc4fa437ef6250e233baed8e11916fa9c1150", size = 57826, upload-time = "2026-04-17T19:52:38.444Z" }, + { url = "https://files.pythonhosted.org/packages/19/a0/c484f69a0ebf88a9b9fd0ac28176f8df66550f46714f1b0c5ec0b360824e/spm_calculator-1.0.0-py3-none-any.whl", hash = "sha256:e354937a5e1a4045d4966ed594a528d8b02866fabaac9bb5672017004b627305", size = 7395384, upload-time = "2026-09-11T15:45:10.492Z" }, ] [[package]] @@ -2749,7 +2841,8 @@ source = { registry = "https://pypi.org/simple" } dependencies = [ { name = "blosc2" }, { name = "numexpr" }, - { name = "numpy" }, + { name = "numpy", version = "2.1.3", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version < '3.14'" }, + { name = "numpy", version = "2.5.3", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version >= '3.14'" }, { name = "packaging" }, { name = "py-cpuinfo" }, ] From c6a99c41ac53bfb688bed4c15022344a0afcc197 Mon Sep 17 00:00:00 2001 From: Max Ghenis Date: Fri, 11 Sep 2026 21:37:24 -0400 Subject: [PATCH 15/46] Rebind the TRACE TROs to the canonical bundle Both sidecars now bind manifest sha256 9b0ec2c8; the US TRO additionally binds the certified dataset 6496cc43, release manifest 6c39eb1e and the published policyengine-us 2.0.0 wheel 4ca18750. Regenerated with a Hugging Face token so the UK sidecar keeps its full private release manifest rather than silently degrading to a limited TRO. The US TRO loses schema:dateCreated and trov:startedAtTime because the producer's release manifest for this build declares no built_at; the generator omits the timestamps rather than inventing one. Co-Authored-By: Claude Fable 5.1 --- .../data/bundle/uk.trace.tro.jsonld | 4 +-- .../data/bundle/us.trace.tro.jsonld | 30 +++++++++---------- 2 files changed, 16 insertions(+), 18 deletions(-) diff --git a/src/policyengine/data/bundle/uk.trace.tro.jsonld b/src/policyengine/data/bundle/uk.trace.tro.jsonld index 4e03f86f..b16ebfb1 100644 --- a/src/policyengine/data/bundle/uk.trace.tro.jsonld +++ b/src/policyengine/data/bundle/uk.trace.tro.jsonld @@ -75,7 +75,7 @@ "@type": "trov:ResearchArtifact", "schema:name": "policyengine.py bundle manifest for uk", "trov:mimeType": "application/json", - "trov:sha256": "d430b8e561d76e5836a17bf506b06b9cd16560eb77f15718dd46f4e78513de85" + "trov:sha256": "9b0ec2c88e450ed865b4f71d4c0c87b2b0ab8a2176dabec51794b0405d6e0fe2" }, { "@id": "composition/1/artifact/data_release_manifest", @@ -102,7 +102,7 @@ "trov:hasFingerprint": { "@id": "composition/1/fingerprint", "@type": "trov:CompositionFingerprint", - "trov:sha256": "ae802a52d5b8d5d5d388ab7b815c78c6d56bd82c3483de25eb93e4aa42b8f5fe" + "trov:sha256": "369b91f53ff615a1c2835a3aecc1a3a1aed393a76d16a1494022651a7d2f1bd2" } }, "trov:hasPerformance": { diff --git a/src/policyengine/data/bundle/us.trace.tro.jsonld b/src/policyengine/data/bundle/us.trace.tro.jsonld index 735cca73..98b523c9 100644 --- a/src/policyengine/data/bundle/us.trace.tro.jsonld +++ b/src/policyengine/data/bundle/us.trace.tro.jsonld @@ -17,7 +17,6 @@ "schema:name": "PolicyEngine", "schema:url": "https://policyengine.org" }, - "schema:dateCreated": "2026-07-28T03:56:03.415544+00:00", "schema:description": "TRACE TRO for certified runtime bundle us-5.3.1 covering the bundle manifest, the certified dataset artifact, the country model wheel, and the country data release manifest when it is available.", "schema:name": "policyengine us certified bundle TRO", "trov:createdWith": { @@ -45,7 +44,7 @@ "trov:hasArtifact": { "@id": "composition/1/artifact/data_release_manifest" }, - "trov:hasLocation": "https://huggingface.co/datasets/policyengine/populace-us/resolve/populace-us-2024-buildp-sparse-rmloss100-cae8640-20260728T011454Z/releases/populace-us-2024-buildp-sparse-rmloss100-cae8640-20260728T011454Z/release_manifest.json" + "trov:hasLocation": "https://huggingface.co/datasets/policyengine/populace-us/resolve/populace-us-2024-spm-20260909/releases/populace-us-2024-spm-20260909/release_manifest.json" }, { "@id": "arrangement/1/location/dataset", @@ -53,7 +52,7 @@ "trov:hasArtifact": { "@id": "composition/1/artifact/dataset" }, - "trov:hasLocation": "https://huggingface.co/datasets/policyengine/populace-us/resolve/populace-us-2024-buildp-sparse-rmloss100-cae8640-20260728T011454Z/populace_us_2024.h5" + "trov:hasLocation": "https://huggingface.co/datasets/policyengine/populace-us/resolve/populace-us-2024-spm-20260909/populace_us_2024.h5" }, { "@id": "arrangement/1/location/model_wheel", @@ -61,7 +60,7 @@ "trov:hasArtifact": { "@id": "composition/1/artifact/model_wheel" }, - "trov:hasLocation": "https://files.pythonhosted.org/packages/e2/5a/0566666f30ad06e416f0cf25b24bdee73acd71897fb2de3c0ac43b59e3e3/policyengine_us-1.764.6-py3-none-any.whl" + "trov:hasLocation": "https://files.pythonhosted.org/packages/fc/fa/b94e315f2cd67919afdc40bd136fc338ec629119b4d655bc5c03a0880874/policyengine_us-2.0.0-py3-none-any.whl" } ] } @@ -75,50 +74,49 @@ "@type": "trov:ResearchArtifact", "schema:name": "policyengine.py bundle manifest for us", "trov:mimeType": "application/json", - "trov:sha256": "d430b8e561d76e5836a17bf506b06b9cd16560eb77f15718dd46f4e78513de85" + "trov:sha256": "9b0ec2c88e450ed865b4f71d4c0c87b2b0ab8a2176dabec51794b0405d6e0fe2" }, { "@id": "composition/1/artifact/data_release_manifest", "@type": "trov:ResearchArtifact", - "schema:name": "populace-data release manifest 0.1.0", + "schema:name": "microcosm-data release manifest 0.1.0", "trov:mimeType": "application/json", - "trov:sha256": "dd949ba3c4c7a56aff8442c6db5a031d2c84c3da59f5d14832f468c358604506" + "trov:sha256": "6c39eb1e61afc1629f2abe57032dced9c0750c60e42b9acad812c7b3476a2989" }, { "@id": "composition/1/artifact/dataset", "@type": "trov:ResearchArtifact", "schema:name": "populace_us_2024", "trov:mimeType": "application/x-hdf5", - "trov:sha256": "48b9d479fb4fd1c3537f9383ce4697d130b6f618658409d74f6233c43b994c7e" + "trov:sha256": "6496cc4393d4d3c6574f76eca231de5898c803b9067645591fd5c4d3e65aee84" }, { "@id": "composition/1/artifact/model_wheel", "@type": "trov:ResearchArtifact", - "schema:name": "policyengine-us==1.764.6 wheel", + "schema:name": "policyengine-us==2.0.0 wheel", "trov:mimeType": "application/zip", - "trov:sha256": "2b2c5c02c26ab212270948db1f30129728b625fc6109fc9ca0c604b714546909" + "trov:sha256": "4ca187509efc2f7d3ace5035c27de88c744d0f112fdc7da027372f3ae07c540a" } ], "trov:hasFingerprint": { "@id": "composition/1/fingerprint", "@type": "trov:CompositionFingerprint", - "trov:sha256": "7c5f0d14060114a68d236c44cc0da748224ec02c4f714914f8b8b1f82aaf0c51" + "trov:sha256": "cad6fe62062bfaebf58a1de4a62b9941c2224fc2697fe27ef8c3225fa1b05bef" } }, "trov:hasPerformance": { "@id": "trp/1", "@type": "trov:TransparentResearchPerformance", - "pe:builtWithModelVersion": "1.764.6", + "pe:builtWithModelVersion": "2.0.0", "pe:certifiedBy": "policyengine.py bundle certification", - "pe:certifiedForModelVersion": "1.764.6", + "pe:certifiedForModelVersion": "2.0.0", "pe:compatibilityBasis": "built_with_model_package", - "pe:dataBuildId": "populace-us-2024-buildp-sparse-rmloss100-cae8640-20260728T011454Z", + "pe:dataBuildId": "populace-us-2024-spm-20260909", "pe:emittedIn": "repository-bundle", - "rdfs:comment": "Certification of build populace-us-2024-buildp-sparse-rmloss100-cae8640-20260728T011454Z for policyengine-us 1.764.6.", + "rdfs:comment": "Certification of build populace-us-2024-spm-20260909 for policyengine-us 2.0.0.", "trov:accessedArrangement": { "@id": "arrangement/1" }, - "trov:startedAtTime": "2026-07-28T03:56:03.415544+00:00", "trov:wasConductedBy": { "@id": "trs" } From bff2c0bfd5c1e5f5a666fb6f7b08bf7405f5fce6 Mon Sep 17 00:00:00 2001 From: Max Ghenis Date: Fri, 11 Sep 2026 21:42:59 -0400 Subject: [PATCH 16/46] Record the published-wheel provenance blocker Co-Authored-By: Claude Fable 5.1 --- PROGRESS.md | 121 ++++++++++++++++++++++++++++++++-------------------- 1 file changed, 74 insertions(+), 47 deletions(-) diff --git a/PROGRESS.md b/PROGRESS.md index 67638374..9eef56e9 100644 --- a/PROGRESS.md +++ b/PROGRESS.md @@ -2,61 +2,88 @@ ## State -**In progress.** Prerequisites all verified; repinning the bundle manifest to the -canonical tuple next. Branch `max/spm-canonical-wrapper-release-20260910`, PR #515 -(draft, open, mergeable), starting head `223bce47`, base `origin/main` `6a56ced4`. +**Binding complete; release BLOCKED on a wheel-provenance defect that is not mine to fix.** -## Prerequisites (all verified 2026-09-11) +Everything the brief asked for has been done: the pins, the dataset certification, the +lock, the TROs and the release guards all bind the published canonical tuple. But while +verifying, I established by direct byte comparison that **the policyengine-us 2.0.0 wheel +published on PyPI is a different, later build than the wheel the acceptance matrix +accepted**, and that the published build changes household values, drops a determinism +fix, weakens its own Core pin, and breaks this PR's own SPM contract test. Details below. + +Branch `max/spm-canonical-wrapper-release-20260910`, PR #515 (draft), starting head +`223bce47`, base `origin/main` `6a56ced4`. + +## Prerequisites (all verified 2026-09-11 from primary sources) | Item | Value | |---|---| -| policyengine-us 2.0.0 on PyPI | HTTP 200; wheel sha256 `4ca187509efc2f7d3ace5035c27de88c744d0f112fdc7da027372f3ae07c540a`, uploaded 2026-09-12T01:11:24Z | -| policyengine-us 2.0.0 deps | `policyengine-core>=3.30.1`, `spm-calculator==1.0.0`, requires-python `<3.15,>=3.11` | -| spm-calculator 1.0.0 on PyPI | HTTP 200; wheel sha256 `e354937a5e1a4045d4966ed594a528d8b02866fabaac9bb5672017004b627305` (matches brief) | -| policyengine-core 3.32.5 on PyPI | HTTP 200; wheel sha256 `9d8c162d5fe5c784ae16c885da3ffbfc39a536add0e111133144d8edd555935f` | -| Core 3.32.5 was the acceptance runtime | `rollout/final-core-matrix-20260911-r2/FINAL-REPORT.md` records Core 3.32.5 `9d8c162d…` — byte-identical to the published wheel. Both gate conditions met. | -| HF tag `populace-us-2024-spm-20260909` | exists on `policyengine/populace-us`; tag commit `9a814a3b3b53c0ecd6e1737b6ec862c31300ef6f` | -| H5 it resolves to | `populace_us_2024.h5` LFS sha256 `6496cc4393d4d3c6574f76eca231de5898c803b9067645591fd5c4d3e65aee84` (matches brief) | -| Certified release manifest | `releases/populace-us-2024-spm-20260909/release_manifest.json`, sha256 `6c39eb1e61afc1629f2abe57032dced9c0750c60e42b9acad812c7b3476a2989` — the local certified file at `rollout/h5-producer-20260911/certified/…` is byte-identical to the Hub-published one | -| PR #515 | draft, open, MERGEABLE, head `223bce47` == local HEAD == origin branch | -| PR #515 CI at dispatch | 9 pass, 4 fail (`Test 3.11/3.12/3.13/3.14`) | - -## Mechanism established by reading source (not assumed) - -- `pyproject.toml`'s `[project.optional-dependencies]` is **generated** from the bundle - manifest — `scripts/generate_bundle_artifacts.py:97-143` rewrites the whole block from - `bundle["extras"]` × `bundle["packages"]`. Pins are therefore edited in - `src/policyengine/data/bundle/manifest.json`, never by hand in pyproject. -- The wrapper **version** is owned by CI, not by this commit: `.github/workflows/push.yaml:136-137` - Versioning runs `make changelog` → `.github/bump_version.py`, whose `infer_bump` - (`bump_version.py:70-88`) returns `major` when any `.breaking.` fragment exists, and - `bump_version.py:91-98` maps 5.3.1 → 6.0.0. So `pyproject` `version` stays 5.3.1 in this PR. -- `bump_version.sync_bundle_versions` (`:120-147`) also owns the manifest's own - `bundle_version` / `policyengine_version` / `packages.policyengine.version` / - per-country `bundle_id`. Those stay 5.3.1 here too. +| policyengine-us 2.0.0 on PyPI | 200; wheel sha256 `4ca187509efc2f7d3ace5035c27de88c744d0f112fdc7da027372f3ae07c540a`, uploaded 2026-09-12T01:11:24Z | +| spm-calculator 1.0.0 on PyPI | 200; wheel sha256 `e354937a5e1a4045d4966ed594a528d8b02866fabaac9bb5672017004b627305` (matches brief) | +| policyengine-core 3.32.5 on PyPI | 200; wheel sha256 `9d8c162d5fe5c784ae16c885da3ffbfc39a536add0e111133144d8edd555935f` | +| Core 3.32.5 gate | Both conditions met: PyPI 200, and `rollout/final-core-matrix-20260911-r2` records the same Core sha256 as its runtime | +| HF tag `populace-us-2024-spm-20260909` | commit `9a814a3b3b53c0ecd6e1737b6ec862c31300ef6f` | +| H5 it resolves to | `6496cc4393d4d3c6574f76eca231de5898c803b9067645591fd5c4d3e65aee84` (matches brief) | +| Certified release manifest | sha256 `6c39eb1e61afc1629f2abe57032dced9c0750c60e42b9acad812c7b3476a2989`; the local file under `rollout/h5-producer-20260911/certified/` is byte-identical to the Hub-published one | +| PR #515 at dispatch | draft, open, MERGEABLE, head `223bce47` == local == origin; 9 checks pass, 4 `Test` jobs fail | -## Done +## BLOCKER: the published country wheel is not the accepted country wheel + +Verified by me directly, comparing zip members of the PyPI wheel against +`rollout/country-deterministic-wheel-20260910/wheels/policyengine_us-2.0.0-py3-none-any.whl` +(sha256 `4208998a…`, the build recorded in `final-core-matrix-20260911-r2/FINAL-RECEIPT.json` +as the acceptance runtime): -- Verified every prerequisite above from the authoritative source (PyPI JSON API, HF Hub - API, `gh pr view`), not from a prior lane's report. -- Read the release machinery: `scripts/bundle.py`, `generate_bundle_artifacts.py`, - `prepare_package_bundle_update.py`, `certify_data_release.py`, `spm_bundle.py`, - `Makefile`, `.github/workflows/push.yaml`, `.github/bump_version.py`. -- Confirmed `changelog.d/` already holds two `.breaking.md` fragments - (`canonical-spm-python.breaking.md`, `canonical-spm-wrapper.breaking.md`), so the - automatic bump is already major. +| Difference | Accepted `4208998a` | Published `4ca18750` | +|---|---|---| +| `policyengine_us/spm.py` | no `CountyRequiringSPMProvider`, no `masked_policyengine_amount` | **both added** | +| `adjusted_gross_income_person.py:24` | `sorted(set(all_alds) - set(PERSON_ALDS))` | `list(set(...))` — **determinism fix dropped**; AGI float summation order now varies with `PYTHONHASHSEED` | +| METADATA core pin | `policyengine-core==3.32.5` | `policyengine-core>=3.30.1` — **weakened** | +| NY CTC post-2024 phase-out, SURVIVING_SPOUSE | `110_000` | `75_000` (effective 2025-01-01) — **value changing** | -## Open question being investigated +Three local 2.0.0 builds exist (`4208998a`, `05c729ae`, `08fe6f7e`); none is the published +one, and the published hash appears nowhere on disk. spm-calculator 1.0.0 is fine — the +published and local wheels are content-identical (all 68 members match; only zip +timestamps differ). Core 3.32.5 matches exactly. + +**Consequences.** The poverty rates in `final-core-matrix-20260911-r2/FINAL-REPORT.md` +(13.018144 / 13.235362 / 13.634486 / 13.638541) cannot honestly be attributed to the +published tuple. And `CountyRequiringSPMProvider` — code that exists only in the published +build — is what makes 9 pre-existing microsim tests fail and makes this PR's own +`test_spm_household::test_state_only_tax_graph_succeeds_and_resource_graph_requires_geography` +fail with "DID NOT RAISE". + +I have therefore **not** rewritten any numeric snapshot or contract assertion to match the +published build. Doing so would launder an unaccepted baseline and bury a determinism +regression behind a green suite. + +## Done -Three different sha256 values for a "policyengine-us 2.0.0" wheel exist across today's -lanes: published `4ca18750…`, acceptance-matrix `4208998a…`, loader-candidate `05c729ae…`. -The first is the only one on PyPI. Determining whether the matrix's numerical acceptance -transfers to the published wheel bytes. +- Verified every prerequisite from primary sources rather than from prior lanes' reports. +- Repinned via the repository tooling only — `bundle.py update-packages`, + `bundle.py set-spm --calculator-version 1.0.0` (which downloads and re-hashes the + published wheel), `bundle.py certify-data`, `bundle.py generate`. `pyproject.toml` + extras are generated from the bundle manifest, so no pin was hand-edited. +- Promoted spm-calculator from the protective `country_dependency` pin to the + registry-verified `runtime_dependency` the `--published-spm` release gate requires. +- **Fixed a real certification defect** (commit `a7080192`). The producer now publishes + under `microcosm-data` with `evidence`-kind sidecars; four rules in + `certification.py` matched the literal `populace-data` and the release-directory path + rewrite matched only `diagnostics`. Certifying this release under the old rules produced + a bundle where **8 of 10 certified artifact URIs returned HTTP 404**, with the + reachability sweep silently disabled. With both names and both kinds recognised, all ten + resolve (verified by HEAD request). Six regression tests added. +- Relocked with uv only, registry sources throughout, no local wheel links. +- Rebound both TRACE TROs with a Hugging Face token so the UK sidecar kept its full + private release manifest instead of silently degrading. +- Ran the full suite. The previously-skipped SPM tests now **run**: skips fell 14 -> 9 and + passes rose 1005 -> 1079. ## Next -1. Repin the bundle manifest to the canonical tuple and regenerate pyproject. -2. Certify the US data release at the tag above into the manifest. -3. Refresh `uv.lock` (uv only, no local wheel links) and run the lock/release guards. -4. Run the full suite as CI does; the SPM tests must run, not skip. -5. Commit, force-with-lease push against `223bce47`, watch CI. Leave draft. +1. Apply only the failure fixes that are safe — those asserting the superseded legacy pin + identity. Leave the numeric and contract failures red and documented. +2. Push with `--force-with-lease` against `223bce47`, watch CI, leave the PR a draft. +3. Escalate the wheel-provenance blocker to the coordinator: either republish + policyengine-us 2.0.0 from the accepted source, or re-run the acceptance matrix on the + published wheel bytes (pinning `PYTHONHASHSEED`, given the dropped ALD fix). From b0dee94dd13fc8f08d2d19e978146519ae821867 Mon Sep 17 00:00:00 2001 From: Max Ghenis Date: Fri, 11 Sep 2026 21:50:51 -0400 Subject: [PATCH 17/46] Retarget the legacy pin assertions at the canonical tuple These tests asserted the pin identity this release deliberately replaces: the country model version, the spm-calculator version, the US data package name and the certified dataset build tag. Each assertion is advanced to the value the repinned bundle manifest now declares; no expected household or microsim number is touched. test_staged_config_preserves_certification_and_fails_release_gate needed its premise rebuilt rather than retargeted. The canonical bundle now ships the promoted runtime pin, so the test de-promotes its own fixture copy first and still asserts that a staged configuration fails the release gate with 'promotion pending'. Co-Authored-By: Claude Fable 5.1 --- tests/test_bundle.py | 2 +- tests/test_bundle_metadata.py | 4 ++-- tests/test_models.py | 6 +++--- tests/test_release_manifests.py | 8 ++++---- tests/test_spm_bundle_tooling.py | 7 ++++++- tests/test_us_regions.py | 2 +- 6 files changed, 17 insertions(+), 12 deletions(-) diff --git a/tests/test_bundle.py b/tests/test_bundle.py index 3a9cac87..4a3f536b 100644 --- a/tests/test_bundle.py +++ b/tests/test_bundle.py @@ -64,7 +64,7 @@ def test_bundle_install_requirements_are_country_scoped(): f"policyengine=={manifest['policyengine_version']}", manifest["packages"]["policyengine-core"]["install_requirement"], manifest["packages"]["policyengine-us"]["install_requirement"], - "spm-calculator==0.3.1", + "spm-calculator==1.0.0", ] assert not any("policyengine-us-data" in req for req in us_requirements) diff --git a/tests/test_bundle_metadata.py b/tests/test_bundle_metadata.py index 1b184d1d..85dd2d01 100644 --- a/tests/test_bundle_metadata.py +++ b/tests/test_bundle_metadata.py @@ -57,7 +57,7 @@ def test_generated_us_extras_preserve_legacy_spm_compatibility(monkeypatch): assert generated == original for extra in ("us", "models", "dev"): section = generated.split(f"\n{extra} = [\n", 1)[1].split("\n]", 1)[0] - assert '"spm-calculator==0.3.1"' in section + assert '"spm-calculator==1.0.0"' in section uk_section = generated.split("\nuk = [\n", 1)[1].split("\n]", 1)[0] assert "spm-calculator" not in uk_section @@ -136,7 +136,7 @@ def test_export_release_assets_writes_bundle_assets(monkeypatch, tmp_path): in (tmp_path / f"policyengine-bundle-{version}.constraints.txt").read_text() ) assert ( - "spm-calculator==0.3.1" + "spm-calculator==1.0.0" in (tmp_path / f"policyengine-bundle-{version}.constraints.txt").read_text() ) assert ( diff --git a/tests/test_models.py b/tests/test_models.py index 1b1066e8..bbf19fd1 100644 --- a/tests/test_models.py +++ b/tests/test_models.py @@ -135,13 +135,13 @@ def test_has_release_manifest_metadata(self): assert us_latest.release_manifest is not None assert us_latest.release_manifest.country_id == "us" assert us_latest.model_package.name == "policyengine-us" - assert us_latest.model_package.version == "1.764.6" - assert us_latest.data_package.name == "populace-data" + assert us_latest.model_package.version == "2.0.0" + assert us_latest.data_package.name == "microcosm-data" assert us_latest.data_package.version == "0.1.0" assert ( us_latest.default_dataset_uri == "hf://policyengine/populace-us/populace_us_2024.h5" - "@populace-us-2024-buildp-sparse-rmloss100-cae8640-20260728T011454Z" + "@populace-us-2024-spm-20260909" ) def test_has_hundreds_of_parameters(self): diff --git a/tests/test_release_manifests.py b/tests/test_release_manifests.py index 24bb05be..47c5403d 100644 --- a/tests/test_release_manifests.py +++ b/tests/test_release_manifests.py @@ -44,10 +44,10 @@ PYPROJECT.read_text(), re.MULTILINE, ).group(1) -US_MODEL_VERSION = "1.764.6" -US_BUILT_WITH_MODEL_VERSION = "1.764.6" +US_MODEL_VERSION = "2.0.0" +US_BUILT_WITH_MODEL_VERSION = "2.0.0" US_DATA_RELEASE_VERSION = "0.1.0" -US_DATA_RELEASE_ID = "populace-us-2024-buildp-sparse-rmloss100-cae8640-20260728T011454Z" +US_DATA_RELEASE_ID = "populace-us-2024-spm-20260909" US_DATA_RELEASE_REVISION = US_DATA_RELEASE_ID US_DATA_RELEASE_PATH = f"releases/{US_DATA_RELEASE_ID}/release_manifest.json" US_DATA_ARTIFACT_REVISION = US_DATA_RELEASE_ID @@ -195,7 +195,7 @@ def test__given_us_manifest__then_has_pinned_model_and_data_packages(self): assert manifest.policyengine_version == POLICYENGINE_VERSION assert manifest.model_package.name == "policyengine-us" assert manifest.model_package.version == US_MODEL_VERSION - assert manifest.data_package.name == "populace-data" + assert manifest.data_package.name == "microcosm-data" assert manifest.data_package.version == US_DATA_RELEASE_VERSION assert manifest.data_package.repo_id == "policyengine/populace-us" assert manifest.data_package.release_manifest_path == US_DATA_RELEASE_PATH diff --git a/tests/test_spm_bundle_tooling.py b/tests/test_spm_bundle_tooling.py index ea93dbf7..bfde4e7e 100644 --- a/tests/test_spm_bundle_tooling.py +++ b/tests/test_spm_bundle_tooling.py @@ -90,13 +90,18 @@ def test_historical_bundle_without_measurements_still_loads(canonical_bundle): def test_staged_config_preserves_certification_and_fails_release_gate(canonical_bundle): + # The canonical bundle now ships the promoted runtime pin, so rebuild the + # pre-promotion staging state this guard is about. + canonical_bundle["packages"].pop("spm-calculator") + for extra in ("us", "models"): + canonical_bundle["extras"][extra].remove("spm-calculator") before = copy.deepcopy(canonical_bundle) updated = configure_spm( canonical_bundle, canonical_bundle["measurements"]["spm"], None ) assert updated["data_releases"] == before["data_releases"] assert updated["packages"] == before["packages"] - assert updated["packages"]["policyengine-us"]["version"] == "1.764.6" + assert updated["packages"]["policyengine-us"]["version"] == "2.0.0" validate_bundle_measurements(updated) with pytest.raises(ValueError, match="promotion pending"): validate_bundle_measurements(updated, require_published_spm=True) diff --git a/tests/test_us_regions.py b/tests/test_us_regions.py index 2158dd53..8a839def 100644 --- a/tests/test_us_regions.py +++ b/tests/test_us_regions.py @@ -107,7 +107,7 @@ def test__given_us_registry__then_has_national_region(self): assert national.region_type == "national" assert national.dataset_path == ( "hf://policyengine/populace-us/populace_us_2024.h5" - "@populace-us-2024-buildp-sparse-rmloss100-cae8640-20260728T011454Z" + "@populace-us-2024-spm-20260909" ) def test__given_us_registry__then_has_51_states(self): From 81c2d26ef0a96037e46a6847351058f48235797c Mon Sep 17 00:00:00 2001 From: Max Ghenis Date: Fri, 11 Sep 2026 21:54:34 -0400 Subject: [PATCH 18/46] Give the synthetic US fixtures the county the certified bundle requires MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit The certified bundle measures SPM at county geography, and spm-calculator 1.0.0 rejects a county selection whose county_fips is absent. These fixtures declared only state_code, so every microsim run against them stopped at SPM_GEOGRAPHY_REQUIRED. Each household now names a real county in its own state — Los Angeles for the CA households, Harris for the TX one. This adds input the certified configuration requires; no expected output value changes. The rejection is not specific to the published country wheel: it comes from spm_calculator/policyengine_adapter.py, which is byte-identical across every 1.0.0 build. Co-Authored-By: Claude Fable 5.1 --- tests/test_extra_variables.py | 5 +++++ tests/test_us_microsim_structural_reforms.py | 5 +++++ 2 files changed, 10 insertions(+) diff --git a/tests/test_extra_variables.py b/tests/test_extra_variables.py index 1631566d..219c0602 100644 --- a/tests/test_extra_variables.py +++ b/tests/test_extra_variables.py @@ -48,6 +48,11 @@ def _us_fixture_dataset(tmp_path): { "household_id": [1], "state_code": ["CA"], + # The certified bundle measures SPM at county geography + # (manifest ``measurements.spm.geography_kind == "county"``), + # so a US household fixture must name its county. State alone + # does not identify an SPM area. + "county_fips": ["06037"], "household_weight": [1_000.0], } ), diff --git a/tests/test_us_microsim_structural_reforms.py b/tests/test_us_microsim_structural_reforms.py index 0d9d0803..0babb9a1 100644 --- a/tests/test_us_microsim_structural_reforms.py +++ b/tests/test_us_microsim_structural_reforms.py @@ -64,6 +64,11 @@ def _us_fixture_dataset(tmp_path, year: int = 2025): { "household_id": [1, 2], "state_code": ["CA", "TX"], + # The certified bundle measures SPM at county geography + # (manifest ``measurements.spm.geography_kind == "county"``), + # so a US household fixture must name its county. State alone + # does not identify an SPM area. + "county_fips": ["06037", "48201"], "household_weight": [1_000.0, 1_000.0], } ), From 78477e53f13dc77b440a68000d366b7d091171cd Mon Sep 17 00:00:00 2001 From: Max Ghenis Date: Fri, 11 Sep 2026 21:57:08 -0400 Subject: [PATCH 19/46] Declare national SPM geography in the reform-application fixtures These fixtures assert federal income tax only and declare no household county, so the certified county-kind SPM configuration rejected every call before any assertion ran. Each call now passes spm={geography_kind: national}, the remedy the SPM contract names. Only the geography declaration is added. Every assertion is untouched, including the pre-existing reduction >= 1000 floor, and no state_code was introduced. Co-Authored-By: Claude Fable 5.1 --- tests/test_us_reform_application.py | 37 ++++++++++++++++++++++------- 1 file changed, 29 insertions(+), 8 deletions(-) diff --git a/tests/test_us_reform_application.py b/tests/test_us_reform_application.py index 6e3b4145..235d91fa 100644 --- a/tests/test_us_reform_application.py +++ b/tests/test_us_reform_application.py @@ -7,6 +7,12 @@ ) +# These fixtures declare no county, and the certified bundle's measurements.spm +# block selects geography_kind="county". Declare national geography explicitly, +# as the SPM contract requires; these tests assert federal income tax only. +NATIONAL_SPM = {"geography_kind": "national"} + + def _double_standard_deduction(year: int) -> dict: """Dict reform: standard deduction doubled from ~$14,600 / $29,200 baseline.""" return { @@ -17,22 +23,30 @@ def _double_standard_deduction(year: int) -> dict: class TestUSHouseholdReformApplication: def test__baseline__then_income_tax_positive(self): - result = pe.us.calculate_household(**HIGH_INCOME_SINGLE_FILER) + result = pe.us.calculate_household( + **HIGH_INCOME_SINGLE_FILER, spm=NATIONAL_SPM + ) assert result.tax_unit.income_tax > 0 def test__doubled_standard_deduction__then_tax_lower(self): - baseline = pe.us.calculate_household(**HIGH_INCOME_SINGLE_FILER) + baseline = pe.us.calculate_household( + **HIGH_INCOME_SINGLE_FILER, spm=NATIONAL_SPM + ) reformed = pe.us.calculate_household( **HIGH_INCOME_SINGLE_FILER, reform=_double_standard_deduction(2024), + spm=NATIONAL_SPM, ) assert reformed.tax_unit.income_tax < baseline.tax_unit.income_tax def test__doubled_standard_deduction__then_reduction_is_meaningful(self): - baseline = pe.us.calculate_household(**HIGH_INCOME_SINGLE_FILER) + baseline = pe.us.calculate_household( + **HIGH_INCOME_SINGLE_FILER, spm=NATIONAL_SPM + ) reformed = pe.us.calculate_household( **HIGH_INCOME_SINGLE_FILER, reform=_double_standard_deduction(2024), + spm=NATIONAL_SPM, ) reduction = baseline.tax_unit.income_tax - reformed.tax_unit.income_tax assert reduction >= 1000, ( @@ -40,17 +54,24 @@ def test__doubled_standard_deduction__then_reduction_is_meaningful(self): ) def test__married_couple_joint_deduction__then_tax_lower(self): - baseline = pe.us.calculate_household(**MARRIED_COUPLE_WITH_KIDS) + baseline = pe.us.calculate_household( + **MARRIED_COUPLE_WITH_KIDS, spm=NATIONAL_SPM + ) reformed = pe.us.calculate_household( **MARRIED_COUPLE_WITH_KIDS, reform=_double_standard_deduction(2024), + spm=NATIONAL_SPM, ) assert reformed.tax_unit.income_tax < baseline.tax_unit.income_tax def test__same_reform_twice__then_deterministic(self): reform = _double_standard_deduction(2024) - first = pe.us.calculate_household(**HIGH_INCOME_SINGLE_FILER, reform=reform) - second = pe.us.calculate_household(**HIGH_INCOME_SINGLE_FILER, reform=reform) + first = pe.us.calculate_household( + **HIGH_INCOME_SINGLE_FILER, reform=reform, spm=NATIONAL_SPM + ) + second = pe.us.calculate_household( + **HIGH_INCOME_SINGLE_FILER, reform=reform, spm=NATIONAL_SPM + ) assert first.tax_unit.income_tax == second.tax_unit.income_tax def test__custom_deduction_values__then_tax_reflects_values(self): @@ -63,9 +84,9 @@ def test__custom_deduction_values__then_tax_reflects_values(self): "gov.irs.deductions.standard.amount.JOINT": {"2024-01-01": 100000}, } small = pe.us.calculate_household( - **HIGH_INCOME_SINGLE_FILER, reform=small_reform + **HIGH_INCOME_SINGLE_FILER, reform=small_reform, spm=NATIONAL_SPM ) large = pe.us.calculate_household( - **HIGH_INCOME_SINGLE_FILER, reform=large_reform + **HIGH_INCOME_SINGLE_FILER, reform=large_reform, spm=NATIONAL_SPM ) assert large.tax_unit.income_tax < small.tax_unit.income_tax From 1b72a31180e9a2ad2c8a2e4438507cded715dcfe Mon Sep 17 00:00:00 2001 From: Max Ghenis Date: Fri, 11 Sep 2026 22:06:08 -0400 Subject: [PATCH 20/46] Apply ruff formatting to the reform-application fixtures Co-Authored-By: Claude Fable 5.1 --- tests/test_us_reform_application.py | 5 +---- 1 file changed, 1 insertion(+), 4 deletions(-) diff --git a/tests/test_us_reform_application.py b/tests/test_us_reform_application.py index 235d91fa..28da60d8 100644 --- a/tests/test_us_reform_application.py +++ b/tests/test_us_reform_application.py @@ -6,7 +6,6 @@ MARRIED_COUPLE_WITH_KIDS, ) - # These fixtures declare no county, and the certified bundle's measurements.spm # block selects geography_kind="county". Declare national geography explicitly, # as the SPM contract requires; these tests assert federal income tax only. @@ -23,9 +22,7 @@ def _double_standard_deduction(year: int) -> dict: class TestUSHouseholdReformApplication: def test__baseline__then_income_tax_positive(self): - result = pe.us.calculate_household( - **HIGH_INCOME_SINGLE_FILER, spm=NATIONAL_SPM - ) + result = pe.us.calculate_household(**HIGH_INCOME_SINGLE_FILER, spm=NATIONAL_SPM) assert result.tax_unit.income_tax > 0 def test__doubled_standard_deduction__then_tax_lower(self): From dbf1b1a1a96797e5d14ca98602ef12fc8f26d0d1 Mon Sep 17 00:00:00 2001 From: Max Ghenis Date: Fri, 11 Sep 2026 22:10:52 -0400 Subject: [PATCH 21/46] Record the final test state and the six blocked failures Co-Authored-By: Claude Fable 5.1 --- PROGRESS.md | 49 ++++++++++++++++++++++++++++++++++++++++--------- 1 file changed, 40 insertions(+), 9 deletions(-) diff --git a/PROGRESS.md b/PROGRESS.md index 9eef56e9..fcfc7d6c 100644 --- a/PROGRESS.md +++ b/PROGRESS.md @@ -2,7 +2,7 @@ ## State -**Binding complete; release BLOCKED on a wheel-provenance defect that is not mine to fix.** +**Binding complete and pushed; release BLOCKED on a wheel-provenance defect that is not mine to fix.** Everything the brief asked for has been done: the pins, the dataset certification, the lock, the TROs and the release guards all bind the published canonical tuple. But while @@ -79,11 +79,42 @@ regression behind a green suite. - Ran the full suite. The previously-skipped SPM tests now **run**: skips fell 14 -> 9 and passes rose 1005 -> 1079. -## Next - -1. Apply only the failure fixes that are safe — those asserting the superseded legacy pin - identity. Leave the numeric and contract failures red and documented. -2. Push with `--force-with-lease` against `223bce47`, watch CI, leave the PR a draft. -3. Escalate the wheel-provenance blocker to the coordinator: either republish - policyengine-us 2.0.0 from the accepted source, or re-run the acceptance matrix on the - published wheel bytes (pinning `PYTHONHASHSEED`, given the dropped ALD fix). +- Triaged all 35 post-repin failures and fixed the 29 that are safe to fix, leaving 6. + `make test` goes from **39 failed / 1005 passed / 14 skipped** to + **6 failed / 1108 passed / 9 skipped**. + - Retargeted 14 assertions that pinned the legacy identity (model version, calculator + version, data package name, dataset build tag). + - Gave the synthetic US microsim fixtures a real county (`06037` Los Angeles for the CA + households, `48201` Harris for the TX one) and declared + `spm={"geography_kind": "national"}` on the household reform fixtures. Both add input + the certified county-kind configuration requires; no expected output was touched, and + the rejection comes from `spm_calculator/policyengine_adapter.py`, byte-identical + across every 1.0.0 build, so this is not published-wheel-specific. +- Verified the automatic bump: `bump_version.infer_bump` reads the two `.breaking.md` + fragments and yields **5.3.1 -> 6.0.0**. `pyproject`'s version is deliberately untouched. +- Local CI gates: ruff format, ruff check, `bundle.py check`, + `bundle.py check --published-spm --include-tros`, `release_lock.py` and the towncrier + changelog check all exit 0. + +## The 6 remaining failures are deliberately left red + +None can be made green without adopting a number or behaviour that has no acceptance +evidence. Each would bury something a reviewer needs to see. + +| Test | Why it stays red | +|---|---| +| `test_spm_household::test_state_only_tax_graph_succeeds_and_resource_graph_requires_geography` | The published wheel's `masked_policyengine_amount` skips SPM evaluation when no unit has housing assistance, so none of the five resource variables raises. That contradicts this PR's own public API docstring at `src/policyengine/tax_benefit_models/us/household.py:191-193`: default outputs require geography "including when housing assistance is zero". The published build breaks a documented contract. | +| `test_us_household_snapshot[us_single_adult_no_income]` | Would freeze `spm_unit.snap` 3596.04 -> 298.00. The ratio is 12.07, i.e. a monthly figure reported as annual — the "January-only annual SNAP" defect the previous lane already recorded as an open country-owner investigation. | +| `test_us_household_snapshot` × 3 (employment income, single parent, married two kids) | CA state income tax and child-benefit drift. Same under both the accepted and the published wheel, so not published-only — but still computed household monetary values with no household-level acceptance evidence, and regeneration is wholesale. | +| `test_us_model_version_surface` | Mixed. Its `data_package_name` key is pin identity, but the same snapshot asserts variable/parameter counts that differ **between** the accepted and published wheels (6155/102506 vs 6157/102525 — the `gov/contrib/trump` and `gov/contrib/states/co` trees exist only in the published build). Blocked by its strictest component. | + +## Next (for the coordinator, not this lane) + +1. Resolve the wheel provenance: either republish policyengine-us 2.0.0 from the accepted + source tree, or re-run the acceptance matrix against the published wheel bytes. If the + latter, pin `PYTHONHASHSEED` and repeat under at least two seeds, because the published + build dropped the ALD ordering fix that the matrix's exact-parity claim depends on. +2. Decide whether the published wheel's relaxed `policyengine-core>=3.30.1` is intended; + the bundle pins 3.32.5 explicitly, so the wrapper is safe, but the country package no + longer enforces the version it was accepted on. +3. Settle the January-only annual SNAP defect before any household snapshot is rebaselined. From 22dbde673b314c82058733b37fc40888cb28d5e9 Mon Sep 17 00:00:00 2001 From: Max Ghenis Date: Fri, 11 Sep 2026 22:22:13 -0400 Subject: [PATCH 22/46] Rebuild the staged-pin premise by role, not by deletion MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Popping spm-calculator from the fixture made the release gate reject on its 'no calculator pin at all' branch, which left the discriminator this test is named for — that a protective country_dependency pin is not a promotion — with no coverage anywhere in the repository. Demoting the role in place restores it. Confirmed by mutation: with the role demotion, deleting the 'if not verified' raise in bundle_validation fails this test; under the previous fixture that mutant survived. Co-Authored-By: Claude Fable 5.1 --- tests/test_spm_bundle_tooling.py | 4 +--- 1 file changed, 1 insertion(+), 3 deletions(-) diff --git a/tests/test_spm_bundle_tooling.py b/tests/test_spm_bundle_tooling.py index bfde4e7e..4eb71113 100644 --- a/tests/test_spm_bundle_tooling.py +++ b/tests/test_spm_bundle_tooling.py @@ -92,9 +92,7 @@ def test_historical_bundle_without_measurements_still_loads(canonical_bundle): def test_staged_config_preserves_certification_and_fails_release_gate(canonical_bundle): # The canonical bundle now ships the promoted runtime pin, so rebuild the # pre-promotion staging state this guard is about. - canonical_bundle["packages"].pop("spm-calculator") - for extra in ("us", "models"): - canonical_bundle["extras"][extra].remove("spm-calculator") + canonical_bundle["packages"]["spm-calculator"]["role"] = "country_dependency" before = copy.deepcopy(canonical_bundle) updated = configure_spm( canonical_bundle, canonical_bundle["measurements"]["spm"], None From e7bfa2bd07522d20023529b487bb716b2772efaa Mon Sep 17 00:00:00 2001 From: Max Ghenis Date: Fri, 11 Sep 2026 22:23:29 -0400 Subject: [PATCH 23/46] Correct the characterisation of the SPM contract failure The published wheel's masked housing-cap evaluation is deliberate, not a defect: its own source comment gives the rationale and a partner notice written today already describes it. The failure is a contract disagreement between the country model and the wrapper's public docstring, not a broken build. Co-Authored-By: Claude Fable 5.1 --- PROGRESS.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/PROGRESS.md b/PROGRESS.md index fcfc7d6c..923cddc2 100644 --- a/PROGRESS.md +++ b/PROGRESS.md @@ -103,7 +103,7 @@ evidence. Each would bury something a reviewer needs to see. | Test | Why it stays red | |---|---| -| `test_spm_household::test_state_only_tax_graph_succeeds_and_resource_graph_requires_geography` | The published wheel's `masked_policyengine_amount` skips SPM evaluation when no unit has housing assistance, so none of the five resource variables raises. That contradicts this PR's own public API docstring at `src/policyengine/tax_benefit_models/us/household.py:191-193`: default outputs require geography "including when housing assistance is zero". The published build breaks a documented contract. | +| `test_spm_household::test_state_only_tax_graph_succeeds_and_resource_graph_requires_geography` | Two PolicyEngine components now disagree about a public contract. The published wheel's `masked_policyengine_amount` evaluates the housing cap for assisted units only, so none of the five resource variables raises; its own source comment gives that as the deliberate intent, and `rollout/fable-continuation-20260911/out/partner-notice-county-fips.md` already tells partners the cap "consults the threshold only for units whose housing assistance is positive". The wrapper's public API docstring at `src/policyengine/tax_benefit_models/us/household.py:191-193` says the opposite — geography is required "including when housing assistance is zero" — and this test enforces it. Resolving that means changing the wrapper's documented public contract, which is the wrapper owner's call, not a repin's. | | `test_us_household_snapshot[us_single_adult_no_income]` | Would freeze `spm_unit.snap` 3596.04 -> 298.00. The ratio is 12.07, i.e. a monthly figure reported as annual — the "January-only annual SNAP" defect the previous lane already recorded as an open country-owner investigation. | | `test_us_household_snapshot` × 3 (employment income, single parent, married two kids) | CA state income tax and child-benefit drift. Same under both the accepted and the published wheel, so not published-only — but still computed household monetary values with no household-level acceptance evidence, and regeneration is wholesale. | | `test_us_model_version_surface` | Mixed. Its `data_package_name` key is pin identity, but the same snapshot asserts variable/parameter counts that differ **between** the accepted and published wheels (6155/102506 vs 6157/102525 — the `gov/contrib/trump` and `gov/contrib/states/co` trees exist only in the published build). Blocked by its strictest component. | From f7a73eef000d87b9d288379db66d2332739adf36 Mon Sep 17 00:00:00 2001 From: Max Ghenis Date: Sat, 12 Sep 2026 00:00:18 -0400 Subject: [PATCH 24/46] Restate the lane brief against published 2.0.1 policyengine-us 2.0.1 is on PyPI (wheel sha256 20e35582...), which lifts the wheel provenance blocker the prior lane recorded, and the root has ruled on the four wrapper-owner decisions it left open. Record both, plus the rulings this lane applies and the repin procedure, before touching any pin. Co-Authored-By: Claude Fable 5.1 --- PROGRESS.md | 152 ++++++++++++++++------------------------------------ 1 file changed, 46 insertions(+), 106 deletions(-) diff --git a/PROGRESS.md b/PROGRESS.md index 923cddc2..ba72c814 100644 --- a/PROGRESS.md +++ b/PROGRESS.md @@ -1,120 +1,60 @@ -# Bind the canonical wrapper to published country 2.0.0; prepare the 6.0.0 release commit +# Finalize wrapper PR #515 on policyengine-us 2.0.1 ## State -**Binding complete and pushed; release BLOCKED on a wheel-provenance defect that is not mine to fix.** +**Starting.** Prior lane (head `e7bfa2bd`) bound the wrapper to policyengine-us **2.0.0** +and left six tests red, blocked on wheel provenance and on wrapper-owner decisions it had +no standing to make. Both blocks are now lifted: -Everything the brief asked for has been done: the pins, the dataset certification, the -lock, the TROs and the release guards all bind the published canonical tuple. But while -verifying, I established by direct byte comparison that **the policyengine-us 2.0.0 wheel -published on PyPI is a different, later build than the wheel the acceptance matrix -accepted**, and that the published build changes household values, drops a determinism -fix, weakens its own Core pin, and breaks this PR's own SPM contract test. Details below. +- **policyengine-us 2.0.1 is published** (verified this lane, 2026-09-12): PyPI returns 200, + wheel sha256 `20e355823bbc89e6c9f413435a7d0b92095cff65541ea366048ee448da025aee`, + sdist `3183f5b1adf4f17963eec46e04bb51da5cd9b6bbda4b38fdebaea7ffa486f660`, + uploaded 2026-09-12T03:56:48Z. It is 2.0.0 plus the above-the-line-deduction determinism + fix (pe-us #9446) that the accepted candidate wheel carried and the merged 2.0.0 lacked. +- **The root has ruled** on the four wrapper-owner decisions (rulings A-E in the brief). -Branch `max/spm-canonical-wrapper-release-20260910`, PR #515 (draft), starting head -`223bce47`, base `origin/main` `6a56ced4`. +This lane repins to 2.0.1, aligns the SPM resource contract with the authoritative country +behaviour, and settles all six red tests under those rulings. -## Prerequisites (all verified 2026-09-11 from primary sources) +## Rulings being applied -| Item | Value | +| Ruling | Substance | |---|---| -| policyengine-us 2.0.0 on PyPI | 200; wheel sha256 `4ca187509efc2f7d3ace5035c27de88c744d0f112fdc7da027372f3ae07c540a`, uploaded 2026-09-12T01:11:24Z | -| spm-calculator 1.0.0 on PyPI | 200; wheel sha256 `e354937a5e1a4045d4966ed594a528d8b02866fabaac9bb5672017004b627305` (matches brief) | -| policyengine-core 3.32.5 on PyPI | 200; wheel sha256 `9d8c162d5fe5c784ae16c885da3ffbfc39a536add0e111133144d8edd555935f` | -| Core 3.32.5 gate | Both conditions met: PyPI 200, and `rollout/final-core-matrix-20260911-r2` records the same Core sha256 as its runtime | -| HF tag `populace-us-2024-spm-20260909` | commit `9a814a3b3b53c0ecd6e1737b6ec862c31300ef6f` | -| H5 it resolves to | `6496cc4393d4d3c6574f76eca231de5898c803b9067645591fd5c4d3e65aee84` (matches brief) | -| Certified release manifest | sha256 `6c39eb1e61afc1629f2abe57032dced9c0750c60e42b9acad812c7b3476a2989`; the local file under `rollout/h5-producer-20260911/certified/` is byte-identical to the Hub-published one | -| PR #515 at dispatch | draft, open, MERGEABLE, head `223bce47` == local == origin; 9 checks pass, 4 `Test` jobs fail | +| A | Country model behaviour is authoritative for the SPM resource contract. Housing cap consults the SPM housing portion only for units with housing assistance to cap. Unassisted units are zero, no geography or composition requirement. Assisted units without county still fail closed (`SPM_GEOGRAPHY_REQUIRED`). Explicit national computes. SPM measurement itself (threshold, poverty) always requires geography. Update the wrapper docstring(s) and `test_spm_household::test_state_only_tax_graph_succeeds_and_resource_graph_requires_geography`. | +| B | Pin policyengine-us==2.0.1, policyengine-core==3.32.5, spm-calculator==1.0.0 exactly; UK unchanged. Regenerate bundle manifest and uv.lock; run the repo's bundle checks. | +| C | `test_us_model_version_surface`: counts are identity facts of the pinned country. Regenerate; report before/after counts. | +| D | `test_us_household_snapshot[us_single_adult_no_income]`: `spm_unit.snap` 3596.04 -> 298.00 is a pre-existing country defect (one month's 2026 allotment reported annual), identical on 1.825.2 and 2.0.x, tracked at PolicyEngine/policyengine-us#9447. Do NOT rebaseline: xfail(strict=True) or exclude the single field, whichever is cleaner. | +| E | `test_us_household_snapshot` x3 (employment income, single parent, married two kids): regenerate, but justify every changed field old -> new against a located country change. Anything unexplained stays red and is reported. | -## BLOCKER: the published country wheel is not the accepted country wheel +## Prerequisites (verified this lane, primary sources) -Verified by me directly, comparing zip members of the PyPI wheel against -`rollout/country-deterministic-wheel-20260910/wheels/policyengine_us-2.0.0-py3-none-any.whl` -(sha256 `4208998a…`, the build recorded in `final-core-matrix-20260911-r2/FINAL-RECEIPT.json` -as the acceptance runtime): - -| Difference | Accepted `4208998a` | Published `4ca18750` | +| Item | Value | Status | |---|---|---| -| `policyengine_us/spm.py` | no `CountyRequiringSPMProvider`, no `masked_policyengine_amount` | **both added** | -| `adjusted_gross_income_person.py:24` | `sorted(set(all_alds) - set(PERSON_ALDS))` | `list(set(...))` — **determinism fix dropped**; AGI float summation order now varies with `PYTHONHASHSEED` | -| METADATA core pin | `policyengine-core==3.32.5` | `policyengine-core>=3.30.1` — **weakened** | -| NY CTC post-2024 phase-out, SURVIVING_SPOUSE | `110_000` | `75_000` (effective 2025-01-01) — **value changing** | - -Three local 2.0.0 builds exist (`4208998a`, `05c729ae`, `08fe6f7e`); none is the published -one, and the published hash appears nowhere on disk. spm-calculator 1.0.0 is fine — the -published and local wheels are content-identical (all 68 members match; only zip -timestamps differ). Core 3.32.5 matches exactly. - -**Consequences.** The poverty rates in `final-core-matrix-20260911-r2/FINAL-REPORT.md` -(13.018144 / 13.235362 / 13.634486 / 13.638541) cannot honestly be attributed to the -published tuple. And `CountyRequiringSPMProvider` — code that exists only in the published -build — is what makes 9 pre-existing microsim tests fail and makes this PR's own -`test_spm_household::test_state_only_tax_graph_succeeds_and_resource_graph_requires_geography` -fail with "DID NOT RAISE". - -I have therefore **not** rewritten any numeric snapshot or contract assertion to match the -published build. Doing so would launder an unaccepted baseline and bury a determinism -regression behind a green suite. +| policyengine-us 2.0.1 wheel sha256 | `20e355823bbc89e6c9f413435a7d0b92095cff65541ea366048ee448da025aee` | verified (PyPI JSON, HTTP 200) | +| policyengine-us 2.0.1 sdist sha256 | `3183f5b1adf4f17963eec46e04bb51da5cd9b6bbda4b38fdebaea7ffa486f660` | verified | +| 2.0.1 declared deps | `policyengine-core>=3.30.1`, `spm-calculator==1.0.0` | verified | +| spm-calculator 1.0.0 wheel sha256 | `e354937a5e1a4045d4966ed594a528d8b02866fabaac9bb5672017004b627305` | carried from prior lane; to re-verify | +| policyengine-core 3.32.5 | pinned exactly by the bundle | to re-verify | +| HF data tag | `populace-us-2024-spm-20260909`, H5 `6496cc43...` | carried; to re-verify | ## Done -- Verified every prerequisite from primary sources rather than from prior lanes' reports. -- Repinned via the repository tooling only — `bundle.py update-packages`, - `bundle.py set-spm --calculator-version 1.0.0` (which downloads and re-hashes the - published wheel), `bundle.py certify-data`, `bundle.py generate`. `pyproject.toml` - extras are generated from the bundle manifest, so no pin was hand-edited. -- Promoted spm-calculator from the protective `country_dependency` pin to the - registry-verified `runtime_dependency` the `--published-spm` release gate requires. -- **Fixed a real certification defect** (commit `a7080192`). The producer now publishes - under `microcosm-data` with `evidence`-kind sidecars; four rules in - `certification.py` matched the literal `populace-data` and the release-directory path - rewrite matched only `diagnostics`. Certifying this release under the old rules produced - a bundle where **8 of 10 certified artifact URIs returned HTTP 404**, with the - reachability sweep silently disabled. With both names and both kinds recognised, all ten - resolve (verified by HEAD request). Six regression tests added. -- Relocked with uv only, registry sources throughout, no local wheel links. -- Rebound both TRACE TROs with a Hugging Face token so the UK sidecar kept its full - private release manifest instead of silently degrading. -- Ran the full suite. The previously-skipped SPM tests now **run**: skips fell 14 -> 9 and - passes rose 1005 -> 1079. - -- Triaged all 35 post-repin failures and fixed the 29 that are safe to fix, leaving 6. - `make test` goes from **39 failed / 1005 passed / 14 skipped** to - **6 failed / 1108 passed / 9 skipped**. - - Retargeted 14 assertions that pinned the legacy identity (model version, calculator - version, data package name, dataset build tag). - - Gave the synthetic US microsim fixtures a real county (`06037` Los Angeles for the CA - households, `48201` Harris for the TX one) and declared - `spm={"geography_kind": "national"}` on the household reform fixtures. Both add input - the certified county-kind configuration requires; no expected output was touched, and - the rejection comes from `spm_calculator/policyengine_adapter.py`, byte-identical - across every 1.0.0 build, so this is not published-wheel-specific. -- Verified the automatic bump: `bump_version.infer_bump` reads the two `.breaking.md` - fragments and yields **5.3.1 -> 6.0.0**. `pyproject`'s version is deliberately untouched. -- Local CI gates: ruff format, ruff check, `bundle.py check`, - `bundle.py check --published-spm --include-tros`, `release_lock.py` and the towncrier - changelog check all exit 0. - -## The 6 remaining failures are deliberately left red - -None can be made green without adopting a number or behaviour that has no acceptance -evidence. Each would bury something a reviewer needs to see. - -| Test | Why it stays red | -|---|---| -| `test_spm_household::test_state_only_tax_graph_succeeds_and_resource_graph_requires_geography` | Two PolicyEngine components now disagree about a public contract. The published wheel's `masked_policyengine_amount` evaluates the housing cap for assisted units only, so none of the five resource variables raises; its own source comment gives that as the deliberate intent, and `rollout/fable-continuation-20260911/out/partner-notice-county-fips.md` already tells partners the cap "consults the threshold only for units whose housing assistance is positive". The wrapper's public API docstring at `src/policyengine/tax_benefit_models/us/household.py:191-193` says the opposite — geography is required "including when housing assistance is zero" — and this test enforces it. Resolving that means changing the wrapper's documented public contract, which is the wrapper owner's call, not a repin's. | -| `test_us_household_snapshot[us_single_adult_no_income]` | Would freeze `spm_unit.snap` 3596.04 -> 298.00. The ratio is 12.07, i.e. a monthly figure reported as annual — the "January-only annual SNAP" defect the previous lane already recorded as an open country-owner investigation. | -| `test_us_household_snapshot` × 3 (employment income, single parent, married two kids) | CA state income tax and child-benefit drift. Same under both the accepted and the published wheel, so not published-only — but still computed household monetary values with no household-level acceptance evidence, and regeneration is wholesale. | -| `test_us_model_version_surface` | Mixed. Its `data_package_name` key is pin identity, but the same snapshot asserts variable/parameter counts that differ **between** the accepted and published wheels (6155/102506 vs 6157/102525 — the `gov/contrib/trump` and `gov/contrib/states/co` trees exist only in the published build). Blocked by its strictest component. | - -## Next (for the coordinator, not this lane) - -1. Resolve the wheel provenance: either republish policyengine-us 2.0.0 from the accepted - source tree, or re-run the acceptance matrix against the published wheel bytes. If the - latter, pin `PYTHONHASHSEED` and repeat under at least two seeds, because the published - build dropped the ALD ordering fix that the matrix's exact-parity claim depends on. -2. Decide whether the published wheel's relaxed `policyengine-core>=3.30.1` is intended; - the bundle pins 3.32.5 explicitly, so the wrapper is safe, but the country package no - longer enforces the version it was accepted on. -3. Settle the January-only annual SNAP defect before any household snapshot is rebaselined. +- Read the prior lane's PROGRESS.md and the full branch history (24 commits off `origin/main`). +- Verified the 2.0.1 prerequisite from PyPI directly. Wheel sha256 matches the brief exactly. +- Established the repin procedure the prior lane used (repository tooling only: + `bundle.py update-packages`, `set-spm`, `certify-data`, `generate`; extras are generated + from the bundle manifest, so no pin is hand-edited). + +## Next + +1. Ruling B: repin to 2.0.1 through the bundle tooling; recertify the US data release for + 2.0.1; regenerate derived artifacts; relock with uv; sync the venv. +2. Ruling A: read the 2.0.1 housing-cap source, rewrite the wrapper docstring and the + contract test to assert the authoritative behaviour. +3. Ruling C: regenerate `us_model_surface`, record before/after counts. +4. Ruling D: mark the `us_single_adult_no_income` SNAP defect xfail(strict=True) / exclude + the field, pointing at pe-us#9447. +5. Ruling E: regenerate the three drifting snapshots, justify every field against a located + country change between 1.764.6 and 2.0.x. +6. Full `make test` as CI runs it; lint, mypy, changelog; push; `gh pr checks 515`; update + the PR body and mark ready for review. From e022f43f0e2d3ca21bc47709aa5c76e14fb3a793 Mon Sep 17 00:00:00 2001 From: Max Ghenis Date: Sat, 12 Sep 2026 00:06:30 -0400 Subject: [PATCH 25/46] Record why the 2.0.1 repin is blocked on a publisher claim policyengine-us 2.0.1 is published and its wheel hash matches the brief, but the certified data release populace-us-2024-spm-20260909 claims compatibility only with policyengine-us==2.0.0. certify-data refuses 2.0.1, and with 2.0.1 installed the gate fires at import of the US model, so calculate_household and every US test error out. That is the documented behaviour and there is no override; manufacturing a claim, or editing the manifest at its immutable release tag, would launder the invariant the gate protects. Enumerated all 25 populace-us tags and 3 branches: no published release covers 2.0.1. Recorded the evidence a publisher needs to widen the claim - both wheels extracted and diffed, variables/ identical at 5981 files, parameters/ identical at 5970 and byte-identical, the only change being list() -> sorted() in two ALD files. Because those file sets are identical, Ruling C's counts are the same number on either pin. The branch stays on the certified 2.0.0 tuple so the rest of the brief can be completed against a model that imports. Co-Authored-By: Claude Fable 5.1 --- PROGRESS.md | 71 +++++++++++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 71 insertions(+) diff --git a/PROGRESS.md b/PROGRESS.md index ba72c814..8d6f4c64 100644 --- a/PROGRESS.md +++ b/PROGRESS.md @@ -37,10 +37,81 @@ behaviour, and settles all six red tests under those rulings. | policyengine-core 3.32.5 | pinned exactly by the bundle | to re-verify | | HF data tag | `populace-us-2024-spm-20260909`, H5 `6496cc43...` | carried; to re-verify | +## BLOCKER (Ruling B): 2.0.1 is published, but no data release is certified for it + +**The repin to policyengine-us 2.0.1 cannot be completed in this repo.** It is blocked on +an act by the *data publisher*, not by anything in the wrapper. + +The certified US data release `populace-us-2024-spm-20260909` publishes this claim: + +```json +"build": {"built_with_model_package": {"name": "policyengine-us", "version": "2.0.0"}}, +"compatible_model_packages": [{"name": "policyengine-us", "specifier": "==2.0.0"}] +``` + +`certify_data_release_compatibility` accepts exactly three bases — build-time match, +matching data build fingerprint, or a publisher `compatible_model_packages` claim. 2.0.1 +satisfies none (the manifest records no `data_build_fingerprint`), so: + +- `bundle.py certify-data --model-version 2.0.1` **refuses**: + *"policyengine-us 2.0.1 matches neither the build-time model (2.0.0) nor any publisher + compatibility claim ['==2.0.0']; a new data build or a published compatibility claim is + required."* +- With 2.0.1 installed, the gate fires at **import of the US model** (`us_latest = + PolicyEngineUSLatest()`, `model.py:512`), raising *"Data release manifest is not certified + for the runtime model version 2.0.1 in country 'us'."* Not just microsimulation — + `pe.us.calculate_household` and every US test error out too. + +This is the documented, intended behaviour. `docs/engineering/skills/data-certification.md`: +*"Neither basis means certification is refused: a new data build or a published +compatibility claim is required."* There is no override flag, and I did not manufacture one: +hand-writing a claim into the bundle, or mutating the published release manifest at an +immutable release tag, would launder exactly the invariant this gate exists to protect. + +I verified no published release covers 2.0.1: all 25 `policyengine/populace-us` tags and all +3 branches were enumerated; `populace-us-2024-spm-20260909` is the newest, and its manifest +carries `==2.0.0` on both the tag and `main`. + +### The claim-widening is substantively justified (evidence for the publisher) + +2.0.1 is 2.0.0 plus pe-us#9446 and nothing else. Verified by extracting and diffing both +published wheels: + +| Check | Result | +|---|---| +| Files differing, whole wheel | 2 variable `.py` files + 1 added test (`tests/core/test_ald_determinism.py`) + dist-info | +| `variables/` file set | **identical** — 5981 files in both | +| `parameters/` file set | **identical** — 5970 files, and byte-identical content | +| Substance of both diffs | `list(set(all_alds) - ...)` -> `sorted(set(all_alds) - ...)` | +| 2.0.1 wheel sha256 | `20e355823bbc89e6c9f413435a7d0b92095cff65541ea366048ee448da025aee` (PyPI-declared == downloaded == brief) | + +The change alters only the *order* of float summation within above-the-line-deduction +aggregation. No variable, parameter, or input schema changed, so the dataset's compatibility +with the model is unaffected — widening the claim to cover 2.0.1 asserts nothing the bytes +do not already support. + +**Unblocking action (data publisher, one line):** publish a populace-us release whose +manifest claims `policyengine-us` compatibility covering 2.0.1 (a new release tag — +never an in-place edit of the existing immutable tag). The wrapper side is then +`bundle.py update-packages --us 2.0.1` + `certify-data --model-version 2.0.1` + relock. + +**This lane therefore leaves the branch on the certified 2.0.0 tuple** and completes every +other part of the brief in full. A branch pinned to 2.0.1 would fail to import the US model +at all, making PR #515 entirely red and unreviewable — strictly worse than what it replaces. + +### Consequence for Ruling C + +Ruling C asks for the variable/parameter counts "regenerated for 2.0.1". Because the +`variables/` and `parameters/` file sets are provably identical between 2.0.0 and 2.0.1, +those counts are the same number on either pin. Regenerating on 2.0.0 yields exactly the +2.0.1 result. + ## Done - Read the prior lane's PROGRESS.md and the full branch history (24 commits off `origin/main`). - Verified the 2.0.1 prerequisite from PyPI directly. Wheel sha256 matches the brief exactly. +- Ran the repin through the tooling, hit the certification gate, and reverted to the certified + 2.0.0 tuple with a clean tree (see BLOCKER above). - Established the repin procedure the prior lane used (repository tooling only: `bundle.py update-packages`, `set-spm`, `certify-data`, `generate`; extras are generated from the bundle manifest, so no pin is hand-edited). From 85b1d711947305fb87eb63fd37a3f0ee79dc05c7 Mon Sep 17 00:00:00 2001 From: Max Ghenis Date: Sat, 12 Sep 2026 00:15:16 -0400 Subject: [PATCH 26/46] Follow the country's housing-cap contract for SPM geography The wrapper's public docstring and households.md said every resource output evaluates the housing cap 'including when housing assistance is zero', so each requires a geography. The country model is authoritative here and says otherwise: spm_unit_capped_housing_subsidy computes assisted = housing_assistance > 0, returns zeros without touching the provider when no unit is assisted, and otherwise calls masked_policyengine_amount, which evaluates only the assisted rows. An unassisted unit's capped subsidy is therefore zero by construction and consults no measurement. Verified against policyengine-us 2.0.0 rather than assumed. On a CA household with no county: all five resource outputs compute and record an empty measurement receipt when housing assistance is zero; all five raise SPM_GEOGRAPHY_REQUIRED when it is positive; spm_unit_spm_threshold and spm_unit_is_in_spm_poverty raise either way; and the wrapper's default outputs, which include SPM poverty, still require the choice. The same source is present byte-identically in 2.0.1, so this contract does not move with the pending repin. Renamed the test from test_state_only_tax_graph_succeeds_and_resource_graph_ requires_geography, whose name asserted the unconditional rule that is wrong. Co-Authored-By: Claude Fable 5.1 --- ...spm-resource-geography-contract.changed.md | 1 + docs/households.md | 29 ++++++---- .../tax_benefit_models/us/household.py | 17 ++++-- tests/test_spm_household.py | 58 ++++++++++++++++--- 4 files changed, 82 insertions(+), 23 deletions(-) create mode 100644 changelog.d/spm-resource-geography-contract.changed.md diff --git a/changelog.d/spm-resource-geography-contract.changed.md b/changelog.d/spm-resource-geography-contract.changed.md new file mode 100644 index 00000000..16ec9fc5 --- /dev/null +++ b/changelog.d/spm-resource-geography-contract.changed.md @@ -0,0 +1 @@ +Documented that SPM geography is required only by results that use the measurement: SPM thresholds and poverty always require it, while resource outputs require it only for units with housing assistance to cap. diff --git a/docs/households.md b/docs/households.md index 7d9e6e78..c5b5cd24 100644 --- a/docs/households.md +++ b/docs/households.md @@ -79,11 +79,18 @@ State alone is insufficient for SPM and raises `SPM_GEOGRAPHY_REQUIRED`. Unknown counties or selected areas raise `SPM_GEOGRAPHY_UNAVAILABLE`; a measured unit with no classified adult raises `SPM_COMPOSITION_REQUIRED`. These are calculator `SPMInputError` exceptions with `code` and `to_dict()` attributes. -Geography is checked when an SPM-dependent formula runs. Net income, benefits, -MTR, income decile and equivalized net income all evaluate the housing cap, -including households receiving zero housing assistance, so each requires this -choice. A genuinely independent tax-only country-model calculation can use -state alone; the wrapper's default outputs include SPM-dependent results. +Geography is checked when an SPM-dependent formula runs, and only for the units +whose result depends on the measurement. SPM measurement itself — thresholds, +the geographic factor and SPM poverty — always requires this choice. Resource +outputs (net income, benefits, MTR, income decile and equivalized net income) +reach the measurement only through the capped housing subsidy, which the country +evaluates for units with housing assistance to cap; a unit receiving no housing +assistance has a capped subsidy of zero by construction and consults no +measurement. So a resource calculation for an unassisted unit succeeds on state +alone and records an empty measurement receipt, while the same calculation for +an assisted unit raises `SPM_GEOGRAPHY_REQUIRED`. A genuinely independent +tax-only country-model calculation can use state alone; the wrapper's default +outputs include SPM poverty and therefore always require the choice. Supply observed inputs such as age, tenure, county and the source-backed `is_spm_independent_minor_role`. Computed SPM thresholds, geographic factors, @@ -92,11 +99,13 @@ resources, poverty outputs and `spm_measurement_adults` / Generic `is_adult`, `spm_unit_count_adults` and `spm_unit_count_children` remain separate benefit inputs; they do not override SPM measurement composition. -These changes require the coordinated canonical country/calculator bundle. -The packaged 5.3.0 production manifest still pins `policyengine-us==1.764.6` -and does not certify this integration. Local-wheel development manifest fixtures -are explicitly uncertified. The measurement receipt identifies a calculation; -it does not certify a population dataset or establish publication readiness. +These changes require the coordinated canonical country/calculator bundle, which +the packaged production manifest now pins and certifies: `policyengine-us` +2.0.0, `policyengine-core` 3.32.5, `spm-calculator` 1.0.0, and the US data +release `populace-us-2024-spm-20260909`. Local-wheel development manifest +fixtures remain explicitly uncertified. The measurement receipt identifies a +calculation; it does not certify a population dataset or establish publication +readiness. ## UK diff --git a/src/policyengine/tax_benefit_models/us/household.py b/src/policyengine/tax_benefit_models/us/household.py index c1b86f23..f6693139 100644 --- a/src/policyengine/tax_benefit_models/us/household.py +++ b/src/policyengine/tax_benefit_models/us/household.py @@ -188,10 +188,19 @@ def calculate_household( values default to ``year``. When axes are present, result values are lists ordered by the axis grid instead of scalars. spm: SPMSelection or mapping selecting a scenario and geography from - the bundle's independently pinned artifact. Default outputs require - household county_fips or an explicit metro/national selection, - including when housing assistance is zero. Formula-owned SPM - amounts and measurement counts cannot be supplied as inputs/axes. + the bundle's independently pinned artifact. Geography is demanded + only by the results that actually use the measurement. SPM + measurement itself — thresholds, the geographic factor and SPM + poverty — always requires household county_fips or an explicit + metro/national selection, and so do the default outputs, which + include SPM poverty. Resource outputs reach the measurement only + through the capped housing subsidy, which the country evaluates + for units with housing assistance to cap: a unit receiving no + housing assistance has a capped subsidy of zero by construction, + so its resources compute on state alone, while an assisted unit + without a geography raises ``SPM_GEOGRAPHY_REQUIRED``. + Formula-owned SPM amounts and measurement counts cannot be + supplied as inputs/axes. Returns: :class:`HouseholdResult` with dot-accessible per-entity diff --git a/tests/test_spm_household.py b/tests/test_spm_household.py index 2bf23600..4c731d04 100644 --- a/tests/test_spm_household.py +++ b/tests/test_spm_household.py @@ -148,29 +148,69 @@ def test_default_resource_outputs_require_a_real_geography(location, settings, c assert caught.value.to_dict()["code"] == code -def test_state_only_tax_graph_succeeds_and_resource_graph_requires_geography( +RESOURCE_VARIABLES = ( + "household_net_income", + "household_benefits", + "marginal_tax_rate", + "household_income_decile", + "equiv_household_net_income", +) + + +def test_state_only_graphs_require_geography_only_where_measurement_is_used( monkeypatch, ): + """Geography is demanded by exactly the results that use the measurement. + + The country owns this contract and the wrapper follows it. Its housing cap + (``spm_unit_capped_housing_subsidy``) consults the canonical SPM housing + portion for units with housing assistance to cap and for no others, so an + unassisted unit's resource graph never reaches the measurement and needs no + geography; an assisted unit's does, and fails closed without one. SPM + measurement itself requires geography either way. + """ from policyengine.tax_benefit_models.us.model import us_latest # Limit only this test's requested outputs. The public wrapper continues to # include resources and SPM poverty by default. monkeypatch.setattr(us_latest, "entity_variables", {"tax_unit": ["income_tax"]}) inputs = household_inputs(household={"state_code": "CA"}) + assisted = household_inputs( + household={"state_code": "CA"}, + spm_unit={"spm_unit_tenure_type": "RENTER", "housing_assistance": 6_000}, + ) + + # A genuinely tax-only graph never consults the measurement. tax = pe.us.calculate_household(**inputs, extra_variables=["housing_assistance"]) assert tax.tax_unit.income_tax > 0 assert tax.spm_unit.housing_assistance == 0 assert tax.to_dict()["provenance"]["spm"]["years"] == {} - for variable in ( - "household_net_income", - "household_benefits", - "marginal_tax_rate", - "household_income_decile", - "equiv_household_net_income", - ): + + # Unassisted: the capped subsidy is zero by construction, so every resource + # output computes on state alone and records an empty measurement receipt. + unassisted_cap = pe.us.calculate_household( + **inputs, extra_variables=["spm_unit_capped_housing_subsidy"] + ) + assert unassisted_cap.spm_unit.spm_unit_capped_housing_subsidy == 0 + assert unassisted_cap.to_dict()["provenance"]["spm"]["years"] == {} + for variable in RESOURCE_VARIABLES: + computed = pe.us.calculate_household(**inputs, extra_variables=[variable]) + assert computed.to_dict()["provenance"]["spm"]["years"] == {} + + # Assisted: the same graph now has a cap to apply, so it fails closed. + for variable in RESOURCE_VARIABLES: with pytest.raises(SPMInputError) as caught: - pe.us.calculate_household(**inputs, extra_variables=[variable]) + pe.us.calculate_household(**assisted, extra_variables=[variable]) assert caught.value.code == "SPM_GEOGRAPHY_REQUIRED" + + # SPM measurement always requires geography, assisted or not. + for situation in (inputs, assisted): + for variable in ("spm_unit_spm_threshold", "spm_unit_is_in_spm_poverty"): + with pytest.raises(SPMInputError) as caught: + pe.us.calculate_household(**situation, extra_variables=[variable]) + assert caught.value.code == "SPM_GEOGRAPHY_REQUIRED" + + # An explicit selection computes for both. for settings, located_inputs in ( ({"geography_kind": "national"}, inputs), ({"geography_kind": "county"}, household_inputs()), From 456af5da279cc1100d7208f7952647ef012b3538 Mon Sep 17 00:00:00 2001 From: Max Ghenis Date: Sat, 12 Sep 2026 00:17:08 -0400 Subject: [PATCH 27/46] Regenerate the US model surface for the pinned country Variable and parameter counts are identity facts of whichever policyengine-us the bundle pins, so the snapshot follows the pin rather than the pin following the snapshot. Measured, not inferred: num_variables_bucketed_100s 57 -> 61 (raw 6157, //100) num_parameters_bucketed_100s 978 -> 1025 (raw 102525, //100) data_package_name populace-data -> microcosm-data The raw counts are identical on policyengine-us 2.0.0 and 2.0.1 - 6157 variables and 102525 leaf parameters on both, counted from CountryTaxBenefitSystem directly under each installed wheel - so this snapshot is already the 2.0.1 value and the pending repin will not move it. That follows from the wheels themselves: their variables/ trees are identical at 5981 files and their parameters/ trees are identical at 5970 files and byte-identical in content, the only difference being list() -> sorted() in two above-the-line-deduction files. data_package_name tracks the certified US data release, which the bundle now sources from microcosm-data rather than the retired populace-data name. Co-Authored-By: Claude Fable 5.1 --- changelog.d/us-model-surface-snapshot.changed.md | 1 + .../household_calculator_snapshots/us_model_surface.json | 6 +++--- 2 files changed, 4 insertions(+), 3 deletions(-) create mode 100644 changelog.d/us-model-surface-snapshot.changed.md diff --git a/changelog.d/us-model-surface-snapshot.changed.md b/changelog.d/us-model-surface-snapshot.changed.md new file mode 100644 index 00000000..7659c631 --- /dev/null +++ b/changelog.d/us-model-surface-snapshot.changed.md @@ -0,0 +1 @@ +Regenerated the US model-version surface snapshot for the pinned country and data package. diff --git a/tests/fixtures/household_calculator_snapshots/us_model_surface.json b/tests/fixtures/household_calculator_snapshots/us_model_surface.json index c5563c26..cf8749e7 100644 --- a/tests/fixtures/household_calculator_snapshots/us_model_surface.json +++ b/tests/fixtures/household_calculator_snapshots/us_model_surface.json @@ -1,11 +1,11 @@ { "country_id": "us", - "data_package_name": "populace-data", + "data_package_name": "microcosm-data", "has_employment_income": true, "has_income_tax": true, "has_region_registry": true, "model_package_name": "policyengine-us", - "num_parameters_bucketed_100s": 978, - "num_variables_bucketed_100s": 57, + "num_parameters_bucketed_100s": 1025, + "num_variables_bucketed_100s": 61, "region_registry_country": "us" } From 746756204e0f5fc677180ced6ab51b7b2662357c Mon Sep 17 00:00:00 2001 From: Max Ghenis Date: Sat, 12 Sep 2026 00:19:36 -0400 Subject: [PATCH 28/46] Refuse to rebaseline the SNAP annualization defect policyengine-us reports one month's 2026 SNAP allotment where the annual value belongs: spm_unit.snap 3596.04 -> 298.00, with household_benefits, household_net_income and spm_unit_net_income following it down. The country's own issue records the same values on 1.825.2 and 2.0.0, so this predates the canonical SPM release and is not drift this pin introduced. https://github.com/PolicyEngine/policyengine-us/issues/9447 Rebaselining would freeze a twelfth of the real benefit into the expected output, so the four contaminated fields are excluded from the comparison by name. The other 42 fields of that case - Medicaid, both poverty flags, the income decile, every demographic - keep protecting against drift, which an xfail over the whole case would have thrown away. Two things make the exclusion honest rather than a way to hide the number. _check_snapshot now preserves the prior value for excluded fields when PE_UPDATE_SNAPSHOTS=1 runs, so a refresh cannot quietly launder the defect (verified: the file's md5 is unchanged across a refresh). And test_snap_annualization_defect_still_present asserts the value is still a single month's allotment, so it fails loudly with removal instructions the moment the country annualizes SNAP again - which a strict xfail would not have done, since the corrected annual figure is about 3,576 rather than the stored 3,596.04 and the case would have gone on xfailing silently. Co-Authored-By: Claude Fable 5.1 --- ...nap-annualization-defect-excluded.fixed.md | 1 + tests/test_household_calculator_snapshot.py | 67 ++++++++++++++++++- 2 files changed, 65 insertions(+), 3 deletions(-) create mode 100644 changelog.d/snap-annualization-defect-excluded.fixed.md diff --git a/changelog.d/snap-annualization-defect-excluded.fixed.md b/changelog.d/snap-annualization-defect-excluded.fixed.md new file mode 100644 index 00000000..ff444319 --- /dev/null +++ b/changelog.d/snap-annualization-defect-excluded.fixed.md @@ -0,0 +1 @@ +Excluded the four US single-adult snapshot fields contaminated by the SNAP annualization defect (policyengine-us#9447) instead of rebaselining them, and added a guard that fails when the country fixes it. diff --git a/tests/test_household_calculator_snapshot.py b/tests/test_household_calculator_snapshot.py index f75816d9..85f01c66 100644 --- a/tests/test_household_calculator_snapshot.py +++ b/tests/test_household_calculator_snapshot.py @@ -52,11 +52,22 @@ def _round(value, places: int = 2): return value -def _check_snapshot(name: str, data: dict) -> None: +def _check_snapshot( + name: str, data: dict, exclude: dict[str, str] | None = None +) -> None: path = SNAPSHOT_DIR / f"{name}.json" rounded = {k: _round(v) for k, v in sorted(data.items())} + excluded = dict(exclude or {}) if UPDATE or not path.exists(): + if excluded and path.exists(): + # A refresh must never launder a known country defect into the + # expected output. Keep the pre-defect value for excluded fields so + # PE_UPDATE_SNAPSHOTS=1 cannot quietly freeze the wrong number. + previous = json.loads(path.read_text()) + for key in excluded: + if key in previous: + rounded[key] = previous[key] path.parent.mkdir(parents=True, exist_ok=True) path.write_text(json.dumps(rounded, indent=2, sort_keys=True) + "\n") if not UPDATE: @@ -65,7 +76,7 @@ def _check_snapshot(name: str, data: dict) -> None: expected = json.loads(path.read_text()) diffs = [] - all_keys = set(expected) | set(rounded) + all_keys = (set(expected) | set(rounded)) - set(excluded) for key in sorted(all_keys): if key not in expected: diffs.append(f" new key: {key}={rounded[key]!r}") @@ -111,6 +122,30 @@ def _check_snapshot(name: str, data: dict) -> None: } +SNAP_ANNUALIZATION_ISSUE = "https://github.com/PolicyEngine/policyengine-us/issues/9447" + +# Fields whose expected value is contaminated by a country defect. They are +# excluded from the comparison rather than rebaselined, so the rest of the case +# keeps protecting against drift while the wrong number is never frozen in. +# +# For 2026 policyengine-us reports one month's SNAP allotment where the annual +# value belongs (spm_unit.snap 3596.04 -> 298.00); the three resource fields +# below are downstream sums of it. The behaviour is identical on +# policyengine-us 1.825.2 and 2.0.x, so it predates the canonical SPM release +# and is not drift this pin introduced. Rebaselining would freeze a twelfth of +# the real benefit into the expected output; instead +# test_snap_annualization_defect_still_present fails loudly the moment the +# country annualizes SNAP again. +COUNTRY_DEFECT_EXCLUSIONS: dict[str, dict[str, str]] = { + "us_single_adult_no_income": { + "spm_unit.snap": SNAP_ANNUALIZATION_ISSUE, + "household.household_benefits": SNAP_ANNUALIZATION_ISSUE, + "household.household_net_income": SNAP_ANNUALIZATION_ISSUE, + "spm_unit.spm_unit_net_income": SNAP_ANNUALIZATION_ISSUE, + }, +} + + @pytest.mark.parametrize("case_name", sorted(US_CASES)) def test_us_household_snapshot(case_name: str) -> None: pytest.importorskip("policyengine_us") @@ -124,7 +159,33 @@ def test_us_household_snapshot(case_name: str) -> None: values = result.to_dict() values.pop("provenance", None) _flatten("", values, out) - _check_snapshot(case_name, out) + _check_snapshot(case_name, out, exclude=COUNTRY_DEFECT_EXCLUSIONS.get(case_name)) + + +def test_snap_annualization_defect_still_present() -> None: + """Fail loudly when policyengine-us#9447 is fixed. + + While the defect stands, the fields in ``COUNTRY_DEFECT_EXCLUSIONS`` are + excluded from ``test_us_household_snapshot[us_single_adult_no_income]``. + A correctly annualized 2026 benefit for a one-person unit with no income is + on the order of 3,500; the country currently returns one month of it. When + that changes this assertion fails: drop the exclusion entry, regenerate + ``us_single_adult_no_income.json``, and delete this test. + """ + pytest.importorskip("policyengine_us") + import policyengine as pe + + result = pe.us.calculate_household( + **US_CASES["us_single_adult_no_income"], spm={"geography_kind": "national"} + ) + snap = result.spm_unit.snap + assert snap < 1_000, ( + f"policyengine-us returned annual SNAP {snap:.2f}, which is no longer a " + f"single month's allotment. {SNAP_ANNUALIZATION_ISSUE} appears fixed: " + "remove the COUNTRY_DEFECT_EXCLUSIONS entry for " + "us_single_adult_no_income, regenerate that snapshot, and delete this " + "test." + ) # UK cases ------------------------------------------------------------------- From fc29be6f4419c2df88f50703d28eb9873ae33c05 Mon Sep 17 00:00:00 2001 From: Max Ghenis Date: Sat, 12 Sep 2026 00:20:09 -0400 Subject: [PATCH 29/46] Record rulings A, C and D as done Co-Authored-By: Claude Fable 5.1 --- PROGRESS.md | 42 ++++++++++++++++++++++++++++++++---------- 1 file changed, 32 insertions(+), 10 deletions(-) diff --git a/PROGRESS.md b/PROGRESS.md index 8d6f4c64..0dcd0194 100644 --- a/PROGRESS.md +++ b/PROGRESS.md @@ -116,16 +116,38 @@ those counts are the same number on either pin. Regenerating on 2.0.0 yields exa `bundle.py update-packages`, `set-spm`, `certify-data`, `generate`; extras are generated from the bundle manifest, so no pin is hand-edited). +- **Ruling A done** (`85b1d711`). Read the authoritative source in the 2.0.1 wheel: + `spm_unit_capped_housing_subsidy` computes `assisted = housing_assistance > 0`, returns + zeros without touching the provider when none is assisted, and otherwise calls + `masked_policyengine_amount`, which evaluates only the assisted rows through + `CountyRequiringSPMProvider`. Probed all four clauses against the installed model rather + than assuming: 5/5 resource outputs compute with an empty measurement receipt when housing + assistance is zero; 5/5 raise `SPM_GEOGRAPHY_REQUIRED` when it is positive; threshold and + poverty raise either way; default outputs still require the choice. Updated the public + docstring, `docs/households.md`, and the contract test (renamed, since its old name + asserted the unconditional rule). 42/42 SPM household tests pass. +- **Ruling C done** (`456af5da`). Regenerated `us_model_surface`: + `num_variables_bucketed_100s` 57 -> 61 (raw 6157), `num_parameters_bucketed_100s` + 978 -> 1025 (raw 102525), `data_package_name` `populace-data` -> `microcosm-data`. + Counted the raw surface under both installed wheels: **identical** on 2.0.0 and 2.0.1 + (6157 / 102525 both), so this snapshot is already the 2.0.1 value. +- **Ruling D done** (`74675620`). Excluded the four contaminated fields rather than + xfailing the case, because all four are downstream of the one defect and an xfail would + have discarded the other 42 fields of coverage. Two guards make it honest: + `_check_snapshot` preserves prior values for excluded fields under + `PE_UPDATE_SNAPSHOTS=1` (verified by md5 across a refresh), and + `test_snap_annualization_defect_still_present` fails loudly on fix. Note a strict xfail + would *not* have failed loudly: the corrected annual figure is ~3,576 against a stored + 3,596.04, so the case would have gone on xfailing silently. +- Confirmed policyengine-us#9447 is OPEN and its body records the same values on 1.825.2 + and 2.0.0, independently corroborating Ruling D. + ## Next -1. Ruling B: repin to 2.0.1 through the bundle tooling; recertify the US data release for - 2.0.1; regenerate derived artifacts; relock with uv; sync the venv. -2. Ruling A: read the 2.0.1 housing-cap source, rewrite the wrapper docstring and the - contract test to assert the authoritative behaviour. -3. Ruling C: regenerate `us_model_surface`, record before/after counts. -4. Ruling D: mark the `us_single_adult_no_income` SNAP defect xfail(strict=True) / exclude - the field, pointing at pe-us#9447. -5. Ruling E: regenerate the three drifting snapshots, justify every field against a located - country change between 1.764.6 and 2.0.x. -6. Full `make test` as CI runs it; lint, mypy, changelog; push; `gh pr checks 515`; update +1. Ruling E: regenerate the three drifting snapshots, justify every field against a located + country change. Root causes already isolated by measurement - the three cases reduce to + exactly two: school meal subsidy 1130.96 -> 1142.24 (+11.28, both child cases) and CA + state income tax (-7.76 at 60k single, -24.05 at 240k joint). Archaeology running. +2. Full `make test` as CI runs it; lint, mypy, changelog; push; `gh pr checks 515`; update the PR body and mark ready for review. +3. Ruling B stays blocked - see BLOCKER above. Hand the publisher the evidence pack. From 7019c9fcce4a9dd0d2cbcb5e7eb8469b9fe9cc45 Mon Sep 17 00:00:00 2001 From: Max Ghenis Date: Sat, 12 Sep 2026 00:23:19 -0400 Subject: [PATCH 30/46] Pin down the SNAP defect with the country's own monthly values An earlier message in this branch put the correctly annualized 2026 figure at about 3,576, treating the stored 3,596.04 as twelve times a single monthly allotment. That is wrong on both counts. The monthly allotment did not move between 1.764.6 and 2.0.x - it is 298.00 for January to September 2026 in both. 3,596.04 was the correct twelve-month sum under the older uprating index (9 x 298 + 3 x 304.68), and on this pin the correct annual figure is 3,607.56 (9 x 298 + 3 x 308.52), the SNAP uprating refresh in 1.775.9 having raised the October step. Established that the defect is in the country and not in this wrapper, by reproducing it on a bare policyengine_us.Simulation with no wrapper in the call: annual snap sums its monthly values, and only some months resolve - 2024 picks up November and December alone (584 = 2 x 292), 2025 picks up all twelve (3522 = 9 x 292 + 3 x 298), 2026 picks up January alone (298), 2027 picks up none (0). That the 2025 sum is exactly right is what rules the wrapper out. Recorded both figures and the month-by-month evidence beside the exclusion, so the next reader does not have to rederive them. Co-Authored-By: Claude Fable 5.1 --- .coverage | Bin 0 -> 86016 bytes tests/test_household_calculator_snapshot.py | 21 ++++++++++++++------ 2 files changed, 15 insertions(+), 6 deletions(-) create mode 100644 .coverage diff --git a/.coverage b/.coverage new file mode 100644 index 0000000000000000000000000000000000000000..f970f660f19e4d3848c0245ae5e676af4f034c3c GIT binary patch literal 86016 zcmeHw349b)ws&p4cUNU434wG&NLZ6j0K=YyO;*`K1f=P7DoIPHJ9KqtAtap&D!y^S z(f3?tfH2>i@28(LgWIUMCOU3!Mv3Fhb5xWBT)-J5LDUek^gDGs-Kjuu=6z3Z`{q~m zOZwDu>z;G}_uO;uxl7&kYgambyrsnBEw}qD(});?B#AT!jpq#qj5Bo`4NlDzT{b*W4g~=P0R;gCfqxnV z#I;6EQc4QhSm(1Bxp<$~?%=(lU(>>Mi|21xY}qh>!OF#!&^^lpy#vrv~EMAth)>=*a zLoVfL=qf6>F9)M9{nJB4mhKFSyn?UEsP`uZ81i=Z_s#I+Wv%_~S2&g7s+R#v6kD5L&aUn6j_W_W&9Pi?V1QWJbpyQsDhNSy=cRB7To3mxcixm{JZQ`?JYq=G zAJQl59?)sEcWM(g2Q=yGCUu7DVbuiYF=i(H6rD#sM=d0eksFBjh&5qGB5h%aMuT;u z04qMM<@V}QScKsetU^V3mX|N`J6*+DQ*)-~<;=>NX|3{jclx|MFIX!)E~lf0cbCFq zwX7rhBJf!yPPg4<6}*li!K|>3ro!(Ntd26fyA;;pm6m$>Qdr&rOGoyY z8A7bXDTJJRbsBK@_s943YiKAf23i8!&3ok zm~8?qpN56iH6TJ|G9a)En*>m@L&#SqVdWdLOOW3B5r8;}Exi?Xue92b_uAZ^VoBR; z6CvHTIQUn1VF|4p7OV4Cv|1jRS0?~-ECwy4?$V+>&^O>LlJS7M21mL>tOtR2*^79W z05Vj?LFxs3BJf_X$BP)p;$>XS3-BofNO5HhFve22L!iU*N?uBE4F`Oy@F8upIo(d5 z%_b?Di3LS7 zClf&WMu@WneeeU-v-O53pdi-@ogM^8!l4zYqV?IUZAH8r*2aTQfZ=!92Ab(o;n3kK z97vRiAq3WyT3q%3u?{utfp0(q=+oHHk0jZK*5HExRcc5$7SoRsUR>k0mpdK6SR_@D zbS(AW@QNoTp23^=kSzqp)if~1(%e94mzDQg3OHkFWrdwr8FPUI=0CXe29A3CR``jX z2R|_u3VvyJC;SqLTqApt-6{G0stLw}AupeLjyz_1z>r36;5f<8M|}cX-2E_eSEM2czDU`h8?-T(M% zMielL5OM$GFf)$0|FPH#*--aCW;Y{NVMJQi_TogAX*6TuEh}JGcmG*z_C_X-%>6gv zYsbk1xZM3WMgs+zRv1J*)0l)sD~9*NWbVHnTj4;WL~0=L$8^~20b(6-|Ft5)G}8Fs zkEA)`|7)J<;D~vYv?lk+3=L%kl_}C!;o*7Y>3wf^(XbO!+L>#)${rVx^HwRbxpe6 zx(eNTUA}Id_P5$b?IdlyR;&4ire2e$$W-N)bcqreFV@ ze%u_AAf9i|AbPj!h|VhOKa#9Fs}39{r0-y#+2Zxu}t=P0d(s4n}>lWVjYfLL~2%ZvcSbfz`WKhlvO3~bu* z;qHzXHNjhtzg^ogPelZp*u(8T)5z@WA=>S8rA0%9hIgI_jtdIfHOKpp5e>au2>7!d zjtjej?*~8cv37q(iD{dFbK~6U{U@~lL7m-yBk|tj9goh)|9o{r;L|Y&3O+0S;;xo? zMtiXT_%rvP)jZsPl0Mv@HB1)12{IFNGu7V0_D^7~krtMw#cXeHH>Y;aIk~z&xwpQ# zt)r`_ubvgj=BAHV?g;iTBZzmJ$9=IximWMk&$zzc<oHoj)bpU(7gk`pbD23fK+s zPTEue;jn=;erU~F57*nSd-}l%^vs$W(n}Npy3w%jy^%?`z&5aZ5)_+3EfSr*Bn2Q()0YM8hGtnbKlXf`#YLHy+3m;gjuzsGJf-8Z|DnO zf!GK#rlrdxp&%VK0pd+f%bqt=*FdB0DWP0^8Oh?y+-r9c7uo@mF zFQW=)6g34mF$ZNmu7Bt^X$NI=GQP8)c>SO=3r1orh}qJTP9|w1DV~eb+p9 zB|P6fiy|(0<|i*`k2}?KFX{UE%XPU0{Rd-nM>RF~TWc@AaiqB;;3SDaRq**9+Z#{4 z`1<(6wMBhjx=kzK?NKv-)_ztPTM#?SxEwA=&Y+&tKGs~76t@hnGV}f#M;v?m+2apZ zy}v)MLc0|1#a%Fxh$iKeY*?hAGMf~UG4T0|6ndV7!| z4%;u@5@b*~ZuGVA(%jWlL#-~5JF8{Ju~(8So(a|+Rqe@Zo6}sNT`a}EiZaHqi{LD6 zJCzwD;rKT?aUon!xsf`v;8f7u5De~bFW3=mqY@Ut?Md6HZLc?xbpO=X#{=?5G@AKv zH)bof_S!|uXzD)8e`ZNb(yUA4&os5)LtiZK<664< zlOJUHe2LZaM2a|Lc&y-%dB&4Hu}fRxF9uf<7iWBxUy#{XU=F-^A)V-Jt2<>Th~yJ9 zUhXgk@&m!I3JUJL)BM<`eIKmZSrgapsp>fQ5VL)5-`M`3hBzARJzWs&ZZ)@d^dE2h zgHAUOBuTJ8^6NHD;#@eLvW5Ecfrg&`mpWGLINkQZl0IhB97&KVrvCEGKj~&Qt;(E6 zyqkCOe0sq3)q^#i$BP!E)jW798B)HKAdZ@s)YP}ub$k@?{OQ)to;&NDEPeL#>KUa~ z=_z;hd_u&KEnjMCiIxMMxAx@B26jd4U$>GE+z@#0U{Bk@_ZJ>L5$k9_U8f_6S5jW+ z=m^g2r|xYkIM>r-K414um3x*X$u^^u_3t!OpWN|NsenL<>kd+ct@}1}{GMv6q3ck` z+f5t^hL-5cgpJoNM|Jz#;y(FuPXG2+o9^z%KYpR(bD5f8rg3or*CbiT;zzB&~1(X-9-`j%*~froM9^#qwd9nO~K-n2yB z*!$TZ?r-k>?9t}lFSH-ei6hQ7AKyo`H}?fI3a3d)C2~G|qVyBN`0Cte@96%$?#(7- zfg!~b0&wgZ)vRrJDR?_m*cMz&9qtH@CEw`^{FIO`_9IuYakv=a{YeT(|WQ=b2~TxRD}>-nmIXzyEwcJ!&#Mnmy@5HA|fQSoi)t4WHinw+nTjo$h?}{h*eM zn*>iY#|=Se#6-B4oqp464Trzo-|ud{o0)z)~gO@7FaSN&;oPz>dt3Y#~8KmbAK$o zUbSOX*Vg6(U?#5F|1)z^2876=?y`)f#>SAu0-`sF4s+r-crax%byEwGQ)bB4Wgxb?hd0-Hf4 zj0QSs5w&eL`RTr(Dezw4cfnXPVU%=tX=5m1Z-fXfI#WRt_pVz)^}f{66Z)%f>*|C5 zy6@u;ML7^rIDX(FkG|)>!w5yy(+{t7)isAB%x`P!0!Ce3Jv|*gJ@w5UAbVT*VI*1k z>_|W$?OLiol(nQL1+HTjQ`zcdIAIq~N=pKozkq5Oy}TQm4QK~>Em2s1(cD{bF`%W? zBcyQiCutI;7YkpZ*aSF9OWVGzKX|ZeQg>Q!lsjtE0Fs88|E@Bs;Kh-n zj@)B;A-t)B*}*!P2HJ6bOUb0oam1LjX3Xy~7JC>T7EzLk=q%3Diave7-$np(dI z?m6DGg8i32mcQDztars3GdbG-;(y#v2>)kE`{+f%KIX#3-1))!{(A#~wzdcQ`hMEh zc08!P^rVMW{hs?BSI3I1!1Aoibcg8{Q@yFewAJKf*KjXzN4ST$2TfJnJzN`iJ68!`4LHyK zm2F`^VL#$>xQXn3_BQqn_CMKU?6d3>?625gvIp7wOi!C0H9gGgS=!WV`o?t5blTKz z`h}^;^fS}FoQEsp?A#V^2DhGD$t~vcO&^%vHT}W#s_8}6%qFw3+}G@G?n`bgm&zrW zHgIar#BOKju+!NrHj_KdE@Q7}7f1-WZFhVrfxvw zy7h>xT!+YtwTLWVgUGVgh|F1q$n5J7nY9v;nJW;Pu^f@SWr$p}6p=Mc5LtaKBC8f7 zvV9REH!ehE+X6(k&PSxE5Rn}Ph$QACVxNb|mbr+S<{-k(M#MM^k##c>F(7}OoREjE z#$SU-?sPV+rNcu!X#!NuOG9D3g zCL&Y@BAdq{azi>Io5mutaSS3GEQn;95y?nHWYK6umX1PXNh%`OjznZ}3L*=W5m}Ig z$ovtA6ec2)mVk&p9uZX>BBNswNsU1yg+s)~B9d%EB*}<~#(>BOJt8_CB3dmXY7HWc z8j(>dL}&()^)w=DDF(U$(D*-r#zJtG5QUe5fP#R6fP#R6fP#R6fP#R6fP#R6fP#R6 zfPw%{1f=nQ#s9~NxWY?8KtVu3KtVu3KtVu3KtVu3KtVu3KtVu306zlK`hUg$$B(-6JvHuY<2B55Y>Qx~e9BOg+~&TLep53)#d z|BrS?kL(T^IpMv6wcK7^TE@Gbf)zHL&+_s`ey6KAYiiEayqsA%Gp$t~?@phW=LKtM zw{G5D>U8s2RbG2V1@DFMF5WKiStU-l-DMTLjv>LU4v*jM^E%;ezhFhXBNjUaxZ&^# zuubH(qMB*su);17TrQK-g&=1Yto`K-ekY zuyg+ZsDSVaO~%VN z)Vc+}NjTd<0C|PoYcJ=0yw_G~clo8dM-yNgNqky9D1h5jEb$+)n-Qy`z#mxa@MgNl z@2l|pfUmZi5e0lnF0OIg%bgBjtj5e3HmHaC6c{s=jOf9aVu#&PCh68FymhIpZA}YM zcj&Aeg&*AMl`++mr-@YJ0>RwD=;@zujWkE@Brb3&`0hVx3K-8C9flKz8}#q#*XrKY zEz`cFovL|Sldb;0>QSl#syHf0eoNi_kM>uHDyGqdB}Xf~o=V)(BrAvxY;|fe?{!w9_M2dJ+r3gd z3ZU4CuT>!zK!(5ue7@v&d2ynaX^h8GIAm5TB$u1S z1!zh`jZaG*Bi3Ox8glOD%e{afY_vHHfvlB9VN1Ha*)6$-Qnj^7n*VPh))4R=f0q4M zHq~^$Db0A$m|=LtkOq7ACF>s0X|;E16Ez1k>FOqRhU#I}1m-bjCjAthM?FU^B#)6B zi1#3ZfkUJ%4AE%7(TfoV({ZIf?J+ZiScg*xxjt2=0e3$R?w1e82%~}XPHdb*!w>#Z zz|@0{X}F_qcPg;0!NzvTs7KvM;2prh+wXI_1S{``>a50wE}+zkL<$r#6%PKPVWukw zT`H3SfnC@nhzz<^CSm0p8FZ;10f=L%p%CI%40+ThLb_{l;4SM62-gzT3BVkSK?|vS znO!IgwO~au9&p#-NOy?!An-1G5$}>3uvKx8dI6sZyw~gTBF3?J85i?{vs4f#fZiv99n_QzRzB5E8^XJ zi4$}J#=2cL=ysF5N63*H9j?MLv<8ywffkoNK&(Rzd*B<;0QxjG^apK!c%nm<8WN7h z^rM6ij=GDa3X+bc-Wy)=0R1Z1WEp4TBP0LSG%&`}+(2oUmG@c-IAdvLg`HO!bAbfr zSe!Q*Q_24yO}Ggzz+PfYO(#sn#uLVkhPMpM^snj*bS=6W+FxrenqO*0sqa=Zs%qv8 zvyFa@o=!DUvE*LjTX^-qafqsUpw=9Y5MM1vA$fIwN%73j)g7*Ur8Vqs}L4O7Yn6x&{nTno{34akC}eSdXXWOi57O^+fr z18bcICa#ORY0;HMHd-X=rbbs5c^;{9K^82vRk%3-zR;D!{kwCZVSuHlGa^Q0xL?#+ zapgyt`BjJeMc5dgXiUUm<|49OWw>8dXMrfOI7JzLxW8@+=mwUa=rANhhWkZoGT?w^ z(rTFhmBZX3H7QC$$PE`U5rn|fCx00dk?Am^dID~x9Dc%Th}+2|@-TfJ^)$@;AEUmP z^QtwfTBd_3(tW5~r#%L*e_sc_iG{Nx|Ldnb3Ojy&Ef@f*pFx3?FW`$;=6%d5_ml`a%Ty?fcJiav|-#NrF z58FD{0Wl(Tb`s<(hOy0VmCYj9&BHoz5181ZsI27zU9^GoQQ|E1!`iyg;JrN>IKbQi zXNk0CX9uyIxketiqNJav*q{&nv0pjo4O~hPhvTq3aJe z#XZbghWb`)>L)V-;kk>6_&g{X_p49rVt*XXn}N~ZInwxl8nKVys@P6;m+2h%{eLw& z4JQqo_3!Fe!ubDm?Zets%{>~mdbjGls-F3pxtXEqy|ju7P#W@fn6t3&d*}eD-3nb~ zSSDT!qn{$P1$tpjI69|fDssh#33lIrYnbQC4+K?VUoeRxeLX;rrGs`z ziw!>T2&*@xHooMF?b!sW$Ff{xxHUNS8vz0=jaE7L!Il4k4Y>Ls<@{y!gL^%|Z^u#m zpz~8wI?XyraXb!&vhqIM68~LmA?2kw1c`h%P*ksh#A6w`9L80H=2*CEQ`D^nL83J~ zWN3e{s9P0XS!AU_R9_dx41zwYNCxRwf*?_wBsa9@PSmZ4Vh#=Mxf6BEqbrN7=T6iu zi>@qk&83%uELbY@@AxFCNH2+=6p>Goit1}Y3M^*!N-V+)&84=Ds9FqyV41{pr64lp z2rL3Iu#9vL5o7S<%_6Z7B)~F3V~7ND3>HKI1NjH6MPhywFc|Uy>$*Z{@?e>;G?)vr zED)IjKmd!cH%#*<(o-#}^Fa(O=3#g-!j-(_|LY9*5Zo=ChW!c4n(j8Kjkg(1a7#H9 z1QY}m1QY}m1QY}m1QY}m1QY~*a0F_K!9l|K5sV6#$L9+T)z>(1u?~#_$^5cCMZg-% z5R9x#J~B~6)Y!ot!#yG}+(eN*JEDL?WTMD!8*s<6(hRM) zN!<owr;t#TZG7YX*PaI8;wk)zKThpQ~ Ji>v@m{VyelLn#0N literal 0 HcmV?d00001 diff --git a/tests/test_household_calculator_snapshot.py b/tests/test_household_calculator_snapshot.py index 85f01c66..1282cff4 100644 --- a/tests/test_household_calculator_snapshot.py +++ b/tests/test_household_calculator_snapshot.py @@ -132,10 +132,18 @@ def _check_snapshot( # value belongs (spm_unit.snap 3596.04 -> 298.00); the three resource fields # below are downstream sums of it. The behaviour is identical on # policyengine-us 1.825.2 and 2.0.x, so it predates the canonical SPM release -# and is not drift this pin introduced. Rebaselining would freeze a twelfth of -# the real benefit into the expected output; instead -# test_snap_annualization_defect_still_present fails loudly the moment the -# country annualizes SNAP again. +# and is not drift this pin introduced. +# +# It is also not the wrapper's: on a bare policyengine_us.Simulation, annual +# `snap` sums its monthly values and only some months resolve — 2024 picks up +# November and December alone (584 = 2 x 292), 2025 picks up all twelve +# (3522 = 9 x 292 + 3 x 298), 2026 picks up January alone (298), and 2027 picks +# up none (0). The stored 3596.04 was the correct twelve-month sum under the +# older uprating index (9 x 298 + 3 x 304.68); on this pin the correct annual +# figure would be 3607.56 (9 x 298 + 3 x 308.52), so rebaselining to 298.00 +# would freeze about a twelfth of the real benefit into the expected output. +# Instead test_snap_annualization_defect_still_present fails loudly the moment +# the country annualizes SNAP again. COUNTRY_DEFECT_EXCLUSIONS: dict[str, dict[str, str]] = { "us_single_adult_no_income": { "spm_unit.snap": SNAP_ANNUALIZATION_ISSUE, @@ -168,8 +176,9 @@ def test_snap_annualization_defect_still_present() -> None: While the defect stands, the fields in ``COUNTRY_DEFECT_EXCLUSIONS`` are excluded from ``test_us_household_snapshot[us_single_adult_no_income]``. A correctly annualized 2026 benefit for a one-person unit with no income is - on the order of 3,500; the country currently returns one month of it. When - that changes this assertion fails: drop the exclusion entry, regenerate + 3607.56 on this pin (nine months at the 298.00 allotment plus three at the + uprated 308.52); the country currently returns January alone. When that + changes this assertion fails: drop the exclusion entry, regenerate ``us_single_adult_no_income.json``, and delete this test. """ pytest.importorskip("policyengine_us") From 04960e6411fe8e9431f3972a4b065f9b71dc197e Mon Sep 17 00:00:00 2001 From: Max Ghenis Date: Sat, 12 Sep 2026 00:26:09 -0400 Subject: [PATCH 31/46] Regenerate three US snapshots against two located country changes Every changed field traces to one of exactly two policyengine-us changes between 1.764.6 and 2.0.x. Both were located in the country's history, and the arithmetic was reproduced from the parameter values rather than asserted. ROOT CAUSE 1 - BLS CPI-U actuals refresh. c991cd844a (PR #9100, released 1.779.1, 2026-07-21) added published monthly CPI-U actuals for Sep 2025 to Jun 2026, replacing the CBO projection that covered them. The index in force at 2026-01-01 moves 323.364 -> 326.588 (denominator 319.086 unchanged). The USDA NSLP and SBP per-meal rates carry uprating: gov.bls.cpi.cpi_u, and no rate or school-day parameter changed in the range, so the free-tier net subsidy is 1116 x index ratio: 1116 x 1.0134070 = 1130.96 before, 1116 x 1.0235109 = 1142.24 after. ROOT CAUSE 2 - California standard deduction uprating un-frozen. df3482f4ef (PR #9059, released 1.776.2, 2026-07-21) moved uprating: gov.states.ca.cpi off the file-level metadata of the CA standard deduction breakdown - where uprate_parameters never read it for breakdown leaves - onto each filing status's values block. The deduction had been frozen at its 2025 value and now indexes into 2026: SINGLE 5,706 -> 5,835.31 (+129.31), JOINT/HOH 11,412 -> 11,670.63 (+258.63). Confirmed against the running model: ca_standard_deduction is 5835.31 for the single case and 11670.63 for the joint and head-of-household cases. PER-FIELD JUSTIFICATION us_single_adult_employment_income (SINGLE, 60,000, CA) - cause 2 only: tax_unit.state_income_tax 1602.86 -> 1595.10 (-7.76) tax_unit.household_state_income_tax 1602.86 -> 1595.10 (-7.76) household.household_tax 11998.86 -> 11991.10 (-7.76) household.household_net_income 48001.14 -> 48008.90 (+7.76) spm_unit.spm_unit_net_income 42025.14 -> 42032.90 (+7.76) Deduction +129.31 at the 6% marginal bracket = 7.7586. Taxable income 54,164.69 sits inside the 2026 single 6% band, so the whole deduction change is marginal. us_single_parent_one_child (HEAD_OF_HOUSEHOLD, 40,000, one child age 5, CA) - cause 1 only: household.household_benefits 1130.96 -> 1142.24 (+11.28) household.household_net_income 40014.58 -> 40025.86 (+11.28) spm_unit.spm_unit_net_income 37586.66 -> 37597.94 (+11.28) Cause 2 also raised this unit's deduction, but its CA tax is 0 before and after (339.83 before credits, fully offset), so it contributes no field here - which is why no tax field appears in this case's diff. us_married_two_kids_high_income (JOINT, 240,000, children 8 and 3, CA) - both: tax_unit.state_income_tax 12690.07 -> 12666.02 (-24.05) cause 2 tax_unit.household_state_income_tax 12690.07 -> 12666.02 (-24.05) cause 2 household.household_tax 64934.07 -> 64910.02 (-24.05) cause 2 household.household_benefits 1130.96 -> 1142.24 (+11.28) cause 1 household.household_net_income 176196.89 -> 176232.22 (+35.33) both spm_unit.spm_unit_net_income 147995.31 -> 148030.66 (+35.33) both Deduction +258.63 at the 9.3% marginal bracket = 24.0526; taxable income 228,329.38 sits inside the 2026 joint 9.3% band. Net income moves by 24.05 + 11.28 = 35.33, which is the observed figure exactly. Both child cases land on the same 1142.24 because each has exactly one K-12 child: the age-3 sibling in the married case is not enrolled. No field was rebaselined without a located cause. The fourth case, us_single_adult_no_income, is untouched: its four contaminated fields stay excluded under policyengine-us#9447, and this refresh left that file byte-identical, which is the exclusion guard doing its job. Co-Authored-By: Claude Fable 5.1 --- ...hold-snapshots-country-uprating.changed.md | 1 + .../us_married_two_kids_high_income.json | 12 +++++----- .../us_single_adult_employment_income.json | 10 ++++---- .../us_single_parent_one_child.json | 6 ++--- tests/test_household_calculator_snapshot.py | 23 +++++++++++-------- 5 files changed, 29 insertions(+), 23 deletions(-) create mode 100644 changelog.d/us-household-snapshots-country-uprating.changed.md diff --git a/changelog.d/us-household-snapshots-country-uprating.changed.md b/changelog.d/us-household-snapshots-country-uprating.changed.md new file mode 100644 index 00000000..5de9dbf4 --- /dev/null +++ b/changelog.d/us-household-snapshots-country-uprating.changed.md @@ -0,0 +1 @@ +Regenerated three US household snapshots for two located country changes: the BLS CPI-U actuals refresh that uprates school meal reimbursement rates, and the California standard deduction whose uprating metadata was un-frozen. diff --git a/tests/fixtures/household_calculator_snapshots/us_married_two_kids_high_income.json b/tests/fixtures/household_calculator_snapshots/us_married_two_kids_high_income.json index e92b7541..a3c2746f 100644 --- a/tests/fixtures/household_calculator_snapshots/us_married_two_kids_high_income.json +++ b/tests/fixtures/household_calculator_snapshots/us_married_two_kids_high_income.json @@ -2,13 +2,13 @@ "family.family_id": 0.0, "family.family_weight": 0.0, "household.congressional_district_geoid": 0.0, - "household.household_benefits": 1130.96, + "household.household_benefits": 1142.24, "household.household_count_people": 4.0, "household.household_id": 0.0, "household.household_income_decile": 10.0, "household.household_market_income": 240000.0, - "household.household_net_income": 176196.89, - "household.household_tax": 64934.07, + "household.household_net_income": 176232.22, + "household.household_tax": 64910.02, "household.household_weight": 1.0, "marital_unit.marital_unit_id": 0.0, "marital_unit.marital_unit_weight": 1.0, @@ -88,15 +88,15 @@ "spm_unit.spm_unit_id": 0.0, "spm_unit.spm_unit_is_in_deep_spm_poverty": 0.0, "spm_unit.spm_unit_is_in_spm_poverty": 0.0, - "spm_unit.spm_unit_net_income": 147995.31, + "spm_unit.spm_unit_net_income": 148030.66, "spm_unit.spm_unit_weight": 1.0, "spm_unit.tanf": 0.0, "tax_unit.ctc": 4400.0, "tax_unit.eitc": 0.0, "tax_unit.employee_payroll_tax": 21480.0, - "tax_unit.household_state_income_tax": 12690.07, + "tax_unit.household_state_income_tax": 12666.02, "tax_unit.income_tax": 30740.0, - "tax_unit.state_income_tax": 12690.07, + "tax_unit.state_income_tax": 12666.02, "tax_unit.tax_unit_id": 0.0, "tax_unit.tax_unit_weight": 1.0 } diff --git a/tests/fixtures/household_calculator_snapshots/us_single_adult_employment_income.json b/tests/fixtures/household_calculator_snapshots/us_single_adult_employment_income.json index 12373778..e3a77657 100644 --- a/tests/fixtures/household_calculator_snapshots/us_single_adult_employment_income.json +++ b/tests/fixtures/household_calculator_snapshots/us_single_adult_employment_income.json @@ -7,8 +7,8 @@ "household.household_id": 0.0, "household.household_income_decile": 10.0, "household.household_market_income": 60000.0, - "household.household_net_income": 48001.14, - "household.household_tax": 11998.86, + "household.household_net_income": 48008.9, + "household.household_tax": 11991.1, "household.household_weight": 1.0, "marital_unit.marital_unit_id": 0.0, "marital_unit.marital_unit_weight": 1.0, @@ -34,15 +34,15 @@ "spm_unit.spm_unit_id": 0.0, "spm_unit.spm_unit_is_in_deep_spm_poverty": 0.0, "spm_unit.spm_unit_is_in_spm_poverty": 0.0, - "spm_unit.spm_unit_net_income": 42025.14, + "spm_unit.spm_unit_net_income": 42032.9, "spm_unit.spm_unit_weight": 1.0, "spm_unit.tanf": 0.0, "tax_unit.ctc": 0.0, "tax_unit.eitc": 0.0, "tax_unit.employee_payroll_tax": 5370.0, - "tax_unit.household_state_income_tax": 1602.86, + "tax_unit.household_state_income_tax": 1595.1, "tax_unit.income_tax": 5020.0, - "tax_unit.state_income_tax": 1602.86, + "tax_unit.state_income_tax": 1595.1, "tax_unit.tax_unit_id": 0.0, "tax_unit.tax_unit_weight": 1.0 } diff --git a/tests/fixtures/household_calculator_snapshots/us_single_parent_one_child.json b/tests/fixtures/household_calculator_snapshots/us_single_parent_one_child.json index 29254dbe..1160dde0 100644 --- a/tests/fixtures/household_calculator_snapshots/us_single_parent_one_child.json +++ b/tests/fixtures/household_calculator_snapshots/us_single_parent_one_child.json @@ -2,12 +2,12 @@ "family.family_id": 0.0, "family.family_weight": 0.0, "household.congressional_district_geoid": 0.0, - "household.household_benefits": 1130.96, + "household.household_benefits": 1142.24, "household.household_count_people": 2.0, "household.household_id": 0.0, "household.household_income_decile": 10.0, "household.household_market_income": 40000.0, - "household.household_net_income": 40014.58, + "household.household_net_income": 40025.86, "household.household_tax": 1116.38, "household.household_weight": 1.0, "marital_unit.marital_unit_id": 0.0, @@ -52,7 +52,7 @@ "spm_unit.spm_unit_id": 0.0, "spm_unit.spm_unit_is_in_deep_spm_poverty": 0.0, "spm_unit.spm_unit_is_in_spm_poverty": 0.0, - "spm_unit.spm_unit_net_income": 37586.66, + "spm_unit.spm_unit_net_income": 37597.94, "spm_unit.spm_unit_weight": 1.0, "spm_unit.tanf": 0.0, "tax_unit.ctc": 2200.0, diff --git a/tests/test_household_calculator_snapshot.py b/tests/test_household_calculator_snapshot.py index 1282cff4..d2bfeb69 100644 --- a/tests/test_household_calculator_snapshot.py +++ b/tests/test_household_calculator_snapshot.py @@ -138,12 +138,18 @@ def _check_snapshot( # `snap` sums its monthly values and only some months resolve — 2024 picks up # November and December alone (584 = 2 x 292), 2025 picks up all twelve # (3522 = 9 x 292 + 3 x 298), 2026 picks up January alone (298), and 2027 picks -# up none (0). The stored 3596.04 was the correct twelve-month sum under the -# older uprating index (9 x 298 + 3 x 304.68); on this pin the correct annual -# figure would be 3607.56 (9 x 298 + 3 x 308.52), so rebaselining to 298.00 -# would freeze about a twelfth of the real benefit into the expected output. -# Instead test_snap_annualization_defect_still_present fails loudly the moment -# the country annualizes SNAP again. +# up none (0). `snap_max_allotment` behaves the same way — 298.00 for +# 2026-01 and 0.00 for every later 2026 month — which places the defect in +# parameter resolution rather than in the `snap` variable itself. +# +# The stored 3596.04 was the correct twelve-month sum under the older uprating +# index (9 x 298 + 3 x 304.68). A correct 2026 annual on this pin is on the +# order of 3,600 — nine months at 298.00 plus three at the uprated October +# rate — but it cannot be measured here, because the months that would carry +# the uprated rate are exactly the ones resolving to zero. Rebaselining to +# 298.00 would therefore freeze roughly a twelfth of the real benefit into the +# expected output. Instead test_snap_annualization_defect_still_present fails +# loudly the moment the country annualizes SNAP again. COUNTRY_DEFECT_EXCLUSIONS: dict[str, dict[str, str]] = { "us_single_adult_no_income": { "spm_unit.snap": SNAP_ANNUALIZATION_ISSUE, @@ -176,9 +182,8 @@ def test_snap_annualization_defect_still_present() -> None: While the defect stands, the fields in ``COUNTRY_DEFECT_EXCLUSIONS`` are excluded from ``test_us_household_snapshot[us_single_adult_no_income]``. A correctly annualized 2026 benefit for a one-person unit with no income is - 3607.56 on this pin (nine months at the 298.00 allotment plus three at the - uprated 308.52); the country currently returns January alone. When that - changes this assertion fails: drop the exclusion entry, regenerate + on the order of 3,600; the country currently returns January alone. When + that changes this assertion fails: drop the exclusion entry, regenerate ``us_single_adult_no_income.json``, and delete this test. """ pytest.importorskip("policyengine_us") From 2019530f0e31b0b0c681bb1479a239e4a463454d Mon Sep 17 00:00:00 2001 From: Max Ghenis Date: Sat, 12 Sep 2026 00:30:45 -0400 Subject: [PATCH 32/46] Correct the SNAP diagnosis and rebaseline the no-income case This reverses `74675620` and `7019c9fc`. Both, and the brief they followed, rest on a premise that direct measurement refutes: there is no SNAP annualization defect. The x12 is intact. What actually happens. A childless 35-year-old with no income is an ABAWD subject to the SNAP time limit, and the case supplies no hours. Two country changes inside the 1.764.6..2.0.x window decide the result: 82745ca239 (2026-08-10) weekly_hours_worked_before_lsr default_value 40 -> 0, so an omitted-hours household now fails the ABAWD 20-hour test in every month. At 1.764.6 it silently passed on the 40-hour default, which is why the old value was twelve full months. 74b0a75e5f (2026-08-04, #8872) adds waived_states.yaml, modelling statewide ABAWD waivers from FNS response letters: 2026-01-01: [CA, IL, NV], then 2026-02-01: [] as those waivers expired 2026-01-31. So the unit is waiver-exempt in January 2026 and time-limited from February: one eligible month at the 298.00 allotment. That one month's allotment happens to equal a plausible-looking annual-vs-monthly ratio is the coincidence that made an ordinary eligibility change look like a period bug. Measured on the pinned model, not inferred. Monthly snap for 2026 is [298, 0 x 11] with is_snap_abawd_exempt True in January and False after. The same household with weekly_hours_worked_before_lsr = 40 returns 3607.571 for 2026 - nine months at 298.00 plus three at the uprated 308.52 - which is the twelve-month sum working correctly. By state with hours omitted: CA, IL and NV return 298.00, TX, NY and FL return 0.00, matching the three waived states. The year series I previously cited as proof of a resolution defect - 584 in 2024, 3522 in 2025, 298 in 2026, 0 in 2027 - is the California waiver schedule read line by line: waived_states.yaml begins 2024-11-01 (so 2024 is November and December, 2 x 292 = 584), covers all of 2025, covers January 2026 alone, and does not reach 2027. I asserted a mechanism I had not read. The comment stating it is deleted rather than softened. Under Ruling E's own standard - regenerate where a country change is located, stay red otherwise - this case now qualifies, so the four fields are rebaselined with the rest: spm_unit.snap 3596.04 -> 298.00 household.household_benefits 3596.04 -> 298.00 household.household_net_income 3595.04 -> 297.00 spm_unit.spm_unit_net_income 3595.04 -> 297.00 Replaced the defect guard with test_snap_work_requirement_inputs_drive_the_ no_income_case, which pins the real mechanism: the waiver-month boundary and the hours-supplied contrast. The old guard asserted snap < 1000 against a defect that does not exist and would have passed silently forever while its docstring stated a false mechanism. policyengine-us#9447 asserts the same false mechanism and should be corrected upstream; this lane does not touch it. Co-Authored-By: Claude Fable 5.1 --- ...nap-annualization-defect-excluded.fixed.md | 1 - ...snapshot-snap-work-requirements.changed.md | 1 + .../us_single_adult_no_income.json | 8 +- tests/test_household_calculator_snapshot.py | 117 +++++++----------- 4 files changed, 49 insertions(+), 78 deletions(-) delete mode 100644 changelog.d/snap-annualization-defect-excluded.fixed.md create mode 100644 changelog.d/us-no-income-snapshot-snap-work-requirements.changed.md diff --git a/changelog.d/snap-annualization-defect-excluded.fixed.md b/changelog.d/snap-annualization-defect-excluded.fixed.md deleted file mode 100644 index ff444319..00000000 --- a/changelog.d/snap-annualization-defect-excluded.fixed.md +++ /dev/null @@ -1 +0,0 @@ -Excluded the four US single-adult snapshot fields contaminated by the SNAP annualization defect (policyengine-us#9447) instead of rebaselining them, and added a guard that fails when the country fixes it. diff --git a/changelog.d/us-no-income-snapshot-snap-work-requirements.changed.md b/changelog.d/us-no-income-snapshot-snap-work-requirements.changed.md new file mode 100644 index 00000000..e18c26ab --- /dev/null +++ b/changelog.d/us-no-income-snapshot-snap-work-requirements.changed.md @@ -0,0 +1 @@ +Regenerated the US single-adult-no-income snapshot for the SNAP ABAWD work-requirement changes, and noted that omitting weekly_hours_worked_before_lsr now reads as zero hours. diff --git a/tests/fixtures/household_calculator_snapshots/us_single_adult_no_income.json b/tests/fixtures/household_calculator_snapshots/us_single_adult_no_income.json index 0f05357a..8147e37e 100644 --- a/tests/fixtures/household_calculator_snapshots/us_single_adult_no_income.json +++ b/tests/fixtures/household_calculator_snapshots/us_single_adult_no_income.json @@ -2,12 +2,12 @@ "family.family_id": 0.0, "family.family_weight": 0.0, "household.congressional_district_geoid": 0.0, - "household.household_benefits": 3596.04, + "household.household_benefits": 298.0, "household.household_count_people": 1.0, "household.household_id": 0.0, "household.household_income_decile": 10.0, "household.household_market_income": 0.0, - "household.household_net_income": 3595.04, + "household.household_net_income": 297.0, "household.household_tax": 1.0, "household.household_weight": 1.0, "marital_unit.marital_unit_id": 0.0, @@ -30,11 +30,11 @@ "person[0].ssi": 0.0, "person[0].tax_unit_id": 0.0, "person[0].unemployment_compensation": 0.0, - "spm_unit.snap": 3596.04, + "spm_unit.snap": 298.0, "spm_unit.spm_unit_id": 0.0, "spm_unit.spm_unit_is_in_deep_spm_poverty": 1.0, "spm_unit.spm_unit_is_in_spm_poverty": 1.0, - "spm_unit.spm_unit_net_income": 3595.04, + "spm_unit.spm_unit_net_income": 297.0, "spm_unit.spm_unit_weight": 1.0, "spm_unit.tanf": 0.0, "tax_unit.ctc": 0.0, diff --git a/tests/test_household_calculator_snapshot.py b/tests/test_household_calculator_snapshot.py index d2bfeb69..56357dc3 100644 --- a/tests/test_household_calculator_snapshot.py +++ b/tests/test_household_calculator_snapshot.py @@ -52,22 +52,11 @@ def _round(value, places: int = 2): return value -def _check_snapshot( - name: str, data: dict, exclude: dict[str, str] | None = None -) -> None: +def _check_snapshot(name: str, data: dict) -> None: path = SNAPSHOT_DIR / f"{name}.json" rounded = {k: _round(v) for k, v in sorted(data.items())} - excluded = dict(exclude or {}) if UPDATE or not path.exists(): - if excluded and path.exists(): - # A refresh must never launder a known country defect into the - # expected output. Keep the pre-defect value for excluded fields so - # PE_UPDATE_SNAPSHOTS=1 cannot quietly freeze the wrong number. - previous = json.loads(path.read_text()) - for key in excluded: - if key in previous: - rounded[key] = previous[key] path.parent.mkdir(parents=True, exist_ok=True) path.write_text(json.dumps(rounded, indent=2, sort_keys=True) + "\n") if not UPDATE: @@ -76,7 +65,7 @@ def _check_snapshot( expected = json.loads(path.read_text()) diffs = [] - all_keys = (set(expected) | set(rounded)) - set(excluded) + all_keys = set(expected) | set(rounded) for key in sorted(all_keys): if key not in expected: diffs.append(f" new key: {key}={rounded[key]!r}") @@ -122,44 +111,6 @@ def _check_snapshot( } -SNAP_ANNUALIZATION_ISSUE = "https://github.com/PolicyEngine/policyengine-us/issues/9447" - -# Fields whose expected value is contaminated by a country defect. They are -# excluded from the comparison rather than rebaselined, so the rest of the case -# keeps protecting against drift while the wrong number is never frozen in. -# -# For 2026 policyengine-us reports one month's SNAP allotment where the annual -# value belongs (spm_unit.snap 3596.04 -> 298.00); the three resource fields -# below are downstream sums of it. The behaviour is identical on -# policyengine-us 1.825.2 and 2.0.x, so it predates the canonical SPM release -# and is not drift this pin introduced. -# -# It is also not the wrapper's: on a bare policyengine_us.Simulation, annual -# `snap` sums its monthly values and only some months resolve — 2024 picks up -# November and December alone (584 = 2 x 292), 2025 picks up all twelve -# (3522 = 9 x 292 + 3 x 298), 2026 picks up January alone (298), and 2027 picks -# up none (0). `snap_max_allotment` behaves the same way — 298.00 for -# 2026-01 and 0.00 for every later 2026 month — which places the defect in -# parameter resolution rather than in the `snap` variable itself. -# -# The stored 3596.04 was the correct twelve-month sum under the older uprating -# index (9 x 298 + 3 x 304.68). A correct 2026 annual on this pin is on the -# order of 3,600 — nine months at 298.00 plus three at the uprated October -# rate — but it cannot be measured here, because the months that would carry -# the uprated rate are exactly the ones resolving to zero. Rebaselining to -# 298.00 would therefore freeze roughly a twelfth of the real benefit into the -# expected output. Instead test_snap_annualization_defect_still_present fails -# loudly the moment the country annualizes SNAP again. -COUNTRY_DEFECT_EXCLUSIONS: dict[str, dict[str, str]] = { - "us_single_adult_no_income": { - "spm_unit.snap": SNAP_ANNUALIZATION_ISSUE, - "household.household_benefits": SNAP_ANNUALIZATION_ISSUE, - "household.household_net_income": SNAP_ANNUALIZATION_ISSUE, - "spm_unit.spm_unit_net_income": SNAP_ANNUALIZATION_ISSUE, - }, -} - - @pytest.mark.parametrize("case_name", sorted(US_CASES)) def test_us_household_snapshot(case_name: str) -> None: pytest.importorskip("policyengine_us") @@ -173,33 +124,53 @@ def test_us_household_snapshot(case_name: str) -> None: values = result.to_dict() values.pop("provenance", None) _flatten("", values, out) - _check_snapshot(case_name, out, exclude=COUNTRY_DEFECT_EXCLUSIONS.get(case_name)) + _check_snapshot(case_name, out) -def test_snap_annualization_defect_still_present() -> None: - """Fail loudly when policyengine-us#9447 is fixed. +def test_snap_work_requirement_inputs_drive_the_no_income_case() -> None: + """Pin why ``us_single_adult_no_income`` reports one month of SNAP. - While the defect stands, the fields in ``COUNTRY_DEFECT_EXCLUSIONS`` are - excluded from ``test_us_household_snapshot[us_single_adult_no_income]``. - A correctly annualized 2026 benefit for a one-person unit with no income is - on the order of 3,600; the country currently returns January alone. When - that changes this assertion fails: drop the exclusion entry, regenerate - ``us_single_adult_no_income.json``, and delete this test. + A childless 35-year-old with no income is an ABAWD subject to the SNAP + time limit, and the case supplies no hours. Two country changes decide the + result: ``weekly_hours_worked_before_lsr`` now defaults to 0 rather than + 40, so an omitted-hours household fails the 20-hour test; and California's + statewide ABAWD waiver runs only through 2026-01-31. The unit is therefore + exempt in January 2026 and time-limited from February, giving one eligible + month rather than a broken annualization — the twelve-month sum is intact, + as the hours-supplied comparison here shows. + + This is the mechanism behind the snapshot's SNAP figure, so it is pinned + separately: if the hours default or the waiver modelling moves, this fails + with the reason rather than leaving an unexplained number in a fixture. """ pytest.importorskip("policyengine_us") - import policyengine as pe - - result = pe.us.calculate_household( - **US_CASES["us_single_adult_no_income"], spm={"geography_kind": "national"} - ) - snap = result.spm_unit.snap - assert snap < 1_000, ( - f"policyengine-us returned annual SNAP {snap:.2f}, which is no longer a " - f"single month's allotment. {SNAP_ANNUALIZATION_ISSUE} appears fixed: " - "remove the COUNTRY_DEFECT_EXCLUSIONS entry for " - "us_single_adult_no_income, regenerate that snapshot, and delete this " - "test." - ) + from policyengine_us import Simulation + + def situation(hours: float | None) -> dict: + person: dict = {"age": {"2026": 35}} + if hours is not None: + person["weekly_hours_worked_before_lsr"] = {"2026": hours} + return { + "people": {"you": person}, + "spm_units": {"sp": {"members": ["you"]}}, + "tax_units": {"tu": {"members": ["you"]}}, + "families": {"f": {"members": ["you"]}}, + "marital_units": {"mu": {"members": ["you"]}}, + "households": {"hh": {"members": ["you"], "state_code": {"2026": "CA"}}}, + } + + omitted = Simulation(situation=situation(None)) + assert omitted.calculate("snap", 2026)[0] == pytest.approx(298.00, abs=0.01) + # Exempt only while the California statewide waiver is in force. + assert bool(omitted.calculate("is_snap_abawd_exempt", "2026-01")[0]) is True + assert bool(omitted.calculate("is_snap_abawd_exempt", "2026-02")[0]) is False + assert omitted.calculate("snap", "2026-01")[0] == pytest.approx(298.00, abs=0.01) + assert omitted.calculate("snap", "2026-02")[0] == pytest.approx(0.0, abs=0.01) + + # Supplying hours satisfies the work requirement, and the same model then + # returns a full, correctly uprated twelve months: 9 x 298 + 3 x 308.52. + working = Simulation(situation=situation(40)) + assert working.calculate("snap", 2026)[0] == pytest.approx(3607.57, abs=0.01) # UK cases ------------------------------------------------------------------- From 49a528915a6bf4432bb1d7bb37b0d77be5f86b55 Mon Sep 17 00:00:00 2001 From: Max Ghenis Date: Sat, 12 Sep 2026 00:31:31 -0400 Subject: [PATCH 33/46] Record rulings E and the reversal of D Co-Authored-By: Claude Fable 5.1 --- .coverage | Bin 86016 -> 0 bytes PROGRESS.md | 25 +++++++++++++++++++++++++ 2 files changed, 25 insertions(+) delete mode 100644 .coverage diff --git a/.coverage b/.coverage deleted file mode 100644 index f970f660f19e4d3848c0245ae5e676af4f034c3c..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 86016 zcmeHw349b)ws&p4cUNU434wG&NLZ6j0K=YyO;*`K1f=P7DoIPHJ9KqtAtap&D!y^S z(f3?tfH2>i@28(LgWIUMCOU3!Mv3Fhb5xWBT)-J5LDUek^gDGs-Kjuu=6z3Z`{q~m zOZwDu>z;G}_uO;uxl7&kYgambyrsnBEw}qD(});?B#AT!jpq#qj5Bo`4NlDzT{b*W4g~=P0R;gCfqxnV z#I;6EQc4QhSm(1Bxp<$~?%=(lU(>>Mi|21xY}qh>!OF#!&^^lpy#vrv~EMAth)>=*a zLoVfL=qf6>F9)M9{nJB4mhKFSyn?UEsP`uZ81i=Z_s#I+Wv%_~S2&g7s+R#v6kD5L&aUn6j_W_W&9Pi?V1QWJbpyQsDhNSy=cRB7To3mxcixm{JZQ`?JYq=G zAJQl59?)sEcWM(g2Q=yGCUu7DVbuiYF=i(H6rD#sM=d0eksFBjh&5qGB5h%aMuT;u z04qMM<@V}QScKsetU^V3mX|N`J6*+DQ*)-~<;=>NX|3{jclx|MFIX!)E~lf0cbCFq zwX7rhBJf!yPPg4<6}*li!K|>3ro!(Ntd26fyA;;pm6m$>Qdr&rOGoyY z8A7bXDTJJRbsBK@_s943YiKAf23i8!&3ok zm~8?qpN56iH6TJ|G9a)En*>m@L&#SqVdWdLOOW3B5r8;}Exi?Xue92b_uAZ^VoBR; z6CvHTIQUn1VF|4p7OV4Cv|1jRS0?~-ECwy4?$V+>&^O>LlJS7M21mL>tOtR2*^79W z05Vj?LFxs3BJf_X$BP)p;$>XS3-BofNO5HhFve22L!iU*N?uBE4F`Oy@F8upIo(d5 z%_b?Di3LS7 zClf&WMu@WneeeU-v-O53pdi-@ogM^8!l4zYqV?IUZAH8r*2aTQfZ=!92Ab(o;n3kK z97vRiAq3WyT3q%3u?{utfp0(q=+oHHk0jZK*5HExRcc5$7SoRsUR>k0mpdK6SR_@D zbS(AW@QNoTp23^=kSzqp)if~1(%e94mzDQg3OHkFWrdwr8FPUI=0CXe29A3CR``jX z2R|_u3VvyJC;SqLTqApt-6{G0stLw}AupeLjyz_1z>r36;5f<8M|}cX-2E_eSEM2czDU`h8?-T(M% zMielL5OM$GFf)$0|FPH#*--aCW;Y{NVMJQi_TogAX*6TuEh}JGcmG*z_C_X-%>6gv zYsbk1xZM3WMgs+zRv1J*)0l)sD~9*NWbVHnTj4;WL~0=L$8^~20b(6-|Ft5)G}8Fs zkEA)`|7)J<;D~vYv?lk+3=L%kl_}C!;o*7Y>3wf^(XbO!+L>#)${rVx^HwRbxpe6 zx(eNTUA}Id_P5$b?IdlyR;&4ire2e$$W-N)bcqreFV@ ze%u_AAf9i|AbPj!h|VhOKa#9Fs}39{r0-y#+2Zxu}t=P0d(s4n}>lWVjYfLL~2%ZvcSbfz`WKhlvO3~bu* z;qHzXHNjhtzg^ogPelZp*u(8T)5z@WA=>S8rA0%9hIgI_jtdIfHOKpp5e>au2>7!d zjtjej?*~8cv37q(iD{dFbK~6U{U@~lL7m-yBk|tj9goh)|9o{r;L|Y&3O+0S;;xo? zMtiXT_%rvP)jZsPl0Mv@HB1)12{IFNGu7V0_D^7~krtMw#cXeHH>Y;aIk~z&xwpQ# zt)r`_ubvgj=BAHV?g;iTBZzmJ$9=IximWMk&$zzc<oHoj)bpU(7gk`pbD23fK+s zPTEue;jn=;erU~F57*nSd-}l%^vs$W(n}Npy3w%jy^%?`z&5aZ5)_+3EfSr*Bn2Q()0YM8hGtnbKlXf`#YLHy+3m;gjuzsGJf-8Z|DnO zf!GK#rlrdxp&%VK0pd+f%bqt=*FdB0DWP0^8Oh?y+-r9c7uo@mF zFQW=)6g34mF$ZNmu7Bt^X$NI=GQP8)c>SO=3r1orh}qJTP9|w1DV~eb+p9 zB|P6fiy|(0<|i*`k2}?KFX{UE%XPU0{Rd-nM>RF~TWc@AaiqB;;3SDaRq**9+Z#{4 z`1<(6wMBhjx=kzK?NKv-)_ztPTM#?SxEwA=&Y+&tKGs~76t@hnGV}f#M;v?m+2apZ zy}v)MLc0|1#a%Fxh$iKeY*?hAGMf~UG4T0|6ndV7!| z4%;u@5@b*~ZuGVA(%jWlL#-~5JF8{Ju~(8So(a|+Rqe@Zo6}sNT`a}EiZaHqi{LD6 zJCzwD;rKT?aUon!xsf`v;8f7u5De~bFW3=mqY@Ut?Md6HZLc?xbpO=X#{=?5G@AKv zH)bof_S!|uXzD)8e`ZNb(yUA4&os5)LtiZK<664< zlOJUHe2LZaM2a|Lc&y-%dB&4Hu}fRxF9uf<7iWBxUy#{XU=F-^A)V-Jt2<>Th~yJ9 zUhXgk@&m!I3JUJL)BM<`eIKmZSrgapsp>fQ5VL)5-`M`3hBzARJzWs&ZZ)@d^dE2h zgHAUOBuTJ8^6NHD;#@eLvW5Ecfrg&`mpWGLINkQZl0IhB97&KVrvCEGKj~&Qt;(E6 zyqkCOe0sq3)q^#i$BP!E)jW798B)HKAdZ@s)YP}ub$k@?{OQ)to;&NDEPeL#>KUa~ z=_z;hd_u&KEnjMCiIxMMxAx@B26jd4U$>GE+z@#0U{Bk@_ZJ>L5$k9_U8f_6S5jW+ z=m^g2r|xYkIM>r-K414um3x*X$u^^u_3t!OpWN|NsenL<>kd+ct@}1}{GMv6q3ck` z+f5t^hL-5cgpJoNM|Jz#;y(FuPXG2+o9^z%KYpR(bD5f8rg3or*CbiT;zzB&~1(X-9-`j%*~froM9^#qwd9nO~K-n2yB z*!$TZ?r-k>?9t}lFSH-ei6hQ7AKyo`H}?fI3a3d)C2~G|qVyBN`0Cte@96%$?#(7- zfg!~b0&wgZ)vRrJDR?_m*cMz&9qtH@CEw`^{FIO`_9IuYakv=a{YeT(|WQ=b2~TxRD}>-nmIXzyEwcJ!&#Mnmy@5HA|fQSoi)t4WHinw+nTjo$h?}{h*eM zn*>iY#|=Se#6-B4oqp464Trzo-|ud{o0)z)~gO@7FaSN&;oPz>dt3Y#~8KmbAK$o zUbSOX*Vg6(U?#5F|1)z^2876=?y`)f#>SAu0-`sF4s+r-crax%byEwGQ)bB4Wgxb?hd0-Hf4 zj0QSs5w&eL`RTr(Dezw4cfnXPVU%=tX=5m1Z-fXfI#WRt_pVz)^}f{66Z)%f>*|C5 zy6@u;ML7^rIDX(FkG|)>!w5yy(+{t7)isAB%x`P!0!Ce3Jv|*gJ@w5UAbVT*VI*1k z>_|W$?OLiol(nQL1+HTjQ`zcdIAIq~N=pKozkq5Oy}TQm4QK~>Em2s1(cD{bF`%W? zBcyQiCutI;7YkpZ*aSF9OWVGzKX|ZeQg>Q!lsjtE0Fs88|E@Bs;Kh-n zj@)B;A-t)B*}*!P2HJ6bOUb0oam1LjX3Xy~7JC>T7EzLk=q%3Diave7-$np(dI z?m6DGg8i32mcQDztars3GdbG-;(y#v2>)kE`{+f%KIX#3-1))!{(A#~wzdcQ`hMEh zc08!P^rVMW{hs?BSI3I1!1Aoibcg8{Q@yFewAJKf*KjXzN4ST$2TfJnJzN`iJ68!`4LHyK zm2F`^VL#$>xQXn3_BQqn_CMKU?6d3>?625gvIp7wOi!C0H9gGgS=!WV`o?t5blTKz z`h}^;^fS}FoQEsp?A#V^2DhGD$t~vcO&^%vHT}W#s_8}6%qFw3+}G@G?n`bgm&zrW zHgIar#BOKju+!NrHj_KdE@Q7}7f1-WZFhVrfxvw zy7h>xT!+YtwTLWVgUGVgh|F1q$n5J7nY9v;nJW;Pu^f@SWr$p}6p=Mc5LtaKBC8f7 zvV9REH!ehE+X6(k&PSxE5Rn}Ph$QACVxNb|mbr+S<{-k(M#MM^k##c>F(7}OoREjE z#$SU-?sPV+rNcu!X#!NuOG9D3g zCL&Y@BAdq{azi>Io5mutaSS3GEQn;95y?nHWYK6umX1PXNh%`OjznZ}3L*=W5m}Ig z$ovtA6ec2)mVk&p9uZX>BBNswNsU1yg+s)~B9d%EB*}<~#(>BOJt8_CB3dmXY7HWc z8j(>dL}&()^)w=DDF(U$(D*-r#zJtG5QUe5fP#R6fP#R6fP#R6fP#R6fP#R6fP#R6 zfPw%{1f=nQ#s9~NxWY?8KtVu3KtVu3KtVu3KtVu3KtVu3KtVu306zlK`hUg$$B(-6JvHuY<2B55Y>Qx~e9BOg+~&TLep53)#d z|BrS?kL(T^IpMv6wcK7^TE@Gbf)zHL&+_s`ey6KAYiiEayqsA%Gp$t~?@phW=LKtM zw{G5D>U8s2RbG2V1@DFMF5WKiStU-l-DMTLjv>LU4v*jM^E%;ezhFhXBNjUaxZ&^# zuubH(qMB*su);17TrQK-g&=1Yto`K-ekY zuyg+ZsDSVaO~%VN z)Vc+}NjTd<0C|PoYcJ=0yw_G~clo8dM-yNgNqky9D1h5jEb$+)n-Qy`z#mxa@MgNl z@2l|pfUmZi5e0lnF0OIg%bgBjtj5e3HmHaC6c{s=jOf9aVu#&PCh68FymhIpZA}YM zcj&Aeg&*AMl`++mr-@YJ0>RwD=;@zujWkE@Brb3&`0hVx3K-8C9flKz8}#q#*XrKY zEz`cFovL|Sldb;0>QSl#syHf0eoNi_kM>uHDyGqdB}Xf~o=V)(BrAvxY;|fe?{!w9_M2dJ+r3gd z3ZU4CuT>!zK!(5ue7@v&d2ynaX^h8GIAm5TB$u1S z1!zh`jZaG*Bi3Ox8glOD%e{afY_vHHfvlB9VN1Ha*)6$-Qnj^7n*VPh))4R=f0q4M zHq~^$Db0A$m|=LtkOq7ACF>s0X|;E16Ez1k>FOqRhU#I}1m-bjCjAthM?FU^B#)6B zi1#3ZfkUJ%4AE%7(TfoV({ZIf?J+ZiScg*xxjt2=0e3$R?w1e82%~}XPHdb*!w>#Z zz|@0{X}F_qcPg;0!NzvTs7KvM;2prh+wXI_1S{``>a50wE}+zkL<$r#6%PKPVWukw zT`H3SfnC@nhzz<^CSm0p8FZ;10f=L%p%CI%40+ThLb_{l;4SM62-gzT3BVkSK?|vS znO!IgwO~au9&p#-NOy?!An-1G5$}>3uvKx8dI6sZyw~gTBF3?J85i?{vs4f#fZiv99n_QzRzB5E8^XJ zi4$}J#=2cL=ysF5N63*H9j?MLv<8ywffkoNK&(Rzd*B<;0QxjG^apK!c%nm<8WN7h z^rM6ij=GDa3X+bc-Wy)=0R1Z1WEp4TBP0LSG%&`}+(2oUmG@c-IAdvLg`HO!bAbfr zSe!Q*Q_24yO}Ggzz+PfYO(#sn#uLVkhPMpM^snj*bS=6W+FxrenqO*0sqa=Zs%qv8 zvyFa@o=!DUvE*LjTX^-qafqsUpw=9Y5MM1vA$fIwN%73j)g7*Ur8Vqs}L4O7Yn6x&{nTno{34akC}eSdXXWOi57O^+fr z18bcICa#ORY0;HMHd-X=rbbs5c^;{9K^82vRk%3-zR;D!{kwCZVSuHlGa^Q0xL?#+ zapgyt`BjJeMc5dgXiUUm<|49OWw>8dXMrfOI7JzLxW8@+=mwUa=rANhhWkZoGT?w^ z(rTFhmBZX3H7QC$$PE`U5rn|fCx00dk?Am^dID~x9Dc%Th}+2|@-TfJ^)$@;AEUmP z^QtwfTBd_3(tW5~r#%L*e_sc_iG{Nx|Ldnb3Ojy&Ef@f*pFx3?FW`$;=6%d5_ml`a%Ty?fcJiav|-#NrF z58FD{0Wl(Tb`s<(hOy0VmCYj9&BHoz5181ZsI27zU9^GoQQ|E1!`iyg;JrN>IKbQi zXNk0CX9uyIxketiqNJav*q{&nv0pjo4O~hPhvTq3aJe z#XZbghWb`)>L)V-;kk>6_&g{X_p49rVt*XXn}N~ZInwxl8nKVys@P6;m+2h%{eLw& z4JQqo_3!Fe!ubDm?Zets%{>~mdbjGls-F3pxtXEqy|ju7P#W@fn6t3&d*}eD-3nb~ zSSDT!qn{$P1$tpjI69|fDssh#33lIrYnbQC4+K?VUoeRxeLX;rrGs`z ziw!>T2&*@xHooMF?b!sW$Ff{xxHUNS8vz0=jaE7L!Il4k4Y>Ls<@{y!gL^%|Z^u#m zpz~8wI?XyraXb!&vhqIM68~LmA?2kw1c`h%P*ksh#A6w`9L80H=2*CEQ`D^nL83J~ zWN3e{s9P0XS!AU_R9_dx41zwYNCxRwf*?_wBsa9@PSmZ4Vh#=Mxf6BEqbrN7=T6iu zi>@qk&83%uELbY@@AxFCNH2+=6p>Goit1}Y3M^*!N-V+)&84=Ds9FqyV41{pr64lp z2rL3Iu#9vL5o7S<%_6Z7B)~F3V~7ND3>HKI1NjH6MPhywFc|Uy>$*Z{@?e>;G?)vr zED)IjKmd!cH%#*<(o-#}^Fa(O=3#g-!j-(_|LY9*5Zo=ChW!c4n(j8Kjkg(1a7#H9 z1QY}m1QY}m1QY}m1QY}m1QY~*a0F_K!9l|K5sV6#$L9+T)z>(1u?~#_$^5cCMZg-% z5R9x#J~B~6)Y!ot!#yG}+(eN*JEDL?WTMD!8*s<6(hRM) zN!<owr;t#TZG7YX*PaI8;wk)zKThpQ~ Ji>v@m{VyelLn#0N diff --git a/PROGRESS.md b/PROGRESS.md index 0dcd0194..c05aa2b6 100644 --- a/PROGRESS.md +++ b/PROGRESS.md @@ -142,6 +142,31 @@ those counts are the same number on either pin. Regenerating on 2.0.0 yields exa - Confirmed policyengine-us#9447 is OPEN and its body records the same values on 1.825.2 and 2.0.0, independently corroborating Ruling D. +- **Ruling E done** (`04960e64`), and **Ruling D reversed on a corrected premise** (`2019530f`). + Fanned out the country archaeology across the four drift signatures, each adversarially + verified from three lenses (arithmetic, provenance, alternative-cause): 12 verdicts, 11 + upholding, 1 refuting. Two root causes explain all three Ruling-E cases, both reproduced + from parameter values to the cent: + - `c991cd844a` (PR #9100, 1.779.1) added published BLS CPI-U actuals, moving the + 2026-01-01 index 323.364 -> 326.588; the NSLP/SBP per-meal rates uprate through it, so + the free-tier net school meal subsidy goes 1130.96 -> 1142.24. + - `df3482f4ef` (PR #9059, 1.776.2) moved `uprating: gov.states.ca.cpi` off the CA standard + deduction's file-level metadata, where `uprate_parameters` never read it, onto each + filing status. The deduction un-freezes into 2026: SINGLE 5,706 -> 5,835.31, + JOINT/HOH 11,412 -> 11,670.63. At the 6% and 9.3% marginal brackets that is -7.76 and + -24.05 exactly. +- **The SNAP finding overturned Ruling D's premise, and I verified it myself before acting.** + There is no annualization defect; the x12 is intact. The case is an ABAWD with no hours + supplied, and two country changes decide it: `82745ca239` dropped + `weekly_hours_worked_before_lsr`'s default from 40 to 0, and `74b0a75e5f` added + `waived_states.yaml`, under which California's statewide ABAWD waiver expires 2026-01-31. + One eligible month at 298.00. Measured directly: monthly snap 2026 is `[298, 0 x 11]`; + the same household with hours = 40 returns 3607.571; CA/IL/NV return 298.00 while + TX/NY/FL return 0.00. The year series I had cited as proof of a resolution defect + (584/3522/298/0 for 2024-2027) is the CA waiver schedule read line by line. So the four + fields were rebaselined under Ruling E's standard and the defect guard was replaced with + a test pinning the real mechanism. + ## Next 1. Ruling E: regenerate the three drifting snapshots, justify every field against a located From 1b6c001c860305d529e91d6f452f3359e29439fa Mon Sep 17 00:00:00 2001 From: Max Ghenis Date: Sat, 12 Sep 2026 00:38:14 -0400 Subject: [PATCH 34/46] Drop a rounded sum a cent off the asserted value The comment gave the twelve-month total as 9 x 298 + 3 x 308.52 = 3607.56 while the assertion pins the measured 3607.571; the uprated October rate is 298 x 333.952/322.561 = 308.5237, which the 308.52 restatement rounds away. Describe the composition instead of restating a sum that does not quite land. Co-Authored-By: Claude Fable 5.1 --- tests/test_household_calculator_snapshot.py | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/tests/test_household_calculator_snapshot.py b/tests/test_household_calculator_snapshot.py index 56357dc3..170f7b4c 100644 --- a/tests/test_household_calculator_snapshot.py +++ b/tests/test_household_calculator_snapshot.py @@ -168,7 +168,8 @@ def situation(hours: float | None) -> dict: assert omitted.calculate("snap", "2026-02")[0] == pytest.approx(0.0, abs=0.01) # Supplying hours satisfies the work requirement, and the same model then - # returns a full, correctly uprated twelve months: 9 x 298 + 3 x 308.52. + # returns a full, correctly uprated twelve months: nine at the 298.00 + # allotment plus three at the October 2026 uprated rate. working = Simulation(situation=situation(40)) assert working.calculate("snap", 2026)[0] == pytest.approx(3607.57, abs=0.01) From 50004dd6976648d680ee40c4a66df62a9b386509 Mon Sep 17 00:00:00 2001 From: Max Ghenis Date: Tue, 15 Sep 2026 03:20:07 -0400 Subject: [PATCH 35/46] Track the 2.2.1 repin lane in PROGRESS.md Co-Authored-By: Claude Fable 5.1 --- PROGRESS.md | 214 +++++++++++----------------------------------------- 1 file changed, 46 insertions(+), 168 deletions(-) diff --git a/PROGRESS.md b/PROGRESS.md index c05aa2b6..f1c94744 100644 --- a/PROGRESS.md +++ b/PROGRESS.md @@ -1,178 +1,56 @@ -# Finalize wrapper PR #515 on policyengine-us 2.0.1 +# Repin wrapper PR #515 to policyengine-us 2.2.1 and populace-us-2024-spm-20260915 -## State +Lane: `wrapper-repin-600`. Worktree +`/Users/maxghenis/spm-rebuild-20260908/worktrees/policyengine-wrapper-repin-600`, +branched from origin head `1b6c001c860305d529e91d6f452f3359e29439fa` (PR #515, +`max/spm-canonical-wrapper-release-20260910`). Goal: main can publish +policyengine 6.0.0 on a country pin and a certified data release that agree. -**Starting.** Prior lane (head `e7bfa2bd`) bound the wrapper to policyengine-us **2.0.0** -and left six tests red, blocked on wheel provenance and on wrapper-owner decisions it had -no standing to make. Both blocks are now lifted: +## Prerequisites (verified this lane, primary sources, 2026-09-15) -- **policyengine-us 2.0.1 is published** (verified this lane, 2026-09-12): PyPI returns 200, - wheel sha256 `20e355823bbc89e6c9f413435a7d0b92095cff65541ea366048ee448da025aee`, - sdist `3183f5b1adf4f17963eec46e04bb51da5cd9b6bbda4b38fdebaea7ffa486f660`, - uploaded 2026-09-12T03:56:48Z. It is 2.0.0 plus the above-the-line-deduction determinism - fix (pe-us #9446) that the accepted candidate wheel carried and the merged 2.0.0 lacked. -- **The root has ruled** on the four wrapper-owner decisions (rulings A-E in the brief). - -This lane repins to 2.0.1, aligns the SPM resource contract with the authoritative country -behaviour, and settles all six red tests under those rulings. - -## Rulings being applied - -| Ruling | Substance | -|---|---| -| A | Country model behaviour is authoritative for the SPM resource contract. Housing cap consults the SPM housing portion only for units with housing assistance to cap. Unassisted units are zero, no geography or composition requirement. Assisted units without county still fail closed (`SPM_GEOGRAPHY_REQUIRED`). Explicit national computes. SPM measurement itself (threshold, poverty) always requires geography. Update the wrapper docstring(s) and `test_spm_household::test_state_only_tax_graph_succeeds_and_resource_graph_requires_geography`. | -| B | Pin policyengine-us==2.0.1, policyengine-core==3.32.5, spm-calculator==1.0.0 exactly; UK unchanged. Regenerate bundle manifest and uv.lock; run the repo's bundle checks. | -| C | `test_us_model_version_surface`: counts are identity facts of the pinned country. Regenerate; report before/after counts. | -| D | `test_us_household_snapshot[us_single_adult_no_income]`: `spm_unit.snap` 3596.04 -> 298.00 is a pre-existing country defect (one month's 2026 allotment reported annual), identical on 1.825.2 and 2.0.x, tracked at PolicyEngine/policyengine-us#9447. Do NOT rebaseline: xfail(strict=True) or exclude the single field, whichever is cleaner. | -| E | `test_us_household_snapshot` x3 (employment income, single parent, married two kids): regenerate, but justify every changed field old -> new against a located country change. Anything unexplained stays red and is reported. | - -## Prerequisites (verified this lane, primary sources) - -| Item | Value | Status | -|---|---|---| -| policyengine-us 2.0.1 wheel sha256 | `20e355823bbc89e6c9f413435a7d0b92095cff65541ea366048ee448da025aee` | verified (PyPI JSON, HTTP 200) | -| policyengine-us 2.0.1 sdist sha256 | `3183f5b1adf4f17963eec46e04bb51da5cd9b6bbda4b38fdebaea7ffa486f660` | verified | -| 2.0.1 declared deps | `policyengine-core>=3.30.1`, `spm-calculator==1.0.0` | verified | -| spm-calculator 1.0.0 wheel sha256 | `e354937a5e1a4045d4966ed594a528d8b02866fabaac9bb5672017004b627305` | carried from prior lane; to re-verify | -| policyengine-core 3.32.5 | pinned exactly by the bundle | to re-verify | -| HF data tag | `populace-us-2024-spm-20260909`, H5 `6496cc43...` | carried; to re-verify | - -## BLOCKER (Ruling B): 2.0.1 is published, but no data release is certified for it - -**The repin to policyengine-us 2.0.1 cannot be completed in this repo.** It is blocked on -an act by the *data publisher*, not by anything in the wrapper. - -The certified US data release `populace-us-2024-spm-20260909` publishes this claim: - -```json -"build": {"built_with_model_package": {"name": "policyengine-us", "version": "2.0.0"}}, -"compatible_model_packages": [{"name": "policyengine-us", "specifier": "==2.0.0"}] -``` - -`certify_data_release_compatibility` accepts exactly three bases — build-time match, -matching data build fingerprint, or a publisher `compatible_model_packages` claim. 2.0.1 -satisfies none (the manifest records no `data_build_fingerprint`), so: - -- `bundle.py certify-data --model-version 2.0.1` **refuses**: - *"policyengine-us 2.0.1 matches neither the build-time model (2.0.0) nor any publisher - compatibility claim ['==2.0.0']; a new data build or a published compatibility claim is - required."* -- With 2.0.1 installed, the gate fires at **import of the US model** (`us_latest = - PolicyEngineUSLatest()`, `model.py:512`), raising *"Data release manifest is not certified - for the runtime model version 2.0.1 in country 'us'."* Not just microsimulation — - `pe.us.calculate_household` and every US test error out too. - -This is the documented, intended behaviour. `docs/engineering/skills/data-certification.md`: -*"Neither basis means certification is refused: a new data build or a published -compatibility claim is required."* There is no override flag, and I did not manufacture one: -hand-writing a claim into the bundle, or mutating the published release manifest at an -immutable release tag, would launder exactly the invariant this gate exists to protect. - -I verified no published release covers 2.0.1: all 25 `policyengine/populace-us` tags and all -3 branches were enumerated; `populace-us-2024-spm-20260909` is the newest, and its manifest -carries `==2.0.0` on both the tag and `main`. - -### The claim-widening is substantively justified (evidence for the publisher) - -2.0.1 is 2.0.0 plus pe-us#9446 and nothing else. Verified by extracting and diffing both -published wheels: - -| Check | Result | +| Prerequisite | Result | |---|---| -| Files differing, whole wheel | 2 variable `.py` files + 1 added test (`tests/core/test_ald_determinism.py`) + dist-info | -| `variables/` file set | **identical** — 5981 files in both | -| `parameters/` file set | **identical** — 5970 files, and byte-identical content | -| Substance of both diffs | `list(set(all_alds) - ...)` -> `sorted(set(all_alds) - ...)` | -| 2.0.1 wheel sha256 | `20e355823bbc89e6c9f413435a7d0b92095cff65541ea366048ee448da025aee` (PyPI-declared == downloaded == brief) | - -The change alters only the *order* of float summation within above-the-line-deduction -aggregation. No variable, parameter, or input schema changed, so the dataset's compatibility -with the model is unaffected — widening the claim to cover 2.0.1 asserts nothing the bytes -do not already support. - -**Unblocking action (data publisher, one line):** publish a populace-us release whose -manifest claims `policyengine-us` compatibility covering 2.0.1 (a new release tag — -never an in-place edit of the existing immutable tag). The wrapper side is then -`bundle.py update-packages --us 2.0.1` + `certify-data --model-version 2.0.1` + relock. - -**This lane therefore leaves the branch on the certified 2.0.0 tuple** and completes every -other part of the brief in full. A branch pinned to 2.0.1 would fail to import the US model -at all, making PR #515 entirely red and unreviewable — strictly worse than what it replaces. - -### Consequence for Ruling C - -Ruling C asks for the variable/parameter counts "regenerated for 2.0.1". Because the -`variables/` and `parameters/` file sets are provably identical between 2.0.0 and 2.0.1, -those counts are the same number on either pin. Regenerating on 2.0.0 yields exactly the -2.0.1 result. +| PyPI has policyengine-us 2.2.1 | Yes. Wheel sha256 `0993a6c73fcdfbe171a796aca9d302741bd8e00ab83388ad81c15a08740318c6`, uploaded 2026-09-15T04:45:46Z. | +| PyPI latest is 2.3.0 (not the target) | Yes, uploaded 06:11:55Z. Deliberately **not** pinned: the certified claim is `>=2.0.0,<2.3` and 2.2.1 is the built-with version. | +| HF tag `populace-us-2024-spm-20260915` exists and is readable | Yes. Release manifest records `build.built_with_model_package` = policyengine-us **2.2.1**, `build.build_id` = `populace-us-2024-spm-20260915`, H5 sha256 `6496cc4393d4d3c6574f76eca231de5898c803b9067645591fd5c4d3e65aee84`. | +| Release carries a publisher compatibility range | Yes. `compatible_model_packages` = `policyengine-us >=2.0.0,<2.3`, basis `publisher_claim`. | +| microcosm main contains PR #928 | Yes. Merged 2026-09-15T06:34:17Z, merge commit `743683b0e121378e93c9b426cd9c79d41d29595b`. | +| Branch head unmoved | Yes, `origin/max/spm-canonical-wrapper-release-20260910` = `1b6c001c`. | + +## Baseline at `1b6c001c` + +- `pyproject.toml` pins `policyengine-us==2.0.0` in the `models`, `us` and `dev` extras; + `policyengine-core==3.32.5`, `spm-calculator==1.0.0`, `policyengine-uk==2.90.2`. +- `src/policyengine/data/bundle/manifest.json` → `data_releases.us` certifies build + `populace-us-2024-spm-20260909` for model version `2.0.0`, basis + `built_with_model_package`. +- The H5 sha256 is byte-identical across the 20260909 and 20260915 releases + (`6496cc43…aee84`); the release-directory evidence files differ. + +## Plan + +1. Re-certify: `scripts/certify_data_release.py` against the 20260915 release manifest at + model version 2.2.1. +2. Regenerate bundle artifacts: `scripts/bundle.py generate --include-tros --strict-tros`. + The pyproject pins are **generated** from `manifest.json`; they are never hand-edited. + `provenance/pyproject_pins.py::update_country_pins` is not used (it misses the `models` + extra). +3. Refresh the lock: `scripts/release_lock.py --refresh`, then `--committed`. +4. Restore the `tests/test_spm_household.py` rewrite from `818c894e` (the version that + needs the new country semantics) and run it against the repinned environment. +5. Full verification: pytest, lint/format, `scripts/bundle.py check --published-spm + --include-tros --strict-tros`. +6. Commit in four steps, push, watch CI on #515, update the PR body. **Do not merge.** + +The version in `pyproject.toml` is not touched: `bump_version.py` derives 6.0.0 from the +two `.breaking.md` fragments at release time. ## Done -- Read the prior lane's PROGRESS.md and the full branch history (24 commits off `origin/main`). -- Verified the 2.0.1 prerequisite from PyPI directly. Wheel sha256 matches the brief exactly. -- Ran the repin through the tooling, hit the certification gate, and reverted to the certified - 2.0.0 tuple with a clean tree (see BLOCKER above). -- Established the repin procedure the prior lane used (repository tooling only: - `bundle.py update-packages`, `set-spm`, `certify-data`, `generate`; extras are generated - from the bundle manifest, so no pin is hand-edited). - -- **Ruling A done** (`85b1d711`). Read the authoritative source in the 2.0.1 wheel: - `spm_unit_capped_housing_subsidy` computes `assisted = housing_assistance > 0`, returns - zeros without touching the provider when none is assisted, and otherwise calls - `masked_policyengine_amount`, which evaluates only the assisted rows through - `CountyRequiringSPMProvider`. Probed all four clauses against the installed model rather - than assuming: 5/5 resource outputs compute with an empty measurement receipt when housing - assistance is zero; 5/5 raise `SPM_GEOGRAPHY_REQUIRED` when it is positive; threshold and - poverty raise either way; default outputs still require the choice. Updated the public - docstring, `docs/households.md`, and the contract test (renamed, since its old name - asserted the unconditional rule). 42/42 SPM household tests pass. -- **Ruling C done** (`456af5da`). Regenerated `us_model_surface`: - `num_variables_bucketed_100s` 57 -> 61 (raw 6157), `num_parameters_bucketed_100s` - 978 -> 1025 (raw 102525), `data_package_name` `populace-data` -> `microcosm-data`. - Counted the raw surface under both installed wheels: **identical** on 2.0.0 and 2.0.1 - (6157 / 102525 both), so this snapshot is already the 2.0.1 value. -- **Ruling D done** (`74675620`). Excluded the four contaminated fields rather than - xfailing the case, because all four are downstream of the one defect and an xfail would - have discarded the other 42 fields of coverage. Two guards make it honest: - `_check_snapshot` preserves prior values for excluded fields under - `PE_UPDATE_SNAPSHOTS=1` (verified by md5 across a refresh), and - `test_snap_annualization_defect_still_present` fails loudly on fix. Note a strict xfail - would *not* have failed loudly: the corrected annual figure is ~3,576 against a stored - 3,596.04, so the case would have gone on xfailing silently. -- Confirmed policyengine-us#9447 is OPEN and its body records the same values on 1.825.2 - and 2.0.0, independently corroborating Ruling D. - -- **Ruling E done** (`04960e64`), and **Ruling D reversed on a corrected premise** (`2019530f`). - Fanned out the country archaeology across the four drift signatures, each adversarially - verified from three lenses (arithmetic, provenance, alternative-cause): 12 verdicts, 11 - upholding, 1 refuting. Two root causes explain all three Ruling-E cases, both reproduced - from parameter values to the cent: - - `c991cd844a` (PR #9100, 1.779.1) added published BLS CPI-U actuals, moving the - 2026-01-01 index 323.364 -> 326.588; the NSLP/SBP per-meal rates uprate through it, so - the free-tier net school meal subsidy goes 1130.96 -> 1142.24. - - `df3482f4ef` (PR #9059, 1.776.2) moved `uprating: gov.states.ca.cpi` off the CA standard - deduction's file-level metadata, where `uprate_parameters` never read it, onto each - filing status. The deduction un-freezes into 2026: SINGLE 5,706 -> 5,835.31, - JOINT/HOH 11,412 -> 11,670.63. At the 6% and 9.3% marginal brackets that is -7.76 and - -24.05 exactly. -- **The SNAP finding overturned Ruling D's premise, and I verified it myself before acting.** - There is no annualization defect; the x12 is intact. The case is an ABAWD with no hours - supplied, and two country changes decide it: `82745ca239` dropped - `weekly_hours_worked_before_lsr`'s default from 40 to 0, and `74b0a75e5f` added - `waived_states.yaml`, under which California's statewide ABAWD waiver expires 2026-01-31. - One eligible month at 298.00. Measured directly: monthly snap 2026 is `[298, 0 x 11]`; - the same household with hours = 40 returns 3607.571; CA/IL/NV return 298.00 while - TX/NY/FL return 0.00. The year series I had cited as proof of a resolution defect - (584/3522/298/0 for 2024-2027) is the CA waiver schedule read line by line. So the four - fields were rebaselined under Ruling E's standard and the defect guard was replaced with - a test pinning the real mechanism. +- [x] Prerequisites verified against primary sources. +- [x] Worktree created at the pinned head. ## Next -1. Ruling E: regenerate the three drifting snapshots, justify every field against a located - country change. Root causes already isolated by measurement - the three cases reduce to - exactly two: school meal subsidy 1130.96 -> 1142.24 (+11.28, both child cases) and CA - state income tax (-7.76 at 60k single, -24.05 at 240k joint). Archaeology running. -2. Full `make test` as CI runs it; lint, mypy, changelog; push; `gh pr checks 515`; update - the PR body and mark ready for review. -3. Ruling B stays blocked - see BLOCKER above. Hand the publisher the evidence pack. +- [ ] Map the certify/generate/lock machinery from the code before running it. +- [ ] Step 1: re-certify. From 7b395e41209b4b2dc7f9884d9ca233d91b736aac Mon Sep 17 00:00:00 2001 From: Max Ghenis Date: Tue, 15 Sep 2026 03:35:29 -0400 Subject: [PATCH 36/46] Certify populace-us-2024-spm-20260915 for policyengine-us 2.2.1 MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Re-certify the US data release against the re-cut Hugging Face tag `populace-us-2024-spm-20260915`, whose release manifest records `built_with_model_package` policyengine-us 2.2.1. The compatibility basis is therefore `built_with_model_package`, not the publisher claim the release also carries. The pyproject pins are generated from the bundle manifest, so the `models`, `us` and `dev` extras move to policyengine-us==2.2.1. spm-calculator stays at 1.0.0, inside 2.2.1's declared `>=1.0.0,<=1.0.0.post1`. The certified H5 is byte-identical to the superseded 20260909 release (sha256 6496cc43…aee84); only the release tag and its evidence files differ. Drop the superseded 20260909 certification fragment: it was never released, and leaving it would announce a certification this version does not ship. Co-Authored-By: Claude Fable 5.1 --- ...-populace-us-2024-spm-20260915.changed.md} | 2 +- pyproject.toml | 6 +- src/policyengine/data/bundle/manifest.json | 78 +++++++++---------- 3 files changed, 43 insertions(+), 43 deletions(-) rename changelog.d/{certify-us-populace-us-2024-spm-20260909.changed.md => certify-us-populace-us-2024-spm-20260915.changed.md} (57%) diff --git a/changelog.d/certify-us-populace-us-2024-spm-20260909.changed.md b/changelog.d/certify-us-populace-us-2024-spm-20260915.changed.md similarity index 57% rename from changelog.d/certify-us-populace-us-2024-spm-20260909.changed.md rename to changelog.d/certify-us-populace-us-2024-spm-20260915.changed.md index 3a021af1..e29691ca 100644 --- a/changelog.d/certify-us-populace-us-2024-spm-20260909.changed.md +++ b/changelog.d/certify-us-populace-us-2024-spm-20260915.changed.md @@ -1 +1 @@ -Certify the US populace data release `populace-us-2024-spm-20260909` (populace_us_2024, policyengine-us 2.0.0) into the PolicyEngine bundle manifest. +Certify the US populace data release `populace-us-2024-spm-20260915` (populace_us_2024, policyengine-us 2.2.1) into the PolicyEngine bundle manifest. diff --git a/pyproject.toml b/pyproject.toml index 7324d429..59f34b09 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -41,7 +41,7 @@ graph = [ ] models = [ "policyengine-core==3.32.5", - "policyengine-us==2.0.0", + "policyengine-us==2.2.1", "policyengine-uk==2.90.2", "spm-calculator==1.0.0", ] @@ -51,7 +51,7 @@ uk = [ ] us = [ "policyengine-core==3.32.5", - "policyengine-us==2.0.0", + "policyengine-us==2.2.1", "spm-calculator==1.0.0", ] dev = [ @@ -69,7 +69,7 @@ dev = [ "mypy>=1.11.0", "pytest-cov>=5.0.0", "policyengine-core==3.32.5", - "policyengine-us==2.0.0", + "policyengine-us==2.2.1", "policyengine-uk==2.90.2", "spm-calculator==1.0.0", ] diff --git a/src/policyengine/data/bundle/manifest.json b/src/policyengine/data/bundle/manifest.json index 10f1d422..26468980 100644 --- a/src/policyengine/data/bundle/manifest.json +++ b/src/policyengine/data/bundle/manifest.json @@ -111,30 +111,30 @@ "version": "policyengine-uk-data-1.56.16" }, "us": { - "build_id": "populace-us-2024-spm-20260909", + "build_id": "populace-us-2024-spm-20260915", "bundle_id": "us-5.3.1", "certification": { - "built_with_model_version": "2.0.0", + "built_with_model_version": "2.2.1", "certified_by": "policyengine.py bundle certification", - "certified_for_model_version": "2.0.0", + "certified_for_model_version": "2.2.1", "compatibility_basis": "built_with_model_package", - "data_build_id": "populace-us-2024-spm-20260909" + "data_build_id": "populace-us-2024-spm-20260915" }, "certified_data_artifact": { - "build_id": "populace-us-2024-spm-20260909", + "build_id": "populace-us-2024-spm-20260915", "data_package": { "name": "microcosm-data", "version": "0.1.0" }, "dataset": "populace_us_2024", "sha256": "6496cc4393d4d3c6574f76eca231de5898c803b9067645591fd5c4d3e65aee84", - "uri": "hf://policyengine/populace-us/populace_us_2024.h5@populace-us-2024-spm-20260909" + "uri": "hf://policyengine/populace-us/populace_us_2024.h5@populace-us-2024-spm-20260915" }, "country_id": "us", "data_package": { "name": "microcosm-data", - "release_manifest_path": "releases/populace-us-2024-spm-20260909/release_manifest.json", - "release_manifest_revision": "populace-us-2024-spm-20260909", + "release_manifest_path": "releases/populace-us-2024-spm-20260915/release_manifest.json", + "release_manifest_revision": "populace-us-2024-spm-20260915", "repo_id": "policyengine/populace-us", "repo_type": "dataset", "version": "0.1.0" @@ -142,73 +142,73 @@ "data_producer": "populace", "datasets": { "build_manifest": { - "path": "releases/populace-us-2024-spm-20260909/build_manifest.json", + "path": "releases/populace-us-2024-spm-20260915/build_manifest.json", "repo_id": "policyengine/populace-us", - "revision": "populace-us-2024-spm-20260909", - "sha256": "73497b656d66eafab042f88ae60d1405aa14570984f8da51a968a676afafca3b" + "revision": "populace-us-2024-spm-20260915", + "sha256": "f3fd734867324bca7619f5f083a8115e32104bea3948d48723b345cfd39fa03b" }, "calibration_diagnostics": { - "path": "releases/populace-us-2024-spm-20260909/calibration_diagnostics.json", + "path": "releases/populace-us-2024-spm-20260915/calibration_diagnostics.json", "repo_id": "policyengine/populace-us", - "revision": "populace-us-2024-spm-20260909", + "revision": "populace-us-2024-spm-20260915", "sha256": "870449b44e86b13b25bcea1a57f0e7af37f4d4db18be815eea3acdf9fe6eb40e" }, "parent_build_manifest": { - "path": "releases/populace-us-2024-spm-20260909/parent_build_manifest.json", + "path": "releases/populace-us-2024-spm-20260915/parent_build_manifest.json", "repo_id": "policyengine/populace-us", - "revision": "populace-us-2024-spm-20260909", + "revision": "populace-us-2024-spm-20260915", "sha256": "1990e8fd37ce66f499b8e6600c5896701bb07015be132a12de64e321a2edc67e" }, "parent_release_manifest": { - "path": "releases/populace-us-2024-spm-20260909/parent_release_manifest.json", + "path": "releases/populace-us-2024-spm-20260915/parent_release_manifest.json", "repo_id": "policyengine/populace-us", - "revision": "populace-us-2024-spm-20260909", + "revision": "populace-us-2024-spm-20260915", "sha256": "dd949ba3c4c7a56aff8442c6db5a031d2c84c3da59f5d14832f468c358604506" }, "populace_us_2024": { "path": "populace_us_2024.h5", "repo_id": "policyengine/populace-us", - "revision": "populace-us-2024-spm-20260909", + "revision": "populace-us-2024-spm-20260915", "sha256": "6496cc4393d4d3c6574f76eca231de5898c803b9067645591fd5c4d3e65aee84" }, "source_enrichment": { - "path": "releases/populace-us-2024-spm-20260909/source_enrichment.json", + "path": "releases/populace-us-2024-spm-20260915/source_enrichment.json", "repo_id": "policyengine/populace-us", - "revision": "populace-us-2024-spm-20260909", - "sha256": "a6142b74d9bbdbda484f21532d699badb85ea5a572934898d55e5e134abe7af5" + "revision": "populace-us-2024-spm-20260915", + "sha256": "85329907b2b9c4299855b0970b1f24ede5819d3b16c312eb7400b4203bd71203" }, "source_enrichment_compatibility": { - "path": "releases/populace-us-2024-spm-20260909/source_enrichment_compatibility.json", + "path": "releases/populace-us-2024-spm-20260915/source_enrichment_compatibility.json", "repo_id": "policyengine/populace-us", - "revision": "populace-us-2024-spm-20260909", - "sha256": "319035464dbe14c47c7bee81494355c84d116f1606bb0f20e628a3ff2a32e574" + "revision": "populace-us-2024-spm-20260915", + "sha256": "63109acffd486453c99c476dece6b0154cc5388e5a430830a30744a2249444d1" }, "source_spm_independence_provenance": { - "path": "releases/populace-us-2024-spm-20260909/source_spm_independence_provenance.json", + "path": "releases/populace-us-2024-spm-20260915/source_spm_independence_provenance.json", "repo_id": "policyengine/populace-us", - "revision": "populace-us-2024-spm-20260909", + "revision": "populace-us-2024-spm-20260915", "sha256": "265e844178ed3856d68c1638820f3089e0e1e1363aa4677e33bebb37cbd5bff3" }, "source_spm_person_independence": { - "path": "releases/populace-us-2024-spm-20260909/source_spm_person_independence.csv", + "path": "releases/populace-us-2024-spm-20260915/source_spm_person_independence.csv", "repo_id": "policyengine/populace-us", - "revision": "populace-us-2024-spm-20260909", + "revision": "populace-us-2024-spm-20260915", "sha256": "22b5968d90fecfeef7614583e493fe10cc16bda8b5be82e6f49a5bc2102d3ce5" }, "us_source_coverage": { - "path": "releases/populace-us-2024-spm-20260909/us_source_coverage.json", + "path": "releases/populace-us-2024-spm-20260915/us_source_coverage.json", "repo_id": "policyengine/populace-us", - "revision": "populace-us-2024-spm-20260909", + "revision": "populace-us-2024-spm-20260915", "sha256": "6406c8686c292015a5bc7265a42402f89935f9395a8b63510efdd4d9562e7ff5" } }, "default_dataset": "populace_us_2024", - "default_dataset_uri": "hf://policyengine/populace-us/populace_us_2024.h5@populace-us-2024-spm-20260909", + "default_dataset_uri": "hf://policyengine/populace-us/populace_us_2024.h5@populace-us-2024-spm-20260915", "model_package": { "name": "policyengine-us", - "sha256": "4ca187509efc2f7d3ace5035c27de88c744d0f112fdc7da027372f3ae07c540a", - "version": "2.0.0", - "wheel_url": "https://files.pythonhosted.org/packages/fc/fa/b94e315f2cd67919afdc40bd136fc338ec629119b4d655bc5c03a0880874/policyengine_us-2.0.0-py3-none-any.whl" + "sha256": "0993a6c73fcdfbe171a796aca9d302741bd8e00ab83388ad81c15a08740318c6", + "version": "2.2.1", + "wheel_url": "https://files.pythonhosted.org/packages/5e/09/86ac3968d2a18e28fce3ba1186fc21728e9f299b96a806d6f204f1e0aaed/policyengine_us-2.2.1-py3-none-any.whl" }, "policyengine_version": "5.3.1", "region_datasets": { @@ -216,10 +216,10 @@ "path_template": "populace_us_2024.h5" } }, - "release_manifest_uri": "https://huggingface.co/datasets/policyengine/populace-us/resolve/populace-us-2024-spm-20260909/releases/populace-us-2024-spm-20260909/release_manifest.json", + "release_manifest_uri": "https://huggingface.co/datasets/policyengine/populace-us/resolve/populace-us-2024-spm-20260915/releases/populace-us-2024-spm-20260915/release_manifest.json", "schema_version": 1, - "source_manifest_uri": "hf://dataset/policyengine/populace-us@populace-us-2024-spm-20260909/releases/populace-us-2024-spm-20260909/release_manifest.json", - "version": "populace-us-2024-spm-20260909" + "source_manifest_uri": "hf://dataset/policyengine/populace-us@populace-us-2024-spm-20260915/releases/populace-us-2024-spm-20260915/release_manifest.json", + "version": "populace-us-2024-spm-20260915" } }, "dataset_overlays": { @@ -313,10 +313,10 @@ "policyengine-us": { "country": "us", "import_name": "policyengine_us", - "install_requirement": "policyengine-us==2.0.0", + "install_requirement": "policyengine-us==2.2.1", "name": "policyengine-us", "role": "country_model", - "version": "2.0.0" + "version": "2.2.1" }, "spm-calculator": { "country": "us", From d3a982b014296ea5c2f6356f32d8ce98ac295b16 Mon Sep 17 00:00:00 2001 From: Max Ghenis Date: Tue, 15 Sep 2026 03:35:34 -0400 Subject: [PATCH 37/46] Resolve the reviewed registry lock onto policyengine-us 2.2.1 MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Resolved with the same registry-only settings `scripts/release_lock.py` enforces (`uv lock --no-config --no-sources --default-index https://pypi.org/simple`). `--refresh` is not the tool for this: it permits only the root version to move and restores the lock on any graph change, which is what a pin move is. `python scripts/release_lock.py` then passes, with one root package at the pyproject version and policyengine-us 2.2.1 from PyPI carrying wheel sha256 0993a6c7…18c6 — the same hash the certification records. Co-Authored-By: Claude Fable 5.1 --- uv.lock | 12 ++++++------ 1 file changed, 6 insertions(+), 6 deletions(-) diff --git a/uv.lock b/uv.lock index d5655d84..279fc5e0 100644 --- a/uv.lock +++ b/uv.lock @@ -1797,9 +1797,9 @@ requires-dist = [ { name = "policyengine-uk", marker = "extra == 'dev'", specifier = "==2.90.2" }, { name = "policyengine-uk", marker = "extra == 'models'", specifier = "==2.90.2" }, { name = "policyengine-uk", marker = "extra == 'uk'", specifier = "==2.90.2" }, - { name = "policyengine-us", marker = "extra == 'dev'", specifier = "==2.0.0" }, - { name = "policyengine-us", marker = "extra == 'models'", specifier = "==2.0.0" }, - { name = "policyengine-us", marker = "extra == 'us'", specifier = "==2.0.0" }, + { name = "policyengine-us", marker = "extra == 'dev'", specifier = "==2.2.1" }, + { name = "policyengine-us", marker = "extra == 'models'", specifier = "==2.2.1" }, + { name = "policyengine-us", marker = "extra == 'us'", specifier = "==2.2.1" }, { name = "psutil", specifier = ">=5.9.0" }, { name = "pydantic", specifier = ">=2.0.0" }, { name = "pytest", marker = "extra == 'dev'" }, @@ -1860,7 +1860,7 @@ wheels = [ [[package]] name = "policyengine-us" -version = "2.0.0" +version = "2.2.1" source = { registry = "https://pypi.org/simple" } dependencies = [ { name = "microdf-python" }, @@ -1870,9 +1870,9 @@ dependencies = [ { name = "tables" }, { name = "tqdm" }, ] -sdist = { url = "https://files.pythonhosted.org/packages/bf/c0/631546d94507cf2506731607e32354d99b9b9415382720e5cb1d02997ad9/policyengine_us-2.0.0.tar.gz", hash = "sha256:bffa42c408dbd657541b14bdced99dbb1251cf0663c66b578155e7d92b4d6099", size = 11875984, upload-time = "2026-09-12T01:11:26.472Z" } +sdist = { url = "https://files.pythonhosted.org/packages/2c/09/763b21371a62635e15e62b53fb696e299f01460907c27313d40f63031eb2/policyengine_us-2.2.1.tar.gz", hash = "sha256:d5300c91f3eae2e47a61f485fdfde91a892b80e95db3e46ff2c4bbd417d76501", size = 11946510, upload-time = "2026-09-15T04:45:50.33Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/fc/fa/b94e315f2cd67919afdc40bd136fc338ec629119b4d655bc5c03a0880874/policyengine_us-2.0.0-py3-none-any.whl", hash = "sha256:4ca187509efc2f7d3ace5035c27de88c744d0f112fdc7da027372f3ae07c540a", size = 14806288, upload-time = "2026-09-12T01:11:24.028Z" }, + { url = "https://files.pythonhosted.org/packages/5e/09/86ac3968d2a18e28fce3ba1186fc21728e9f299b96a806d6f204f1e0aaed/policyengine_us-2.2.1-py3-none-any.whl", hash = "sha256:0993a6c73fcdfbe171a796aca9d302741bd8e00ab83388ad81c15a08740318c6", size = 14885537, upload-time = "2026-09-15T04:45:46.649Z" }, ] [[package]] From 207895fe864eade6f8fb94006f54649aab3ea8f6 Mon Sep 17 00:00:00 2001 From: Max Ghenis Date: Tue, 15 Sep 2026 03:35:41 -0400 Subject: [PATCH 38/46] Regenerate the bundled TRACE sidecars for the repinned bundle MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Both sidecars pin the bundle manifest bytes, so the UK sidecar moves too even though its data release is unchanged. The US sidecar now pins the 20260915 release manifest (sha256 d5c9e2a3…dc01), the 2.2.1 wheel and its URL, and records the built-with compatibility basis. Prepared with `bundle.py generate --include-tros` and verified with `bundle.py check --published-spm --include-tros --strict-tros` (exit 0). Strict generation cannot prepare a sidecar for a changed data release by design — it refuses to substitute remote bytes over a reviewed pin — so preparation stays the separate local step the module documents. Co-Authored-By: Claude Fable 5.1 --- .../data/bundle/uk.trace.tro.jsonld | 4 ++-- .../data/bundle/us.trace.tro.jsonld | 24 +++++++++---------- 2 files changed, 14 insertions(+), 14 deletions(-) diff --git a/src/policyengine/data/bundle/uk.trace.tro.jsonld b/src/policyengine/data/bundle/uk.trace.tro.jsonld index b16ebfb1..ac6a207f 100644 --- a/src/policyengine/data/bundle/uk.trace.tro.jsonld +++ b/src/policyengine/data/bundle/uk.trace.tro.jsonld @@ -75,7 +75,7 @@ "@type": "trov:ResearchArtifact", "schema:name": "policyengine.py bundle manifest for uk", "trov:mimeType": "application/json", - "trov:sha256": "9b0ec2c88e450ed865b4f71d4c0c87b2b0ab8a2176dabec51794b0405d6e0fe2" + "trov:sha256": "cb714acc65b9b131887e8d423b1a8a4dfd5f4c3833f5dd27ab992b2024166a60" }, { "@id": "composition/1/artifact/data_release_manifest", @@ -102,7 +102,7 @@ "trov:hasFingerprint": { "@id": "composition/1/fingerprint", "@type": "trov:CompositionFingerprint", - "trov:sha256": "369b91f53ff615a1c2835a3aecc1a3a1aed393a76d16a1494022651a7d2f1bd2" + "trov:sha256": "846ccd2880c885e9704ed9b8830de5d5c07ea0f7489661f35167f40e9a7f6177" } }, "trov:hasPerformance": { diff --git a/src/policyengine/data/bundle/us.trace.tro.jsonld b/src/policyengine/data/bundle/us.trace.tro.jsonld index 98b523c9..07ba8c9c 100644 --- a/src/policyengine/data/bundle/us.trace.tro.jsonld +++ b/src/policyengine/data/bundle/us.trace.tro.jsonld @@ -44,7 +44,7 @@ "trov:hasArtifact": { "@id": "composition/1/artifact/data_release_manifest" }, - "trov:hasLocation": "https://huggingface.co/datasets/policyengine/populace-us/resolve/populace-us-2024-spm-20260909/releases/populace-us-2024-spm-20260909/release_manifest.json" + "trov:hasLocation": "https://huggingface.co/datasets/policyengine/populace-us/resolve/populace-us-2024-spm-20260915/releases/populace-us-2024-spm-20260915/release_manifest.json" }, { "@id": "arrangement/1/location/dataset", @@ -52,7 +52,7 @@ "trov:hasArtifact": { "@id": "composition/1/artifact/dataset" }, - "trov:hasLocation": "https://huggingface.co/datasets/policyengine/populace-us/resolve/populace-us-2024-spm-20260909/populace_us_2024.h5" + "trov:hasLocation": "https://huggingface.co/datasets/policyengine/populace-us/resolve/populace-us-2024-spm-20260915/populace_us_2024.h5" }, { "@id": "arrangement/1/location/model_wheel", @@ -60,7 +60,7 @@ "trov:hasArtifact": { "@id": "composition/1/artifact/model_wheel" }, - "trov:hasLocation": "https://files.pythonhosted.org/packages/fc/fa/b94e315f2cd67919afdc40bd136fc338ec629119b4d655bc5c03a0880874/policyengine_us-2.0.0-py3-none-any.whl" + "trov:hasLocation": "https://files.pythonhosted.org/packages/5e/09/86ac3968d2a18e28fce3ba1186fc21728e9f299b96a806d6f204f1e0aaed/policyengine_us-2.2.1-py3-none-any.whl" } ] } @@ -74,14 +74,14 @@ "@type": "trov:ResearchArtifact", "schema:name": "policyengine.py bundle manifest for us", "trov:mimeType": "application/json", - "trov:sha256": "9b0ec2c88e450ed865b4f71d4c0c87b2b0ab8a2176dabec51794b0405d6e0fe2" + "trov:sha256": "cb714acc65b9b131887e8d423b1a8a4dfd5f4c3833f5dd27ab992b2024166a60" }, { "@id": "composition/1/artifact/data_release_manifest", "@type": "trov:ResearchArtifact", "schema:name": "microcosm-data release manifest 0.1.0", "trov:mimeType": "application/json", - "trov:sha256": "6c39eb1e61afc1629f2abe57032dced9c0750c60e42b9acad812c7b3476a2989" + "trov:sha256": "d5c9e2a33d097294af60a816fafb1bdcfbbd81cdda70ae4637daceb829dedc01" }, { "@id": "composition/1/artifact/dataset", @@ -93,27 +93,27 @@ { "@id": "composition/1/artifact/model_wheel", "@type": "trov:ResearchArtifact", - "schema:name": "policyengine-us==2.0.0 wheel", + "schema:name": "policyengine-us==2.2.1 wheel", "trov:mimeType": "application/zip", - "trov:sha256": "4ca187509efc2f7d3ace5035c27de88c744d0f112fdc7da027372f3ae07c540a" + "trov:sha256": "0993a6c73fcdfbe171a796aca9d302741bd8e00ab83388ad81c15a08740318c6" } ], "trov:hasFingerprint": { "@id": "composition/1/fingerprint", "@type": "trov:CompositionFingerprint", - "trov:sha256": "cad6fe62062bfaebf58a1de4a62b9941c2224fc2697fe27ef8c3225fa1b05bef" + "trov:sha256": "73d73bf16a7624feb60ae4fe4cffc3caf71a6896af67c8095633b3a1d0a1f153" } }, "trov:hasPerformance": { "@id": "trp/1", "@type": "trov:TransparentResearchPerformance", - "pe:builtWithModelVersion": "2.0.0", + "pe:builtWithModelVersion": "2.2.1", "pe:certifiedBy": "policyengine.py bundle certification", - "pe:certifiedForModelVersion": "2.0.0", + "pe:certifiedForModelVersion": "2.2.1", "pe:compatibilityBasis": "built_with_model_package", - "pe:dataBuildId": "populace-us-2024-spm-20260909", + "pe:dataBuildId": "populace-us-2024-spm-20260915", "pe:emittedIn": "repository-bundle", - "rdfs:comment": "Certification of build populace-us-2024-spm-20260909 for policyengine-us 2.0.0.", + "rdfs:comment": "Certification of build populace-us-2024-spm-20260915 for policyengine-us 2.2.1.", "trov:accessedArrangement": { "@id": "arrangement/1" }, From 88039b314d51d1c60bf77ef20192a11c6d34f6dd Mon Sep 17 00:00:00 2001 From: Max Ghenis Date: Tue, 15 Sep 2026 03:49:01 -0400 Subject: [PATCH 39/46] Restore the household acceptance rewrite for the 2.2.1 resource contract MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Takes the file from 818c894e (sha256 061cbc1b…12f6), not from e98d280a, whose copy is byte-identical to the version this branch already carries. policyengine-us 2.2.1 routes `household_benefits` through `housing_assistance`; 2.0.0 routed it through `spm_unit_capped_housing_subsidy` (verified in both published wheels). Household resource outputs therefore no longer reach the measurement for an assisted unit, and the fail-closed set narrows to the SPM unit's own capped subsidy, benefits and net income. 2.2.1 also adds `spm_unit_allocated_housing_subsidy` and `spm_unit_allocated_tenant_payment`, which the rewrite's positive controls assert on and which 2.0.0 does not ship. 42 passed against the repinned environment. Co-Authored-By: Claude Fable 5.1 --- tests/test_spm_household.py | 83 ++++++++++++++++++++++++++++++++----- 1 file changed, 73 insertions(+), 10 deletions(-) diff --git a/tests/test_spm_household.py b/tests/test_spm_household.py index 4c731d04..674de8a6 100644 --- a/tests/test_spm_household.py +++ b/tests/test_spm_household.py @@ -162,12 +162,10 @@ def test_state_only_graphs_require_geography_only_where_measurement_is_used( ): """Geography is demanded by exactly the results that use the measurement. - The country owns this contract and the wrapper follows it. Its housing cap - (``spm_unit_capped_housing_subsidy``) consults the canonical SPM housing - portion for units with housing assistance to cap and for no others, so an - unassisted unit's resource graph never reaches the measurement and needs no - geography; an assisted unit's does, and fails closed without one. SPM - measurement itself requires geography either way. + Ordinary benefits and income use the actual housing award independently of + SPM geography. The country's cap consults the canonical housing portion only + for units allocated assistance, so assisted SPM resources require geography. + The threshold and SPM poverty status require geography either way. """ from policyengine.tax_benefit_models.us.model import us_latest @@ -175,9 +173,16 @@ def test_state_only_graphs_require_geography_only_where_measurement_is_used( # include resources and SPM poverty by default. monkeypatch.setattr(us_latest, "entity_variables", {"tax_unit": ["income_tax"]}) inputs = household_inputs(household={"state_code": "CA"}) + # Match the country integration control: computed HUD award and contribution, + # with low enough earnings for a positive national/county SPM housing cap. assisted = household_inputs( - household={"state_code": "CA"}, - spm_unit={"spm_unit_tenure_type": "RENTER", "housing_assistance": 6_000}, + year=2024, + people=[{"age": 40, "employment_income": 24_000, "pre_subsidy_rent": 36_000}], + household={"state_code": "CA", "pha_payment_standard": 36_000}, + spm_unit={ + "spm_unit_tenure_type": "RENTER", + "receives_housing_assistance": True, + }, ) # A genuinely tax-only graph never consults the measurement. @@ -197,8 +202,25 @@ def test_state_only_graphs_require_geography_only_where_measurement_is_used( computed = pe.us.calculate_household(**inputs, extra_variables=[variable]) assert computed.to_dict()["provenance"]["spm"]["years"] == {} - # Assisted: the same graph now has a cap to apply, so it fails closed. + # Assisted ordinary resources still use the actual award, without an SPM cap. + assisted_ordinary = {} for variable in RESOURCE_VARIABLES: + computed = pe.us.calculate_household(**assisted, extra_variables=[variable]) + entity = ( + computed.person[0] + if variable == "marginal_tax_rate" + else computed.household + ) + assisted_ordinary[variable] = entity[variable] + assert math.isfinite(entity[variable]) + assert computed.to_dict()["provenance"]["spm"]["years"] == {} + + # Only the assisted SPM resource graph needs geography for its housing cap. + for variable in ( + "spm_unit_capped_housing_subsidy", + "spm_unit_benefits", + "spm_unit_net_income", + ): with pytest.raises(SPMInputError) as caught: pe.us.calculate_household(**assisted, extra_variables=[variable]) assert caught.value.code == "SPM_GEOGRAPHY_REQUIRED" @@ -210,7 +232,7 @@ def test_state_only_graphs_require_geography_only_where_measurement_is_used( pe.us.calculate_household(**situation, extra_variables=[variable]) assert caught.value.code == "SPM_GEOGRAPHY_REQUIRED" - # An explicit selection computes for both. + # Preserve the unassisted national and county controls. for settings, located_inputs in ( ({"geography_kind": "national"}, inputs), ({"geography_kind": "county"}, household_inputs()), @@ -223,6 +245,47 @@ def test_state_only_graphs_require_geography_only_where_measurement_is_used( assert math.isfinite(located.household.household_net_income) assert math.isfinite(located.person[0].marginal_tax_rate) + # Persist assisted positive controls through the same public wrapper path. + assisted_county = deepcopy(assisted) + assisted_county["household"]["county_fips"] = "06037" + actual_awards = [] + for settings, located_inputs in ( + ({"geography_kind": "national"}, assisted), + ({"geography_kind": "county"}, assisted_county), + ): + located = pe.us.calculate_household( + **located_inputs, + spm=settings, + extra_variables=[ + *RESOURCE_VARIABLES, + "housing_assistance", + "spm_unit_allocated_housing_subsidy", + "spm_unit_allocated_tenant_payment", + "spm_unit_capped_housing_subsidy", + "spm_unit_benefits", + "spm_unit_net_income", + ], + ) + subsidy = located.spm_unit.spm_unit_allocated_housing_subsidy + capped = located.spm_unit.spm_unit_capped_housing_subsidy + assert 0 < capped < subsidy + assert located.spm_unit.spm_unit_allocated_tenant_payment > 0 + assert math.isfinite(located.spm_unit.spm_unit_benefits) + assert math.isfinite(located.spm_unit.spm_unit_net_income) + actual_awards.append(located.spm_unit.housing_assistance) + for variable in RESOURCE_VARIABLES: + entity = ( + located.person[0] + if variable == "marginal_tax_rate" + else located.household + ) + assert entity[variable] == assisted_ordinary[variable] + receipt = located.to_dict()["provenance"]["spm"] + assert receipt["years"][str(assisted["year"])] + assert receipt["geography_kind"] == settings["geography_kind"] + assert receipt["geographies"] + assert actual_awards[0] == actual_awards[1] > 0 + def test_adultless_measurement_has_a_structured_composition_error(): with pytest.raises(SPMInputError) as caught: From 26a584a1a2e826016be900a246dabeb25a628f0c Mon Sep 17 00:00:00 2001 From: Max Ghenis Date: Tue, 15 Sep 2026 03:49:13 -0400 Subject: [PATCH 40/46] Repoint the identity-fact test constants at the new certified pair `US_MODEL_VERSION`, `US_BUILT_WITH_MODEL_VERSION` and `US_DATA_RELEASE_ID` in tests/test_release_manifests.py, the `us_latest` model version and default dataset URI in tests/test_models.py, the national region dataset path in tests/test_us_regions.py, and the staged-config package assertion in tests/test_spm_bundle_tooling.py all restate the pinned country and data release. The remaining `20260909` strings in the suite are invalid-`as_of` parametrisations and are unrelated. The household snapshots did not drift and were not regenerated. Co-Authored-By: Claude Fable 5.1 --- tests/test_models.py | 4 ++-- tests/test_release_manifests.py | 6 +++--- tests/test_spm_bundle_tooling.py | 2 +- tests/test_us_regions.py | 2 +- 4 files changed, 7 insertions(+), 7 deletions(-) diff --git a/tests/test_models.py b/tests/test_models.py index bbf19fd1..e9c3e5eb 100644 --- a/tests/test_models.py +++ b/tests/test_models.py @@ -135,13 +135,13 @@ def test_has_release_manifest_metadata(self): assert us_latest.release_manifest is not None assert us_latest.release_manifest.country_id == "us" assert us_latest.model_package.name == "policyengine-us" - assert us_latest.model_package.version == "2.0.0" + assert us_latest.model_package.version == "2.2.1" assert us_latest.data_package.name == "microcosm-data" assert us_latest.data_package.version == "0.1.0" assert ( us_latest.default_dataset_uri == "hf://policyengine/populace-us/populace_us_2024.h5" - "@populace-us-2024-spm-20260909" + "@populace-us-2024-spm-20260915" ) def test_has_hundreds_of_parameters(self): diff --git a/tests/test_release_manifests.py b/tests/test_release_manifests.py index 47c5403d..f1c75d17 100644 --- a/tests/test_release_manifests.py +++ b/tests/test_release_manifests.py @@ -44,10 +44,10 @@ PYPROJECT.read_text(), re.MULTILINE, ).group(1) -US_MODEL_VERSION = "2.0.0" -US_BUILT_WITH_MODEL_VERSION = "2.0.0" +US_MODEL_VERSION = "2.2.1" +US_BUILT_WITH_MODEL_VERSION = "2.2.1" US_DATA_RELEASE_VERSION = "0.1.0" -US_DATA_RELEASE_ID = "populace-us-2024-spm-20260909" +US_DATA_RELEASE_ID = "populace-us-2024-spm-20260915" US_DATA_RELEASE_REVISION = US_DATA_RELEASE_ID US_DATA_RELEASE_PATH = f"releases/{US_DATA_RELEASE_ID}/release_manifest.json" US_DATA_ARTIFACT_REVISION = US_DATA_RELEASE_ID diff --git a/tests/test_spm_bundle_tooling.py b/tests/test_spm_bundle_tooling.py index 4eb71113..ea3c0097 100644 --- a/tests/test_spm_bundle_tooling.py +++ b/tests/test_spm_bundle_tooling.py @@ -99,7 +99,7 @@ def test_staged_config_preserves_certification_and_fails_release_gate(canonical_ ) assert updated["data_releases"] == before["data_releases"] assert updated["packages"] == before["packages"] - assert updated["packages"]["policyengine-us"]["version"] == "2.0.0" + assert updated["packages"]["policyengine-us"]["version"] == "2.2.1" validate_bundle_measurements(updated) with pytest.raises(ValueError, match="promotion pending"): validate_bundle_measurements(updated, require_published_spm=True) diff --git a/tests/test_us_regions.py b/tests/test_us_regions.py index 8a839def..57414498 100644 --- a/tests/test_us_regions.py +++ b/tests/test_us_regions.py @@ -107,7 +107,7 @@ def test__given_us_registry__then_has_national_region(self): assert national.region_type == "national" assert national.dataset_path == ( "hf://policyengine/populace-us/populace_us_2024.h5" - "@populace-us-2024-spm-20260909" + "@populace-us-2024-spm-20260915" ) def test__given_us_registry__then_has_51_states(self): From f7894ebc07d2c0edd2b45dec05b4c02595bfc244 Mon Sep 17 00:00:00 2001 From: Max Ghenis Date: Tue, 15 Sep 2026 03:49:23 -0400 Subject: [PATCH 41/46] Correct the resource-geography contract in the shipped prose MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit The `calculate_household` docstring and docs/households.md both stated that resource outputs reach the measurement through the capped housing subsidy, so an assisted unit's resources fail closed without a geography. policyengine-us 2.2.1 falsifies that: its `household_benefits` parameter lists `housing_assistance`, where 2.0.0 listed `spm_unit_capped_housing_subsidy` (checked in both published wheels), so household resource outputs compute on state alone whether or not the unit is assisted. Only the SPM unit's capped subsidy, benefits and net income still require a geography for an assisted unit, which is what the restored acceptance test asserts. Two unreleased changelog fragments stated the same superseded contract and the superseded 2.0.0 pin; both are corrected, and a fragment records the repin. Nothing here would have been caught by a test — these are mechanism claims in prose that the pinned code contradicts. docs/households.md also carried the old pin and release id. Co-Authored-By: Claude Fable 5.1 --- changelog.d/canonical-tuple-pins.changed.md | 2 +- ...spm-resource-geography-contract.changed.md | 2 +- ...apper-repin-policyengine-us-221.changed.md | 1 + docs/households.md | 27 ++++++++++--------- .../tax_benefit_models/us/household.py | 15 ++++++----- 5 files changed, 27 insertions(+), 20 deletions(-) create mode 100644 changelog.d/wrapper-repin-policyengine-us-221.changed.md diff --git a/changelog.d/canonical-tuple-pins.changed.md b/changelog.d/canonical-tuple-pins.changed.md index 2702de27..02506e46 100644 --- a/changelog.d/canonical-tuple-pins.changed.md +++ b/changelog.d/canonical-tuple-pins.changed.md @@ -1 +1 @@ -Pin the canonical PolicyEngine tuple: policyengine-us 2.0.0, policyengine-core 3.32.5 and spm-calculator 1.0.0. +Pin the canonical PolicyEngine tuple: policyengine-us 2.2.1, policyengine-core 3.32.5 and spm-calculator 1.0.0. diff --git a/changelog.d/spm-resource-geography-contract.changed.md b/changelog.d/spm-resource-geography-contract.changed.md index 16ec9fc5..40b9b9e9 100644 --- a/changelog.d/spm-resource-geography-contract.changed.md +++ b/changelog.d/spm-resource-geography-contract.changed.md @@ -1 +1 @@ -Documented that SPM geography is required only by results that use the measurement: SPM thresholds and poverty always require it, while resource outputs require it only for units with housing assistance to cap. +Documented that SPM geography is required only by results that use the measurement: SPM thresholds and poverty always require it, household resource outputs never do, and the SPM unit's capped housing subsidy, benefits and net income require it for units allocated housing assistance. diff --git a/changelog.d/wrapper-repin-policyengine-us-221.changed.md b/changelog.d/wrapper-repin-policyengine-us-221.changed.md new file mode 100644 index 00000000..1c7a4f0d --- /dev/null +++ b/changelog.d/wrapper-repin-policyengine-us-221.changed.md @@ -0,0 +1 @@ +Repin the US bundle, registry lock and TRACE sidecars onto policyengine-us 2.2.1, the build-time model the certified data release records. The certified microdata is byte-identical to the superseded `populace-us-2024-spm-20260909` release. diff --git a/docs/households.md b/docs/households.md index c5b5cd24..81b8e43e 100644 --- a/docs/households.md +++ b/docs/households.md @@ -81,16 +81,19 @@ unit with no classified adult raises `SPM_COMPOSITION_REQUIRED`. These are calculator `SPMInputError` exceptions with `code` and `to_dict()` attributes. Geography is checked when an SPM-dependent formula runs, and only for the units whose result depends on the measurement. SPM measurement itself — thresholds, -the geographic factor and SPM poverty — always requires this choice. Resource -outputs (net income, benefits, MTR, income decile and equivalized net income) -reach the measurement only through the capped housing subsidy, which the country -evaluates for units with housing assistance to cap; a unit receiving no housing -assistance has a capped subsidy of zero by construction and consults no -measurement. So a resource calculation for an unassisted unit succeeds on state -alone and records an empty measurement receipt, while the same calculation for -an assisted unit raises `SPM_GEOGRAPHY_REQUIRED`. A genuinely independent -tax-only country-model calculation can use state alone; the wrapper's default -outputs include SPM poverty and therefore always require the choice. +the geographic factor and SPM poverty — always requires this choice. Household +resource outputs (net income, benefits, MTR, income decile and equivalized net +income) use the actual housing award rather than the capped SPM subsidy, so +they succeed on state alone and record an empty measurement receipt whether or +not the unit is assisted. The SPM unit's own resources are what reach the +measurement: the country's cap (`spm_unit_capped_housing_subsidy`, and through +it `spm_unit_benefits` and `spm_unit_net_income`) consults the canonical +housing portion for units allocated assistance, so an assisted unit without a +geography raises `SPM_GEOGRAPHY_REQUIRED` there; a unit allocated none has a +capped subsidy of zero by construction and consults no measurement. A +genuinely independent tax-only country-model calculation can use state alone; +the wrapper's default outputs include SPM poverty and therefore always require +the choice. Supply observed inputs such as age, tenure, county and the source-backed `is_spm_independent_minor_role`. Computed SPM thresholds, geographic factors, @@ -101,8 +104,8 @@ separate benefit inputs; they do not override SPM measurement composition. These changes require the coordinated canonical country/calculator bundle, which the packaged production manifest now pins and certifies: `policyengine-us` -2.0.0, `policyengine-core` 3.32.5, `spm-calculator` 1.0.0, and the US data -release `populace-us-2024-spm-20260909`. Local-wheel development manifest +2.2.1, `policyengine-core` 3.32.5, `spm-calculator` 1.0.0, and the US data +release `populace-us-2024-spm-20260915`. Local-wheel development manifest fixtures remain explicitly uncertified. The measurement receipt identifies a calculation; it does not certify a population dataset or establish publication readiness. diff --git a/src/policyengine/tax_benefit_models/us/household.py b/src/policyengine/tax_benefit_models/us/household.py index f6693139..20f4c777 100644 --- a/src/policyengine/tax_benefit_models/us/household.py +++ b/src/policyengine/tax_benefit_models/us/household.py @@ -193,12 +193,15 @@ def calculate_household( measurement itself — thresholds, the geographic factor and SPM poverty — always requires household county_fips or an explicit metro/national selection, and so do the default outputs, which - include SPM poverty. Resource outputs reach the measurement only - through the capped housing subsidy, which the country evaluates - for units with housing assistance to cap: a unit receiving no - housing assistance has a capped subsidy of zero by construction, - so its resources compute on state alone, while an assisted unit - without a geography raises ``SPM_GEOGRAPHY_REQUIRED``. + include SPM poverty. Household resource outputs use the actual + housing award rather than the capped SPM subsidy, so they compute + on state alone whether or not the unit is assisted. The country's + cap (``spm_unit_capped_housing_subsidy``, and through it + ``spm_unit_benefits`` and ``spm_unit_net_income``) consults the + canonical housing portion for units allocated assistance, so an + assisted unit without a geography raises + ``SPM_GEOGRAPHY_REQUIRED`` there; a unit allocated none has a + capped subsidy of zero by construction. Formula-owned SPM amounts and measurement counts cannot be supplied as inputs/axes. From 625b41324ae423d354d94b0a062ec636ea0c3a38 Mon Sep 17 00:00:00 2001 From: Max Ghenis Date: Tue, 15 Sep 2026 03:49:28 -0400 Subject: [PATCH 42/46] Record the lane state and the corrected command sequence Co-Authored-By: Claude Fable 5.1 --- PROGRESS.md | 82 ++++++++++++++++++++++++++++++++--------------------- 1 file changed, 50 insertions(+), 32 deletions(-) diff --git a/PROGRESS.md b/PROGRESS.md index f1c94744..4cb6382d 100644 --- a/PROGRESS.md +++ b/PROGRESS.md @@ -12,45 +12,63 @@ policyengine 6.0.0 on a country pin and a certified data release that agree. |---|---| | PyPI has policyengine-us 2.2.1 | Yes. Wheel sha256 `0993a6c73fcdfbe171a796aca9d302741bd8e00ab83388ad81c15a08740318c6`, uploaded 2026-09-15T04:45:46Z. | | PyPI latest is 2.3.0 (not the target) | Yes, uploaded 06:11:55Z. Deliberately **not** pinned: the certified claim is `>=2.0.0,<2.3` and 2.2.1 is the built-with version. | -| HF tag `populace-us-2024-spm-20260915` exists and is readable | Yes. Release manifest records `build.built_with_model_package` = policyengine-us **2.2.1**, `build.build_id` = `populace-us-2024-spm-20260915`, H5 sha256 `6496cc4393d4d3c6574f76eca231de5898c803b9067645591fd5c4d3e65aee84`. | -| Release carries a publisher compatibility range | Yes. `compatible_model_packages` = `policyengine-us >=2.0.0,<2.3`, basis `publisher_claim`. | +| HF tag `populace-us-2024-spm-20260915` exists and is readable | Yes. `build.built_with_model_package` = policyengine-us **2.2.1**, `build.build_id` = `populace-us-2024-spm-20260915`, H5 sha256 `6496cc4393d4d3c6574f76eca231de5898c803b9067645591fd5c4d3e65aee84`. | +| Release carries a publisher compatibility range | Yes. `policyengine-us >=2.0.0,<2.3`, basis `publisher_claim`. Not the basis used — see below. | | microcosm main contains PR #928 | Yes. Merged 2026-09-15T06:34:17Z, merge commit `743683b0e121378e93c9b426cd9c79d41d29595b`. | | Branch head unmoved | Yes, `origin/max/spm-canonical-wrapper-release-20260910` = `1b6c001c`. | -## Baseline at `1b6c001c` - -- `pyproject.toml` pins `policyengine-us==2.0.0` in the `models`, `us` and `dev` extras; - `policyengine-core==3.32.5`, `spm-calculator==1.0.0`, `policyengine-uk==2.90.2`. -- `src/policyengine/data/bundle/manifest.json` → `data_releases.us` certifies build - `populace-us-2024-spm-20260909` for model version `2.0.0`, basis - `built_with_model_package`. -- The H5 sha256 is byte-identical across the 20260909 and 20260915 releases - (`6496cc43…aee84`); the release-directory evidence files differ. - -## Plan - -1. Re-certify: `scripts/certify_data_release.py` against the 20260915 release manifest at - model version 2.2.1. -2. Regenerate bundle artifacts: `scripts/bundle.py generate --include-tros --strict-tros`. - The pyproject pins are **generated** from `manifest.json`; they are never hand-edited. - `provenance/pyproject_pins.py::update_country_pins` is not used (it misses the `models` - extra). -3. Refresh the lock: `scripts/release_lock.py --refresh`, then `--committed`. -4. Restore the `tests/test_spm_household.py` rewrite from `818c894e` (the version that - needs the new country semantics) and run it against the repinned environment. -5. Full verification: pytest, lint/format, `scripts/bundle.py check --published-spm - --include-tros --strict-tros`. -6. Commit in four steps, push, watch CI on #515, update the PR body. **Do not merge.** - -The version in `pyproject.toml` is not touched: `bump_version.py` derives 6.0.0 from the -two `.breaking.md` fragments at release time. +## Sequence actually run (corrected from the brief) + +The brief's order and two of its flags do not exist or cannot succeed. Verified +against the code, not assumed: + +1. **`--data-producer populace` is required for the US.** `certification.py:753` + defaults a missing producer to `legacy` for every country except UK, and the + legacy strategy raises unconditionally. +2. **`scripts/release_lock.py --committed` does not exist.** The script has + exactly one flag, `--refresh` (`release_lock.py:184`). +3. **`--refresh` cannot perform a repin.** It permits only the root package + version to move and restores the lock on any graph change + (`release_lock.py:169-179`). Run as instructed it failed with "Versioning + changed the reviewed dependency graph" and left `uv.lock` byte-identical. + The repin needs an ordinary registry-only `uv lock`, then plain + `release_lock.py` — which is what PR CI runs + (`pr_code_changes.yaml:120`) and what `docs/release-bundles.md:50-56` + prescribes. +4. **`generate --include-tros --strict-tros` cannot prepare a sidecar for a + changed data release.** Strict generation refuses to substitute remote bytes + over the reviewed pin in the committed TRO + (`generate_trace_tros.py:114-121`), and separately requires the lock to + already carry the certified model (`:143-164`). Preparation is the + non-strict `generate --include-tros`; `check … --strict-tros` is the gate. + +Working order: certify → `uv lock` → `release_lock.py` → `generate +--include-tros` → `check --published-spm --include-tros --strict-tros`. ## Done - [x] Prerequisites verified against primary sources. -- [x] Worktree created at the pinned head. +- [x] Re-certified: basis **`built_with_model_package`**, zero warnings, because + the release's build-time model equals the certified version. +- [x] Generated pins: `models`, `us`, `dev` extras at `policyengine-us==2.2.1`; + `spm-calculator==1.0.0` kept, inside 2.2.1's `>=1.0.0,<=1.0.0.post1`. +- [x] Lock resolved to 2.2.1 from PyPI with the certified wheel hash; + `release_lock.py` exits 0 with one root package at 5.3.1. +- [x] TRACE sidecars regenerated; `bundle.py check --published-spm + --include-tros --strict-tros` exits 0. +- [x] Restored `tests/test_spm_household.py` from `818c894e` + (sha256 `061cbc1b…`): 42 passed against the repinned environment. +- [x] Updated the identity-fact pins in four test modules. +- [x] Corrected two shipped mechanism claims that 2.2.1 falsifies (the public + `calculate_household` docstring and `docs/households.md`), plus two + unreleased changelog fragments that stated the superseded contract and pin. +- [x] Dropped the superseded `certify-us-…20260909` fragment. +- [x] Lint/format clean under the ruff CI installs (0.16.7). ## Next -- [ ] Map the certify/generate/lock machinery from the code before running it. -- [ ] Step 1: re-certify. +- [ ] Push to `origin/max/spm-canonical-wrapper-release-20260910`, watch CI on #515, + update the PR body. Do not mark ready, do not merge. + +The version in `pyproject.toml` is untouched: `bump_version.py` derives 6.0.0 +from the two `.breaking.md` fragments at release time. From 01ef3b533096d6118549f7b20649804540b864ee Mon Sep 17 00:00:00 2001 From: Max Ghenis Date: Tue, 15 Sep 2026 04:21:15 -0400 Subject: [PATCH 43/46] Name the whole fail-closed chain, and correct the calculator pin in the bundle doc Probing the pinned 2.2.1 model directly (assisted unit, state only, default outputs narrowed the way the acceptance test narrows them) shows five SPM-unit results raise `SPM_GEOGRAPHY_REQUIRED`, not three: `spm_unit_capped_housing_subsidy`, `spm_unit_benefits`, `spm_unit_net_income`, and downstream of net income `spm_unit_oecd_equiv_net_income` and `spm_unit_income_decile`. The previous wording named three and read as exhaustive. The same probe confirms every ordinary resource output, and `housing_assistance` itself, computes on state alone for that unit. The docstring also called marginal tax rate a household output; it is a Person variable. Both passages now say 'the ordinary resource outputs' and enumerate where the entity matters. docs/bundles.md still said the manifest pins `spm-calculator==0.3.1`. That was true on main and has been false on this branch since the canonical tuple moved to 1.0.0. The historical note about reproducing the published 5.3.0 package set with 0.3.1 is still accurate and is left alone. Co-Authored-By: Claude Fable 5.1 --- PROGRESS.md | 25 ++++++++++++++++++ ...spm-resource-geography-contract.changed.md | 2 +- docs/bundles.md | 8 +++--- docs/households.md | 26 ++++++++++--------- .../tax_benefit_models/us/household.py | 22 +++++++++------- 5 files changed, 57 insertions(+), 26 deletions(-) diff --git a/PROGRESS.md b/PROGRESS.md index 4cb6382d..e1f5165f 100644 --- a/PROGRESS.md +++ b/PROGRESS.md @@ -65,6 +65,31 @@ Working order: certify → `uv lock` → `release_lock.py` → `generate - [x] Dropped the superseded `certify-us-…20260909` fragment. - [x] Lint/format clean under the ruff CI installs (0.16.7). +## Review round + +An adversarial review of the pushed diff raised 29 findings across six dimensions; +28 were refuted as pre-existing, out of scope, or wrong. Two things were changed +as a result, both verified against the installed 2.2.1 model rather than taken on +the reviewer's word: + +- **The fail-closed set is five variables, not three.** Probing the pinned model + directly (assisted unit, state only, default outputs narrowed the way the + acceptance test narrows them) shows `spm_unit_capped_housing_subsidy`, + `spm_unit_benefits`, `spm_unit_net_income`, `spm_unit_oecd_equiv_net_income` + and `spm_unit_income_decile` all raise `SPM_GEOGRAPHY_REQUIRED`; the last two + are downstream of `spm_unit_net_income`. The corrected prose now names the + whole chain instead of reading as an exhaustive list of three. The same probe + confirms every ordinary resource output, and `housing_assistance` itself, + computes on state alone for the assisted unit. +- **`docs/bundles.md` still said the manifest pins `spm-calculator==0.3.1`.** + True on `main`, false on this branch since the canonical-tuple commit. Corrected + to 1.0.0; the separate historical note about reproducing the published 5.3.0 + package set with 0.3.1 is still accurate and is left alone. + +The certification dimension came back clean under independent verification: all ten +artifacts match the live release's bytes by sha256, the manifest carries no +remaining `20260909` or `2.0.0` string, and the model wheel hash matches PyPI. + ## Next - [ ] Push to `origin/max/spm-canonical-wrapper-release-20260910`, watch CI on #515, diff --git a/changelog.d/spm-resource-geography-contract.changed.md b/changelog.d/spm-resource-geography-contract.changed.md index 40b9b9e9..9e13f1f1 100644 --- a/changelog.d/spm-resource-geography-contract.changed.md +++ b/changelog.d/spm-resource-geography-contract.changed.md @@ -1 +1 @@ -Documented that SPM geography is required only by results that use the measurement: SPM thresholds and poverty always require it, household resource outputs never do, and the SPM unit's capped housing subsidy, benefits and net income require it for units allocated housing assistance. +Documented that SPM geography is required only by results that use the measurement: SPM thresholds and poverty always require it, the ordinary resource outputs never do, and the SPM unit's capped housing subsidy and everything downstream of it require it for units allocated housing assistance. diff --git a/docs/bundles.md b/docs/bundles.md index ca26ce3a..0c33ec82 100644 --- a/docs/bundles.md +++ b/docs/bundles.md @@ -84,10 +84,10 @@ Derived artifacts are: - `src/policyengine/data/bundle/{country}.trace.tro.jsonld` - GitHub release assets exported from the bundle manifest -The US model in this bundle uses the SPM calculator 0.3.1 API. The manifest pins -`spm-calculator==0.3.1` in the US package scaffold and the generated `us`, -`models`, and `dev` extras. The model formulas and certified dataset artifacts -remain unchanged. +The US model in this bundle uses the SPM calculator 1.0.0 API. The manifest pins +`spm-calculator==1.0.0` as a US runtime dependency, and the generated `us`, +`models` and `dev` extras carry that pin. The model formulas and certified +dataset artifacts remain unchanged. Previously published versions cannot acquire a new dependency constraint. To reproduce the historical 5.3.0 US package set, include the compatible calculator diff --git a/docs/households.md b/docs/households.md index 81b8e43e..bac0225e 100644 --- a/docs/households.md +++ b/docs/households.md @@ -81,19 +81,21 @@ unit with no classified adult raises `SPM_COMPOSITION_REQUIRED`. These are calculator `SPMInputError` exceptions with `code` and `to_dict()` attributes. Geography is checked when an SPM-dependent formula runs, and only for the units whose result depends on the measurement. SPM measurement itself — thresholds, -the geographic factor and SPM poverty — always requires this choice. Household -resource outputs (net income, benefits, MTR, income decile and equivalized net -income) use the actual housing award rather than the capped SPM subsidy, so +the geographic factor and SPM poverty — always requires this choice. The +ordinary resource outputs — household net income, benefits, income decile and +equivalized net income, and each person's marginal tax rate — take the +household's housing assistance amount rather than the capped SPM subsidy, so they succeed on state alone and record an empty measurement receipt whether or -not the unit is assisted. The SPM unit's own resources are what reach the -measurement: the country's cap (`spm_unit_capped_housing_subsidy`, and through -it `spm_unit_benefits` and `spm_unit_net_income`) consults the canonical -housing portion for units allocated assistance, so an assisted unit without a -geography raises `SPM_GEOGRAPHY_REQUIRED` there; a unit allocated none has a -capped subsidy of zero by construction and consults no measurement. A -genuinely independent tax-only country-model calculation can use state alone; -the wrapper's default outputs include SPM poverty and therefore always require -the choice. +not the unit is allocated assistance. The country's cap +(`spm_unit_capped_housing_subsidy`) is what consults the canonical housing +portion, and only for units allocated assistance. So for an assisted unit +without a geography the cap and everything downstream of it — +`spm_unit_benefits`, `spm_unit_net_income` and in turn +`spm_unit_oecd_equiv_net_income` and `spm_unit_income_decile` — raise +`SPM_GEOGRAPHY_REQUIRED`, while a unit allocated none has a capped subsidy of +zero by construction and consults no measurement. A genuinely independent +tax-only country-model calculation can use state alone; the wrapper's default +outputs include SPM poverty and therefore always require the choice. Supply observed inputs such as age, tenure, county and the source-backed `is_spm_independent_minor_role`. Computed SPM thresholds, geographic factors, diff --git a/src/policyengine/tax_benefit_models/us/household.py b/src/policyengine/tax_benefit_models/us/household.py index 20f4c777..9801214b 100644 --- a/src/policyengine/tax_benefit_models/us/household.py +++ b/src/policyengine/tax_benefit_models/us/household.py @@ -193,15 +193,19 @@ def calculate_household( measurement itself — thresholds, the geographic factor and SPM poverty — always requires household county_fips or an explicit metro/national selection, and so do the default outputs, which - include SPM poverty. Household resource outputs use the actual - housing award rather than the capped SPM subsidy, so they compute - on state alone whether or not the unit is assisted. The country's - cap (``spm_unit_capped_housing_subsidy``, and through it - ``spm_unit_benefits`` and ``spm_unit_net_income``) consults the - canonical housing portion for units allocated assistance, so an - assisted unit without a geography raises - ``SPM_GEOGRAPHY_REQUIRED`` there; a unit allocated none has a - capped subsidy of zero by construction. + include SPM poverty. The ordinary resource outputs take the + household's housing assistance amount rather than the capped SPM + subsidy, so they compute on state alone whether or not the unit is + allocated assistance. The country's cap + (``spm_unit_capped_housing_subsidy``) is what consults the + canonical housing portion, and only for units allocated + assistance, so for an assisted unit without a geography the cap + and everything downstream of it — ``spm_unit_benefits``, + ``spm_unit_net_income`` and in turn + ``spm_unit_oecd_equiv_net_income`` and + ``spm_unit_income_decile`` — raise ``SPM_GEOGRAPHY_REQUIRED``; + a unit allocated none has a capped subsidy of zero by + construction. Formula-owned SPM amounts and measurement counts cannot be supplied as inputs/axes. From eaa95b47a59a5a600558d294e4da7863ce5808a8 Mon Sep 17 00:00:00 2001 From: Max Ghenis Date: Tue, 15 Sep 2026 05:07:01 -0400 Subject: [PATCH 44/46] Close out the lane log Co-Authored-By: Claude Fable 5.1 --- PROGRESS.md | 25 +++++++++++++++++++++---- 1 file changed, 21 insertions(+), 4 deletions(-) diff --git a/PROGRESS.md b/PROGRESS.md index e1f5165f..7558979e 100644 --- a/PROGRESS.md +++ b/PROGRESS.md @@ -90,10 +90,27 @@ The certification dimension came back clean under independent verification: all artifacts match the live release's bytes by sha256, the manifest carries no remaining `20260909` or `2.0.0` string, and the model wheel hash matches PyPI. -## Next - -- [ ] Push to `origin/max/spm-canonical-wrapper-release-20260910`, watch CI on #515, - update the PR body. Do not mark ready, do not merge. +## Delivered + +- [x] Pushed to `origin/max/spm-canonical-wrapper-release-20260910`. +- [x] PR body carries a "Repin to 2.2.1 and populace-us-2024-spm-20260915" section, + appended to the existing body, calling out the country-package version change + as `docs/engineering/skills/repository-guidance.md` requires. +- [x] CI watched to completion on the pushed head. +- [x] Left as a draft. Not marked ready, not merged. + +## Open for the wrapper owner + +- `scripts/check_release_credentials.py` is the first release gate and has never run in + CI, because the release workflow has not run since it was added. It fails closed on + write-scope and fine-grained Hugging Face tokens; whether the repository secret + satisfies it cannot be checked from a lane. +- `make lint` and CI's Lint job disagree. The lock pins ruff 0.12.11, which still emits + `UP038` (11 pre-existing hits in files this change does not touch); CI pip-installs + ruff unpinned and gets 0.16.7, where the rule is gone. Both pass on their own terms. +- The wrapper is pinned below PyPI's latest policyengine-us on purpose. 2.3.0 ships the + same certified bytes as its own default but sits outside the release's `<2.3` claim. + If that claim is widened, revisit the pin; nothing here depends on it staying. The version in `pyproject.toml` is untouched: `bump_version.py` derives 6.0.0 from the two `.breaking.md` fragments at release time. From 4abf0e6b948dbcd7083859f185bc8087a34719b2 Mon Sep 17 00:00:00 2001 From: Max Ghenis Date: Tue, 15 Sep 2026 05:46:03 -0400 Subject: [PATCH 45/46] Drop the lane progress log before release Lane bookkeeping does not ship; the release record lives in the changelog fragments and the bundle manifest. Co-Authored-By: Claude Fable 5.1 --- PROGRESS.md | 116 ---------------------------------------------------- 1 file changed, 116 deletions(-) delete mode 100644 PROGRESS.md diff --git a/PROGRESS.md b/PROGRESS.md deleted file mode 100644 index 7558979e..00000000 --- a/PROGRESS.md +++ /dev/null @@ -1,116 +0,0 @@ -# Repin wrapper PR #515 to policyengine-us 2.2.1 and populace-us-2024-spm-20260915 - -Lane: `wrapper-repin-600`. Worktree -`/Users/maxghenis/spm-rebuild-20260908/worktrees/policyengine-wrapper-repin-600`, -branched from origin head `1b6c001c860305d529e91d6f452f3359e29439fa` (PR #515, -`max/spm-canonical-wrapper-release-20260910`). Goal: main can publish -policyengine 6.0.0 on a country pin and a certified data release that agree. - -## Prerequisites (verified this lane, primary sources, 2026-09-15) - -| Prerequisite | Result | -|---|---| -| PyPI has policyengine-us 2.2.1 | Yes. Wheel sha256 `0993a6c73fcdfbe171a796aca9d302741bd8e00ab83388ad81c15a08740318c6`, uploaded 2026-09-15T04:45:46Z. | -| PyPI latest is 2.3.0 (not the target) | Yes, uploaded 06:11:55Z. Deliberately **not** pinned: the certified claim is `>=2.0.0,<2.3` and 2.2.1 is the built-with version. | -| HF tag `populace-us-2024-spm-20260915` exists and is readable | Yes. `build.built_with_model_package` = policyengine-us **2.2.1**, `build.build_id` = `populace-us-2024-spm-20260915`, H5 sha256 `6496cc4393d4d3c6574f76eca231de5898c803b9067645591fd5c4d3e65aee84`. | -| Release carries a publisher compatibility range | Yes. `policyengine-us >=2.0.0,<2.3`, basis `publisher_claim`. Not the basis used — see below. | -| microcosm main contains PR #928 | Yes. Merged 2026-09-15T06:34:17Z, merge commit `743683b0e121378e93c9b426cd9c79d41d29595b`. | -| Branch head unmoved | Yes, `origin/max/spm-canonical-wrapper-release-20260910` = `1b6c001c`. | - -## Sequence actually run (corrected from the brief) - -The brief's order and two of its flags do not exist or cannot succeed. Verified -against the code, not assumed: - -1. **`--data-producer populace` is required for the US.** `certification.py:753` - defaults a missing producer to `legacy` for every country except UK, and the - legacy strategy raises unconditionally. -2. **`scripts/release_lock.py --committed` does not exist.** The script has - exactly one flag, `--refresh` (`release_lock.py:184`). -3. **`--refresh` cannot perform a repin.** It permits only the root package - version to move and restores the lock on any graph change - (`release_lock.py:169-179`). Run as instructed it failed with "Versioning - changed the reviewed dependency graph" and left `uv.lock` byte-identical. - The repin needs an ordinary registry-only `uv lock`, then plain - `release_lock.py` — which is what PR CI runs - (`pr_code_changes.yaml:120`) and what `docs/release-bundles.md:50-56` - prescribes. -4. **`generate --include-tros --strict-tros` cannot prepare a sidecar for a - changed data release.** Strict generation refuses to substitute remote bytes - over the reviewed pin in the committed TRO - (`generate_trace_tros.py:114-121`), and separately requires the lock to - already carry the certified model (`:143-164`). Preparation is the - non-strict `generate --include-tros`; `check … --strict-tros` is the gate. - -Working order: certify → `uv lock` → `release_lock.py` → `generate ---include-tros` → `check --published-spm --include-tros --strict-tros`. - -## Done - -- [x] Prerequisites verified against primary sources. -- [x] Re-certified: basis **`built_with_model_package`**, zero warnings, because - the release's build-time model equals the certified version. -- [x] Generated pins: `models`, `us`, `dev` extras at `policyengine-us==2.2.1`; - `spm-calculator==1.0.0` kept, inside 2.2.1's `>=1.0.0,<=1.0.0.post1`. -- [x] Lock resolved to 2.2.1 from PyPI with the certified wheel hash; - `release_lock.py` exits 0 with one root package at 5.3.1. -- [x] TRACE sidecars regenerated; `bundle.py check --published-spm - --include-tros --strict-tros` exits 0. -- [x] Restored `tests/test_spm_household.py` from `818c894e` - (sha256 `061cbc1b…`): 42 passed against the repinned environment. -- [x] Updated the identity-fact pins in four test modules. -- [x] Corrected two shipped mechanism claims that 2.2.1 falsifies (the public - `calculate_household` docstring and `docs/households.md`), plus two - unreleased changelog fragments that stated the superseded contract and pin. -- [x] Dropped the superseded `certify-us-…20260909` fragment. -- [x] Lint/format clean under the ruff CI installs (0.16.7). - -## Review round - -An adversarial review of the pushed diff raised 29 findings across six dimensions; -28 were refuted as pre-existing, out of scope, or wrong. Two things were changed -as a result, both verified against the installed 2.2.1 model rather than taken on -the reviewer's word: - -- **The fail-closed set is five variables, not three.** Probing the pinned model - directly (assisted unit, state only, default outputs narrowed the way the - acceptance test narrows them) shows `spm_unit_capped_housing_subsidy`, - `spm_unit_benefits`, `spm_unit_net_income`, `spm_unit_oecd_equiv_net_income` - and `spm_unit_income_decile` all raise `SPM_GEOGRAPHY_REQUIRED`; the last two - are downstream of `spm_unit_net_income`. The corrected prose now names the - whole chain instead of reading as an exhaustive list of three. The same probe - confirms every ordinary resource output, and `housing_assistance` itself, - computes on state alone for the assisted unit. -- **`docs/bundles.md` still said the manifest pins `spm-calculator==0.3.1`.** - True on `main`, false on this branch since the canonical-tuple commit. Corrected - to 1.0.0; the separate historical note about reproducing the published 5.3.0 - package set with 0.3.1 is still accurate and is left alone. - -The certification dimension came back clean under independent verification: all ten -artifacts match the live release's bytes by sha256, the manifest carries no -remaining `20260909` or `2.0.0` string, and the model wheel hash matches PyPI. - -## Delivered - -- [x] Pushed to `origin/max/spm-canonical-wrapper-release-20260910`. -- [x] PR body carries a "Repin to 2.2.1 and populace-us-2024-spm-20260915" section, - appended to the existing body, calling out the country-package version change - as `docs/engineering/skills/repository-guidance.md` requires. -- [x] CI watched to completion on the pushed head. -- [x] Left as a draft. Not marked ready, not merged. - -## Open for the wrapper owner - -- `scripts/check_release_credentials.py` is the first release gate and has never run in - CI, because the release workflow has not run since it was added. It fails closed on - write-scope and fine-grained Hugging Face tokens; whether the repository secret - satisfies it cannot be checked from a lane. -- `make lint` and CI's Lint job disagree. The lock pins ruff 0.12.11, which still emits - `UP038` (11 pre-existing hits in files this change does not touch); CI pip-installs - ruff unpinned and gets 0.16.7, where the rule is gone. Both pass on their own terms. -- The wrapper is pinned below PyPI's latest policyengine-us on purpose. 2.3.0 ships the - same certified bytes as its own default but sits outside the release's `<2.3` claim. - If that claim is widened, revisit the pin; nothing here depends on it staying. - -The version in `pyproject.toml` is untouched: `bump_version.py` derives 6.0.0 -from the two `.breaking.md` fragments at release time. From d9e54e4870300b77d68eb1f17249934025bb7a55 Mon Sep 17 00:00:00 2001 From: Max Ghenis Date: Tue, 15 Sep 2026 06:24:56 -0400 Subject: [PATCH 46/46] Say what this release ships, and pin all five fail-closed resource variables The README and the bundle fragment still called the coordinated bundle pending; this release carries it. Two test notes said the calculator was unpublished; it has been on PyPI since September 11. The geography test now covers the five SPM-unit variables the shipped contract names, not three. Co-Authored-By: Claude Fable 5.1 --- README.md | 2 +- changelog.d/canonical-spm-bundle.changed.md | 2 +- tests/test_spm_household.py | 9 ++++++--- tests/test_us_native_alignment.py | 4 ++-- 4 files changed, 10 insertions(+), 7 deletions(-) diff --git a/README.md b/README.md index d26eaac4..ea392936 100644 --- a/README.md +++ b/README.md @@ -37,7 +37,7 @@ print(us.tax_unit.income_tax, us.household.household_net_income) US default outputs require observed county FIPS or an explicit national/SPM-area choice. See the [SPM household contract](docs/households.md#spm-geography-and-measurement-selection) -for settings, provenance and the pending coordinated bundle release. +for settings, provenance and the coordinated bundle this release ships. ### Population analysis diff --git a/changelog.d/canonical-spm-bundle.changed.md b/changelog.d/canonical-spm-bundle.changed.md index decd5812..36bef33a 100644 --- a/changelog.d/canonical-spm-bundle.changed.md +++ b/changelog.d/canonical-spm-bundle.changed.md @@ -1 +1 @@ -Validate independently pinned SPM bundle settings, verify published calculator wheel identities, and support explicitly uncertified local-wheel development fixtures while production promotion remains pending. +Validate independently pinned SPM bundle settings, verify published calculator wheel identities, and support explicitly uncertified local-wheel development fixtures ahead of the production promotion that follows this release. diff --git a/tests/test_spm_household.py b/tests/test_spm_household.py index 674de8a6..5e63ca8e 100644 --- a/tests/test_spm_household.py +++ b/tests/test_spm_household.py @@ -11,8 +11,8 @@ import numpy as np import pytest -# The canonical calculator API is not published yet. Skip rather than weaken -# these cases: they must run against the real module, never a substitute. +# These cases must run against the real published calculator module, never a +# substitute; skip only if the pinned module is absent from the environment. pytest.importorskip("spm_calculator.errors") pytest.importorskip("spm_calculator.release") pytest.importorskip("spm_calculator.rolling_forecast") @@ -215,11 +215,14 @@ def test_state_only_graphs_require_geography_only_where_measurement_is_used( assert math.isfinite(entity[variable]) assert computed.to_dict()["provenance"]["spm"]["years"] == {} - # Only the assisted SPM resource graph needs geography for its housing cap. + # Only the assisted SPM resource graph needs geography for its housing cap: + # the five variables the shipped contract names, and nothing household-level. for variable in ( "spm_unit_capped_housing_subsidy", "spm_unit_benefits", "spm_unit_net_income", + "spm_unit_oecd_equiv_net_income", + "spm_unit_income_decile", ): with pytest.raises(SPMInputError) as caught: pe.us.calculate_household(**assisted, extra_variables=[variable]) diff --git a/tests/test_us_native_alignment.py b/tests/test_us_native_alignment.py index ba1a813b..59b39f01 100644 --- a/tests/test_us_native_alignment.py +++ b/tests/test_us_native_alignment.py @@ -7,8 +7,8 @@ import pandas as pd import pytest -# The canonical calculator API is not published yet. Skip rather than weaken -# these cases: they must run against the real module, never a substitute. +# These cases must run against the real published calculator module, never a +# substitute; skip only if the pinned module is absent from the environment. pytest.importorskip("spm_calculator.release") pytest.importorskip("spm_calculator.rolling_forecast") pytest.importorskip("spm_calculator.policyengine_adapter")