Skip to content

Commit f0d2a6e

Browse files
committed
feat: Error handling module and application to routes
1 parent f789559 commit f0d2a6e

File tree

7 files changed

+189
-98
lines changed

7 files changed

+189
-98
lines changed

policyengine_api/api.py

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

1515
# Endpoints
16+
from policyengine_api.routes.error_routes import ErrorHandlers
1617
from policyengine_api.routes.economy_routes import economy_bp
1718
from policyengine_api.routes.household_routes import household_bp
1819
from policyengine_api.routes.simulation_analysis_routes import (
@@ -107,6 +108,8 @@
107108

108109
app.register_blueprint(tracer_analysis_bp)
109110

111+
ErrorHandlers.init_app(app)
112+
110113

111114
@app.route("/liveness-check", methods=["GET"])
112115
def liveness_check():

policyengine_api/routes/economy_routes.py

Lines changed: 2 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,7 @@
55
from policyengine_api.constants import COUNTRY_PACKAGE_VERSIONS
66
from flask import request, Response
77
import json
8+
from werkzeug.exceptions import InternalServerError
89

910
economy_bp = Blueprint("economy", __name__)
1011
economy_service = EconomyService()
@@ -46,12 +47,4 @@ def get_economic_impact(country_id, policy_id, baseline_policy_id):
4647
)
4748
return result
4849
except Exception as e:
49-
return Response(
50-
{
51-
"status": "error",
52-
"message": "An error occurred while calculating the economic impact. Details: "
53-
+ str(e),
54-
"result": None,
55-
},
56-
500,
57-
)
50+
raise InternalServerError(str(e))
Lines changed: 156 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,156 @@
1+
import json
2+
from flask import Response, Flask
3+
from werkzeug.exceptions import (
4+
HTTPException,
5+
NotFound,
6+
BadRequest,
7+
Unauthorized,
8+
Forbidden,
9+
InternalServerError,
10+
)
11+
12+
13+
class ErrorRoutes:
14+
"""
15+
Error routing class
16+
"""
17+
18+
@staticmethod
19+
def init_app(app: Flask) -> None:
20+
"""
21+
Register all error handlers with the Flask app
22+
23+
Args:
24+
app (Flask): The Flask app to register error handlers
25+
"""
26+
app.register_error_handler(404, ErrorRoutes.handle_404)
27+
app.register_error_handler(400, ErrorRoutes.handle_400)
28+
app.register_error_handler(401, ErrorRoutes.handle_401)
29+
app.register_error_handler(403, ErrorRoutes.handle_403)
30+
app.register_error_handler(500, ErrorRoutes.handle_500)
31+
app.register_error_handler(
32+
HTTPException, ErrorRoutes.handle_http_exception
33+
)
34+
app.register_error_handler(Exception, ErrorRoutes.handle_generic_error)
35+
36+
@staticmethod
37+
def handle_http_exception(error: HTTPException) -> Response:
38+
"""Generic handler for HTTPException; should be raised if no specific handler is found"""
39+
return Response(
40+
json.dumps(
41+
{
42+
"status": "error",
43+
"message": error.description,
44+
"result": None,
45+
}
46+
),
47+
error.code,
48+
)
49+
50+
@staticmethod
51+
def handle_404(error: NotFound) -> Response:
52+
"""Specific handler for 404 Not Found errors"""
53+
return Response(
54+
json.dumps(
55+
{
56+
"status": "error",
57+
"message": (
58+
str(error)
59+
if str(error)
60+
else "The requested resource was not found"
61+
),
62+
"result": None,
63+
}
64+
),
65+
404,
66+
)
67+
68+
@staticmethod
69+
def handle_400(error: BadRequest) -> Response:
70+
"""Specific handler for 400 Bad Request errors"""
71+
return Response(
72+
json.dumps(
73+
{
74+
"status": "error",
75+
"message": (
76+
str(error)
77+
if str(error)
78+
else "The request was malformed"
79+
),
80+
"result": None,
81+
}
82+
),
83+
400,
84+
)
85+
86+
@staticmethod
87+
def handle_401(error: Unauthorized) -> Response:
88+
"""Specific handler for 401 Unauthorized errors"""
89+
return Response(
90+
json.dumps(
91+
{
92+
"status": "error",
93+
"message": (
94+
str(error)
95+
if str(error)
96+
else "Authentication is required"
97+
),
98+
"result": None,
99+
}
100+
),
101+
401,
102+
)
103+
104+
@staticmethod
105+
def handle_403(error: Forbidden) -> Response:
106+
"""Specific handler for 403 Forbidden errors"""
107+
return Response(
108+
json.dumps(
109+
{
110+
"status": "error",
111+
"message": (
112+
str(error)
113+
if str(error)
114+
else "You do not have permission to access this resource"
115+
),
116+
"result": None,
117+
}
118+
),
119+
403,
120+
)
121+
122+
@staticmethod
123+
def handle_500(error: InternalServerError) -> Response:
124+
"""Specific handler for 500 Internal Server Error"""
125+
return Response(
126+
json.dumps(
127+
{
128+
"status": "error",
129+
"message": (
130+
str(error)
131+
if str(error)
132+
else "An internal server error occurred"
133+
),
134+
"result": None,
135+
}
136+
),
137+
500,
138+
)
139+
140+
@staticmethod
141+
def handle_generic_error(error: Exception) -> Response:
142+
"""Handler for any unhandled exceptions"""
143+
return Response(
144+
json.dumps(
145+
{
146+
"status": "error",
147+
"message": (
148+
str(error)
149+
if str(error)
150+
else "An unexpected error occurred"
151+
),
152+
"result": None,
153+
}
154+
),
155+
500,
156+
)

