Skip to content

Commit 6092796

Browse files
authored
Merge pull request #130 from PolicyEngine/fix/alembic-head-issue
Fix Alembic multiple head revisions
2 parents 5b2cc84 + 7757b8e commit 6092796

File tree

9 files changed

+141
-39
lines changed

9 files changed

+141
-39
lines changed

CHANGELOG.md

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,10 @@
1+
0.3.1 (2026-03-11)
2+
3+
# Fixed
4+
5+
- Add Alembic merge migration to resolve multiple head revisions from parallel feature branches (#129)
6+
7+
18
0.3.0 (2026-03-11)
29

310
# Added

alembic/versions/20260305_0cbd97809414_add_adds_subtracts_to_variables.py

Lines changed: 7 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -5,26 +5,27 @@
55
Create Date: 2026-03-05 20:26:07.571012
66
77
"""
8+
89
from typing import Sequence, Union
910

1011
import sqlalchemy as sa
1112

1213
from alembic import op
1314

1415
# revision identifiers, used by Alembic.
15-
revision: str = '0cbd97809414'
16-
down_revision: Union[str, Sequence[str], None] = '886921687770'
16+
revision: str = "0cbd97809414"
17+
down_revision: Union[str, Sequence[str], None] = "886921687770"
1718
branch_labels: Union[str, Sequence[str], None] = None
1819
depends_on: Union[str, Sequence[str], None] = None
1920

2021

2122
def upgrade() -> None:
2223
"""Upgrade schema."""
23-
op.add_column('variables', sa.Column('adds', sa.JSON(), nullable=True))
24-
op.add_column('variables', sa.Column('subtracts', sa.JSON(), nullable=True))
24+
op.add_column("variables", sa.Column("adds", sa.JSON(), nullable=True))
25+
op.add_column("variables", sa.Column("subtracts", sa.JSON(), nullable=True))
2526

2627

2728
def downgrade() -> None:
2829
"""Downgrade schema."""
29-
op.drop_column('variables', 'subtracts')
30-
op.drop_column('variables', 'adds')
30+
op.drop_column("variables", "subtracts")
31+
op.drop_column("variables", "adds")
Lines changed: 30 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,30 @@
1+
"""merge_parallel_branches
2+
3+
Revision ID: db93db748457
4+
Revises: 0cbd97809414, add_variable_label, 67608331ee8a, add_filter_strategy
5+
Create Date: 2026-03-11 22:30:07.234183
6+
7+
"""
8+
9+
from typing import Sequence, Union
10+
11+
# revision identifiers, used by Alembic.
12+
revision: str = "db93db748457"
13+
down_revision: Union[str, Sequence[str], None] = (
14+
"0cbd97809414",
15+
"add_variable_label",
16+
"67608331ee8a",
17+
"add_filter_strategy",
18+
)
19+
branch_labels: Union[str, Sequence[str], None] = None
20+
depends_on: Union[str, Sequence[str], None] = None
21+
22+
23+
def upgrade() -> None:
24+
"""Upgrade schema."""
25+
pass
26+
27+
28+
def downgrade() -> None:
29+
"""Downgrade schema."""
30+
pass

pyproject.toml

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
[project]
22
name = "policyengine-api-v2"
3-
version = "0.3.0"
3+
version = "0.3.1"
44
description = "FastAPI service for PolicyEngine microsimulations"
55
readme = "README.md"
66
requires-python = ">=3.13"

src/policyengine_api/api/analysis.py

Lines changed: 18 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -857,15 +857,19 @@ def build_dynamic(dynamic_id):
857857
# Reconstruct scoping strategy from DB columns (if applicable)
858858
from policyengine_api.utils.strategy_reconstruction import reconstruct_strategy
859859

860-
baseline_region = session.get(Region, baseline_sim.region_id) if baseline_sim.region_id else None
860+
baseline_region = (
861+
session.get(Region, baseline_sim.region_id) if baseline_sim.region_id else None
862+
)
861863
baseline_strategy = reconstruct_strategy(
862864
filter_strategy=baseline_sim.filter_strategy,
863865
filter_field=baseline_sim.filter_field,
864866
filter_value=baseline_sim.filter_value,
865867
region_type=baseline_region.region_type.value if baseline_region else None,
866868
)
867869

868-
reform_region = session.get(Region, reform_sim.region_id) if reform_sim.region_id else None
870+
reform_region = (
871+
session.get(Region, reform_sim.region_id) if reform_sim.region_id else None
872+
)
869873
reform_strategy = reconstruct_strategy(
870874
filter_strategy=reform_sim.filter_strategy,
871875
filter_field=reform_sim.filter_field,
@@ -1038,15 +1042,19 @@ def build_dynamic(dynamic_id):
10381042
# Reconstruct scoping strategy from DB columns (if applicable)
10391043
from policyengine_api.utils.strategy_reconstruction import reconstruct_strategy
10401044

1041-
baseline_region = session.get(Region, baseline_sim.region_id) if baseline_sim.region_id else None
1045+
baseline_region = (
1046+
session.get(Region, baseline_sim.region_id) if baseline_sim.region_id else None
1047+
)
10421048
baseline_strategy = reconstruct_strategy(
10431049
filter_strategy=baseline_sim.filter_strategy,
10441050
filter_field=baseline_sim.filter_field,
10451051
filter_value=baseline_sim.filter_value,
10461052
region_type=baseline_region.region_type.value if baseline_region else None,
10471053
)
10481054

1049-
reform_region = session.get(Region, reform_sim.region_id) if reform_sim.region_id else None
1055+
reform_region = (
1056+
session.get(Region, reform_sim.region_id) if reform_sim.region_id else None
1057+
)
10501058
reform_strategy = reconstruct_strategy(
10511059
filter_strategy=reform_sim.filter_strategy,
10521060
filter_field=reform_sim.filter_field,
@@ -1249,7 +1257,9 @@ def economic_impact(
12491257
# Extract filter parameters from region (if present)
12501258
filter_field = region.filter_field if region and region.requires_filter else None
12511259
filter_value = region.filter_value if region and region.requires_filter else None
1252-
filter_strategy = region.filter_strategy if region and region.requires_filter else None
1260+
filter_strategy = (
1261+
region.filter_strategy if region and region.requires_filter else None
1262+
)
12531263

12541264
# Get model version
12551265
model_version = _get_model_version(request.tax_benefit_model_name, session)
@@ -1446,7 +1456,9 @@ def economy_custom(
14461456
region_obj.filter_value if region_obj and region_obj.requires_filter else None
14471457
)
14481458
filter_strategy = (
1449-
region_obj.filter_strategy if region_obj and region_obj.requires_filter else None
1459+
region_obj.filter_strategy
1460+
if region_obj and region_obj.requires_filter
1461+
else None
14501462
)
14511463

14521464
model_version = _get_model_version(request.tax_benefit_model_name, session)

tests/test_analysis.py

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -981,7 +981,9 @@ def test__given_constituency_region__then_region_has_weight_replacement_strategy
981981

982982
# Then
983983
assert resolved_region is not None
984-
assert resolved_region.filter_strategy == FILTER_STRATEGIES["WEIGHT_REPLACEMENT"]
984+
assert (
985+
resolved_region.filter_strategy == FILTER_STRATEGIES["WEIGHT_REPLACEMENT"]
986+
)
985987

986988
def test__given_national_region__then_filter_strategy_is_none(
987989
self, session: Session

tests/test_strategy_reconstruction.py

Lines changed: 27 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -25,7 +25,6 @@
2525
REGION_TYPES,
2626
)
2727

28-
2928
# ---------------------------------------------------------------------------
3029
# Mock strategy classes (match the real constructor signatures)
3130
# ---------------------------------------------------------------------------
@@ -277,9 +276,18 @@ def test__given_constituency_weight_replacement__then_gcs_config_matches(self):
277276
)
278277

279278
# Then
280-
assert result.weight_matrix_bucket == EXPECTED_CONSTITUENCY_CONFIG["weight_matrix_bucket"]
281-
assert result.weight_matrix_key == EXPECTED_CONSTITUENCY_CONFIG["weight_matrix_key"]
282-
assert result.lookup_csv_bucket == EXPECTED_CONSTITUENCY_CONFIG["lookup_csv_bucket"]
279+
assert (
280+
result.weight_matrix_bucket
281+
== EXPECTED_CONSTITUENCY_CONFIG["weight_matrix_bucket"]
282+
)
283+
assert (
284+
result.weight_matrix_key
285+
== EXPECTED_CONSTITUENCY_CONFIG["weight_matrix_key"]
286+
)
287+
assert (
288+
result.lookup_csv_bucket
289+
== EXPECTED_CONSTITUENCY_CONFIG["lookup_csv_bucket"]
290+
)
283291
assert result.lookup_csv_key == EXPECTED_CONSTITUENCY_CONFIG["lookup_csv_key"]
284292

285293
def test__given_local_authority_weight_replacement__then_returns_weight_replacement_instance(
@@ -306,10 +314,21 @@ def test__given_local_authority_weight_replacement__then_gcs_config_matches(self
306314
)
307315

308316
# Then
309-
assert result.weight_matrix_bucket == EXPECTED_LOCAL_AUTHORITY_CONFIG["weight_matrix_bucket"]
310-
assert result.weight_matrix_key == EXPECTED_LOCAL_AUTHORITY_CONFIG["weight_matrix_key"]
311-
assert result.lookup_csv_bucket == EXPECTED_LOCAL_AUTHORITY_CONFIG["lookup_csv_bucket"]
312-
assert result.lookup_csv_key == EXPECTED_LOCAL_AUTHORITY_CONFIG["lookup_csv_key"]
317+
assert (
318+
result.weight_matrix_bucket
319+
== EXPECTED_LOCAL_AUTHORITY_CONFIG["weight_matrix_bucket"]
320+
)
321+
assert (
322+
result.weight_matrix_key
323+
== EXPECTED_LOCAL_AUTHORITY_CONFIG["weight_matrix_key"]
324+
)
325+
assert (
326+
result.lookup_csv_bucket
327+
== EXPECTED_LOCAL_AUTHORITY_CONFIG["lookup_csv_bucket"]
328+
)
329+
assert (
330+
result.lookup_csv_key == EXPECTED_LOCAL_AUTHORITY_CONFIG["lookup_csv_key"]
331+
)
313332

314333

315334
# ---------------------------------------------------------------------------

tests/test_variable_labels.py

Lines changed: 47 additions & 16 deletions
Original file line numberDiff line numberDiff line change
@@ -1,13 +1,14 @@
11
"""Tests for variable label field across all variable endpoints."""
22

3-
import pytest
4-
5-
from test_fixtures.fixtures_variables import ( # noqa: F811
3+
from test_fixtures.fixtures_variables import (
64
create_variable,
7-
uk_model_version,
8-
us_model_version,
95
)
10-
6+
from test_fixtures.fixtures_variables import (
7+
uk_model_version as uk_model_version, # noqa: F811
8+
)
9+
from test_fixtures.fixtures_variables import (
10+
us_model_version as us_model_version, # noqa: F811
11+
)
1112

1213
# ---------------------------------------------------------------------------
1314
# GET /variables - label in list responses
@@ -18,7 +19,10 @@ class TestListVariablesLabel:
1819
"""Tests that label is returned when listing variables."""
1920

2021
def test_label_returned_in_response(
21-
self, client, session, us_model_version # noqa: F811
22+
self,
23+
client,
24+
session,
25+
us_model_version, # noqa: F811
2226
):
2327
"""Variable with a label should include it in the list response."""
2428
create_variable(
@@ -35,7 +39,10 @@ def test_label_returned_in_response(
3539
assert data[0]["label"] == "Employment income"
3640

3741
def test_null_label_returned_when_absent(
38-
self, client, session, us_model_version # noqa: F811
42+
self,
43+
client,
44+
session,
45+
us_model_version, # noqa: F811
3946
):
4047
"""Variable without a label should return null."""
4148
create_variable(
@@ -52,7 +59,10 @@ def test_null_label_returned_when_absent(
5259
assert data[0]["label"] is None
5360

5461
def test_empty_label_returned(
55-
self, client, session, us_model_version # noqa: F811
62+
self,
63+
client,
64+
session,
65+
us_model_version, # noqa: F811
5666
):
5767
"""Variable with an empty string label should return it as-is."""
5868
create_variable(
@@ -76,7 +86,10 @@ class TestSearchVariablesByLabel:
7686
"""Tests that the search parameter matches against labels."""
7787

7888
def test_search_matches_label(
79-
self, client, session, us_model_version # noqa: F811
89+
self,
90+
client,
91+
session,
92+
us_model_version, # noqa: F811
8093
):
8194
"""Searching for a term in the label should return the variable."""
8295
create_variable(
@@ -105,7 +118,10 @@ def test_search_matches_label(
105118
assert data[0]["name"] == "employment_income"
106119

107120
def test_search_label_case_insensitive(
108-
self, client, session, us_model_version # noqa: F811
121+
self,
122+
client,
123+
session,
124+
us_model_version, # noqa: F811
109125
):
110126
"""Label search should be case-insensitive."""
111127
create_variable(
@@ -126,7 +142,10 @@ def test_search_label_case_insensitive(
126142
assert len(response.json()) == 1
127143

128144
def test_search_partial_label_match(
129-
self, client, session, us_model_version # noqa: F811
145+
self,
146+
client,
147+
session,
148+
us_model_version, # noqa: F811
130149
):
131150
"""Partial label matches should be returned."""
132151
create_variable(
@@ -156,7 +175,10 @@ class TestGetVariableLabel:
156175
"""Tests that label is returned when fetching a single variable."""
157176

158177
def test_label_in_get_response(
159-
self, client, session, us_model_version # noqa: F811
178+
self,
179+
client,
180+
session,
181+
us_model_version, # noqa: F811
160182
):
161183
"""GET /variables/{id} should include the label field."""
162184
var = create_variable(
@@ -171,7 +193,10 @@ def test_label_in_get_response(
171193
assert response.json()["label"] == "Employment income"
172194

173195
def test_null_label_in_get_response(
174-
self, client, session, us_model_version # noqa: F811
196+
self,
197+
client,
198+
session,
199+
us_model_version, # noqa: F811
175200
):
176201
"""GET /variables/{id} should return null for missing label."""
177202
var = create_variable(
@@ -195,7 +220,10 @@ class TestVariablesByNameLabel:
195220
"""Tests that label is included in by-name lookup responses."""
196221

197222
def test_label_in_by_name_response(
198-
self, client, session, us_model_version # noqa: F811
223+
self,
224+
client,
225+
session,
226+
us_model_version, # noqa: F811
199227
):
200228
"""POST /variables/by-name should include the label field."""
201229
create_variable(
@@ -215,7 +243,10 @@ def test_label_in_by_name_response(
215243
assert data[0]["label"] == "Employment income"
216244

217245
def test_mixed_labels_in_by_name_response(
218-
self, client, session, us_model_version # noqa: F811
246+
self,
247+
client,
248+
session,
249+
us_model_version, # noqa: F811
219250
):
220251
"""Variables with and without labels should both be returned correctly."""
221252
create_variable(

uv.lock

Lines changed: 1 addition & 1 deletion
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

0 commit comments

Comments
 (0)