Skip to content

Commit 948f336

Browse files
authored
Merge pull request #2038 from PolicyEngine/fix/1988-migrate-household
Migrate `household` endpoints to new API structure
2 parents 62ab0c2 + 3268c82 commit 948f336

27 files changed

+1122
-419
lines changed

changelog_entry.yaml

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,4 @@
1+
- bump: minor
2+
changes:
3+
changed:
4+
- Refactored household endpoints to match new API structure

policyengine_api/api.py

Lines changed: 8 additions & 19 deletions
Original file line numberDiff line numberDiff line change
@@ -13,7 +13,9 @@
1313
# from werkzeug.middleware.profiler import ProfilerMiddleware
1414

1515
# Endpoints
16+
from policyengine_api.routes.error_routes import error_bp
1617
from policyengine_api.routes.economy_routes import economy_bp
18+
from policyengine_api.routes.household_routes import household_bp
1719
from policyengine_api.routes.simulation_analysis_routes import (
1820
simulation_analysis_bp,
1921
)
@@ -22,9 +24,6 @@
2224

2325
from .endpoints import (
2426
get_home,
25-
get_household,
26-
post_household,
27-
update_household,
2827
get_policy,
2928
set_policy,
3029
get_policy_search,
@@ -56,19 +55,13 @@
5655

5756
CORS(app)
5857

58+
app.register_blueprint(error_bp)
59+
5960
app.route("/", methods=["GET"])(get_home)
6061

6162
app.register_blueprint(metadata_bp)
6263

63-
app.route("/<country_id>/household/<household_id>", methods=["GET"])(
64-
get_household
65-
)
66-
67-
app.route("/<country_id>/household", methods=["POST"])(post_household)
68-
69-
app.route("/<country_id>/household/<household_id>", methods=["PUT"])(
70-
update_household
71-
)
64+
app.register_blueprint(household_bp)
7265

7366
app.route("/<country_id>/policy/<policy_id>", methods=["GET"])(get_policy)
7467

@@ -94,12 +87,10 @@
9487
)
9588

9689
# Routes for economy microsimulation
97-
app.register_blueprint(economy_bp, url_prefix="/<country_id>/economy")
90+
app.register_blueprint(economy_bp)
9891

9992
# Routes for AI analysis of economy microsim runs
100-
app.register_blueprint(
101-
simulation_analysis_bp, url_prefix="/<country_id>/simulation-analysis"
102-
)
93+
app.register_blueprint(simulation_analysis_bp)
10394

10495
app.route("/<country_id>/user-policy", methods=["POST"])(set_user_policy)
10596

@@ -117,9 +108,7 @@
117108

118109
app.route("/simulations", methods=["GET"])(get_simulations)
119110

120-
app.register_blueprint(
121-
tracer_analysis_bp, url_prefix="/<country_id>/tracer-analysis"
122-
)
111+
app.register_blueprint(tracer_analysis_bp)
123112

124113

125114
@app.route("/liveness-check", methods=["GET"])

policyengine_api/endpoints/__init__.py