policyengine_api/routes/household_routes.py

Lines changed: 18 additions & 53 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,5 @@
11
from flask import Blueprint, Response, request
2+
from werkzeug.exceptions import NotFound, InternalServerError, BadRequest
23
import json
34

45
from policyengine_api.constants import COUNTRY_PACKAGE_VERSIONS
@@ -14,7 +15,9 @@
1415
household_service = HouseholdService()
1516

1617

17-
@household_bp.route("/<country_id>/household/<int:household_id>", methods=["GET"])
18+
@household_bp.route(
19+
"/<country_id>/household/<int:household_id>", methods=["GET"]
20+
)
1821
@validate_country
1922
def get_household(country_id: str, household_id: int) -> Response:
2023
"""
@@ -31,15 +34,7 @@ def get_household(country_id: str, household_id: int) -> Response:
3134
country_id, household_id
3235
)
3336
if household is None:
34-
return Response(
35-
json.dumps(
36-
{
37-
"status": "error",
38-
"message": f"Household #{household_id} not found.",
39-
}
40-
),
41-
status=404,
42-
)
37+
raise NotFound(f"Household #{household_id} not found.")
4338
else:
4439
return Response(
4540
json.dumps(
@@ -52,14 +47,8 @@ def get_household(country_id: str, household_id: int) -> Response:
5247
status=200,
5348
)
5449
except Exception as e:
55-
return Response(
56-
json.dumps(
57-
{
58-
"status": "error",
59-
"message": f"An error occurred while fetching household #{household_id}. Details: {str(e)}",
60-
}
61-
),
62-
status=500,
50+
raise InternalServerError(
51+
f"An error occurred while fetching household #{household_id}. Details: {str(e)}"
6352
)
6453

6554

@@ -77,10 +66,7 @@ def post_household(country_id: str) -> Response:
7766
payload = request.json
7867
is_payload_valid, message = validate_household_payload(payload)
7968
if not is_payload_valid:
80-
return Response(
81-
status=400,
82-
response=f"Unable to create new household; details: {message}",
83-
)
69+
raise BadRequest(f"Unable to create new household; details: {message}")
8470

8571
try:
8672
# The household label appears to be unimplemented at this time,
@@ -107,19 +93,14 @@ def post_household(country_id: str) -> Response:
10793
)
10894

10995
except Exception as e:
110-
return Response(
111-
json.dumps(
112-
{
113-
"status": "error",
114-
"message": f"An error occurred while creating a new household. Details: {str(e)}",
115-
}
116-
),
117-
status=500,
118-
mimetype="application/json",
96+
raise InternalServerError(
97+
f"An error occurred while creating a new household. Details: {str(e)}"
11998
)
12099

121100

122-
@household_bp.route("/<country_id>/household/<int:household_id>", methods=["PUT"])
101+
@household_bp.route(
102+
"/<country_id>/household/<int:household_id>", methods=["PUT"]
103+
)
123104
@validate_country
124105
def update_household(country_id: str, household_id: int) -> Response:
125106
"""
@@ -134,9 +115,8 @@ def update_household(country_id: str, household_id: int) -> Response:
134115
payload = request.json
135116
is_payload_valid, message = validate_household_payload(payload)
136117
if not is_payload_valid:
137-
return Response(
138-
status=400,
139-
response=f"Unable to update household #{household_id}; details: {message}",
118+
raise BadRequest(
119+
f"Unable to update household #{household_id}; details: {message}"
140120
)
141121

142122
try:
@@ -149,15 +129,7 @@ def update_household(country_id: str, household_id: int) -> Response:
149129
country_id, household_id
150130
)
151131
if household is None:
152-
return Response(
153-
json.dumps(
154-
{
155-
"status": "error",
156-
"message": f"Household #{household_id} not found.",
157-
}
158-
),
159-
status=404,
160-
)
132+
raise NotFound(f"Household #{household_id} not found.")
161133

