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
7 changes: 7 additions & 0 deletions src/policyengine_api/api/analysis.py
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,13 @@

This is an async operation - the endpoint returns immediately with a report_id,
and you poll /analysis/economic-impact/{report_id} until status is "completed".

WORKFLOW for full economic analysis:
1. Create a policy with parameter changes: POST /policies
2. Get a dataset: GET /datasets (look for UK/US datasets)
3. Start analysis: POST /analysis/economic-impact with policy_id and dataset_id
4. Check status: GET /analysis/economic-impact/{report_id} - repeat until status="completed"
5. Review results: The completed response includes decile_impacts and program_statistics
"""

import math
Expand Down
4 changes: 4 additions & 0 deletions src/policyengine_api/api/datasets.py
Original file line number Diff line number Diff line change
Expand Up @@ -23,6 +23,10 @@ def list_datasets(session: Session = Depends(get_session)):

Returns datasets that can be used with the /analysis/economic-impact endpoint.
Each dataset represents population microdata for a specific country and year.

USAGE: For UK analysis, look for datasets with names containing "uk" or "frs".
For US analysis, look for datasets with names containing "us" or "cps".
Use the dataset's id when calling /analysis/economic-impact.
"""
datasets = session.exec(select(Dataset)).all()
return datasets
Expand Down
22 changes: 19 additions & 3 deletions src/policyengine_api/api/parameters.py
Original file line number Diff line number Diff line change
Expand Up @@ -21,15 +21,31 @@
@router.get("/", response_model=List[ParameterRead])
@cache(expire=3600) # Cache for 1 hour
def list_parameters(
skip: int = 0, limit: int = 100, session: Session = Depends(get_session)
skip: int = 0,
limit: int = 100,
search: str | None = None,
session: Session = Depends(get_session),
):
"""List available parameters with pagination.
"""List available parameters with pagination and search.

Parameters are policy levers (e.g. tax rates, thresholds, benefit amounts)
that can be modified in reforms. Use parameter names when creating policies.

Use the `search` parameter to filter by parameter name, label, or description.
For example: search="basic_rate" or search="income tax"
"""
query = select(Parameter)

if search:
search_filter = (
Parameter.name.contains(search)
| Parameter.label.contains(search)
| Parameter.description.contains(search)
)
query = query.where(search_filter)

parameters = session.exec(
select(Parameter).order_by(Parameter.name).offset(skip).limit(limit)
query.order_by(Parameter.name).offset(skip).limit(limit)
).all()
return parameters

Expand Down
86 changes: 83 additions & 3 deletions src/policyengine_api/api/policies.py
Original file line number Diff line number Diff line change
Expand Up @@ -3,29 +3,109 @@
Policies represent tax-benefit parameter reforms that can be compared against
baseline (current law). Create a policy, then use its ID with the household
calculation or economic impact endpoints to see the reform's effects.

WORKFLOW: To analyze a policy reform (e.g. lowering UK basic income tax rate to 16%):

1. Search for the parameter: GET /parameters?search=basic_rate
2. Note the parameter_id from the results
3. Create a policy with parameter values:
POST /policies
{
"name": "Lower basic rate to 16p",
"description": "Reduce UK basic income tax rate from 20p to 16p",
"parameter_values": [
{
"parameter_id": "<uuid-from-step-1>",
"value_json": 0.16,
"start_date": "2026-01-01T00:00:00Z",
"end_date": null
}
]
}
4. Test on a household: POST /household/impact with the policy_id
5. Run population analysis: POST /analysis/economic-impact with policy_id and dataset_id
6. Poll GET /analysis/economic-impact/{report_id} until status="completed"
"""

from datetime import datetime
from typing import List
from uuid import UUID

from fastapi import APIRouter, Depends, HTTPException
from sqlmodel import Session, select

from policyengine_api.models import Policy, PolicyCreate, PolicyRead
from policyengine_api.models import (
Parameter,
ParameterValue,
Policy,
PolicyCreate,
PolicyRead,
)
from policyengine_api.services.database import get_session

router = APIRouter(prefix="/policies", tags=["policies"])


@router.post("/", response_model=PolicyRead)
def create_policy(policy: PolicyCreate, session: Session = Depends(get_session)):
"""Create a new policy reform.
"""Create a new policy reform with parameter values.

Policies define changes to tax-benefit parameters. After creating a policy,
use its ID with /household/calculate or /analysis/economic-impact to see effects.

Include parameter_values in the request to specify which parameters to change:
{
"name": "Lower basic rate to 16p",
"description": "Reduce UK basic income tax rate from 20p to 16p",
"parameter_values": [
{
"parameter_id": "uuid-from-parameters-search",
"value_json": 0.16,
"start_date": "2026-01-01T00:00:00Z",
"end_date": null
}
]
}
"""
db_policy = Policy.model_validate(policy)
# Create the policy
db_policy = Policy(name=policy.name, description=policy.description)
session.add(db_policy)
session.flush() # Get the policy ID before adding parameter values

# Create associated parameter values
for pv_data in policy.parameter_values:
# Validate parameter exists
param = session.get(Parameter, pv_data["parameter_id"])
if not param:
raise HTTPException(
status_code=404,
detail=f"Parameter {pv_data['parameter_id']} not found",
)

# Parse dates
start_date = (
datetime.fromisoformat(pv_data["start_date"].replace("Z", "+00:00"))
if isinstance(pv_data["start_date"], str)
else pv_data["start_date"]
)
end_date = None
if pv_data.get("end_date"):
end_date = (
datetime.fromisoformat(pv_data["end_date"].replace("Z", "+00:00"))
if isinstance(pv_data["end_date"], str)
else pv_data["end_date"]
)

# Create parameter value
db_pv = ParameterValue(
parameter_id=pv_data["parameter_id"],
value_json=pv_data["value_json"],
start_date=start_date,
end_date=end_date,
policy_id=db_policy.id,
)
session.add(db_pv)

session.commit()
session.refresh(db_policy)
return db_policy
Expand Down
26 changes: 23 additions & 3 deletions src/policyengine_api/models/policy.py
Original file line number Diff line number Diff line change
Expand Up @@ -29,9 +29,29 @@ class Policy(PolicyBase, table=True):


class PolicyCreate(PolicyBase):
"""Schema for creating policies."""

pass
"""Schema for creating policies.

When creating a policy with parameter values, provide a list of
parameter value definitions. Each parameter value needs:
- parameter_id: UUID of the parameter to modify
- value_json: The new value (number, string, or nested object)
- start_date: When this value takes effect
- end_date: Optional end date (null for indefinite)

Example:
{
"name": "Lower basic rate to 16p",
"description": "Reduce UK basic income tax rate",
"parameter_values": [{
"parameter_id": "uuid-here",
"value_json": 0.16,
"start_date": "2026-01-01T00:00:00Z",
"end_date": null
}]
}
"""

parameter_values: list[dict] = []


class PolicyRead(PolicyBase):
Expand Down