Skip to content

Commit 349fcf2

Browse files
authored
Merge pull request #49 from NHSDigital/AMB-1728_use_fhir_resources_in_imms_get
Amb 1728 use fhir resources in imms get
2 parents 5c25dcf + 46f73ac commit 349fcf2

16 files changed

+399
-128
lines changed

lambda_code/Makefile

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,6 @@ package: build
66
docker run --rm -v $(shell pwd)/build:/build imms-lambda-build
77

88
test:
9-
DYNAMODB_TABLE_NAME=example-table python -m unittest
9+
python -m unittest
1010

1111
.PHONY: build package test

lambda_code/requirements.txt

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -3,3 +3,4 @@ boto3==1.28.68
33
moto
44
requests
55
responses
6+
pydantic

lambda_code/src/dynamodb.py

Lines changed: 3 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -6,12 +6,13 @@
66

77

88
class EventTable:
9-
def __init__(self, table_name=os.environ["DYNAMODB_TABLE_NAME"], endpoint_url=None):
9+
def __init__(self, table_name=None, endpoint_url=None):
10+
if not table_name:
11+
table_name = os.environ["DYNAMODB_TABLE_NAME"]
1012
db = boto3.resource("dynamodb", endpoint_url=endpoint_url, region_name='eu-west-2')
1113
self.table = db.Table(table_name)
1214

1315
def get_event_by_id(self, event_id):
14-
# TODO: the main index doesn't need sort-key. You can use get_item instead of query
1516
response = self.table.get_item(Key={"PK": event_id})
1617

1718
if "Item" in response:

lambda_code/src/fhir_controller.py

Lines changed: 43 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,43 @@
1+
import json
2+
import re
3+
import uuid
4+
5+
from fhir_service import FhirService
6+
from models.errors import Severity, Code, create_operation_outcome
7+
8+
9+
class FhirController:
10+
immunisation_id_pattern = r"^[A-Za-z0-9\-.]{1,64}$"
11+
12+
def __init__(self, fhir_service: FhirService):
13+
self.fhir_service = fhir_service
14+
15+
def get_immunisation_by_id(self, aws_event) -> dict:
16+
imms_id = aws_event["pathParameters"]["id"]
17+
18+
if not re.match(self.immunisation_id_pattern, imms_id):
19+
msg = "the provided event ID is either missing or not in the expected format."
20+
api_error = create_operation_outcome(resource_id=str(uuid.uuid4()), severity=Severity.error,
21+
code=Code.invalid,
22+
diagnostics=msg)
23+
return FhirController.create_response(400, json.dumps(api_error.dict()))
24+
25+
resource = self.fhir_service.get_immunisation_by_id(imms_id)
26+
if resource:
27+
return FhirController.create_response(200, resource.json())
28+
else:
29+
msg = "The requested resource was not found."
30+
api_error = create_operation_outcome(resource_id=str(uuid.uuid4()), severity=Severity.error,
31+
code=Code.not_found,
32+
diagnostics=msg)
33+
return FhirController.create_response(404, json.dumps(api_error.dict()))
34+
35+
@staticmethod
36+
def create_response(status_code, body):
37+
return {
38+
"statusCode": status_code,
39+
"headers": {
40+
"Content-Type": "application/fhir+json",
41+
},
42+
"body": body
43+
}

lambda_code/src/fhir_repository.py

Lines changed: 23 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,23 @@
1+
import json
2+
import os
3+
from typing import Optional
4+
5+
import boto3
6+
7+
8+
class FhirRepository:
9+
def __init__(self, table_name=None, endpoint_url=None):
10+
if not table_name:
11+
table_name = os.environ["DYNAMODB_TABLE_NAME"]
12+
db = boto3.resource("dynamodb", endpoint_url=endpoint_url, region_name='eu-west-2')
13+
self.table = db.Table(table_name)
14+
15+
16+
class ImmunisationRepository(FhirRepository):
17+
def get_immunisation_by_id(self, imms_id: str) -> Optional[dict]:
18+
response = self.table.get_item(Key={"PK": imms_id})
19+
20+
if "Item" in response:
21+
return json.loads(response["Item"]["Event"])
22+
else:
23+
return None

lambda_code/src/fhir_service.py

Lines changed: 20 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,20 @@
1+
from typing import Optional
2+
3+
from fhir.resources.immunization import Immunization
4+
5+
from fhir_repository import ImmunisationRepository
6+
7+
8+
class FhirService:
9+
10+
def __init__(self, imms_repo: ImmunisationRepository):
11+
self.immunisation_repo = imms_repo
12+
13+
def get_immunisation_by_id(self, imms_id: str) -> Optional[Immunization]:
14+
imms = self.immunisation_repo.get_immunisation_by_id(imms_id)
15+
if imms:
16+
# TODO: This shouldn't raise an exception since, we validate the message before storing it,
17+
# but what if the stored message is different from the requested FHIR version?
18+
return Immunization.parse_obj(imms)
19+
else:
20+
return None
Lines changed: 16 additions & 69 deletions
Original file line numberDiff line numberDiff line change
@@ -1,77 +1,24 @@
11
import uuid
22