162134
# Next, update the household
163135
household_service.update_household(
@@ -177,13 +149,6 @@ def update_household(country_id: str, household_id: int) -> Response:
177149
mimetype="application/json",
178150
)
179151
except Exception as e:
180-
return Response(
181-
json.dumps(
182-
{
183-
"status": "error",
184-
"message": f"An error occurred while updating household #{household_id}. Details: {str(e)}",
185-
}
186-
),
187-
status=500,
188-
mimetype="application/json",
152+
raise InternalServerError(
153+
f"An error occurred while updating household #{household_id}. Details: {str(e)}"
189154
)

policyengine_api/routes/simulation_analysis_routes.py

Lines changed: 4 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,5 @@
11
from flask import Blueprint, request, Response, stream_with_context
2+
from werkzeug.exceptions import InternalServerError, BadRequest
23
import json
34
from policyengine_api.utils.payload_validators import validate_country
45
from policyengine_api.services.simulation_analysis_service import (
@@ -26,9 +27,7 @@ def execute_simulation_analysis(country_id):
2627

2728
is_payload_valid, message = validate_sim_analysis_payload(payload)
2829
if not is_payload_valid:
29-
return Response(
30-
status=400, response=f"Invalid JSON data; details: {message}"
31-
)
30+
raise BadRequest(f"Invalid JSON data; details: {message}")
3231

3332
currency: str = payload.get("currency")
3433
selected_version: str = payload.get("selected_version")
@@ -70,14 +69,6 @@ def execute_simulation_analysis(country_id):
7069

7170
return response
7271
except Exception as e:
73-
return Response(
74-
json.dumps(
75-
{
76-
"status": "error",
77-
"message": "An error occurred while executing the simulation analysis. Details: "
78-
+ str(e),
79-
"result": None,
80-
}
81-
),
82-
status=500,
72+
raise InternalServerError(
73+
f"An error occurred while executing the simulation analysis. Details: {str(e)}"
8374
)

0 commit comments

Comments
 (0)