From 38e93c24203c4bfed4024ddbb56678846533cf30 Mon Sep 17 00:00:00 2001 From: Anthony Volk Date: Fri, 1 May 2026 19:36:51 +0200 Subject: [PATCH 1/5] Add runtime metadata for bundle validation --- changelog.d/add-runtime-metadata.added.md | 1 + policyengine_core/__init__.py | 1 + policyengine_core/build_metadata.py | 149 ++++++++++++++++++++++ tests/core/test_build_metadata.py | 25 ++++ 4 files changed, 176 insertions(+) create mode 100644 changelog.d/add-runtime-metadata.added.md create mode 100644 policyengine_core/build_metadata.py create mode 100644 tests/core/test_build_metadata.py diff --git a/changelog.d/add-runtime-metadata.added.md b/changelog.d/add-runtime-metadata.added.md new file mode 100644 index 000000000..f0236267c --- /dev/null +++ b/changelog.d/add-runtime-metadata.added.md @@ -0,0 +1 @@ +Added dependency-free runtime metadata for bundle validation. diff --git a/policyengine_core/__init__.py b/policyengine_core/__init__.py index 3745adf6d..40273ff14 100644 --- a/policyengine_core/__init__.py +++ b/policyengine_core/__init__.py @@ -1,2 +1,3 @@ +from policyengine_core.build_metadata import get_runtime_metadata from policyengine_core.simulations import Microsimulation, Simulation from policyengine_core.taxbenefitsystems import TaxBenefitSystem diff --git a/policyengine_core/build_metadata.py b/policyengine_core/build_metadata.py new file mode 100644 index 000000000..adf6cbe05 --- /dev/null +++ b/policyengine_core/build_metadata.py @@ -0,0 +1,149 @@ +from __future__ import annotations + +import importlib.metadata +import json +import subprocess +from pathlib import Path +from typing import Any, Dict, Optional + + +PACKAGE_NAME = "policyengine-core" +_PACKAGE_DIR = Path(__file__).resolve().parent + +__all__ = ["get_runtime_metadata"] + + +def get_runtime_metadata() -> Dict[str, Any]: + """Return JSON-compatible metadata describing this core runtime. + + The payload intentionally avoids importing the bundle orchestrator. It is + shaped to be validated by policyengine-bundles when that package is + available in release or integration-test workflows. + """ + + distribution = _get_distribution() + metadata: Dict[str, Any] = { + "name": PACKAGE_NAME, + "version": _get_package_version(distribution), + } + + git_sha = _get_direct_url_git_sha(distribution) or _get_local_git_sha() + if git_sha is not None: + metadata["git_sha"] = git_sha + + source_path = _get_source_path() + if source_path is not None: + metadata["source_path"] = source_path + + return metadata + + +def _get_distribution() -> Optional[importlib.metadata.Distribution]: + try: + return importlib.metadata.distribution(PACKAGE_NAME) + except importlib.metadata.PackageNotFoundError: + return None + + +def _get_package_version( + distribution: Optional[importlib.metadata.Distribution], +) -> str: + if distribution is not None: + return distribution.version + + version = _get_pyproject_version() + if version is not None: + return version + + raise importlib.metadata.PackageNotFoundError(PACKAGE_NAME) + + +def _get_pyproject_version() -> Optional[str]: + git_root = _find_git_root(_PACKAGE_DIR) + if git_root is None: + return None + + pyproject_path = git_root / "pyproject.toml" + if not pyproject_path.exists(): + return None + + for line in pyproject_path.read_text().splitlines(): + stripped = line.strip() + if stripped.startswith("version = "): + return stripped.split("=", 1)[1].strip().strip('"') + + return None + + +def _get_direct_url_git_sha( + distribution: Optional[importlib.metadata.Distribution], +) -> Optional[str]: + if distribution is None: + return None + + direct_url = _read_direct_url(distribution) + if direct_url is None: + return None + + vcs_info = direct_url.get("vcs_info") + if not isinstance(vcs_info, dict): + return None + + commit_id = vcs_info.get("commit_id") + if isinstance(commit_id, str) and commit_id: + return commit_id + + return None + + +def _read_direct_url( + distribution: importlib.metadata.Distribution, +) -> Optional[Dict[str, Any]]: + direct_url_text = distribution.read_text("direct_url.json") + if direct_url_text is None: + return None + + try: + direct_url = json.loads(direct_url_text) + except json.JSONDecodeError: + return None + + if isinstance(direct_url, dict): + return direct_url + + return None + + +def _get_local_git_sha() -> Optional[str]: + git_root = _find_git_root(_PACKAGE_DIR) + if git_root is None: + return None + + try: + result = subprocess.run( + ["git", "-C", str(git_root), "rev-parse", "HEAD"], + check=True, + capture_output=True, + text=True, + ) + except (OSError, subprocess.CalledProcessError): + return None + + git_sha = result.stdout.strip() + return git_sha or None + + +def _get_source_path() -> Optional[str]: + git_root = _find_git_root(_PACKAGE_DIR) + if git_root is None: + return None + + return str(_PACKAGE_DIR) + + +def _find_git_root(start: Path) -> Optional[Path]: + for path in (start, *start.parents): + if (path / ".git").exists(): + return path + + return None diff --git a/tests/core/test_build_metadata.py b/tests/core/test_build_metadata.py new file mode 100644 index 000000000..47db3e5a5 --- /dev/null +++ b/tests/core/test_build_metadata.py @@ -0,0 +1,25 @@ +from __future__ import annotations + +import importlib.metadata +import json + +import pytest + +from policyengine_core import get_runtime_metadata + + +def test_runtime_metadata_has_core_identity(): + metadata = get_runtime_metadata() + + assert metadata["name"] == "policyengine-core" + assert metadata["version"] == importlib.metadata.version("policyengine-core") + + +def test_runtime_metadata_is_json_compatible(): + json.dumps(get_runtime_metadata()) + + +def test_runtime_metadata_uses_bundle_contract_when_available(): + validation = pytest.importorskip("policyengine_bundles") + + validation.load_component_metadata(get_runtime_metadata()) From b1f230555437fb7f51c8993d25197fbea630c562 Mon Sep 17 00:00:00 2001 From: Anthony Volk Date: Mon, 4 May 2026 20:07:50 +0200 Subject: [PATCH 2/5] Avoid local source metadata in runtime identity --- policyengine_core/build_metadata.py | 65 +---------------------------- tests/core/test_build_metadata.py | 31 ++++++++++++++ 2 files changed, 32 insertions(+), 64 deletions(-) diff --git a/policyengine_core/build_metadata.py b/policyengine_core/build_metadata.py index adf6cbe05..8ce93305f 100644 --- a/policyengine_core/build_metadata.py +++ b/policyengine_core/build_metadata.py @@ -2,13 +2,10 @@ import importlib.metadata import json -import subprocess -from pathlib import Path from typing import Any, Dict, Optional PACKAGE_NAME = "policyengine-core" -_PACKAGE_DIR = Path(__file__).resolve().parent __all__ = ["get_runtime_metadata"] @@ -27,14 +24,10 @@ def get_runtime_metadata() -> Dict[str, Any]: "version": _get_package_version(distribution), } - git_sha = _get_direct_url_git_sha(distribution) or _get_local_git_sha() + git_sha = _get_direct_url_git_sha(distribution) if git_sha is not None: metadata["git_sha"] = git_sha - source_path = _get_source_path() - if source_path is not None: - metadata["source_path"] = source_path - return metadata @@ -51,30 +44,9 @@ def _get_package_version( if distribution is not None: return distribution.version - version = _get_pyproject_version() - if version is not None: - return version - raise importlib.metadata.PackageNotFoundError(PACKAGE_NAME) -def _get_pyproject_version() -> Optional[str]: - git_root = _find_git_root(_PACKAGE_DIR) - if git_root is None: - return None - - pyproject_path = git_root / "pyproject.toml" - if not pyproject_path.exists(): - return None - - for line in pyproject_path.read_text().splitlines(): - stripped = line.strip() - if stripped.startswith("version = "): - return stripped.split("=", 1)[1].strip().strip('"') - - return None - - def _get_direct_url_git_sha( distribution: Optional[importlib.metadata.Distribution], ) -> Optional[str]: @@ -112,38 +84,3 @@ def _read_direct_url( return direct_url return None - - -def _get_local_git_sha() -> Optional[str]: - git_root = _find_git_root(_PACKAGE_DIR) - if git_root is None: - return None - - try: - result = subprocess.run( - ["git", "-C", str(git_root), "rev-parse", "HEAD"], - check=True, - capture_output=True, - text=True, - ) - except (OSError, subprocess.CalledProcessError): - return None - - git_sha = result.stdout.strip() - return git_sha or None - - -def _get_source_path() -> Optional[str]: - git_root = _find_git_root(_PACKAGE_DIR) - if git_root is None: - return None - - return str(_PACKAGE_DIR) - - -def _find_git_root(start: Path) -> Optional[Path]: - for path in (start, *start.parents): - if (path / ".git").exists(): - return path - - return None diff --git a/tests/core/test_build_metadata.py b/tests/core/test_build_metadata.py index 47db3e5a5..f86cff0f5 100644 --- a/tests/core/test_build_metadata.py +++ b/tests/core/test_build_metadata.py @@ -6,6 +6,7 @@ import pytest from policyengine_core import get_runtime_metadata +from policyengine_core import build_metadata def test_runtime_metadata_has_core_identity(): @@ -19,6 +20,36 @@ def test_runtime_metadata_is_json_compatible(): json.dumps(get_runtime_metadata()) +def test_runtime_metadata_does_not_include_local_source_path(): + assert "source_path" not in get_runtime_metadata() + + +def test_runtime_metadata_uses_pep_610_vcs_commit(monkeypatch): + class Distribution: + version = "1.2.3" + + def read_text(self, name): + if name == "direct_url.json": + return json.dumps( + { + "vcs_info": { + "vcs": "git", + "commit_id": "abc123", + } + } + ) + + return None + + monkeypatch.setattr(build_metadata, "_get_distribution", Distribution) + + assert get_runtime_metadata() == { + "name": "policyengine-core", + "version": "1.2.3", + "git_sha": "abc123", + } + + def test_runtime_metadata_uses_bundle_contract_when_available(): validation = pytest.importorskip("policyengine_bundles") From ea7f6acd0b8b8f98ad4c9ade66b0cc557277ff56 Mon Sep 17 00:00:00 2001 From: Anthony Volk Date: Mon, 4 May 2026 20:23:36 +0200 Subject: [PATCH 3/5] Validate runtime metadata against bundle contract --- .github/workflows/pr.yaml | 21 ++++++++++++++++++++- 1 file changed, 20 insertions(+), 1 deletion(-) diff --git a/.github/workflows/pr.yaml b/.github/workflows/pr.yaml index 414c08db1..1e1fd7dd0 100644 --- a/.github/workflows/pr.yaml +++ b/.github/workflows/pr.yaml @@ -25,6 +25,25 @@ jobs: echo "Types: added, changed, fixed, removed, breaking" exit 1 fi + 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.11" + - name: Install uv + uses: astral-sh/setup-uv@v8.1.0 + - name: Install core + 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 tests/core/test_build_metadata.py Test: strategy: matrix: @@ -100,4 +119,4 @@ jobs: run: python -m pytest -m smoke --reruns 2 --reruns-delay 5 -v -s env: RUN_SMOKE_TESTS: "0" - POLICYENGINE_GITHUB_MICRODATA_AUTH_TOKEN: ${{ secrets.GITHUB_TOKEN }} \ No newline at end of file + POLICYENGINE_GITHUB_MICRODATA_AUTH_TOKEN: ${{ secrets.GITHUB_TOKEN }} From c7b0943ecfdb70c0cf12e7a910c266018408e32c Mon Sep 17 00:00:00 2001 From: Anthony Volk Date: Mon, 4 May 2026 20:34:43 +0200 Subject: [PATCH 4/5] Simplify runtime metadata distribution lookup --- policyengine_core/build_metadata.py | 25 +++---------------------- tests/core/test_build_metadata.py | 6 +++++- 2 files changed, 8 insertions(+), 23 deletions(-) diff --git a/policyengine_core/build_metadata.py b/policyengine_core/build_metadata.py index 8ce93305f..6c0a535da 100644 --- a/policyengine_core/build_metadata.py +++ b/policyengine_core/build_metadata.py @@ -18,10 +18,10 @@ def get_runtime_metadata() -> Dict[str, Any]: available in release or integration-test workflows. """ - distribution = _get_distribution() + distribution = importlib.metadata.distribution(PACKAGE_NAME) metadata: Dict[str, Any] = { "name": PACKAGE_NAME, - "version": _get_package_version(distribution), + "version": distribution.version, } git_sha = _get_direct_url_git_sha(distribution) @@ -31,28 +31,9 @@ def get_runtime_metadata() -> Dict[str, Any]: return metadata -def _get_distribution() -> Optional[importlib.metadata.Distribution]: - try: - return importlib.metadata.distribution(PACKAGE_NAME) - except importlib.metadata.PackageNotFoundError: - return None - - -def _get_package_version( - distribution: Optional[importlib.metadata.Distribution], -) -> str: - if distribution is not None: - return distribution.version - - raise importlib.metadata.PackageNotFoundError(PACKAGE_NAME) - - def _get_direct_url_git_sha( - distribution: Optional[importlib.metadata.Distribution], + distribution: importlib.metadata.Distribution, ) -> Optional[str]: - if distribution is None: - return None - direct_url = _read_direct_url(distribution) if direct_url is None: return None diff --git a/tests/core/test_build_metadata.py b/tests/core/test_build_metadata.py index f86cff0f5..5a14a52d9 100644 --- a/tests/core/test_build_metadata.py +++ b/tests/core/test_build_metadata.py @@ -41,7 +41,11 @@ def read_text(self, name): return None - monkeypatch.setattr(build_metadata, "_get_distribution", Distribution) + monkeypatch.setattr( + build_metadata.importlib.metadata, + "distribution", + lambda name: Distribution(), + ) assert get_runtime_metadata() == { "name": "policyengine-core", From 8bf62e6c245e523d0ed92a3909aaec1de0a40598 Mon Sep 17 00:00:00 2001 From: Anthony Volk Date: Mon, 4 May 2026 21:10:31 +0200 Subject: [PATCH 5/5] Run bundle metadata contract on Python 3.14 --- .github/workflows/pr.yaml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/pr.yaml b/.github/workflows/pr.yaml index 1e1fd7dd0..ef16e3455 100644 --- a/.github/workflows/pr.yaml +++ b/.github/workflows/pr.yaml @@ -33,7 +33,7 @@ jobs: - name: Set up Python uses: actions/setup-python@v6 with: - python-version: "3.11" + python-version: "3.14" - name: Install uv uses: astral-sh/setup-uv@v8.1.0 - name: Install core