Skip to content

Commit 8f49b7f

Browse files
Akol125mfjarvis
andauthored
VED-222-FHIR-FLAT-JSON (#398)
* refactor test * changed specs * snake_case * corrections * empty string * date conversion * coverage * add milliseconds logic * datetime conversion * unit test and coverage --------- Co-authored-by: Matt Jarvis <[email protected]>
1 parent a9b33b4 commit 8f49b7f

File tree

9 files changed

+113
-99
lines changed

9 files changed

+113
-99
lines changed

delta_backend/Dockerfile

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,4 @@
1-
FROM public.ecr.aws/lambda/python:3.10 AS base
1+
FROM public.ecr.aws/lambda/python:3.11 AS base
22

33
# Create a non-root user with a specific UID and GID
44
RUN mkdir -p /home/appuser && \

delta_backend/poetry.lock

Lines changed: 27 additions & 53 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

delta_backend/pyproject.toml

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -9,7 +9,7 @@ packages = [
99
]
1010

1111
[tool.poetry.dependencies]
12-
python = "~3.10"
12+
python = "~3.11"
1313
boto3 = "~1.26.90"
1414
mypy-boto3-dynamodb = "^1.26.164"
1515
moto = "~4.2.11"

delta_backend/src/ConversionChecker.py

Lines changed: 16 additions & 28 deletions
Original file line numberDiff line numberDiff line change
@@ -2,7 +2,7 @@
22
# Handles the transformation logic for each field based on the schema
33
# Root and base type expression checker functions
44
import ExceptionMessages
5-
from datetime import datetime,timezone
5+
from datetime import datetime,timedelta, timezone
66
from zoneinfo import ZoneInfo
77
import re
88
from LookUpData import LookUpData
@@ -122,7 +122,6 @@ def _convertToDate(self, expressionRule, fieldName, fieldValue, summarise, repor
122122
self._log_error(fieldName, fieldValue, "Value is not a string")
123123
return ""
124124
try:
125-
fieldValue = re.sub(r"\.\d+(?=[+-]\d{2}:\d{2}$)", "", fieldValue) # Remove milliseconds
126125
dt = datetime.fromisoformat(fieldValue)
127126
return dt.strftime(expressionRule)
128127
except ValueError as e:
@@ -136,40 +135,29 @@ def _convertToDateTime(self, expressionRule, fieldName, fieldValue, summarise, r
136135
if not fieldValue:
137136
return ""
138137

139-
# Reject partial dates like "2024" or "2024-05"
140-
if re.match(r"^\d{4}(-\d{2})?$", fieldValue):
141-
raise RecordError(
142-
ExceptionMessages.RECORD_CHECK_FAILED,
143-
f"{fieldName} rejected: partial datetime not accepted.",
144-
f"Invalid partial datetime: {fieldValue}",
145-
)
146138
try:
147139
dt = datetime.fromisoformat(fieldValue)
148-
except ValueError:
140+
if dt.tzinfo is None:
141+
dt = dt.replace(tzinfo=timezone.utc)
142+
except Exception as e:
149143
if report_unexpected_exception:
150-
return f"Unexpected format: {fieldValue}"
144+
self._log_error(fieldName, fieldValue, e)
145+
return ""
151146

152147
# Allow only +00:00 or +01:00 offsets (UTC and BST) and reject unsupported timezones
153148
offset = dt.utcoffset()
154-
allowed_offsets = [ZoneInfo("UTC").utcoffset(dt),
155-
ZoneInfo("Europe/London").utcoffset(dt)]
156-
if offset not in allowed_offsets:
157-
raise RecordError(
158-
ExceptionMessages.RECORD_CHECK_FAILED,
159-
f"{fieldName} rejected: unsupported timezone.",
160-
f"Unsupported offset: {offset}",
161-
)
162-
163-
# Convert to UTC
164-
dt_utc = dt.astimezone(ZoneInfo("UTC")).replace(microsecond=0)
165-
166-
format_str = expressionRule.replace("format:", "")
149+
allowed_offsets = [timedelta(hours=0), timedelta(hours=1)]
150+
if offset is not None and offset not in allowed_offsets:
151+
if report_unexpected_exception:
152+
self._log_error(fieldName, fieldValue, "Unsupported Format or offset")
153+
return ""
167154

168-
if format_str == "csv-utc":
169-
formatted = dt_utc.strftime("%Y%m%dT%H%M%S%z")
170-
return formatted.replace("+0000", "00").replace("+0100", "01")
155+
# remove microseconds
156+
dt_format = dt.replace(microsecond=0)
171157

172-
return dt_utc.strftime(format_str)
158+
formatted = dt_format.strftime("%Y%m%dT%H%M%S%z")
159+
return formatted.replace("+0000", "00").replace("+0100", "01")
160+
173161

174162
# Not Empty Validate - Returns exactly what is in the extracted fields no parsing or logic needed
175163
def _convertToNotEmpty(self, expressionRule, fieldName, fieldValue, summarise, report_unexpected_exception):

delta_backend/src/ConversionLayout.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -68,7 +68,7 @@
6868
"expression": {
6969
"expressionName": "Date Convert",
7070
"expressionType": "DATETIME",
71-
"expressionRule": "format:csv-utc"
71+
"expressionRule": "fhir-date"
7272
}
7373
},
7474
{

delta_backend/src/Converter.py

Lines changed: 6 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -137,7 +137,12 @@ def extract_patient_details(self, json_data, FlatFieldName):
137137
self._cached_values = {}
138138

139139
if not self._cached_values:
140-
occurrence_time = datetime.fromisoformat(json_data.get("occurrenceDateTime", ""))
140+
try:
141+
occurrence_time = datetime.fromisoformat(json_data.get("occurrenceDateTime", ""))
142+
except Exception as e:
143+
message = "DateTime conversion error [%s]: %s" % (e.__class__.__name__, e)
144+
error = self._log_error(message, code=ExceptionMessages.UNEXPECTED_EXCEPTION)
145+
return error
141146
patient = get_patient(json_data)
142147
if not patient:
143148
return None

delta_backend/tests/sample_data/fhir_sample.json

Lines changed: 4 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -72,8 +72,8 @@
7272
"patient": {
7373
"reference": "#Pat1"
7474
},
75-
"occurrenceDateTime": "2021-02-07T13:28:17+00:00",
76-
"recorded": "2025-02-07",
75+
"occurrenceDateTime": "2021-05-07T12:24:37+00:00",
76+
"recorded": "2025-01-01T00:00:00.000000",
7777
"primarySource": "True",
7878
"manufacturer": {
7979
"display": "AstraZeneca Ltd"
@@ -86,7 +86,8 @@
8686
}
8787
},
8888
"lotNumber": "4120Z001",
89-
"expirationDate": "2024-10-12",
89+
"expirationDate": "2024-10-05",
90+
9091
"site": {
9192
"coding": [
9293
{

delta_backend/tests/test_convert_to_flat_json.py

Lines changed: 41 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -153,7 +153,7 @@ def test_fhir_converter_json_direct_data(self):
153153
self.assertEqual(flatJSON, expected_imms)
154154

155155
errorRecords = FHIRConverter.getErrorRecords()
156-
156+
157157
self.assertEqual(len(errorRecords), 0)
158158

159159
def test_fhir_converter_json_error_scenario(self):
@@ -461,22 +461,52 @@ def test_convert_to_date_time(self, MockLookUpData):
461461

462462
checker = ConversionChecker(dataParser, summarise=False, report_unexpected_exception=True)
463463

464-
valid_date_time = "2022-01-01T12:00:00+00:00"
465-
result = checker._convertToDateTime("%Y%m%dT%H%M%S", "fieldName", valid_date_time, False, True)
466-
self.assertEqual(result, "20220101T120000")
464+
valid_date_time = "2025-01-01T12:00:00+00:00"
465+
result = checker._convertToDateTime("fhir-date", "fieldName", valid_date_time, False, True)
466+
self.assertEqual(result, "20250101T12000000")
467+
468+
valid_fhir_date = "2025-01-01T13:28:17+00:00"
469+
result = checker._convertToDateTime("fhir-date", "fieldName", valid_fhir_date, False, True)
470+
self.assertEqual(result, "20250101T13281700")
467471

468-
valid_csv_utc = "2022-01-01T13:28:17+00:00"
469-
result = checker._convertToDateTime("format:csv-utc", "fieldName", valid_csv_utc, False, True)
470-
self.assertEqual(result, "20220101T13281700")
472+
valid_fhir_date = "2022-01-01"
473+
result = checker._convertToDateTime("", "fieldName", valid_fhir_date, False, True)
474+
self.assertEqual(result, "20220101T00000000")
475+
476+
valid_fhir_date = "2025-05-01T13:28:17+01:00"
477+
result = checker._convertToDateTime("fhir-date", "fieldName", valid_fhir_date, False, True)
478+
self.assertEqual(result, "20250501T13281701")
471479

472480
invalid_date_time = "invalid_date_time"
473-
result = checker._convertToDateTime("format:%Y%m%dT%H%M%S", "fieldName", invalid_date_time, False, True)
474-
self.assertEqual(result, "Unexpected format: invalid_date_time")
481+
result = checker._convertToDateTime("fhir-date", "fieldName", invalid_date_time, False, True)
482+
self.assertEqual(result, "")
483+
484+
valid_date_time = "2025-01-01T12:00:00+03:00"
485+
result = checker._convertToDateTime("fhir-date", "fieldName", valid_date_time, False, True)
486+
self.assertEqual(result, "")
487+
488+
messages = [err["message"] for err in checker.errorRecords]
489+
print(f"Error Test Case, {messages}")
490+
491+
self.assertIn("Unexpected exception [ValueError]", messages[0])
492+
self.assertIn("Unsupported Format or offset", messages[1])
493+
494+
# Confirm Total Errors Per conversion
495+
self.assertEqual(len(checker.errorRecords), 2)
475496

476-
# Empty input returns blank
477-
result = checker._convertToDateTime("format:%Y%m%dT%H%M%S", "fieldName", "", False, True)
497+
# Test for value Error
498+
checker._log_error = Mock()
499+
500+
valid_fhir_date = "2022-01"
501+
result = checker._convertToDateTime("fhir-date", "fieldName", valid_fhir_date, False, True)
478502
self.assertEqual(result, "")
479503

504+
# ensure we logged exactly that ValueError
505+
checker._log_error.assert_called_once()
506+
field, value, err = checker._log_error.call_args[0]
507+
self.assertEqual((field, value), ("fieldName", valid_fhir_date))
508+
self.assertIsInstance(err, ValueError)
509+
480510
@patch("ConversionChecker.LookUpData")
481511
def test_convert_to_boolean(self, MockLookUpData):
482512
dataParser = Mock()

delta_backend/tests/test_converter.py

Lines changed: 16 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,7 @@
11
import unittest
22
from unittest.mock import patch, MagicMock
33
from Converter import Converter
4+
import ExceptionMessages
45

56

67
class TestConverter(unittest.TestCase):
@@ -109,6 +110,21 @@ def test_extract_patient_details(self, mock_practitioner, mock_site_code, mock_p
109110
result = converter.extract_patient_details(json_data, "PERFORMING_PROFESSIONAL_SURNAME")
110111
self.assertEqual(result, "Smith")
111112

113+
# Pass a malformed datetime to trigger the exception
114+
json_data = {"occurrenceDateTime": "invalid-date-time"}
115+
116+
with patch.object(Converter, '_log_error', return_value="mocked-error") as mock_log_error:
117+
converter = Converter({})
118+
result = converter.extract_patient_details(json_data, "PERSON_SURNAME")
119+
120+
mock_log_error.assert_called_once()
121+
error_message = mock_log_error.call_args[0][0]
122+
error_code = mock_log_error.call_args[1]["code"]
123+
124+
self.assertIn("DateTime conversion error", error_message)
125+
self.assertIn("ValueError", error_message)
126+
self.assertEqual(error_code, ExceptionMessages.UNEXPECTED_EXCEPTION)
127+
self.assertEqual(result, "mocked-error")
112128

113129
if __name__ == "__main__":
114130
unittest.main()

0 commit comments

Comments
 (0)