diff --git a/.github/workflows/pr_code_changes.yaml b/.github/workflows/pr_code_changes.yaml index 17c973b5e..3d579f668 100644 --- a/.github/workflows/pr_code_changes.yaml +++ b/.github/workflows/pr_code_changes.yaml @@ -42,6 +42,25 @@ jobs: uses: astral-sh/setup-uv@v8.1.0 - name: Check formatting run: uvx ruff format --check . + BundleMetadataContract: + name: Validate bundle metadata contract + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v6 + - name: Set up Python + uses: actions/setup-python@v6 + with: + python-version: "3.14" + - name: Install uv + uses: astral-sh/setup-uv@v8.1.0 + - name: Install package + run: uv pip install --system . + - name: Install bundle validation tooling + # Pin the test-only bundle contract dependency until policyengine-bundles + # has published releases suitable for ordinary dependency specifiers. + run: uv pip install --system "policyengine-bundles @ git+https://github.com/PolicyEngine/policyengine-bundles@8ae9f56fefcf89f69b8a7e3bc49928509c6207be" + - name: Validate runtime metadata contract + run: python -m pytest policyengine_uk/tests/test_build_metadata.py Test: runs-on: macos-latest permissions: diff --git a/changelog.d/add-bundle-runtime-metadata.changed.md b/changelog.d/add-bundle-runtime-metadata.changed.md new file mode 100644 index 000000000..e692fd11d --- /dev/null +++ b/changelog.d/add-bundle-runtime-metadata.changed.md @@ -0,0 +1 @@ +Added runtime metadata with installed policyengine-core identity for bundle validation. diff --git a/policyengine_uk/build_metadata.py b/policyengine_uk/build_metadata.py index 3d856af4a..09e5fbca2 100644 --- a/policyengine_uk/build_metadata.py +++ b/policyengine_uk/build_metadata.py @@ -6,6 +6,8 @@ from pathlib import Path import subprocess +from policyengine_core import get_runtime_metadata as get_core_runtime_metadata + PACKAGE_NAME = "policyengine-uk" PACKAGE_ROOT = Path(__file__).resolve().parent DATA_BUILD_SURFACE = ( @@ -39,11 +41,8 @@ def _iter_surface_files() -> list[Path]: return files -def _get_package_version() -> str | None: - try: - return metadata.version(PACKAGE_NAME) - except metadata.PackageNotFoundError: - return None +def _get_package_version() -> str: + return metadata.version(PACKAGE_NAME) def _get_git_sha() -> str | None: @@ -73,10 +72,15 @@ def get_data_build_fingerprint() -> str: return f"sha256:{digest.hexdigest()}" -def get_data_build_metadata() -> dict[str, str | None]: +def get_runtime_metadata() -> dict[str, object]: return { "name": PACKAGE_NAME, "version": _get_package_version(), "git_sha": _get_git_sha(), "data_build_fingerprint": get_data_build_fingerprint(), + "core": get_core_runtime_metadata(), } + + +def get_data_build_metadata() -> dict[str, object]: + return get_runtime_metadata() diff --git a/policyengine_uk/tests/test_build_metadata.py b/policyengine_uk/tests/test_build_metadata.py index 7d67460a7..2088277bf 100644 --- a/policyengine_uk/tests/test_build_metadata.py +++ b/policyengine_uk/tests/test_build_metadata.py @@ -1,10 +1,22 @@ -from contextlib import ExitStack +import importlib.util +from pathlib import Path +import sys from unittest.mock import patch -from policyengine_uk.build_metadata import ( - get_data_build_fingerprint, - get_data_build_metadata, +import pytest + +BUILD_METADATA_PATH = Path(__file__).resolve().parents[1] / "build_metadata.py" +SPEC = importlib.util.spec_from_file_location( + "policyengine_uk_build_metadata_under_test", + BUILD_METADATA_PATH, ) +build_metadata = importlib.util.module_from_spec(SPEC) +sys.modules[SPEC.name] = build_metadata +SPEC.loader.exec_module(build_metadata) + +get_data_build_fingerprint = build_metadata.get_data_build_fingerprint +get_data_build_metadata = build_metadata.get_data_build_metadata +get_runtime_metadata = build_metadata.get_runtime_metadata def test_data_build_fingerprint_is_stable_within_process(): @@ -17,33 +29,48 @@ def test_data_build_fingerprint_is_stable_within_process(): assert first == second -def test_get_data_build_metadata_includes_version_git_sha_and_fingerprint(): +def test_get_runtime_metadata_includes_required_bundle_fields(monkeypatch): get_data_build_fingerprint.cache_clear() - with ExitStack() as stack: - stack.enter_context( - patch( - "policyengine_uk.build_metadata._get_package_version", - return_value="2.74.0", - ) - ) - stack.enter_context( - patch( - "policyengine_uk.build_metadata._get_git_sha", - return_value="deadbeef", - ) - ) - stack.enter_context( - patch( - "policyengine_uk.build_metadata.get_data_build_fingerprint", - return_value="sha256:fingerprint", - ) - ) - metadata = get_data_build_metadata() - - assert metadata == { - "name": "policyengine-uk", - "version": "2.74.0", - "git_sha": "deadbeef", - "data_build_fingerprint": "sha256:fingerprint", + monkeypatch.setattr(build_metadata, "_get_package_version", lambda: "2.74.0") + monkeypatch.setattr(build_metadata, "_get_git_sha", lambda: "deadbeef") + monkeypatch.setattr( + build_metadata, + "get_data_build_fingerprint", + lambda: "sha256:fingerprint", + ) + monkeypatch.setattr( + build_metadata, + "get_core_runtime_metadata", + lambda: { + "name": "policyengine-core", + "version": "3.26.0", + "git_sha": "coredeadbeef", + }, + ) + + metadata = get_runtime_metadata() + + assert metadata["name"] == "policyengine-uk" + assert metadata["version"] == "2.74.0" + assert metadata["git_sha"] == "deadbeef" + assert metadata["data_build_fingerprint"] == "sha256:fingerprint" + assert metadata["core"] == { + "name": "policyengine-core", + "version": "3.26.0", + "git_sha": "coredeadbeef", } + + +def test_get_data_build_metadata_uses_runtime_metadata(): + with patch( + f"{SPEC.name}.get_runtime_metadata", + return_value={"name": "policyengine-uk"}, + ): + assert get_data_build_metadata() == {"name": "policyengine-uk"} + + +def test_runtime_metadata_uses_bundle_contract_when_available(): + policyengine_bundles = pytest.importorskip("policyengine_bundles") + + policyengine_bundles.load_component_metadata(get_runtime_metadata()) diff --git a/pyproject.toml b/pyproject.toml index 20125f119..ea2f32508 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -26,7 +26,7 @@ classifiers = [ ] requires-python = ">=3.9" dependencies = [ - "policyengine-core>=3.25.0", + "policyengine-core>=3.26.0", "microdf-python>=1.2.1", "pydantic>=2.11.7", "tables>=3.9.2,<3.10.2; python_version < '3.10'", diff --git a/uv.lock b/uv.lock index c94db0882..a99714ab0 100644 --- a/uv.lock +++ b/uv.lock @@ -1557,7 +1557,7 @@ wheels = [ [[package]] name = "policyengine-core" -version = "3.25.0" +version = "3.26.0" source = { registry = "https://pypi.org/simple" } dependencies = [ { name = "dpath", marker = "python_full_version >= '3.11'" }, @@ -1577,9 +1577,9 @@ dependencies = [ { name = "standard-imghdr", marker = "python_full_version >= '3.11'" }, { name = "wheel", marker = "python_full_version >= '3.11'" }, ] -sdist = { url = "https://files.pythonhosted.org/packages/08/e3/40f11fe87ae718f88359dba6bf5971a1bb9b322dd48069c7881db9006791/policyengine_core-3.25.0.tar.gz", hash = "sha256:3b59a59046465d2f5c959cfe278c598e7deaa94d04a4134f4742d9f24cdbd6de", size = 464281, upload-time = "2026-04-18T00:28:02.949Z" } +sdist = { url = "https://files.pythonhosted.org/packages/4e/69/adb6407c97de5260a938344f9eafa9979bf8f97aec8c628538d906ecdec2/policyengine_core-3.26.0.tar.gz", hash = "sha256:a571026ef418653ec18f087463cf37e9be730e90ad4376cb10997f0ddf9f8eda", size = 468190, upload-time = "2026-05-04T19:26:27.707Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/2c/f8/fd60f3c7f02d27c5c83a713cd7779707b4ddff6cd76143a9b4def1c7ec4d/policyengine_core-3.25.0-py3-none-any.whl", hash = "sha256:397127f8842dea12638880c231e6bdec346fb9c9259b7775bb060c06a3b0190b", size = 230805, upload-time = "2026-04-18T00:28:01.311Z" }, + { url = "https://files.pythonhosted.org/packages/bc/f3/0e98b30d4eb7b309c3f1f1d8c2354595f78319ce2442eda069f02a47f4d1/policyengine_core-3.26.0-py3-none-any.whl", hash = "sha256:d63a4622233b61c4c5fc64d4f65030d65b2564ac63ac87b17d545d63cdf17194", size = 232135, upload-time = "2026-05-04T19:26:25.693Z" }, ] [[package]] @@ -1617,7 +1617,7 @@ requires-dist = [ { name = "furo", marker = "extra == 'dev'", specifier = "<2023" }, { name = "jupyter-book", marker = "extra == 'dev'", specifier = ">=2.0.0a0" }, { name = "microdf-python", specifier = ">=1.2.1" }, - { name = "policyengine-core", specifier = ">=3.25.0" }, + { name = "policyengine-core", specifier = ">=3.26.0" }, { name = "pydantic", specifier = ">=2.11.7" }, { name = "pytest", marker = "extra == 'dev'" }, { name = "pytest-cov", marker = "extra == 'dev'" },