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
40 changes: 40 additions & 0 deletions .github/bump_version.py
Original file line number Diff line number Diff line change
Expand Up @@ -153,12 +153,51 @@ def sync_release_manifest_versions(manifest_dir: Path, new_version: str):
print(f" Updated {manifest_path}")


def sync_stack_versions(stack_path: Path, new_version: str):
if not stack_path.exists():
return
text = stack_path.read_text()
replacements = [
(
r'(^stack_version\s*=\s*")([^"]+)(")',
rf"\g<1>{new_version}\g<3>",
),
(
r'(^policyengine_version\s*=\s*")([^"]+)(")',
rf"\g<1>{new_version}\g<3>",
),
(
r'(\[packages\.policyengine\]\s+name\s*=\s*"policyengine"\s+version\s*=\s*")([^"]+)(")',
rf"\g<1>{new_version}\g<3>",
),
]
updated = text
for pattern, replacement in replacements:
updated, count = re.subn(
pattern,
replacement,
updated,
count=1,
flags=re.MULTILINE,
)
if count == 0:
print(
f"Could not update {stack_path}: missing stack version field.",
file=sys.stderr,
)
sys.exit(1)
if updated != text:
stack_path.write_text(updated)
print(f" Updated {stack_path}")


def main():
root = Path(__file__).resolve().parent.parent
pyproject = root / "pyproject.toml"
changelog = root / "CHANGELOG.md"
changelog_dir = root / "changelog.d"
manifest_dir = root / "src" / "policyengine" / "data" / "release_manifests"
stack_path = root / "policyengine-stack.toml"

current = get_current_version(pyproject, changelog, root)
bump = infer_bump(changelog_dir)
Expand All @@ -168,6 +207,7 @@ def main():

update_file(pyproject, new)
sync_release_manifest_versions(manifest_dir, new)
sync_stack_versions(stack_path, new)


