Skip to content

Commit 0782bd9

Browse files
committed
Initial refactoring
1 parent aae8113 commit 0782bd9

File tree

11 files changed

+158
-253
lines changed

11 files changed

+158
-253
lines changed

backend/src/controller/fhir_api_exception_handler.py

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -9,17 +9,25 @@
99
from controller.aws_apig_response_utils import create_response
1010
from models.errors import (
1111
Code,
12+
CustomValidationError,
13+
IdentifierDuplicationError,
14+
InvalidJsonError,
1215
ResourceNotFoundError,
1316
Severity,
1417
UnauthorizedError,
1518
UnauthorizedVaxError,
19+
UnhandledResponseError,
1620
create_operation_outcome,
1721
)
1822

1923
_CUSTOM_EXCEPTION_TO_STATUS_MAP: dict[Type[Exception], int] = {
24+
InvalidJsonError: 400,
25+
CustomValidationError: 400,
2026
UnauthorizedError: 403,
2127
UnauthorizedVaxError: 403,
2228
ResourceNotFoundError: 404,
29+
IdentifierDuplicationError: 422,
30+
UnhandledResponseError: 500,
2331
}
2432

2533

backend/src/controller/fhir_controller.py

Lines changed: 20 additions & 41 deletions
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,7 @@
55
import urllib.parse
66
import uuid
77
from decimal import Decimal
8+
from json import JSONDecodeError
89
from typing import Optional
910