Lines changed: 0 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,10 +1,7 @@
11
from .home import get_home
22
from .household import (
3-
get_household,
4-
post_household,
53
get_household_under_policy,
64
get_calculate,
7-
update_household,
85
)
96
from .policy import (
107
get_policy,

policyengine_api/endpoints/household.py

Lines changed: 0 additions & 168 deletions
Original file line numberDiff line numberDiff line change
@@ -74,174 +74,6 @@ def get_household_year(household):
7474
return household_year
7575

7676

77-
@validate_country
78-
def get_household(country_id: str, household_id: str) -> dict:
79-
"""Get a household's input data with a given ID.
80-
81-
Args:
82-
country_id (str): The country ID.
83-
household_id (str): The household ID.
84-
"""
85-
86-
# Retrieve from the household table
87-
row = database.query(
88-
f"SELECT * FROM household WHERE id = ? AND country_id = ?",
89-
(household_id, country_id),
90-
).fetchone()
91-
92-
if row is not None:
93-
household = dict(row)
94-
household["household_json"] = json.loads(household["household_json"])
95-
return dict(
96-
status="ok",
97-
message=None,
98-
result=household,
99-
)
100-
else:
101-
response_body = dict(
102-
status="error",
103-
message=f"Household #{household_id} not found.",
104-
)
105-
return Response(
106-
json.dumps(response_body),
107-
status=404,
108-
mimetype="application/json",
109-
)
110-
111-
112-
@validate_country
113-
def post_household(country_id: str) -> dict:
114-
"""Set a household's input data.
115-
116-
Args:
117-
country_id (str): The country ID.
118-
"""
119-
120-
payload = request.json
121-
label = payload.get("label")
122-
household_json = payload.get("data")
123-
household_hash = hash_object(household_json)
124-
api_version = COUNTRY_PACKAGE_VERSIONS.get(country_id)
125-
126-
try:
127-
database.query(
128-
f"INSERT INTO household (country_id, household_json, household_hash, label, api_version) VALUES (?, ?, ?, ?, ?)",
129-
(
130-
country_id,
131-
json.dumps(household_json),
132-
household_hash,
133-
label,
134-
api_version,
135-
),
136-
)
137-
except sqlalchemy.exc.IntegrityError:
138-
pass
139-
140-
household_id = database.query(
141-
f"SELECT id FROM household WHERE country_id = ? AND household_hash = ?",
142-
(country_id, household_hash),
143-
).fetchone()["id"]
144-
145-
response_body = dict(
146-
status="ok",
147-
message=None,
148-
result=dict(
149-
household_id=household_id,
150-
),
151-
)
152-
return Response(
153-
json.dumps(response_body),
154-
status=201,
155-
mimetype="application/json",
156-
)
157-
158-
159-
@validate_country
160-
def update_household(country_id: str, household_id: str) -> Response:
161-
"""
162-
Update a household via UPDATE request
163-
164-
Args: country_id (str): The country ID
165-
"""
166-
167-
# Fetch existing household first
168-
try:
169-
row = database.query(
170-
f"SELECT * FROM household WHERE id = ? AND country_id = ?",
171-
(household_id, country_id),
172-
).fetchone()
173-
174-
if row is not None:
175-
household = dict(row)
176-
household["household_json"] = json.loads(
177-
household["household_json"]
178-
)
179-
household["label"]
180-
else:
181-
response_body = dict(
182-
status="error",
183-
message=f"Household #{household_id} not found.",
184-
)
185-
return Response(
186-
json.dumps(response_body),
187-
status=404,
188-
mimetype="application/json",
189-
)
190-
except Exception as e:
191-
logging.exception(e)
192-
response_body = dict(
193-
status="error",
194-
message=f"Error fetching household #{household_id} while updating: {e}",
195-
)
196-
return Response(
197-
json.dumps(response_body),
198-
status=500,
199-
mimetype="application/json",
200-
)
201-
202-
payload = request.json
203-
label = payload.get("label") or household["label"]
204-
household_json = payload.get("data") or household["household_json"]
205-
household_hash = hash_object(household_json)
206-
api_version = COUNTRY_PACKAGE_VERSIONS.get(country_id)
207-
208-
try:
209-
database.query(
210-
f"UPDATE household SET household_json = ?, household_hash = ?, label = ?, api_version = ? WHERE id = ?",
211-
(
212-
json.dumps(household_json),
213-
household_hash,
214-
label,
215-
api_version,
216-
household_id,
217-
),
218-
)
219-
except Exception as e:
220-
logging.exception(e)
221-
response_body = dict(
222-
status="error",
223-
message=f"Error fetching household #{household_id} while updating: {e}",
224-
)
225-
return Response(
226-
json.dumps(response_body),
227-
status=500,
228-
mimetype="application/json",
229-
)
230-
231-
response_body = dict(
232-
status="ok",
233-
message=None,
234-
result=dict(
235-
household_id=household_id,
236-
),
237-
)
238-
return Response(
239-
json.dumps(response_body),
240-
status=200,
241-
mimetype="application/json",
242-
)
243-
244-
24577
@validate_country
24678
def get_household_under_policy(
24779
country_id: str, household_id: str, policy_id: str

policyengine_api/routes/economy_routes.py

Lines changed: 16 additions & 24 deletions
Original file line numberDiff line numberDiff line change
@@ -3,15 +3,18 @@
33
from policyengine_api.utils import get_current_law_policy_id
44
from policyengine_api.utils.payload_validators import validate_country
55
from policyengine_api.constants import COUNTRY_PACKAGE_VERSIONS
6-
from flask import request, Response
6+
from flask import request
77
import json
88

99
economy_bp = Blueprint("economy", __name__)
1010
economy_service = EconomyService()
1111

1212

1313
@validate_country
14-
@economy_bp.route("/<policy_id>/over/<baseline_policy_id>", methods=["GET"])
14+
@economy_bp.route(
15+
"/<country_id>/economy/<int:policy_id>/over/<int:baseline_policy_id>",
16+
methods=["GET"],
17+
)
1518
def get_economic_impact(country_id, policy_id, baseline_policy_id):
1619

1720
policy_id = int(policy_id or get_current_law_policy_id(country_id))
@@ -30,25 +33,14 @@ def get_economic_impact(country_id, policy_id, baseline_policy_id):
3033
"version", COUNTRY_PACKAGE_VERSIONS.get(country_id)
3134
)
3235

33-
try:
34-
result = economy_service.get_economic_impact(
35-
country_id,
36-
policy_id,
37-
baseline_policy_id,
38-
region,
39-
dataset,
40-
time_period,
41-
options,
42-
api_version,
43-
)
44-
return result
45-
except Exception as e:
46-
return Response(
47-
{
48-
"status": "error",
49-
"message": "An error occurred while calculating the economic impact. Details: "
50-
+ str(e),
51-
"result": None,
52-
},
53-
500,
54-
)
36+
result = economy_service.get_economic_impact(
37+
country_id,
38+
policy_id,
39+
baseline_policy_id,
40+
region,
41+
dataset,
42+
time_period,
43+
options,
44+
api_version,
45+
)
46+
return result
Lines changed: 67 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,67 @@
1+
import json
2+
from flask import Response, Blueprint
3+
from werkzeug.exceptions import (
4+
HTTPException,
5+
)
6+
7+
error_bp = Blueprint("error", __name__)
8+
9+
10+
@error_bp.app_errorhandler(404)
11+
def response_404(error) -> Response:
12+
"""Specific handler for 404 Not Found errors"""
13+
return make_error_response(error, 404)
14+
15+
16+
@error_bp.app_errorhandler(400)
17+
def response_400(error) -> Response:
18+
"""Specific handler for 400 Bad Request errors"""
19+
return make_error_response(error, 400)
20+
21+
22+
@error_bp.app_errorhandler(401)
23+
def response_401(error) -> Response:
24+
"""Specific handler for 401 Unauthorized errors"""
25+
return make_error_response(error, 401)
26+
27+
28+
@error_bp.app_errorhandler(403)
29+
def response_403(error) -> Response:
30+
"""Specific handler for 403 Forbidden errors"""
31+
return make_error_response(error, 403)
32+
33+
34+
@error_bp.app_errorhandler(500)
35+
def response_500(error) -> Response:
36+
"""Specific handler for 500 Internal Server errors"""
37+
return make_error_response(error, 500)
38+
39+
40+
@error_bp.app_errorhandler(HTTPException)
41+
def response_http_exception(error: HTTPException) -> Response:
42+
"""Generic handler for HTTPException; should be raised if no specific handler is found"""
43+
return make_error_response(str(error), error.code)
44+
45+
46+
@error_bp.app_errorhandler(Exception)
47+
def response_generic_error(error: Exception) -> Response:
48+
"""Handler for any unhandled exceptions"""
49+
return make_error_response(str(error), 500)
50+
51+
52+
def make_error_response(
53+
error,
54+
status_code: int,
55+
) -> Response:
56+
"""Create a generic error response"""
57+
return Response(
58+
json.dumps(
59+
{
60+
"status": "error",
61+
"message": str(error),
62+
"result": None,
63+
}
64+
),
65+
status_code,
66+
mimetype="application/json",
67+
)

0 commit comments

Comments
 (0)