3-
from dynamodb import EventTable
4-
import json
5-
import re
6-
7-
8-
def create_response(event_id, message, code):
9-
return {
10-
"resourceType": "OperationOutcome",
11-
"id": event_id,
12-
"meta": {
13-
"profile": [
14-
"https://simplifier.net/guide/UKCoreDevelopment2/ProfileUKCore-OperationOutcome"
15-
]
16-
},
17-
"issue": [
18-
{
19-
"severity": "error",
20-
"code": code,
21-
"details": {
22-
"coding": [
23-
{
24-
"system": "https://fhir.nhs.uk/Codesystem/http-error-codes",
25-
"code": code.upper()
26-
}
27-
]
28-
},
29-
"diagnostics": message
30-
}
31-
]
32-
}
3+
from fhir_controller import FhirController
4+
from fhir_repository import ImmunisationRepository
5+
from fhir_service import FhirService
6+
from models.errors import Severity, Code, create_operation_outcome
337

348

359
def get_imms_handler(event, context):
36-
dynamo_service = EventTable()
37-
return get_imms(event, dynamo_service)
38-
39-
40-
# create function which receives event and instance of dynamodb
41-
def get_imms(event, dynamo_service):
42-
event_id = event["pathParameters"]["id"]
43-
44-
def is_valid_id(_event_id):
45-
pattern = r'^[A-Za-z0-9\-.]{1,64}$'
46-
return re.match(pattern, _event_id) is not None
47-
48-
if not is_valid_id(event_id) or not event_id:
49-
return {
50-
"statusCode": 400,
51-
"headers": {
52-
"Content-Type": "application/fhir+json",
53-
},
54-
"body": json.dumps(
55-
create_response(str(uuid.uuid4()),
56-
"he provided event ID is either missing or not in the expected format.",
57-
"invalid"))
58-
}
10+
imms_repo = ImmunisationRepository()
11+
service = FhirService(imms_repo=imms_repo)
12+
controller = FhirController(fhir_service=service)
5913

60-
message = dynamo_service.get_event_by_id(event_id)
61-
if message is None:
14+
return get_immunisation_by_id(event, controller)
6215

63-
return {
64-
"statusCode": 404,
65-
"headers": {
66-
"Content-Type": "application/fhir+json",
67-
},
68-
"body": json.dumps(create_response(str(uuid.uuid4()), "The requested resource was not found.", "not-found"))
69-
}
7016

71-
return {
72-
'statusCode': 200,
73-
"headers": {
74-
"Content-Type": "application/fhir+json",
75-
},
76-
'body': json.dumps(message)
77-
}
17+
def get_immunisation_by_id(event, controller: FhirController):
18+
try:
19+
return controller.get_immunisation_by_id(event)
20+
except Exception as e:
21+
exp_error = create_operation_outcome(resource_id=str(uuid.uuid4()), severity=Severity.error,
22+
code=Code.not_found,
23+
diagnostics=str(e))
24+
return FhirController.create_response(500, exp_error.json())

lambda_code/src/models/errors.py

Lines changed: 40 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,40 @@
1+
from enum import Enum
2+
3+
from fhir.resources.operationoutcome import OperationOutcome
4+
5+
6+
class Severity(str, Enum):
7+
error = "error"
8+
9+
10+
class Code(str, Enum):
11+
not_found = "not-found"
12+
invalid = "invalid"
13+
14+
15+
def create_operation_outcome(resource_id: str, severity: Severity, code: Code, diagnostics: str) -> OperationOutcome:
16+
model = {
17+
"resourceType": "OperationOutcome",
18+
"id": resource_id,
19+
"meta": {
20+
"profile": [
21+
"https://simplifier.net/guide/UKCoreDevelopment2/ProfileUKCore-OperationOutcome"
22+
]
23+
},
24+
"issue": [
25+
{
26+
"severity": severity,
27+
"code": code,
28+
"details": {
29+
"coding": [
30+
{
31+
"system": "https://fhir.nhs.uk/Codesystem/http-error-codes",
32+
"code": code.upper()
33+
}
34+
]
35+
},
36+
"diagnostics": diagnostics
37+
}
38+
]
39+
}
40+
return OperationOutcome.parse_obj(model)

lambda_code/src/requirements.txt

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,4 @@
11
fhir.resources==7.0.2
22
boto3==1.28.68
33
requests
4+
pydantic
Lines changed: 25 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,25 @@
1+
import os
2+
import sys
3+
import unittest
4+
5+
sys.path.append(f"{os.path.dirname(os.path.abspath(__file__))}/../src")
6+
7+
from models.errors import Severity, Code, create_operation_outcome
8+
9+
10+
class TestApiErrors(unittest.TestCase):
11+
def test_error_to_uk_core2(self):
12+
code = Code.not_found
13+
14+
severity = Severity.error
15+
diag = "a-diagnostic"
16+
error_id = "a-id"
17+
18+
error = create_operation_outcome(resource_id=error_id, severity=severity, code=code, diagnostics=diag).dict()
19+
20+
issue = error["issue"][0]
21+
self.assertEqual(error["id"], error_id)
22+
self.assertEqual(issue["code"], "not-found")
23+
self.assertEqual(issue["severity"], "error")
24+
self.assertEqual(issue["diagnostics"], diag)
25+
self.assertEqual(issue["details"]["coding"][0]["code"], "NOT-FOUND")

0 commit comments

Comments
 (0)