diff --git a/README.md b/README.md index 6f6f75ce1..c94869a6f 100644 --- a/README.md +++ b/README.md @@ -38,6 +38,18 @@ The repository contains three main API services: Each service generates OpenAPI specs and Python client libraries for integration testing. +For society-wide report output, the request path is still: + +`app-v2 -> policyengine-api (legacy api-v1 broker) -> policyengine-api-v2 simulation service -> policyengine.py` + +That distinction matters because a frontend change may still need a simulation deploy even when `app-v2` does not call `api-v2` directly. + +### `policyengine` compatibility policy + +- `projects/policyengine-api-simulation` intentionally tracks the `policyengine` `0.x` maintenance line. +- Do not bump it to `1+` without an explicit migration plan for the legacy broker and integration suite. +- The simulation project's `pyproject.toml` is the source of truth for the pinned `policyengine` version; the Modal image reads from that file rather than repeating a separate hardcoded pin. + ## Development workflow ### Making changes @@ -173,4 +185,4 @@ Configure GitHub environments with these variables: 2. Make changes and test locally 3. Ensure `make test-complete` passes 4. Open a PR with a clear description -5. Wait for CI checks to pass \ No newline at end of file +5. Wait for CI checks to pass diff --git a/projects/policyengine-api-simulation/README.md b/projects/policyengine-api-simulation/README.md index 18babb5ae..94afd5015 100644 --- a/projects/policyengine-api-simulation/README.md +++ b/projects/policyengine-api-simulation/README.md @@ -1,3 +1,46 @@ # policyengine-api-simulation -PolicyEngine Simulation API service. \ No newline at end of file +Economic comparison service for PolicyEngine's API stack. + +## Where this service sits + +- `app-v2` still fetches reports and economy comparisons from `https://api.policyengine.org` (`policyengine-api`, the legacy broker). +- The legacy broker forwards society-wide economy comparison work to this simulation service. +- In production, the gateway routes requests to a versioned Modal app built from [src/modal/app.py](./src/modal/app.py). + +That means changes to report-output payloads can require coordinated work across three repos: + +1. `policyengine.py` for the underlying calculation output +2. `policyengine-api-v2` for the simulation runtime pin and deployment +3. `policyengine-api` and/or `policyengine-app-v2` for cache and UI handling + +## `policyengine` version policy + +This service intentionally tracks the `policyengine` `0.x` maintenance line. + +- Do not bump to `1+` just because the main `policyengine.py` repo has newer releases. +- The legacy broker and report/economy contracts still depend on the pre-`1.0` API surface. +- A `1+` migration needs an explicit compatibility plan across `policyengine-api`, integration tests, and production rollout. + +## Source of truth for the `policyengine` pin + +[pyproject.toml](./pyproject.toml) is the source of truth for the pinned `policyengine` version. + +- [src/modal/policyengine_dependency.py](./src/modal/policyengine_dependency.py) reads that pin for Modal image builds. +- [tests/test_policyengine_dependency_source.py](./tests/test_policyengine_dependency_source.py) verifies the helper and Modal app stay aligned with `pyproject.toml`. + +If you need to bump `policyengine`, update the dependency in `pyproject.toml`, then run the checks below. + +## Local checks + +```bash +uv run pytest tests/test_policyengine_dependency_source.py +uv run pytest +docker build -f projects/policyengine-api-simulation/Dockerfile . +``` + +Deploy the simulation service with: + +```bash +modal deploy src/modal/app.py +``` diff --git a/projects/policyengine-api-simulation/src/modal/app.py b/projects/policyengine-api-simulation/src/modal/app.py index 51463ac6a..952c6d3ea 100644 --- a/projects/policyengine-api-simulation/src/modal/app.py +++ b/projects/policyengine-api-simulation/src/modal/app.py @@ -11,10 +11,12 @@ import os from src.modal._image_setup import snapshot_models +from src.modal.policyengine_dependency import get_policyengine_dependency # Get versions from environment or use defaults US_VERSION = os.environ.get("POLICYENGINE_US_VERSION", "1.562.3") UK_VERSION = os.environ.get("POLICYENGINE_UK_VERSION", "2.65.9") +POLICYENGINE_DEPENDENCY = get_policyengine_dependency() def get_app_name(us_version: str, uk_version: str) -> str: @@ -47,7 +49,7 @@ def get_app_name(us_version: str, uk_version: str) -> str: .pip_install( f"policyengine-us=={US_VERSION}", f"policyengine-uk=={UK_VERSION}", - "policyengine==0.13.0", + POLICYENGINE_DEPENDENCY, "tables>=3.10.2", "logfire", ) diff --git a/projects/policyengine-api-simulation/src/modal/policyengine_dependency.py b/projects/policyengine-api-simulation/src/modal/policyengine_dependency.py new file mode 100644 index 000000000..6d599bd99 --- /dev/null +++ b/projects/policyengine-api-simulation/src/modal/policyengine_dependency.py @@ -0,0 +1,21 @@ +"""Helpers for keeping the simulation service's policyengine pin in one place.""" + +from pathlib import Path +import tomllib + +POLICYENGINE_DEPENDENCY_PREFIX = "policyengine==" +PROJECT_ROOT = Path(__file__).resolve().parents[2] +PYPROJECT_PATH = PROJECT_ROOT / "pyproject.toml" + + +def get_policyengine_dependency() -> str: + """Read the pinned policyengine dependency from pyproject.toml.""" + with PYPROJECT_PATH.open("rb") as file: + pyproject = tomllib.load(file) + + dependencies = pyproject["project"]["dependencies"] + return next( + dependency + for dependency in dependencies + if dependency.startswith(POLICYENGINE_DEPENDENCY_PREFIX) + ) diff --git a/projects/policyengine-api-simulation/tests/test_policyengine_dependency_source.py b/projects/policyengine-api-simulation/tests/test_policyengine_dependency_source.py index bef75549d..2fd7c87e2 100644 --- a/projects/policyengine-api-simulation/tests/test_policyengine_dependency_source.py +++ b/projects/policyengine-api-simulation/tests/test_policyengine_dependency_source.py @@ -1,9 +1,10 @@ """Regression tests for the policyengine dependency version configuration.""" -import re import tomllib from pathlib import Path +from src.modal.policyengine_dependency import get_policyengine_dependency + REPO_ROOT = Path(__file__).parent.parent PYPROJECT_PATH = REPO_ROOT / "pyproject.toml" MODAL_APP_PATH = REPO_ROOT / "src" / "modal" / "app.py" @@ -22,19 +23,20 @@ def _get_pyproject_policyengine_dependency(pyproject: dict) -> str: ) -def _get_modal_policyengine_dependency(modal_source: str) -> str: - match = re.search( - r'"(policyengine==[^"]+)"', - modal_source, - ) - assert match is not None, "Modal app should install a pinned policyengine version" - return match.group(1) - - -def test_policyengine_dependency_version_is_pinned_consistently(): +def test_policyengine_dependency_version_is_read_from_pyproject(): pyproject = _load_toml(PYPROJECT_PATH) pyproject_dependency = _get_pyproject_policyengine_dependency(pyproject) - modal_dependency = _get_modal_policyengine_dependency(MODAL_APP_PATH.read_text()) assert pyproject_dependency.startswith(POLICYENGINE_DEPENDENCY_PREFIX) - assert modal_dependency == pyproject_dependency + assert get_policyengine_dependency() == pyproject_dependency + + +def test_modal_app_uses_shared_dependency_helper(): + modal_source = MODAL_APP_PATH.read_text() + + assert ( + "from src.modal.policyengine_dependency import get_policyengine_dependency" + in modal_source + ) + assert "POLICYENGINE_DEPENDENCY = get_policyengine_dependency()" in modal_source + assert "POLICYENGINE_DEPENDENCY," in modal_source