Skip to content
Merged
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
11 changes: 11 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
@@ -1,3 +1,14 @@
0.4.0 (2026-03-13)

# Added

- Standardize all endpoints on `country_id` instead of `tax_benefit_model_name` (#109)
- Default metadata endpoints (variables, parameters, datasets) to latest model version with optional version pinning (#109)
- Dual policy IDs (`baseline_policy_id` / `reform_policy_id`) on reports and `EXECUTION_DEFERRED` report status (#109)
- Auto-start `EXECUTION_DEFERRED` reports on GET endpoint access (#109)
- Convert VARCHAR enum columns to native PostgreSQL enums with `values_callable` (#109)


0.3.1 (2026-03-11)

# Fixed
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,85 @@
"""rename_tax_benefit_model_name_to_country_id

Revision ID: 62385cd8049d
Revises: 886921687770
Create Date: 2026-03-09 16:48:30.899791

"""

from typing import Sequence, Union

import sqlalchemy as sa
import sqlmodel.sql.sqltypes

from alembic import op

# revision identifiers, used by Alembic.
revision: str = "62385cd8049d"
down_revision: Union[str, Sequence[str], None] = "886921687770"
branch_labels: Union[str, Sequence[str], None] = None
depends_on: Union[str, Sequence[str], None] = None


def upgrade() -> None:
"""Upgrade schema: rename tax_benefit_model_name → country_id with data migration."""
# 1. Add country_id columns (nullable initially)
op.add_column(
"households",
sa.Column("country_id", sqlmodel.sql.sqltypes.AutoString(), nullable=True),
)
op.add_column(
"household_jobs",
sa.Column("country_id", sqlmodel.sql.sqltypes.AutoString(), nullable=True),
)

# 2. Populate country_id from tax_benefit_model_name
op.execute("""
UPDATE households SET country_id = CASE
WHEN tax_benefit_model_name LIKE '%_us' OR tax_benefit_model_name LIKE '%-us' THEN 'us'
WHEN tax_benefit_model_name LIKE '%_uk' OR tax_benefit_model_name LIKE '%-uk' THEN 'uk'
ELSE 'us'
END
""")
op.execute("""
UPDATE household_jobs SET country_id = CASE
WHEN tax_benefit_model_name LIKE '%_us' OR tax_benefit_model_name LIKE '%-us' THEN 'us'
WHEN tax_benefit_model_name LIKE '%_uk' OR tax_benefit_model_name LIKE '%-uk' THEN 'uk'
ELSE 'us'
END
""")

# 3. Make country_id non-nullable
op.alter_column("households", "country_id", nullable=False)
op.alter_column("household_jobs", "country_id", nullable=False)

# 4. Drop old columns
op.drop_column("households", "tax_benefit_model_name")
op.drop_column("household_jobs", "tax_benefit_model_name")


def downgrade() -> None:
"""Downgrade schema: restore tax_benefit_model_name from country_id."""
# 1. Re-add tax_benefit_model_name columns (nullable initially)
op.add_column(
"households", sa.Column("tax_benefit_model_name", sa.VARCHAR(), nullable=True)
)
op.add_column(
"household_jobs",
sa.Column("tax_benefit_model_name", sa.VARCHAR(), nullable=True),
)

# 2. Populate from country_id
op.execute(
"UPDATE households SET tax_benefit_model_name = 'policyengine_' || country_id"
)
op.execute(
"UPDATE household_jobs SET tax_benefit_model_name = 'policyengine_' || country_id"
)

# 3. Make non-nullable
op.alter_column("households", "tax_benefit_model_name", nullable=False)
op.alter_column("household_jobs", "tax_benefit_model_name", nullable=False)

# 4. Drop country_id columns
op.drop_column("households", "country_id")
op.drop_column("household_jobs", "country_id")
Original file line number Diff line number Diff line change
@@ -0,0 +1,31 @@
"""add_execution_deferred_to_reportstatus

Revision ID: f887cb5490bc
Revises: 62385cd8049d
Create Date: 2026-03-10 21:27:32.072364

"""

from typing import Sequence, Union

from alembic import op

# revision identifiers, used by Alembic.
revision: str = "f887cb5490bc"
down_revision: Union[str, Sequence[str], None] = "62385cd8049d"
branch_labels: Union[str, Sequence[str], None] = None
depends_on: Union[str, Sequence[str], None] = None


def upgrade() -> None:
"""Add EXECUTION_DEFERRED value to the reportstatus enum."""
op.execute("ALTER TYPE reportstatus ADD VALUE IF NOT EXISTS 'EXECUTION_DEFERRED'")


def downgrade() -> None:
"""Downgrade: PostgreSQL does not support removing enum values.

The 'EXECUTION_DEFERRED' value will remain in the enum type.
To fully remove it, drop and recreate the type (requires migrating data).
"""
pass
Original file line number Diff line number Diff line change
@@ -0,0 +1,92 @@
"""convert_varchar_enums_to_native_pg_enums

Revision ID: dac22a838dda
Revises: f887cb5490bc
Create Date: 2026-03-11 01:37:08.928795

"""

from typing import Sequence, Union

from alembic import op

# revision identifiers, used by Alembic.
revision: str = "dac22a838dda"
down_revision: Union[str, Sequence[str], None] = "f887cb5490bc"
branch_labels: Union[str, Sequence[str], None] = None
depends_on: Union[str, Sequence[str], None] = None


def upgrade() -> None:
"""Convert VARCHAR enum columns to native PostgreSQL enum types.

The enum types may already exist with UPPERCASE values (created by
SQLAlchemy's default create_all behavior). Since the columns are still
VARCHAR, the types are unused — drop and recreate with lowercase values
matching the data and the values_callable convention.
"""
# Drop any pre-existing enum types (unused — columns are still VARCHAR)
op.execute("DROP TYPE IF EXISTS regiontype CASCADE")
op.execute("DROP TYPE IF EXISTS reporttype CASCADE")
op.execute("DROP TYPE IF EXISTS deciletype CASCADE")

# Create PG enum types with lowercase values
op.execute("""
CREATE TYPE regiontype AS ENUM (
'national', 'country', 'state', 'congressional_district',
'constituency', 'local_authority', 'city', 'place'
)
""")
op.execute("""
CREATE TYPE reporttype AS ENUM (
'economy_comparison', 'household_comparison', 'household_single'
)
""")
op.execute("CREATE TYPE deciletype AS ENUM ('income', 'wealth')")

# Alter columns from VARCHAR to enum.
# LOWER() handles any databases where values were previously uppercased.
op.execute("""
ALTER TABLE regions
ALTER COLUMN region_type TYPE regiontype
USING LOWER(region_type)::regiontype
""")
op.execute("""
ALTER TABLE reports
ALTER COLUMN report_type TYPE reporttype
USING LOWER(report_type)::reporttype
""")
# decile_type has a VARCHAR default that must be dropped before type change
op.execute("ALTER TABLE intra_decile_impacts ALTER COLUMN decile_type DROP DEFAULT")
op.execute("""
ALTER TABLE intra_decile_impacts
ALTER COLUMN decile_type TYPE deciletype
USING LOWER(decile_type)::deciletype
""")
op.execute(
"ALTER TABLE intra_decile_impacts ALTER COLUMN decile_type SET DEFAULT 'income'::deciletype"
)


def downgrade() -> None:
"""Revert native PG enum columns back to VARCHAR."""
op.execute("""
ALTER TABLE regions
ALTER COLUMN region_type TYPE VARCHAR
USING region_type::text
""")
op.execute("""
ALTER TABLE reports
ALTER COLUMN report_type TYPE VARCHAR
USING report_type::text
""")
op.execute("""
ALTER TABLE intra_decile_impacts
ALTER COLUMN decile_type TYPE VARCHAR
USING decile_type::text
""")

# Drop the PG enum types
op.execute("DROP TYPE IF EXISTS regiontype")
op.execute("DROP TYPE IF EXISTS reporttype")
op.execute("DROP TYPE IF EXISTS deciletype")
5 changes: 3 additions & 2 deletions scripts/seed_regions.py
Original file line number Diff line number Diff line change
Expand Up @@ -33,6 +33,7 @@
RegionDatasetLink,
TaxBenefitModel,
)
from policyengine_api.models.region import RegionType # noqa: E402


def _group_us_datasets(
Expand Down Expand Up @@ -195,7 +196,7 @@ def seed_us_regions(
db_region = Region(
code=pe_region.code,
label=pe_region.label,
region_type=pe_region.region_type,
region_type=RegionType(pe_region.region_type),
requires_filter=pe_region.requires_filter,
filter_field=pe_region.filter_field,
filter_value=pe_region.filter_value,
Expand Down Expand Up @@ -293,7 +294,7 @@ def seed_uk_regions(session: Session) -> tuple[int, int, int]:
db_region = Region(
code=pe_region.code,
label=pe_region.label,
region_type=pe_region.region_type,
region_type=RegionType(pe_region.region_type),
requires_filter=pe_region.requires_filter,
filter_field=pe_region.filter_field,
filter_value=pe_region.filter_value,
Expand Down
14 changes: 7 additions & 7 deletions src/policyengine_api/agent_sandbox.py
Original file line number Diff line number Diff line change
Expand Up @@ -53,9 +53,9 @@ def configure_logfire(traceparent: str | None = None):

## CRITICAL: Always filter by country

When searching for parameters or datasets, ALWAYS include tax_benefit_model_name:
- "policyengine-uk" for UK questions
- "policyengine-us" for US questions
When searching for parameters or datasets, ALWAYS include country_id:
- "uk" for UK questions
- "us" for US questions

Parameters and datasets from both countries are in the same database. Without the filter, you'll get mixed results and waste turns finding the right ones.

Expand All @@ -66,14 +66,14 @@ def configure_logfire(traceparent: str | None = None):
- Poll GET /household/calculate/{job_id} until completed

2. **Parameter lookup**:
- GET /parameters/?search=...&tax_benefit_model_name=policyengine-uk (ALWAYS include country filter)
- GET /parameters/?search=...&country_id=uk (ALWAYS include country filter)
- GET /parameter-values/?parameter_id=...&current=true for the current value

3. **Economic impact analysis** (budget impact, decile impacts):
- GET /parameters/?search=...&tax_benefit_model_name=policyengine-uk to find parameter_id
- GET /parameters/?search=...&country_id=uk to find parameter_id
- POST /policies/ to create reform with parameter_values
- GET /datasets/?tax_benefit_model_name=policyengine-uk to find dataset_id
- POST /analysis/economic-impact with tax_benefit_model_name, policy_id and dataset_id
- GET /datasets/?country_id=uk to find dataset_id
- POST /analysis/economic-impact with country_id, policy_id and dataset_id
- GET /analysis/economic-impact/{report_id} for results (includes decile_impacts and program_statistics)

## Response formatting
Expand Down
Loading
Loading