if __name__ == "__main__":
Expand Down
22 changes: 22 additions & 0 deletions .github/workflows/pr_code_changes.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -6,8 +6,11 @@ on:
paths:
- src/**
- tests/**
- scripts/**
- .github/**
- changelog.d/**
- pyproject.toml
- policyengine-stack.toml
workflow_dispatch:

jobs:
Expand Down Expand Up @@ -69,6 +72,25 @@ jobs:
run: uv pip install --system . h5py
- name: Smoke-import core modules
run: python -c "import policyengine; from policyengine.core import Dataset, Policy, Simulation; from policyengine.outputs import aggregate, poverty, inequality; print('import OK')"
StackVerification:
name: Verify stack metadata
runs-on: ubuntu-latest
env:
POLICYENGINE_SKIP_COUNTRY_IMPORTS: "1"
steps:
- uses: actions/checkout@v6
- name: Install uv
uses: astral-sh/setup-uv@v8.1.0
- name: Set up Python
uses: actions/setup-python@v6
with:
python-version: '3.13'
- name: Check generated stack artifacts
run: python scripts/generate_stack_artifacts.py --check
- name: Install stack model extra
run: uv pip install -e ".[models]" --system
- name: Verify stack
run: policyengine stack verify --extra models --check-uris
Test:
runs-on: macos-latest
strategy:
Expand Down
19 changes: 17 additions & 2 deletions .github/workflows/push.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -117,6 +117,8 @@ jobs:
python-version: '3.13'
- name: Build changelog
run: pip install yaml-changelog towncrier && make changelog
- name: Generate stack artifacts
run: python scripts/generate_stack_artifacts.py
- name: Preview changelog update
run: ".github/get-changelog-diff.sh"
- name: Install package for TRO regeneration
Expand All @@ -125,7 +127,7 @@ jobs:
env:
HUGGING_FACE_TOKEN: ${{ secrets.HUGGING_FACE_TOKEN }}
run: python scripts/generate_trace_tros.py
- name: Update changelog and TROs
- name: Update changelog, stack metadata, and TROs
uses: EndBug/add-and-commit@v9
with:
add: "."
Expand Down Expand Up @@ -154,6 +156,15 @@ jobs:
run: ".github/publish-git-tag.sh"
- name: Build package
run: python -m build
- name: Export stack release assets
run: python scripts/export_stack_release_assets.py --dist-dir dist
- name: Verify stack release metadata
env:
POLICYENGINE_SKIP_COUNTRY_IMPORTS: "1"
run: |
VERSION=$(python .github/fetch_version.py)
policyengine stack verify --extra models --check-uris --json \
> "dist/policyengine-stack-$VERSION.verification.json"
- name: Publish a Python distribution to PyPI
uses: pypa/gh-action-pypi-publish@release/v1
with:
Expand All @@ -166,4 +177,8 @@ jobs:
gh release create "$VERSION" \
--title "v$VERSION" \
--notes "See [CHANGELOG.md](https://github.com/PolicyEngine/policyengine.py/blob/main/CHANGELOG.md) for details." \
--latest
--latest \
"dist/policyengine-stack-$VERSION.json" \
"dist/policyengine-stack-$VERSION.constraints.txt" \
"dist/policyengine-stack-$VERSION.citation.txt" \
"dist/policyengine-stack-$VERSION.verification.json"
1 change: 1 addition & 0 deletions changelog.d/stack-system.added.md
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
Add a pip-native PolicyEngine stack manifest, generated extras, stack CLI, and fast stack verification workflow.
41 changes: 41 additions & 0 deletions docs/stacks.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,41 @@
# PolicyEngine stacks

A PolicyEngine stack is the exact first-party package set certified for a
`policyengine` release. Installation is standard pip:

```bash
pip install "policyengine[full]==4.19.1"
pip install "policyengine[us-full]==4.19.1"
pip install "policyengine[models]==4.19.1"
```

The stack source of truth is `policyengine-stack.toml`. Generated artifacts are:

- `pyproject.toml` extras
- `src/policyengine/data/stack/manifest.json`
- GitHub release assets exported from the packaged manifest

## Stack-only PRs

Run:

```bash
python scripts/prepare_stack_update.py \
--core 3.27.0 \
--us 1.730.0 \
--uk 2.91.0 \
--us-data 1.118.0 \
--uk-data 1.45.0
```

This updates stack metadata and creates a patch changelog fragment. Do not bump
the `policyengine` version manually in the PR; the existing release workflow
bumps the package and stack versions together after merge.

CI checks generated artifacts, installs `.[models]`, and verifies the packaged
stack metadata with lightweight URI checks. Full data
package installation is available through `policyengine[full]`; this includes
both `policyengine-us-data` and `policyengine-uk-data` when their package
versions are installable for the target Python/platform. Dataset artifact
versions and release manifest URIs are recorded separately in the stack manifest
for citation and verification.
72 changes: 72 additions & 0 deletions policyengine-stack.toml
Original file line number Diff line number Diff line change
@@ -0,0 +1,72 @@
schema_version = 1
stack_version = "4.4.2"
policyengine_version = "4.4.2"

[packages.policyengine]
name = "policyengine"
version = "4.4.2"
import_name = "policyengine"
role = "stack_carrier"

[packages.policyengine-core]
name = "policyengine-core"
version = "3.26.1"
import_name = "policyengine_core"
role = "runtime_dependency"

[packages.policyengine-us]
name = "policyengine-us"
version = "1.687.0"
import_name = "policyengine_us"
role = "country_model"
country = "us"

[packages.policyengine-uk]
name = "policyengine-uk"
version = "2.88.14"
import_name = "policyengine_uk"
role = "country_model"
country = "uk"

[packages.policyengine-us-data]
name = "policyengine-us-data"
version = "1.78.2"
import_name = "policyengine_us_data"
role = "country_data"
country = "us"
optional = true
markers = "python_version >= '3.12' and python_version < '3.15'"

[packages.policyengine-uk-data]
name = "policyengine-uk-data"
version = "1.11.1"
import_name = "policyengine_uk_data"
role = "country_data"
country = "uk"
optional = true

[extras]
models = ["policyengine-core", "policyengine-us", "policyengine-uk"]
data = ["policyengine-us-data", "policyengine-uk-data"]
full = ["policyengine-core", "policyengine-us", "policyengine-uk", "policyengine-us-data", "policyengine-uk-data"]
us = ["policyengine-core", "policyengine-us"]
uk = ["policyengine-core", "policyengine-uk"]
us-data = ["policyengine-us-data"]
uk-data = ["policyengine-uk-data"]
us-full = ["policyengine-core", "policyengine-us", "policyengine-us-data"]
uk-full = ["policyengine-core", "policyengine-uk", "policyengine-uk-data"]

[countries.us]
model_package = "policyengine-us"
data_package = "policyengine-us-data"
default_dataset = "enhanced_cps_2024"
default_dataset_uri = "hf://policyengine/policyengine-us-data/enhanced_cps_2024.h5@1.78.2"
release_manifest_uri = "https://huggingface.co/policyengine/policyengine-us-data/resolve/9cb665df0a546f9c3d79b496f8eb2dd55859d38d/releases/1.78.2/release_manifest.json"

[countries.uk]
model_package = "policyengine-uk"
data_package = "policyengine-uk-data"
data_artifact_version = "1.55.5"
default_dataset = "enhanced_frs_2023_24"
default_dataset_uri = "hf://policyengine/policyengine-uk-data-private/enhanced_frs_2023_24.h5@1.55.5"
release_manifest_uri = "https://huggingface.co/policyengine/policyengine-uk-data-private/resolve/1.55.5/release_manifest.json"
44 changes: 38 additions & 6 deletions pyproject.toml
Original file line number Diff line number Diff line change
Expand Up @@ -40,14 +40,46 @@ plotting = [
graph = [
"networkx>=3.0",
]
uk = [
"policyengine_core>=3.26.0",
models = [
"policyengine-core==3.26.1",
"policyengine-us==1.687.0",
"policyengine-uk==2.88.14",
]
data = [
"policyengine-us-data==1.78.2; python_version >= '3.12' and python_version < '3.15'",
"policyengine-uk-data==1.11.1",
]
full = [
"policyengine-core==3.26.1",
"policyengine-us==1.687.0",
"policyengine-uk==2.88.14",
"policyengine-us-data==1.78.2; python_version >= '3.12' and python_version < '3.15'",
"policyengine-uk-data==1.11.1",
]
us = [
"policyengine_core>=3.26.0",
"policyengine-core==3.26.1",
"policyengine-us==1.687.0",
]
uk = [
"policyengine-core==3.26.1",
"policyengine-uk==2.88.14",
]
us-data = [
"policyengine-us-data==1.78.2; python_version >= '3.12' and python_version < '3.15'",
]
uk-data = [
"policyengine-uk-data==1.11.1",
]
us-full = [
"policyengine-core==3.26.1",
"policyengine-us==1.687.0",
"policyengine-us-data==1.78.2; python_version >= '3.12' and python_version < '3.15'",
]
uk-full = [
"policyengine-core==3.26.1",
"policyengine-uk==2.88.14",
"policyengine-uk-data==1.11.1",
]
dev = [
"pytest",
"furo",
Expand All @@ -59,12 +91,12 @@ dev = [
"plotly>=5.0.0",
"pytest-asyncio>=0.26.0",
"ruff>=0.9.0",
"policyengine_core>=3.26.0",
"policyengine-uk==2.88.14",
"policyengine-us==1.687.0",
"towncrier>=24.8.0",
"mypy>=1.11.0",
"pytest-cov>=5.0.0",
"policyengine-core==3.26.1",
"policyengine-us==1.687.0",
"policyengine-uk==2.88.14",
]

[tool.setuptools]
Expand Down
54 changes: 54 additions & 0 deletions scripts/export_stack_release_assets.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,54 @@
"""Export stack metadata files for the GitHub release."""

from __future__ import annotations

import argparse
import json
from pathlib import Path

from generate_stack_artifacts import REPO_ROOT, STACK_MANIFEST


def main() -> int:
parser = argparse.ArgumentParser()
parser.add_argument("--dist-dir", type=Path, default=REPO_ROOT / "dist")
args = parser.parse_args()

stack = json.loads(STACK_MANIFEST.read_text())
version = stack["stack_version"]
args.dist_dir.mkdir(parents=True, exist_ok=True)

manifest_path = args.dist_dir / f"policyengine-stack-{version}.json"
manifest_path.write_text(json.dumps(stack, indent=2, sort_keys=True) + "\n")

constraints_path = args.dist_dir / f"policyengine-stack-{version}.constraints.txt"
full_packages = ["policyengine", *stack["extras"]["full"]]
constraints = [
stack["packages"][package]["install_requirement"] for package in full_packages
]
constraints_path.write_text("\n".join(constraints) + "\n")

citation_path = args.dist_dir / f"policyengine-stack-{version}.citation.txt"
citation_path.write_text(
"\n".join(
[
f"PolicyEngine stack {version}",
f"PolicyEngine package version: {stack['policyengine_version']}",
"Components:",
*(
f"- {component['name']} {component['version']}"
for _, component in sorted(stack["packages"].items())
),
]
)
+ "\n"
)

print(manifest_path)
print(constraints_path)
print(citation_path)
return 0


if __name__ == "__main__":
raise SystemExit(main())
Loading