Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
14 changes: 13 additions & 1 deletion README.md
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down Expand Up @@ -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
5. Wait for CI checks to pass
45 changes: 44 additions & 1 deletion projects/policyengine-api-simulation/README.md
Original file line number Diff line number Diff line change
@@ -1,3 +1,46 @@
# policyengine-api-simulation

PolicyEngine Simulation API service.
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
```
4 changes: 3 additions & 1 deletion projects/policyengine-api-simulation/src/modal/app.py
Original file line number Diff line number Diff line change
Expand Up @@ -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:
Expand Down Expand Up @@ -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",
)
Expand Down
Original file line number Diff line number Diff line change
@@ -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)
)
Original file line number Diff line number Diff line change
@@ -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"
Expand All @@ -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
Loading