1011
from aws_lambda_typing.events import APIGatewayProxyEventV1
@@ -19,6 +20,7 @@
1920
from models.errors import (
2021
Code,
2122
IdentifierDuplicationError,
23+
InvalidJsonError,
2224
ParameterException,
2325
ResourceNotFoundError,
2426
Severity,
@@ -48,7 +50,8 @@ def make_controller(
4850

4951

5052
class FhirController:
51-
immunization_id_pattern = r"^[A-Za-z0-9\-.]{1,64}$"
53+
_IMMUNIZATION_ID_PATTERN = r"^[A-Za-z0-9\-.]{1,64}$"
54+
_API_SERVICE_URL = get_service_url()
5255

5356
def __init__(
5457
self,
@@ -105,46 +108,22 @@ def get_immunization_by_id(self, aws_event: APIGatewayProxyEventV1) -> dict:
105108

106109
return create_response(200, resource.json(), {E_TAG_HEADER_NAME: version})
107110

108-
def create_immunization(self, aws_event):
109-
if not aws_event.get("headers"):
110-
return create_response(
111-
403,
112-
create_operation_outcome(
113-
resource_id=str(uuid.uuid4()),
114-
severity=Severity.error,
115-
code=Code.forbidden,
116-
diagnostics="Unauthorized request",
117-
),
118-
)
119-
120-
supplier_system = self._identify_supplier_system(aws_event)
111+
@fhir_api_exception_handler
112+
def create_immunization(self, aws_event: APIGatewayProxyEventV1) -> dict:
113+
supplier_system = get_supplier_system_header(aws_event)
121114

122115
try:
123-
immunisation = json.loads(aws_event["body"], parse_float=Decimal)
124-
except json.decoder.JSONDecodeError as e:
125-
return self._create_bad_request(f"Request's body contains malformed JSON: {e}")
126-
try:
127-
resource = self.fhir_service.create_immunization(immunisation, supplier_system)
128-
if "diagnostics" in resource:
129-
exp_error = create_operation_outcome(
130-
resource_id=str(uuid.uuid4()),
131-
severity=Severity.error,
132-
code=Code.invariant,
133-
diagnostics=resource["diagnostics"],
134-
)
135-
return create_response(400, json.dumps(exp_error))
136-
else:
137-
location = f"{get_service_url()}/Immunization/{resource.id}"
138-
version = "1"
139-
return create_response(201, None, {"Location": location, "E-Tag": version})
140-
except ValidationError as error:
141-
return create_response(400, error.to_operation_outcome())
142-
except IdentifierDuplicationError as duplicate:
143-
return create_response(422, duplicate.to_operation_outcome())
144-
except UnhandledResponseError as unhandled_error:
145-
return create_response(500, unhandled_error.to_operation_outcome())
146-
except UnauthorizedVaxError as unauthorized:
147-
return create_response(403, unauthorized.to_operation_outcome())
116+
immunisation: dict = json.loads(aws_event["body"], parse_float=Decimal)
117+
except JSONDecodeError as e:
118+
raise InvalidJsonError(message=str(f"Request's body contains malformed JSON: {e}"))
119+
120+
created_resource_id = self.fhir_service.create_immunization(immunisation, supplier_system)
121+
122+
return create_response(
123+
status_code=201,
124+
body=None,
125+
headers={"Location": f"{self._API_SERVICE_URL}/Immunization/{created_resource_id}", "E-Tag": "1"},
126+
)
148127

149128
def update_immunization(self, aws_event):
150129
try:
@@ -178,7 +157,7 @@ def update_immunization(self, aws_event):
178157
)
179158
return create_response(400, json.dumps(exp_error))
180159
# Validate the imms id in the path params and body of request - end
181-
except json.decoder.JSONDecodeError as e:
160+
except JSONDecodeError as e:
182161
return self._create_bad_request(f"Request's body contains malformed JSON: {e}")
183162
except Exception as e:
184163
return self._create_bad_request(f"Request's body contains string: {e}")
@@ -407,7 +386,7 @@ def search_immunizations(self, aws_event: APIGatewayProxyEventV1) -> dict:
407386
return create_response(200, json.dumps(result_json_dict))
408387

409388
def _validate_id(self, _id: str) -> Optional[dict]:
410-
if not re.match(self.immunization_id_pattern, _id):
389+
if not re.match(self._IMMUNIZATION_ID_PATTERN, _id):
411390
msg = "Validation errors: the provided event ID is either missing or not in the expected format."
412391
return create_operation_outcome(
413392
resource_id=str(uuid.uuid4()),

backend/src/create_imms_handler.py

Lines changed: 1 addition & 15 deletions
Original file line numberDiff line numberDiff line change
@@ -1,14 +1,10 @@
11
import argparse
22
import logging
33
import pprint
4-
import uuid
54

6-
from constants import GENERIC_SERVER_ERROR_DIAGNOSTICS_MESSAGE
7-
from controller.aws_apig_response_utils import create_response
85
from controller.fhir_controller import FhirController, make_controller
96
from local_lambda import load_string
107
from log_structure import function_info
11-
from models.errors import Code, Severity, create_operation_outcome
128

139
logging.basicConfig(level="INFO")
1410
logger = logging.getLogger()
@@ -20,17 +16,7 @@ def create_imms_handler(event, _context):
2016

2117

2218
def create_immunization(event, controller: FhirController):
23-
try:
24-
return controller.create_immunization(event)
25-
except Exception: # pylint: disable = broad-exception-caught
26-
logger.exception("Unhandled exception")
27-
exp_error = create_operation_outcome(
28-
resource_id=str(uuid.uuid4()),
29-
severity=Severity.error,
30-
code=Code.server_error,
31-
diagnostics=GENERIC_SERVER_ERROR_DIAGNOSTICS_MESSAGE,
32-
)
33-
return create_response(500, exp_error)
19+
return controller.create_immunization(event)
3420

3521

3622
if __name__ == "__main__":

backend/src/models/errors.py

Lines changed: 15 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -201,6 +201,21 @@ def to_operation_outcome(self) -> dict:
201201
)
202202

203203

204+
@dataclass
205+
class InvalidJsonError(RuntimeError):
206+
"""Raised when client provides an invalid JSON payload"""
207+
208+
message: str
209+
210+
def to_operation_outcome(self) -> dict:
211+
return create_operation_outcome(
212+
resource_id=str(uuid.uuid4()),
213+
severity=Severity.error,
214+
code=Code.invalid,
215+
diagnostics=self.message,
216+
)
217+
218+
204219
def create_operation_outcome(resource_id: str, severity: Severity, code: Code, diagnostics: str) -> dict:
205220
"""Create an OperationOutcome object. Do not use `fhir.resource` library since it adds unnecessary validations"""
206221
return {

backend/src/repository/fhir_repository.py

Lines changed: 19 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,5 @@
11
import os
22
import time
3-
import uuid
43
from dataclasses import dataclass
54
from typing import Optional, Tuple
65

@@ -17,6 +16,7 @@
1716
ResourceNotFoundError,
1817
UnhandledResponseError,
1918
)
19+
from models.utils.generic_utils import get_contained_patient
2020
from models.utils.validation_utils import (
2121
check_identifier_system_value,
2222
get_vaccine_type,
@@ -151,15 +151,22 @@ def get_immunization_by_id_all(self, imms_id: str, imms: dict) -> Optional[dict]
151151
else:
152152
return None
153153

154-
def create_immunization(self, immunization: dict, patient: any, supplier_system: str) -> dict:
155-
new_id = str(uuid.uuid4())
156-
immunization["id"] = new_id
157-
attr = RecordAttributes(immunization, patient)
154+
def check_immunization_identifier_exists(self, system: str, unique_id: str) -> bool:
155+
"""Checks whether an immunization with the given immunization identifier (system + local ID) exists."""
156+
response = self.table.query(
157+
IndexName="IdentifierGSI",
158+
KeyConditionExpression=Key("IdentifierPK").eq(f"{system}#{unique_id}"),
159+
)
158160

159-
query_response = _query_identifier(self.table, "IdentifierGSI", "IdentifierPK", attr.identifier)
161+
if "Items" in response and len(response["Items"]) > 0:
162+
return True
160163

161-
if query_response is not None:
162-
raise IdentifierDuplicationError(identifier=attr.identifier)
164+
return False
165+
166+
def create_immunization(self, immunization: dict, supplier_system: str) -> str:
167+
"""Creates a new immunization record returning the unique id if successful."""
168+
patient = get_contained_patient(immunization)
169+
attr = RecordAttributes(immunization, patient)
163170

164171
response = self.table.put_item(
165172
Item={
@@ -174,10 +181,10 @@ def create_immunization(self, immunization: dict, patient: any, supplier_system:
174181
}
175182
)
176183

177-
if response["ResponseMetadata"]["HTTPStatusCode"] == 200:
178-
return immunization
179-
else:
180-
raise UnhandledResponseError(message="Non-200 response from dynamodb", response=response)
184+
if response["ResponseMetadata"]["HTTPStatusCode"] != 200:
185+
raise UnhandledResponseError(message="Non-200 response from dynamodb", response=dict(response))
186+
187+
return immunization.get("id")
181188

182189
def update_immunization(
183190
self,

backend/src/service/fhir_service.py

Lines changed: 13 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,7 @@
11
import datetime
22
import logging
33
import os
4+
import uuid
45
from enum import Enum
56
from typing import Optional, Union
67
from uuid import uuid4
@@ -22,6 +23,7 @@
2223
from filter import Filter
2324
from models.errors import (
2425
CustomValidationError,
26+
IdentifierDuplicationError,
2527
InvalidPatientId,
2628
MandatoryError,
2729
ResourceNotFoundError,
@@ -132,26 +134,30 @@ def get_immunization_by_id_all(self, imms_id: str, imms: dict) -> Optional[dict]
132134
imms_resp = self.immunization_repo.get_immunization_by_id_all(imms_id, imms)
133135
return imms_resp
134136

135-
def create_immunization(self, immunization: dict, supplier_system: str) -> dict | Immunization:
137+
def create_immunization(self, immunization: dict, supplier_system: str) -> str:
136138
if immunization.get("id") is not None:
137139
raise CustomValidationError("id field must not be present for CREATE operation")
138140

139141
try:
140142
self.validator.validate(immunization)
141143
except (ValidationError, ValueError, MandatoryError) as error:
142144
raise CustomValidationError(message=str(error)) from error
143-
patient = self._validate_patient(immunization)
144-
145-
if "diagnostics" in patient:
146-
return patient
147145

148146
vaccination_type = get_vaccine_type(immunization)
149147

150148
if not self.authoriser.authorise(supplier_system, ApiOperationCode.CREATE, {vaccination_type}):
151149
raise UnauthorizedVaxError()
152150

153-
immunisation = self.immunization_repo.create_immunization(immunization, patient, supplier_system)
154-
return Immunization.parse_obj(immunisation)
151+
# TODO - consider only using FHIR entities in service layer
152+
identifier_system = immunization["identifier"][0]["system"]
153+
identifier_value = immunization["identifier"][0]["value"]
154+
155+
if self.immunization_repo.check_immunization_identifier_exists(identifier_system, identifier_value):
156+
raise IdentifierDuplicationError(identifier=f"{identifier_system}#{identifier_value}")
157+
158+
# Set ID for the requested new record
159+
immunization["id"] = str(uuid.uuid4())
160+
return self.immunization_repo.create_immunization(immunization, supplier_system)
155161

156162
def update_immunization(
157163
self,

backend/tests/controller/test_fhir_api_exception_handler.py

Lines changed: 9 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -3,7 +3,13 @@
33
from unittest.mock import patch
44

55
from controller.fhir_api_exception_handler import fhir_api_exception_handler
6-
from models.errors import ResourceNotFoundError, UnauthorizedError, UnauthorizedVaxError
6+
from models.errors import (
7+
CustomValidationError,
8+
InvalidJsonError,
9+
ResourceNotFoundError,
10+
UnauthorizedError,
11+
UnauthorizedVaxError,
12+
)
713

814

915
class TestFhirApiExceptionHandler(unittest.TestCase):
@@ -27,6 +33,8 @@ def dummy_func():
2733
def test_exception_handler_handles_custom_exception_and_returns_fhir_response(self):
2834
"""Test that custom exceptions are handled by the wrapper and a valid response is returned to the client"""
2935
test_cases = [
36+
(InvalidJsonError("Invalid JSON provided"), 400, "invalid", "Invalid JSON provided"),
37+
(CustomValidationError("This field was invalid"), 400, "invariant", "This field was invalid"),
3038
(UnauthorizedError(), 403, "forbidden", "Unauthorized request"),
3139
(
3240
UnauthorizedVaxError(),

0 commit comments

Comments
 (0)