diff --git a/changelog.d/uk-bundle-2-88-40.changed.md b/changelog.d/uk-bundle-2-88-40.changed.md new file mode 100644 index 00000000..05f6f298 --- /dev/null +++ b/changelog.d/uk-bundle-2-88-40.changed.md @@ -0,0 +1 @@ +- Refresh the bundled UK release manifest to policyengine-uk 2.88.40 while preserving the policyengine-uk-data 1.55.10 dataset certification. diff --git a/docs/engineering/skills/release-bundles.md b/docs/engineering/skills/release-bundles.md index de68c808..3e426053 100644 --- a/docs/engineering/skills/release-bundles.md +++ b/docs/engineering/skills/release-bundles.md @@ -70,7 +70,33 @@ Unexpected files are a reason to stop and inspect the diff. ## Testing -For release-bundle script or manifest changes, run: +For every real bundle refresh, first run the targeted bundle validator for each +updated country. Pass the intended versions and certification basis explicitly +so the check proves the refresh landed on the expected model/data pair: + +```bash +POLICYENGINE_SKIP_COUNTRY_IMPORTS=1 uv run --extra dev python \ + scripts/validate_release_bundle.py \ + --country uk \ + --expected-model-version 2.89.0 \ + --expected-data-version 1.55.10 \ + --expected-built-with-model-version 2.88.20 \ + --expected-compatibility-basis legacy_compatible_model_package +``` + +This validator must check the bundled manifest, the `uk_latest` / `us_latest` +release-bundle metadata exposed by the country wrapper, and the bundled TRACE +TRO sidecar. Do not substitute the broad pytest modules for this check; those +modules can spend a long time in country-package import/collection and are a +lower-signal first pass for ordinary bundle refreshes. + +If the validator reports that the TRO is missing `data_release_manifest` or has +`pe:dataReleaseManifestStatus = unavailable`, stop and fix data-release-manifest +fetching/authentication before proceeding. Current US and UK bundles are +release-manifest-backed, so a limited TRO is a regression unless the user has +explicitly asked for an older/no-manifest data artifact. + +For release-bundle script or manifest logic changes, also run: ```bash POLICYENGINE_SKIP_COUNTRY_IMPORTS=1 uv run pytest --noconftest \ diff --git a/pyproject.toml b/pyproject.toml index 6eaca6be..0e650326 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -45,7 +45,7 @@ graph = [ ] uk = [ "policyengine_core>=3.26.1", - "policyengine-uk==2.88.20", + "policyengine-uk==2.88.40", ] us = [ "policyengine_core==3.26.1", @@ -63,7 +63,7 @@ dev = [ "pytest-asyncio>=0.26.0", "ruff>=0.9.0", "policyengine_core==3.26.1", - "policyengine-uk==2.88.20", + "policyengine-uk==2.88.40", "policyengine-us==1.715.2", "towncrier>=24.8.0", "mypy>=1.11.0", diff --git a/scripts/validate_release_bundle.py b/scripts/validate_release_bundle.py new file mode 100644 index 00000000..225354c2 --- /dev/null +++ b/scripts/validate_release_bundle.py @@ -0,0 +1,467 @@ +"""Validate a bundled country release manifest after a bundle refresh. + +This is intentionally narrower than the full pytest modules. It checks the +user-facing bundle metadata, the country model wrapper's exposed bundle, and the +TRACE TRO sidecar without running representative-data calculations. +""" + +from __future__ import annotations + +import argparse +import hashlib +import json +import os +import sys +from importlib import import_module +from pathlib import Path +from typing import Any, Optional + +os.environ.setdefault("POLICYENGINE_SKIP_COUNTRY_IMPORTS", "1") + +from jsonschema import Draft202012Validator + +from policyengine.provenance.manifest import ( + CountryReleaseManifest, + get_release_manifest, +) +from policyengine.provenance.trace import canonical_json_bytes + +COUNTRIES = ("us", "uk") +EXPECTED_TRO_ARTIFACTS = { + "bundle_manifest", + "data_release_manifest", + "dataset", + "model_wheel", +} +VALID_COMPATIBILITY_BASES = { + "exact_build_model_version", + "matching_data_build_fingerprint", + "legacy_compatible_model_package", +} +ROOT = Path(__file__).resolve().parents[1] +MANIFEST_DIR = ROOT / "src" / "policyengine" / "data" / "release_manifests" +TRACE_SCHEMA_PATH = ( + ROOT / "src" / "policyengine" / "data" / "schemas" / "trace_tro.schema.json" +) + + +def _sha256(value: Optional[str]) -> bool: + if value is None or len(value) != 64: + return False + try: + int(value, 16) + except ValueError: + return False + return True + + +def _hf_revision(uri: str) -> Optional[str]: + if not uri.startswith("hf://") or "@" not in uri: + return None + return uri.rsplit("@", 1)[1] or None + + +def _short_artifact_id(artifact_id: str) -> str: + return artifact_id.rsplit("/", 1)[-1] + + +def _artifact_by_id(tro: dict[str, Any]) -> dict[str, dict[str, Any]]: + artifacts = tro["@graph"][0]["trov:hasComposition"]["trov:hasArtifact"] + return {_short_artifact_id(artifact["@id"]): artifact for artifact in artifacts} + + +def _location_by_id(tro: dict[str, Any]) -> dict[str, dict[str, Any]]: + locations = tro["@graph"][0]["trov:hasArrangement"][0]["trov:hasArtifactLocation"] + return {_short_artifact_id(location["@id"]): location for location in locations} + + +def _performance(tro: dict[str, Any]) -> dict[str, Any]: + return tro["@graph"][0]["trov:hasPerformance"] + + +class BundleValidator: + def __init__(self) -> None: + self.failures: list[str] = [] + + def check(self, condition: bool, message: str) -> None: + if not condition: + self.failures.append(message) + + def check_equal(self, label: str, actual: Any, expected: Any) -> None: + self.check( + actual == expected, f"{label}: expected {expected!r}, got {actual!r}" + ) + + def validate_manifest( + self, + manifest: CountryReleaseManifest, + *, + expected_model_version: Optional[str], + expected_data_version: Optional[str], + expected_built_with_model_version: Optional[str], + expected_compatibility_basis: Optional[str], + ) -> None: + country = manifest.country_id + model_name = f"policyengine-{country}" + data_name = f"policyengine-{country}-data" + artifact = manifest.certified_data_artifact + certification = manifest.certification + + self.check(manifest.bundle_id is not None, "bundle_id is missing") + if manifest.bundle_id is not None: + self.check_equal( + "bundle_id country prefix", + manifest.bundle_id.split("-", 1)[0], + country, + ) + self.check_equal("model package name", manifest.model_package.name, model_name) + self.check_equal("data package name", manifest.data_package.name, data_name) + self.check( + _sha256(manifest.model_package.sha256), + "model wheel sha256 missing or invalid", + ) + self.check( + bool(manifest.model_package.wheel_url), + "model wheel URL missing from manifest", + ) + self.check( + bool(manifest.data_package.release_manifest_revision), + "data release manifest revision must be pinned to an immutable revision", + ) + + if expected_model_version is not None: + self.check_equal( + "model package version", + manifest.model_package.version, + expected_model_version, + ) + if expected_data_version is not None: + self.check_equal( + "data package version", + manifest.data_package.version, + expected_data_version, + ) + + self.check(artifact is not None, "certified_data_artifact is missing") + self.check(certification is not None, "certification is missing") + if artifact is None or certification is None: + return + + if artifact.data_package is not None: + self.check_equal( + "certified artifact data package name", + artifact.data_package.name, + manifest.data_package.name, + ) + self.check_equal( + "certified artifact data package version", + artifact.data_package.version, + manifest.data_package.version, + ) + + self.check_equal( + "default dataset URI", + manifest.default_dataset_uri, + artifact.uri, + ) + self.check( + _sha256(artifact.sha256), "certified dataset sha256 missing or invalid" + ) + self.check_equal( + "certification data build id", + certification.data_build_id, + artifact.build_id, + ) + self.check_equal( + "certified model version", + certification.certified_for_model_version, + manifest.model_package.version, + ) + self.check( + certification.compatibility_basis in VALID_COMPATIBILITY_BASES, + f"unexpected compatibility basis {certification.compatibility_basis!r}", + ) + self.check( + certification.built_with_model_version is not None, + "built_with_model_version is missing", + ) + self.check( + certification.data_build_fingerprint is not None, + "data_build_fingerprint is missing", + ) + if certification.compatibility_basis == "exact_build_model_version": + self.check_equal( + "exact-build certified model version", + certification.certified_for_model_version, + certification.built_with_model_version, + ) + if ( + certification.built_with_model_version != manifest.model_package.version + and certification.compatibility_basis == "exact_build_model_version" + ): + self.failures.append( + "compatibility_basis cannot be exact_build_model_version when the " + "data was built with a different model version" + ) + if expected_built_with_model_version is not None: + self.check_equal( + "built-with model version", + certification.built_with_model_version, + expected_built_with_model_version, + ) + if expected_compatibility_basis is not None: + self.check_equal( + "compatibility basis", + certification.compatibility_basis, + expected_compatibility_basis, + ) + + revision = _hf_revision(artifact.uri) + self.check( + revision is not None, + "certified dataset URI must be an hf:// URI with @revision", + ) + if manifest.data_package.release_manifest_revision is not None: + self.check_equal( + "certified dataset revision", + revision, + manifest.data_package.release_manifest_revision, + ) + + dataset_ref = manifest.datasets.get(manifest.default_dataset) + self.check(dataset_ref is not None, "default dataset is missing from datasets") + if dataset_ref is not None: + self.check_equal( + "default dataset sha256", + dataset_ref.sha256, + artifact.sha256, + ) + + def validate_model_wrapper( + self, country: str, manifest: CountryReleaseManifest + ) -> None: + module = import_module(f"policyengine.tax_benefit_models.{country}") + model_version = getattr(module, f"{country}_latest") + certification = manifest.certification + artifact = manifest.certified_data_artifact + + self.check_equal( + "wrapper model version", + model_version.model_package.version, + manifest.model_package.version, + ) + self.check_equal( + "wrapper data version", + model_version.data_package.version, + manifest.data_package.version, + ) + self.check_equal( + "wrapper default dataset URI", + model_version.default_dataset_uri, + manifest.default_dataset_uri, + ) + self.check_equal( + "wrapper release manifest country", + model_version.release_manifest.country_id, + country, + ) + + bundle = model_version.release_bundle + self.check_equal( + "release bundle model version", + bundle["model_version"], + manifest.model_package.version, + ) + self.check_equal( + "release bundle data version", + bundle["data_version"], + manifest.data_package.version, + ) + if certification is not None: + self.check_equal( + "release bundle compatibility basis", + bundle["compatibility_basis"], + certification.compatibility_basis, + ) + self.check_equal( + "release bundle data build model version", + bundle["data_build_model_version"], + certification.built_with_model_version, + ) + self.check_equal( + "release bundle data build fingerprint", + bundle["data_build_fingerprint"], + certification.data_build_fingerprint, + ) + if artifact is not None: + self.check_equal( + "release bundle certified data build id", + bundle["certified_data_build_id"], + artifact.build_id, + ) + + def validate_tro( + self, + country: str, + manifest: CountryReleaseManifest, + *, + allow_limited_tro: bool, + ) -> None: + tro_path = MANIFEST_DIR / f"{country}.trace.tro.jsonld" + self.check(tro_path.is_file(), f"{tro_path} is missing") + if not tro_path.is_file(): + return + + tro = json.loads(tro_path.read_text()) + schema = json.loads(TRACE_SCHEMA_PATH.read_text()) + errors = sorted(Draft202012Validator(schema).iter_errors(tro), key=str) + for error in errors: + path = ".".join(str(part) for part in error.absolute_path) + self.failures.append( + f"TRO schema error at {path or ''}: {error.message}" + ) + + artifacts = _artifact_by_id(tro) + locations = _location_by_id(tro) + artifact_ids = set(artifacts) + required_artifacts = set(EXPECTED_TRO_ARTIFACTS) + if allow_limited_tro: + required_artifacts.remove("data_release_manifest") + self.check( + required_artifacts.issubset(artifact_ids), + f"TRO artifacts missing: {sorted(required_artifacts - artifact_ids)}", + ) + if not allow_limited_tro: + self.check( + "data_release_manifest" in artifact_ids, + "TRO must include the data_release_manifest artifact", + ) + + performance = _performance(tro) + certification = manifest.certification + artifact = manifest.certified_data_artifact + self.check_equal( + "TRO certified model version", + performance.get("pe:certifiedForModelVersion"), + manifest.model_package.version, + ) + if certification is not None: + self.check_equal( + "TRO built-with model version", + performance.get("pe:builtWithModelVersion"), + certification.built_with_model_version, + ) + self.check_equal( + "TRO compatibility basis", + performance.get("pe:compatibilityBasis"), + certification.compatibility_basis, + ) + self.check_equal( + "TRO data build fingerprint", + performance.get("pe:dataBuildFingerprint"), + certification.data_build_fingerprint, + ) + self.check_equal( + "TRO data build id", + performance.get("pe:dataBuildId"), + certification.data_build_id, + ) + if not allow_limited_tro: + self.check( + performance.get("pe:dataReleaseManifestStatus") != "unavailable", + "TRO says the data release manifest is unavailable", + ) + + bundle_artifact = artifacts.get("bundle_manifest") + if bundle_artifact is not None: + expected_manifest_hash = canonical_json_bytes( + manifest.model_dump(mode="json") + ) + self.check_equal( + "TRO bundle manifest sha256", + bundle_artifact.get("trov:sha256"), + hashlib.sha256(expected_manifest_hash).hexdigest(), + ) + dataset_artifact = artifacts.get("dataset") + if dataset_artifact is not None and artifact is not None: + self.check_equal( + "TRO dataset sha256", + dataset_artifact.get("trov:sha256"), + artifact.sha256, + ) + model_wheel = artifacts.get("model_wheel") + if model_wheel is not None: + self.check_equal( + "TRO model wheel sha256", + model_wheel.get("trov:sha256"), + manifest.model_package.sha256, + ) + self.check( + f"{manifest.model_package.name}=={manifest.model_package.version}" + in model_wheel.get("schema:name", ""), + "TRO model wheel name does not match the manifest model package", + ) + model_wheel_location = locations.get("model_wheel") + if model_wheel_location is not None: + self.check_equal( + "TRO model wheel location", + model_wheel_location.get("trov:hasLocation"), + manifest.model_package.wheel_url, + ) + + +def parse_args(argv: Optional[list[str]]) -> argparse.Namespace: + parser = argparse.ArgumentParser(description=__doc__) + parser.add_argument("--country", required=True, choices=COUNTRIES) + parser.add_argument("--expected-model-version") + parser.add_argument("--expected-data-version") + parser.add_argument("--expected-built-with-model-version") + parser.add_argument( + "--expected-compatibility-basis", choices=sorted(VALID_COMPATIBILITY_BASES) + ) + parser.add_argument( + "--allow-limited-tro", + action="store_true", + help=( + "Allow a TRO that omits the data_release_manifest artifact. Do not " + "use this for current US/UK release-manifest-backed bundles." + ), + ) + return parser.parse_args(argv) + + +def main(argv: Optional[list[str]] = None) -> int: + args = parse_args(argv) + validator = BundleValidator() + manifest = get_release_manifest(args.country) + + validator.validate_manifest( + manifest, + expected_model_version=args.expected_model_version, + expected_data_version=args.expected_data_version, + expected_built_with_model_version=args.expected_built_with_model_version, + expected_compatibility_basis=args.expected_compatibility_basis, + ) + validator.validate_model_wrapper(args.country, manifest) + validator.validate_tro( + args.country, + manifest, + allow_limited_tro=args.allow_limited_tro, + ) + + if validator.failures: + print(f"{args.country}: release bundle validation failed", file=sys.stderr) + for failure in validator.failures: + print(f"- {failure}", file=sys.stderr) + return 1 + + print( + f"{args.country}: release bundle valid " + f"({manifest.model_package.name} {manifest.model_package.version}, " + f"{manifest.data_package.name} {manifest.data_package.version})" + ) + return 0 + + +if __name__ == "__main__": + raise SystemExit(main()) diff --git a/src/policyengine/data/release_manifests/uk.json b/src/policyengine/data/release_manifests/uk.json index a9c8f7ae..c19b644a 100644 --- a/src/policyengine/data/release_manifests/uk.json +++ b/src/policyengine/data/release_manifests/uk.json @@ -5,9 +5,9 @@ "policyengine_version": "4.13.1", "model_package": { "name": "policyengine-uk", - "version": "2.88.20", - "sha256": "8c3dacb868f3fb18296b8ef2475edaf543f57b8056d24a58bca59b108651f272", - "wheel_url": "https://files.pythonhosted.org/packages/32/f0/c0e7dbcc049501dc968da0a67de4976f305228328f96fe0ad08c65301c4f/policyengine_uk-2.88.20-py3-none-any.whl" + "version": "2.88.40", + "sha256": "156212836b5a213af864ac7a5f978ecd480bace848ccb1d2c07d732b15d38ea2", + "wheel_url": "https://files.pythonhosted.org/packages/cc/a4/2718ab27eb6c12b4c6857c1e49c7a6e8ad09f5cba74548c797daa425bbf5/policyengine_uk-2.88.40-py3-none-any.whl" }, "data_package": { "name": "policyengine-uk-data", @@ -27,10 +27,10 @@ "sha256": "584ae33d80ca0431254610a3f8254d132da73477d31966d6446282861ecae50d" }, "certification": { - "compatibility_basis": "exact_build_model_version", + "compatibility_basis": "legacy_compatible_model_package", "data_build_id": "policyengine-uk-data-1.55.10", "built_with_model_version": "2.88.20", - "certified_for_model_version": "2.88.20", + "certified_for_model_version": "2.88.40", "data_build_fingerprint": "sha256:77f149725a36055fd89961855230401852b0712d301c6e26d6d16565c6b23809", "certified_by": "policyengine.py bundled manifest" }, diff --git a/src/policyengine/data/release_manifests/uk.trace.tro.jsonld b/src/policyengine/data/release_manifests/uk.trace.tro.jsonld index 0cf83170..3f808285 100644 --- a/src/policyengine/data/release_manifests/uk.trace.tro.jsonld +++ b/src/policyengine/data/release_manifests/uk.trace.tro.jsonld @@ -61,7 +61,7 @@ "trov:hasArtifact": { "@id": "composition/1/artifact/model_wheel" }, - "trov:hasLocation": "https://files.pythonhosted.org/packages/32/f0/c0e7dbcc049501dc968da0a67de4976f305228328f96fe0ad08c65301c4f/policyengine_uk-2.88.20-py3-none-any.whl" + "trov:hasLocation": "https://files.pythonhosted.org/packages/cc/a4/2718ab27eb6c12b4c6857c1e49c7a6e8ad09f5cba74548c797daa425bbf5/policyengine_uk-2.88.40-py3-none-any.whl" } ] } @@ -75,7 +75,7 @@ "@type": "trov:ResearchArtifact", "schema:name": "policyengine.py bundle manifest for uk", "trov:mimeType": "application/json", - "trov:sha256": "1f63e1039a8c1915d0e1304011cb9939ef8434dfdf1928dd461ce9950a93d49c" + "trov:sha256": "f65940bce69b056996189bf139bd586031c41e03ce051ff89d643d26335f9861" }, { "@id": "composition/1/artifact/data_release_manifest", @@ -94,15 +94,15 @@ { "@id": "composition/1/artifact/model_wheel", "@type": "trov:ResearchArtifact", - "schema:name": "policyengine-uk==2.88.20 wheel", + "schema:name": "policyengine-uk==2.88.40 wheel", "trov:mimeType": "application/zip", - "trov:sha256": "8c3dacb868f3fb18296b8ef2475edaf543f57b8056d24a58bca59b108651f272" + "trov:sha256": "156212836b5a213af864ac7a5f978ecd480bace848ccb1d2c07d732b15d38ea2" } ], "trov:hasFingerprint": { "@id": "composition/1/fingerprint", "@type": "trov:CompositionFingerprint", - "trov:sha256": "181cf6350363e41a1589ca8689109f724ad2510dba4cbd94ea5900e3e9db15f9" + "trov:sha256": "adfca6bd19d650f60b40d6382455ce4ee611edd9feb64b1d216cebfb490449a7" } }, "trov:hasPerformance": { @@ -110,15 +110,12 @@ "@type": "trov:TransparentResearchPerformance", "pe:builtWithModelVersion": "2.88.20", "pe:certifiedBy": "policyengine.py bundled manifest", - "pe:certifiedForModelVersion": "2.88.20", - "pe:ciGitRef": "refs/heads/main", - "pe:ciGitSha": "8cdb43974b723b981b5e82be87da236818de9f5d", - "pe:ciRunUrl": "https://github.com/PolicyEngine/policyengine.py/actions/runs/26775379456", - "pe:compatibilityBasis": "exact_build_model_version", + "pe:certifiedForModelVersion": "2.88.40", + "pe:compatibilityBasis": "legacy_compatible_model_package", "pe:dataBuildFingerprint": "sha256:77f149725a36055fd89961855230401852b0712d301c6e26d6d16565c6b23809", "pe:dataBuildId": "policyengine-uk-data-1.55.10", - "pe:emittedIn": "github-actions", - "rdfs:comment": "Certification of build policyengine-uk-data-1.55.10 for policyengine-uk 2.88.20.", + "pe:emittedIn": "local", + "rdfs:comment": "Certification of build policyengine-uk-data-1.55.10 for policyengine-uk 2.88.40.", "trov:accessedArrangement": { "@id": "arrangement/1" }, diff --git a/tests/test_models.py b/tests/test_models.py index 4b220d74..f9e52c62 100644 --- a/tests/test_models.py +++ b/tests/test_models.py @@ -29,7 +29,7 @@ def test_has_release_manifest_metadata(self): assert uk_latest.release_manifest is not None assert uk_latest.release_manifest.country_id == "uk" assert uk_latest.model_package.name == "policyengine-uk" - assert uk_latest.model_package.version == "2.88.20" + assert uk_latest.model_package.version == "2.88.40" assert uk_latest.data_package.name == "policyengine-uk-data" assert uk_latest.data_package.version == "1.55.10" assert ( diff --git a/tests/test_release_manifests.py b/tests/test_release_manifests.py index 63d283ea..55d680cd 100644 --- a/tests/test_release_manifests.py +++ b/tests/test_release_manifests.py @@ -128,7 +128,7 @@ def test__given_uk_manifest__then_has_pinned_model_and_data_packages(self): assert manifest.country_id == "uk" assert manifest.policyengine_version == POLICYENGINE_VERSION assert manifest.model_package.name == "policyengine-uk" - assert manifest.model_package.version == "2.88.20" + assert manifest.model_package.version == "2.88.40" assert manifest.data_package.name == "policyengine-uk-data" assert manifest.data_package.version == "1.55.10" assert ( @@ -141,8 +141,12 @@ def test__given_uk_manifest__then_has_pinned_model_and_data_packages(self): assert manifest.certified_data_artifact.dataset == "enhanced_frs_2023_24" assert manifest.certification is not None assert manifest.certification.data_build_id == "policyengine-uk-data-1.55.10" + assert ( + manifest.certification.compatibility_basis + == "legacy_compatible_model_package" + ) assert manifest.certification.built_with_model_version == "2.88.20" - assert manifest.certification.certified_for_model_version == "2.88.20" + assert manifest.certification.certified_for_model_version == "2.88.40" assert ( manifest.certification.data_build_fingerprint == "sha256:77f149725a36055fd89961855230401852b0712d301c6e26d6d16565c6b23809" @@ -661,7 +665,7 @@ def test__given_manifest_certification__then_release_bundle_exposes_it(self): assert bundle["default_dataset_uri"] == manifest.default_dataset_uri assert bundle["certified_data_build_id"] == "policyengine-uk-data-1.55.10" assert bundle["data_build_model_version"] == "2.88.20" - assert bundle["compatibility_basis"] == "exact_build_model_version" + assert bundle["compatibility_basis"] == "legacy_compatible_model_package" assert bundle["certified_by"] == "policyengine.py bundled manifest" def test__given_runtime_certification__then_release_bundle_prefers_runtime_value( diff --git a/uv.lock b/uv.lock index 27e46fcf..9d1f1083 100644 --- a/uv.lock +++ b/uv.lock @@ -2820,7 +2820,7 @@ wheels = [ [[package]] name = "policyengine" -version = "4.13.0" +version = "4.13.1" source = { editable = "." } dependencies = [ { name = "diskcache" }, @@ -2895,8 +2895,8 @@ requires-dist = [ { name = "policyengine-core", marker = "extra == 'dev'", specifier = "==3.26.1" }, { name = "policyengine-core", marker = "extra == 'uk'", specifier = ">=3.26.1" }, { name = "policyengine-core", marker = "extra == 'us'", specifier = "==3.26.1" }, - { name = "policyengine-uk", marker = "extra == 'dev'", specifier = "==2.88.20" }, - { name = "policyengine-uk", marker = "extra == 'uk'", specifier = "==2.88.20" }, + { name = "policyengine-uk", marker = "extra == 'dev'", specifier = "==2.88.40" }, + { name = "policyengine-uk", marker = "extra == 'uk'", specifier = "==2.88.40" }, { name = "policyengine-us", marker = "extra == 'dev'", specifier = "==1.715.2" }, { name = "policyengine-us", marker = "extra == 'us'", specifier = "==1.715.2" }, { name = "psutil", specifier = ">=5.9.0" }, @@ -2944,7 +2944,7 @@ wheels = [ [[package]] name = "policyengine-uk" -version = "2.88.20" +version = "2.88.40" source = { registry = "https://pypi.org/simple" } dependencies = [ { name = "microdf-python" }, @@ -2954,9 +2954,9 @@ dependencies = [ { 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/fb/11/64c8b0269e68d42ffdc58c74b1975dcb6a67487de526855182ecc2479fb1/policyengine_uk-2.88.20.tar.gz", hash = "sha256:3c3939f4b4dc78be2747ec459bad2b5f341580be031af4004a554ce0c3f59682", size = 1189714, upload-time = "2026-05-20T17:38:13.426Z" } +sdist = { url = "https://files.pythonhosted.org/packages/59/7e/8a79030d390cea83e8ac484fb50b3df22d07ce1b54ce7c9c81403686d05f/policyengine_uk-2.88.40.tar.gz", hash = "sha256:39df71d30fc812c946665b46fe499b9feadd12a9314ed7e4dc80b526dc611137", size = 1199290, upload-time = "2026-06-02T14:55:22.475Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/32/f0/c0e7dbcc049501dc968da0a67de4976f305228328f96fe0ad08c65301c4f/policyengine_uk-2.88.20-py3-none-any.whl", hash = "sha256:8c3dacb868f3fb18296b8ef2475edaf543f57b8056d24a58bca59b108651f272", size = 1918240, upload-time = "2026-05-20T17:38:11.347Z" }, + { url = "https://files.pythonhosted.org/packages/cc/a4/2718ab27eb6c12b4c6857c1e49c7a6e8ad09f5cba74548c797daa425bbf5/policyengine_uk-2.88.40-py3-none-any.whl", hash = "sha256:156212836b5a213af864ac7a5f978ecd480bace848ccb1d2c07d732b15d38ea2", size = 1935271, upload-time = "2026-06-02T14:55:20.217Z" }, ] [[package]]