From a4c525e59b8ec7ba80cb0ef01ea9f0b8c21a6874 Mon Sep 17 00:00:00 2001 From: Anthony Volk Date: Mon, 11 May 2026 15:13:07 +0200 Subject: [PATCH 1/2] Expose policyengine bundle metadata --- README.md | 14 +++ src/policyengine_api/api/__init__.py | 2 + src/policyengine_api/api/metadata.py | 16 +++ src/policyengine_api/config/settings.py | 1 + src/policyengine_api/main.py | 2 + .../services/bundle_metadata.py | 58 ++++++++++ tests/test_bundle_metadata.py | 102 ++++++++++++++++++ 7 files changed, 195 insertions(+) create mode 100644 src/policyengine_api/api/metadata.py create mode 100644 src/policyengine_api/services/bundle_metadata.py create mode 100644 tests/test_bundle_metadata.py diff --git a/README.md b/README.md index e4531d0e..a11763ec 100644 --- a/README.md +++ b/README.md @@ -61,6 +61,20 @@ The API can run in several configurations depending on what you need: | **API + Modal deployed** | Everything, Modal functions run on Modal.com | Add `make modal-deploy` | | **Full stack** | Everything + agent endpoint | Add `ANTHROPIC_API_KEY` to `.env` | +### Bundle provenance + +The API records the installed `policyengine` bundle metadata on new simulation +records and exposes the process-level bundle at: + +```bash +curl http://localhost:8000/metadata/bundle +``` + +Set `POLICYENGINE_BUNDLE_STRICT=true` to fail startup when the installed +`policyengine` package set does not match its vendored bundle. This is intended +for reproducible deployments; local development can leave it disabled while the +bundle rollout is in progress. + To connect Modal: ```bash diff --git a/src/policyengine_api/api/__init__.py b/src/policyengine_api/api/__init__.py index 3e1db4a2..ad3fa08c 100644 --- a/src/policyengine_api/api/__init__.py +++ b/src/policyengine_api/api/__init__.py @@ -11,6 +11,7 @@ household, household_analysis, households, + metadata, outputs, parameter_values, parameters, @@ -43,6 +44,7 @@ api_router.include_router(household.router) api_router.include_router(household_analysis.router) api_router.include_router(households.router) +api_router.include_router(metadata.router) api_router.include_router(analysis.router) api_router.include_router(agent.router) api_router.include_router(user_household_associations.router) diff --git a/src/policyengine_api/api/metadata.py b/src/policyengine_api/api/metadata.py new file mode 100644 index 00000000..651bf3da --- /dev/null +++ b/src/policyengine_api/api/metadata.py @@ -0,0 +1,16 @@ +from __future__ import annotations + +from typing import Any + +from fastapi import APIRouter + +from policyengine_api.services.bundle_metadata import current_bundle_metadata + +router = APIRouter(prefix="/metadata", tags=["metadata"]) + + +@router.get("/bundle") +def get_bundle_metadata() -> dict[str, Any]: + """Return the policyengine bundle metadata used by this API process.""" + + return current_bundle_metadata() diff --git a/src/policyengine_api/config/settings.py b/src/policyengine_api/config/settings.py index a744e0e7..073a9041 100644 --- a/src/policyengine_api/config/settings.py +++ b/src/policyengine_api/config/settings.py @@ -37,6 +37,7 @@ class Settings(BaseSettings): api_version: str = _get_version() api_port: int = 8000 debug: bool = False + policyengine_bundle_strict: bool = False # Seeding limit_seed_parameters: bool = False diff --git a/src/policyengine_api/main.py b/src/policyengine_api/main.py index 2fae1dff..f03fc145 100644 --- a/src/policyengine_api/main.py +++ b/src/policyengine_api/main.py @@ -12,6 +12,7 @@ from policyengine_api.api import api_router from policyengine_api.config.settings import settings +from policyengine_api.services.bundle_metadata import current_bundle_metadata console = Console() @@ -49,6 +50,7 @@ async def lifespan(app: FastAPI): console.print("[bold green]Initializing cache...[/bold green]") FastAPICache.init(InMemoryBackend(), prefix="fastapi-cache") console.print("[bold green]Cache initialized[/bold green]") + current_bundle_metadata(strict=settings.policyengine_bundle_strict) # Warn if the agent callback HMAC secret is unset. Without a configured # secret, ``policyengine_api.security`` falls back to a per-process diff --git a/src/policyengine_api/services/bundle_metadata.py b/src/policyengine_api/services/bundle_metadata.py new file mode 100644 index 00000000..b77af1ef --- /dev/null +++ b/src/policyengine_api/services/bundle_metadata.py @@ -0,0 +1,58 @@ +from __future__ import annotations + +import functools +from typing import Any + + +class BundleMetadataUnavailable(RuntimeError): + """Raised when the installed policyengine package cannot expose a bundle.""" + + +@functools.lru_cache(maxsize=1) +def current_bundle_metadata(*, strict: bool = False) -> dict[str, Any]: + """Return bundle metadata from the installed policyengine package.""" + + try: + import policyengine.bundle as pe_bundle + except Exception as exc: # pragma: no cover - import failure shape is env-specific + if strict: + raise BundleMetadataUnavailable( + "Installed policyengine package does not expose bundle metadata." + ) from exc + return { + "available": False, + "error": "Installed policyengine package does not expose bundle metadata.", + } + + try: + if strict: + pe_bundle.require_bundle(strict=True) + manifest = pe_bundle.get_bundle_manifest() + except Exception as exc: + if strict: + raise + return { + "available": False, + "error": str(exc), + } + + return { + "available": True, + "policyengine_version": manifest["policyengine"]["version"], + "bundle_version": manifest["bundle_version"], + "bundle_digest": manifest.get("bundle_digest"), + "profiles": { + profile_name: { + "packages": profile.get("packages", []), + "countries": profile.get("countries", []), + "install_targets": profile.get("install_targets", {}), + } + for profile_name, profile in manifest.get("profiles", {}).items() + }, + "packages": manifest.get("packages", {}), + "validation_report": manifest.get("validation_report"), + } + + +def reset_bundle_metadata_cache() -> None: + current_bundle_metadata.cache_clear() diff --git a/tests/test_bundle_metadata.py b/tests/test_bundle_metadata.py new file mode 100644 index 00000000..70d271d7 --- /dev/null +++ b/tests/test_bundle_metadata.py @@ -0,0 +1,102 @@ +from __future__ import annotations + +import importlib +import sys +import types + +from policyengine_api.services import bundle_metadata + + +def test_bundle_metadata_endpoint_returns_current_bundle(client, monkeypatch): + payload = { + "available": True, + "bundle_version": "4.4.2", + "bundle_digest": "sha256:test", + "policyengine_version": "4.4.2", + "profiles": {}, + "packages": {}, + "validation_report": "validation-report.json", + } + monkeypatch.setattr( + "policyengine_api.api.metadata.current_bundle_metadata", + lambda: payload, + ) + + response = client.get("/metadata/bundle") + + assert response.status_code == 200 + assert response.json() == payload + + +def test_current_bundle_metadata_reads_policyengine_bundle(monkeypatch): + fake_bundle = types.ModuleType("policyengine.bundle") + fake_bundle.get_bundle_manifest = lambda: { + "bundle_version": "4.4.2", + "bundle_digest": "sha256:test", + "policyengine": {"version": "4.4.2"}, + "profiles": { + "us": { + "packages": ["policyengine", "policyengine-us"], + "countries": ["us"], + "install_targets": {}, + } + }, + "packages": {"policyengine": {"version": "4.4.2"}}, + "validation_report": "validation-report.json", + } + fake_bundle.require_bundle = lambda strict=True: None + fake_policyengine = types.ModuleType("policyengine") + fake_policyengine.bundle = fake_bundle + monkeypatch.setitem(sys.modules, "policyengine", fake_policyengine) + monkeypatch.setitem(sys.modules, "policyengine.bundle", fake_bundle) + bundle_metadata.reset_bundle_metadata_cache() + + metadata = bundle_metadata.current_bundle_metadata(strict=True) + + assert metadata["available"] is True + assert metadata["bundle_version"] == "4.4.2" + assert metadata["profiles"]["us"]["packages"] == [ + "policyengine", + "policyengine-us", + ] + bundle_metadata.reset_bundle_metadata_cache() + + +def test_simulation_creation_records_bundle_metadata(session, monkeypatch): + analysis = importlib.import_module("policyengine_api.api.analysis") + monkeypatch.setattr( + analysis, + "current_bundle_metadata", + lambda: {"available": True, "bundle_version": "4.4.2"}, + ) + + from policyengine_api.api.analysis import _get_or_create_simulation + from policyengine_api.models import ( + SimulationStatus, + SimulationType, + TaxBenefitModel, + TaxBenefitModelVersion, + ) + + model = TaxBenefitModel(name="policyengine-us", description="US model") + session.add(model) + session.commit() + session.refresh(model) + model_version = TaxBenefitModelVersion(model_id=model.id, version="1.0.0") + session.add(model_version) + session.commit() + session.refresh(model_version) + + simulation = _get_or_create_simulation( + simulation_type=SimulationType.HOUSEHOLD, + model_version_id=model_version.id, + policy_id=None, + dynamic_id=None, + session=session, + ) + + assert simulation.status == SimulationStatus.PENDING + assert simulation.bundle_metadata == { + "available": True, + "bundle_version": "4.4.2", + } From b1f8fa1c1ec9747a51a8608802e00d2f398d71cb Mon Sep 17 00:00:00 2001 From: Anthony Volk Date: Mon, 11 May 2026 15:13:17 +0200 Subject: [PATCH 2/2] Record bundle metadata on simulations --- ...0511_add_bundle_metadata_to_simulations.py | 35 ++++++++++++++++++ changelog.d/bundle-metadata.changed.md | 1 + pyproject.toml | 6 +-- src/policyengine_api/api/analysis.py | 2 + src/policyengine_api/api/simulations.py | 4 ++ src/policyengine_api/models/simulation.py | 4 ++ uv.lock | 37 ++++++++++--------- 7 files changed, 69 insertions(+), 20 deletions(-) create mode 100644 alembic/versions/20260511_add_bundle_metadata_to_simulations.py create mode 100644 changelog.d/bundle-metadata.changed.md diff --git a/alembic/versions/20260511_add_bundle_metadata_to_simulations.py b/alembic/versions/20260511_add_bundle_metadata_to_simulations.py new file mode 100644 index 00000000..6feb628f --- /dev/null +++ b/alembic/versions/20260511_add_bundle_metadata_to_simulations.py @@ -0,0 +1,35 @@ +"""add_bundle_metadata_to_simulations + +Revision ID: add_bundle_metadata +Revises: fb663a6e28e4 +Create Date: 2026-05-11 + +""" + +from typing import Sequence, Union + +import sqlalchemy as sa +from sqlalchemy.dialects import postgresql + +from alembic import op + +# revision identifiers, used by Alembic. +revision: str = "add_bundle_metadata" +down_revision: Union[str, Sequence[str], None] = "fb663a6e28e4" +branch_labels: Union[str, Sequence[str], None] = None +depends_on: Union[str, Sequence[str], None] = None + + +def upgrade() -> None: + op.add_column( + "simulations", + sa.Column( + "bundle_metadata", + postgresql.JSON(astext_type=sa.Text()), + nullable=True, + ), + ) + + +def downgrade() -> None: + op.drop_column("simulations", "bundle_metadata") diff --git a/changelog.d/bundle-metadata.changed.md b/changelog.d/bundle-metadata.changed.md new file mode 100644 index 00000000..ba133e97 --- /dev/null +++ b/changelog.d/bundle-metadata.changed.md @@ -0,0 +1 @@ +Expose PolicyEngine bundle metadata and record it on simulation records. diff --git a/pyproject.toml b/pyproject.toml index 074b7fca..0790cf66 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -11,9 +11,9 @@ dependencies = [ "psycopg2-binary>=2.9.10", "supabase>=2.10.0", "storage3>=0.8.1", - "policyengine>=3.2.3", - "policyengine-uk==2.75.1", - "policyengine-us==1.666.1", + "policyengine==4.4.2", + "policyengine-uk==2.88.14", + "policyengine-us==1.687.0", "pydantic>=2.9.2", "pydantic-settings>=2.6.0", "rich>=13.9.4", diff --git a/src/policyengine_api/api/analysis.py b/src/policyengine_api/api/analysis.py index 9cfd2346..f429fd20 100644 --- a/src/policyengine_api/api/analysis.py +++ b/src/policyengine_api/api/analysis.py @@ -68,6 +68,7 @@ resolve_shared_runtime_model_version_from_db, ) from policyengine_api.security import require_api_key +from policyengine_api.services.bundle_metadata import current_bundle_metadata from policyengine_api.services.database import get_session from policyengine_api.services.model_resolver import ( resolve_country_from_simulation, @@ -322,6 +323,7 @@ def _get_or_create_simulation( filter_strategy=filter_strategy, region_id=region_id, year=year, + bundle_metadata=current_bundle_metadata(), ) from sqlalchemy.exc import IntegrityError diff --git a/src/policyengine_api/api/simulations.py b/src/policyengine_api/api/simulations.py index ba0196d4..ebe591b3 100644 --- a/src/policyengine_api/api/simulations.py +++ b/src/policyengine_api/api/simulations.py @@ -69,6 +69,7 @@ class HouseholdSimulationResponse(BaseModel): household_id: UUID | None = None policy_id: UUID | None = None household_result: dict[str, Any] | None = None + bundle_metadata: dict[str, Any] | None = None error_message: str | None = None @@ -117,6 +118,7 @@ class EconomySimulationResponse(BaseModel): filter_field: str | None = None filter_value: str | None = None region: RegionInfo | None = None + bundle_metadata: dict[str, Any] | None = None error_message: str | None = None @@ -196,6 +198,7 @@ def _build_household_response(simulation: Simulation) -> HouseholdSimulationResp household_id=simulation.household_id, policy_id=simulation.policy_id, household_result=simulation.household_result, + bundle_metadata=simulation.bundle_metadata, error_message=simulation.error_message, ) @@ -224,6 +227,7 @@ def _build_economy_response( filter_field=simulation.filter_field, filter_value=simulation.filter_value, region=region_info, + bundle_metadata=simulation.bundle_metadata, error_message=simulation.error_message, ) diff --git a/src/policyengine_api/models/simulation.py b/src/policyengine_api/models/simulation.py index f95100cb..9418c077 100644 --- a/src/policyengine_api/models/simulation.py +++ b/src/policyengine_api/models/simulation.py @@ -81,6 +81,9 @@ class Simulation(SimulationBase, table=True): household_result: dict[str, Any] | None = Field( default=None, sa_column=Column(JSON) ) + bundle_metadata: dict[str, Any] | None = Field( + default=None, sa_column=Column(JSON) + ) # Relationships dataset: "Dataset" = Relationship( @@ -153,3 +156,4 @@ class SimulationRead(SimulationBase): started_at: datetime | None completed_at: datetime | None household_result: dict[str, Any] | None = None + bundle_metadata: dict[str, Any] | None = None diff --git a/uv.lock b/uv.lock index 9edb60c8..1aabdb17 100644 --- a/uv.lock +++ b/uv.lock @@ -758,6 +758,7 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/49/e8/58c7f85958bda41dafea50497cbd59738c5c43dbbea5ee83d651234398f4/greenlet-3.2.4-cp313-cp313-macosx_11_0_universal2.whl", hash = "sha256:1a921e542453fe531144e91e1feedf12e07351b1cf6c9e8a3325ea600a715a31", size = 272814, upload-time = "2025-08-07T13:15:50.011Z" }, { url = "https://files.pythonhosted.org/packages/62/dd/b9f59862e9e257a16e4e610480cfffd29e3fae018a68c2332090b53aac3d/greenlet-3.2.4-cp313-cp313-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:cd3c8e693bff0fff6ba55f140bf390fa92c994083f838fece0f63be121334945", size = 641073, upload-time = "2025-08-07T13:42:57.23Z" }, { url = "https://files.pythonhosted.org/packages/f7/0b/bc13f787394920b23073ca3b6c4a7a21396301ed75a655bcb47196b50e6e/greenlet-3.2.4-cp313-cp313-manylinux2014_ppc64le.manylinux_2_17_ppc64le.whl", hash = "sha256:710638eb93b1fa52823aa91bf75326f9ecdfd5e0466f00789246a5280f4ba0fc", size = 655191, upload-time = "2025-08-07T13:45:29.752Z" }, + { url = "https://files.pythonhosted.org/packages/f2/d6/6adde57d1345a8d0f14d31e4ab9c23cfe8e2cd39c3baf7674b4b0338d266/greenlet-3.2.4-cp313-cp313-manylinux2014_s390x.manylinux_2_17_s390x.whl", hash = "sha256:c5111ccdc9c88f423426df3fd1811bfc40ed66264d35aa373420a34377efc98a", size = 649516, upload-time = "2025-08-07T13:53:16.314Z" }, { url = "https://files.pythonhosted.org/packages/7f/3b/3a3328a788d4a473889a2d403199932be55b1b0060f4ddd96ee7cdfcad10/greenlet-3.2.4-cp313-cp313-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:d76383238584e9711e20ebe14db6c88ddcedc1829a9ad31a584389463b5aa504", size = 652169, upload-time = "2025-08-07T13:18:32.861Z" }, { url = "https://files.pythonhosted.org/packages/ee/43/3cecdc0349359e1a527cbf2e3e28e5f8f06d3343aaf82ca13437a9aa290f/greenlet-3.2.4-cp313-cp313-manylinux_2_24_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:23768528f2911bcd7e475210822ffb5254ed10d71f4028387e5a99b4c6699671", size = 610497, upload-time = "2025-08-07T13:18:31.636Z" }, { url = "https://files.pythonhosted.org/packages/b8/19/06b6cf5d604e2c382a6f31cafafd6f33d5dea706f4db7bdab184bad2b21d/greenlet-3.2.4-cp313-cp313-musllinux_1_1_aarch64.whl", hash = "sha256:00fadb3fedccc447f517ee0d3fd8fe49eae949e1cd0f6a611818f4f6fb7dc83b", size = 1121662, upload-time = "2025-08-07T13:42:41.117Z" }, @@ -768,6 +769,7 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/22/5c/85273fd7cc388285632b0498dbbab97596e04b154933dfe0f3e68156c68c/greenlet-3.2.4-cp314-cp314-macosx_11_0_universal2.whl", hash = "sha256:49a30d5fda2507ae77be16479bdb62a660fa51b1eb4928b524975b3bde77b3c0", size = 273586, upload-time = "2025-08-07T13:16:08.004Z" }, { url = "https://files.pythonhosted.org/packages/d1/75/10aeeaa3da9332c2e761e4c50d4c3556c21113ee3f0afa2cf5769946f7a3/greenlet-3.2.4-cp314-cp314-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:299fd615cd8fc86267b47597123e3f43ad79c9d8a22bebdce535e53550763e2f", size = 686346, upload-time = "2025-08-07T13:42:59.944Z" }, { url = "https://files.pythonhosted.org/packages/c0/aa/687d6b12ffb505a4447567d1f3abea23bd20e73a5bed63871178e0831b7a/greenlet-3.2.4-cp314-cp314-manylinux2014_ppc64le.manylinux_2_17_ppc64le.whl", hash = "sha256:c17b6b34111ea72fc5a4e4beec9711d2226285f0386ea83477cbb97c30a3f3a5", size = 699218, upload-time = "2025-08-07T13:45:30.969Z" }, + { url = "https://files.pythonhosted.org/packages/dc/8b/29aae55436521f1d6f8ff4e12fb676f3400de7fcf27fccd1d4d17fd8fecd/greenlet-3.2.4-cp314-cp314-manylinux2014_s390x.manylinux_2_17_s390x.whl", hash = "sha256:b4a1870c51720687af7fa3e7cda6d08d801dae660f75a76f3845b642b4da6ee1", size = 694659, upload-time = "2025-08-07T13:53:17.759Z" }, { url = "https://files.pythonhosted.org/packages/92/2e/ea25914b1ebfde93b6fc4ff46d6864564fba59024e928bdc7de475affc25/greenlet-3.2.4-cp314-cp314-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:061dc4cf2c34852b052a8620d40f36324554bc192be474b9e9770e8c042fd735", size = 695355, upload-time = "2025-08-07T13:18:34.517Z" }, { url = "https://files.pythonhosted.org/packages/72/60/fc56c62046ec17f6b0d3060564562c64c862948c9d4bc8aa807cf5bd74f4/greenlet-3.2.4-cp314-cp314-manylinux_2_24_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:44358b9bf66c8576a9f57a590d5f5d6e72fa4228b763d0e43fee6d3b06d3a337", size = 657512, upload-time = "2025-08-07T13:18:33.969Z" }, { url = "https://files.pythonhosted.org/packages/23/6e/74407aed965a4ab6ddd93a7ded3180b730d281c77b765788419484cdfeef/greenlet-3.2.4-cp314-cp314-musllinux_1_2_aarch64.whl", hash = "sha256:2917bdf657f5859fbf3386b12d68ede4cf1f04c90c3a6bc1f013dd68a22e2269", size = 1612508, upload-time = "2025-11-04T12:42:23.427Z" }, @@ -1900,24 +1902,25 @@ wheels = [ [[package]] name = "policyengine" -version = "3.2.3" +version = "4.4.2" source = { registry = "https://pypi.org/simple" } dependencies = [ + { name = "jsonschema" }, { name = "microdf-python" }, + { name = "packaging" }, { name = "pandas" }, - { name = "plotly" }, { name = "psutil" }, { name = "pydantic" }, { name = "requests" }, ] -sdist = { url = "https://files.pythonhosted.org/packages/f6/73/0617495f360e3ae7e2e146f4bc19434aaa72a469a34043cc8b4ef1066a5e/policyengine-3.2.3.tar.gz", hash = "sha256:cdbc0b1eb60726b2c8ba58838f7950bf0a2ddb5aad960f099e8f70e5ad819d39", size = 243693, upload-time = "2026-03-17T22:02:05.64Z" } +sdist = { url = "https://files.pythonhosted.org/packages/32/a5/4556d22ac1c42afc7bc2e7bab43fe98c4e7ad4f58eefdbd91409f8c03152/policyengine-4.4.2.tar.gz", hash = "sha256:8f3273f1908352b24b0958527899d2444ade515b7e5cf94c1fb7b798604099c2", size = 519148, upload-time = "2026-05-09T10:18:49.929Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/95/66/5616d6df1c88500dd21b9f4f2593bf3e37a74b1b62d06d7056093ccf9768/policyengine-3.2.3-py3-none-any.whl", hash = "sha256:2fdf9f65513f9c8c3235f315617b4cbc6cd7dc2169bd3102e133b83a8133e790", size = 105831, upload-time = "2026-03-17T22:02:04.499Z" }, + { url = "https://files.pythonhosted.org/packages/44/c6/75c1516c087c64419def4edd4841dd0d4748c9e304423b49ae7c61dcfb61/policyengine-4.4.2-py3-none-any.whl", hash = "sha256:403303224e94665521f7d648a54918df8f28c56b97a83d29799d66cfd92bb7a2", size = 159125, upload-time = "2026-05-09T10:18:48.493Z" }, ] [[package]] name = "policyengine-api-v2" -version = "0.6.4" +version = "0.6.6" source = { editable = "." } dependencies = [ { name = "alembic" }, @@ -1970,9 +1973,9 @@ requires-dist = [ { name = "httpx", marker = "extra == 'dev'", specifier = ">=0.27.2" }, { name = "logfire", extras = ["fastapi", "httpx", "sqlalchemy"], specifier = ">=0.60.0" }, { name = "modal", specifier = ">=0.68.0" }, - { name = "policyengine", specifier = ">=3.2.3" }, - { name = "policyengine-uk", specifier = "==2.75.1" }, - { name = "policyengine-us", specifier = "==1.666.1" }, + { name = "policyengine", specifier = "==4.4.2" }, + { name = "policyengine-uk", specifier = "==2.88.14" }, + { name = "policyengine-us", specifier = "==1.687.0" }, { name = "psycopg2-binary", specifier = ">=2.9.10" }, { name = "pydantic", specifier = ">=2.9.2" }, { name = "pydantic-settings", specifier = ">=2.6.0" }, @@ -1995,7 +1998,7 @@ dev = [{ name = "pytest-asyncio", specifier = ">=1.3.0" }] [[package]] name = "policyengine-core" -version = "3.25.2" +version = "3.26.1" source = { registry = "https://pypi.org/simple" } dependencies = [ { name = "dpath" }, @@ -2015,14 +2018,14 @@ dependencies = [ { name = "standard-imghdr" }, { name = "wheel" }, ] -sdist = { url = "https://files.pythonhosted.org/packages/d1/ed/117c487e09bc2d5dae1d39baef2d317158433f66004f38f19f6ed82110eb/policyengine_core-3.25.2.tar.gz", hash = "sha256:de80b64b969e3c6b5a3046e29e5b9f2ce56c82f874064f65556fa60c8c423f17", size = 466431, upload-time = "2026-04-21T17:37:40.654Z" } +sdist = { url = "https://files.pythonhosted.org/packages/e7/56/666e1e708cbd61078989edc943d5389d45123beb7124a5e3180171656ff6/policyengine_core-3.26.1.tar.gz", hash = "sha256:dc4e3007bcd137cbe608042d067cedb889a9b8671db3d08c8e237f1ac3e324b4", size = 472159, upload-time = "2026-05-07T23:46:43.228Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/a2/22/b2297927a2091a22267c112e8d5d5d2b374d0f1ce67f257816fee1388170/policyengine_core-3.25.2-py3-none-any.whl", hash = "sha256:c884961937940e16730fb473d486728ed8c66250dc65df15257ad611e6655b09", size = 231186, upload-time = "2026-04-21T17:37:39.148Z" }, + { url = "https://files.pythonhosted.org/packages/15/2f/9be635fe4dfb2fe65d200c33695f5a96ef98a2921f06ff6d465384b0e551/policyengine_core-3.26.1-py3-none-any.whl", hash = "sha256:185374b3c1fe13dc951637c49a9853211ca61a8a9971eb9cc4c4b07b1477240a", size = 232190, upload-time = "2026-05-07T23:46:41.797Z" }, ] [[package]] name = "policyengine-uk" -version = "2.75.1" +version = "2.88.14" source = { registry = "https://pypi.org/simple" } dependencies = [ { name = "microdf-python" }, @@ -2030,14 +2033,14 @@ dependencies = [ { name = "pydantic" }, { name = "tables" }, ] -sdist = { url = "https://files.pythonhosted.org/packages/15/02/56d6a79e120c882c8aa292fd82f3d90225d938630eb9427dff40bcc416b1/policyengine_uk-2.75.1.tar.gz", hash = "sha256:34478b267bf5c3110a9202535b757fb9c53d3813a86546b31e7b7c33cf9283c9", size = 1102676, upload-time = "2026-03-10T10:54:32.338Z" } +sdist = { url = "https://files.pythonhosted.org/packages/8c/2b/ab82aa30c5d27176fd9d449ff5ed9708d0080b00912f7dc2efa0af0fd87e/policyengine_uk-2.88.14.tar.gz", hash = "sha256:21a4387ae52dcb5430b6d790edcc321816ed47147a3a0e21ffd482a36834d352", size = 1185947, upload-time = "2026-05-09T04:19:25.284Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/52/28/cb9eff0ad163563a5dcb9667addb3e4a7929696a45b34b90d46a41f0d622/policyengine_uk-2.75.1-py3-none-any.whl", hash = "sha256:77d2552be3e2df1bbcb95a463a99aee22f9c4f8976c58a1d3cde2831c4454d3a", size = 1697838, upload-time = "2026-03-10T10:54:30.57Z" }, + { url = "https://files.pythonhosted.org/packages/a3/fc/276fb639a46bda35523329d8968bcc4089fde9e97fab82722c0ec853c6cc/policyengine_uk-2.88.14-py3-none-any.whl", hash = "sha256:ed10005ba7d0c973c0966ebbf7672853fb3caaa0456b8bf485fb13f8c323d975", size = 1913671, upload-time = "2026-05-09T04:19:23.364Z" }, ] [[package]] name = "policyengine-us" -version = "1.666.1" +version = "1.687.0" source = { registry = "https://pypi.org/simple" } dependencies = [ { name = "microdf-python" }, @@ -2047,9 +2050,9 @@ dependencies = [ { name = "tables" }, { name = "tqdm" }, ] -sdist = { url = "https://files.pythonhosted.org/packages/7c/ed/779ae0f296a5652b1a27c0ff9a210d0907cb28887b693fe390e4816bcadb/policyengine_us-1.666.1.tar.gz", hash = "sha256:796fa45584a7e5795595fc31688a2df9841c35b0811dfe83dfa6df250140a2f4", size = 9309724, upload-time = "2026-04-23T22:36:52.07Z" } +sdist = { url = "https://files.pythonhosted.org/packages/26/49/d34803002b058e7ad91861975d26aa7d2bac3b4988756290e1588e00a776/policyengine_us-1.687.0.tar.gz", hash = "sha256:eb73607fcc54d72429af3830456f83b9ec9dd84161b6cfa86402637dfeb19fa7", size = 9464817, upload-time = "2026-05-05T17:14:34.513Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/e6/c1/aa62214930a4ce1c0bc0906f10b19dc5d67128cfedc1b96dbd4330c113b3/policyengine_us-1.666.1-py3-none-any.whl", hash = "sha256:33ef9c7f0c72bd4d17bca922d4ab4f65b42edc4ab41c950137938880ed6de13b", size = 9589718, upload-time = "2026-04-23T22:36:48.87Z" }, + { url = "https://files.pythonhosted.org/packages/c3/36/5633f5a3996c915494154ec3852011b1a239ea06d9f08cb6287ab709618c/policyengine_us-1.687.0-py3-none-any.whl", hash = "sha256:cac7da3aa9ba4bf57009eee75d798217bbef7e1c5ca17646d472fad715ab634f", size = 9954513, upload-time = "2026-05-05T17:14:30.958Z" }, ] [[package]]