Skip to content

Commit 15ab0e9

Browse files
authored
VED-241 SNOMED converter logic and validation (currently disabled)
1 parent 1bfb0a8 commit 15ab0e9

File tree

14 files changed

+297
-195
lines changed

14 files changed

+297
-195
lines changed

delta_backend/.coveragerc

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,3 @@
1+
[report]
2+
omit =
3+
tests/*

delta_backend/Makefile

Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -8,4 +8,16 @@ package: build
88
test:
99
python -m unittest
1010

11+
check-conversion:
12+
python tests/check_conversion.py
13+
14+
coverage-run:
15+
coverage run -m unittest discover -v
16+
17+
coverage-report:
18+
coverage report -m
19+
20+
coverage-html:
21+
coverage html
22+
1123
.PHONY: build package

delta_backend/poetry.lock

Lines changed: 19 additions & 2 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: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -13,6 +13,8 @@ python = "~3.11"
1313
boto3 = "~1.26.90"
1414
mypy-boto3-dynamodb = "^1.26.164"
1515
moto = "~4.2.11"
16+
python-stdnum = "^1.20"
17+
coverage = "^7.8.0"
1618

1719
[tool.poetry.group.dev.dependencies]
1820
coverage = "^7.8.0"

delta_backend/src/ConversionLayout.py

Lines changed: 6 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -153,11 +153,11 @@
153153
}
154154
},
155155
{
156-
"fieldNameFHIR": "extension|0|valueCodeableConcept|coding|0|code",
156+
"fieldNameFHIR": "extension|#:https://fhir.hl7.org.uk/StructureDefinition/Extension-UKCore-VaccinationProcedure|valueCodeableConcept|coding|#:http://snomed.info/sct|code",
157157
"fieldNameFlat": "VACCINATION_PROCEDURE_CODE",
158158
"expression": {
159159
"expressionName": "Not Empty",
160-
"expressionType": "NOTEMPTY",
160+
"expressionType": "SNOMED",
161161
"expressionRule": ""
162162
}
163163
},
@@ -184,7 +184,7 @@
184184
"fieldNameFlat": "VACCINE_PRODUCT_CODE",
185185
"expression": {
186186
"expressionName": "Not Empty",
187-
"expressionType": "NOTEMPTY",
187+
"expressionType": "SNOMED",
188188
"expressionRule": ""
189189
}
190190
},
@@ -229,7 +229,7 @@
229229
"fieldNameFlat": "SITE_OF_VACCINATION_CODE",
230230
"expression": {
231231
"expressionName": "Not Empty",
232-
"expressionType": "NOTEMPTY",
232+
"expressionType": "SNOMED",
233233
"expressionRule": ""
234234
}
235235
},
@@ -247,7 +247,7 @@
247247
"fieldNameFlat": "ROUTE_OF_VACCINATION_CODE",
248248
"expression": {
249249
"expressionName": "Not Empty",
250-
"expressionType": "NOTEMPTY",
250+
"expressionType": "SNOMED",
251251
"expressionRule": ""
252252
}
253253
},
@@ -292,7 +292,7 @@
292292
"fieldNameFlat": "INDICATION_CODE",
293293
"expression": {
294294
"expressionName": "Not Empty",
295-
"expressionType": "NOTEMPTY",
295+
"expressionType": "SNOMED",
296296
"expressionRule": ""
297297
}
298298
},

delta_backend/src/Converter.py

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -51,7 +51,7 @@ def _getSchemaParser(self, schemafile):
5151
return schemaParser
5252

5353
# Convert data against converter schema
54-
def _convertData(self, ConversionValidate, expression, dataParser, json_data):
54+
def _convertData(self, ConversionValidate: ConversionChecker, expression, dataParser: FHIRParser, json_data):
5555

5656
FHIRFieldName = expression["fieldNameFHIR"]
5757
FlatFieldName = expression["fieldNameFlat"]
@@ -60,7 +60,7 @@ def _convertData(self, ConversionValidate, expression, dataParser, json_data):
6060
expressionRule = expression["expression"]["expressionRule"]
6161

6262
try:
63-
conversionValues = dataParser.getKeyValue(FHIRFieldName)
63+
conversionValues = dataParser.getKeyValue(FHIRFieldName, expressionType, expressionRule)
6464
except Exception as e:
6565
message = "Data get value Unexpected exception [%s]: %s" % (e.__class__.__name__, e)
6666
error = self._log_error(message, code=ExceptionMessages.PARSING_ERROR)

delta_backend/src/FHIRParser.py

Lines changed: 33 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
# FHIR JSON importer and data access
22
import json
3-
3+
from utils import is_valid_simple_snomed
44

55
class FHIRParser:
66
# parser variables
@@ -10,6 +10,26 @@ class FHIRParser:
1010
def parseFHIRData(self, fhirData):
1111
self.FHIRFile = json.loads(fhirData) if isinstance(fhirData, str) else fhirData
1212

13+
14+
def _validate_expression_rule(self, expression_type, expression_rule, key_value_pair):
15+
"""
16+
Applies expression rules for filtering key-value pairs during searches.
17+
18+
This method provides a flexible foundation for implementing various filtering
19+
or validation rules, enabling more dynamic and configurable search behavior.
20+
While it currently supports only SNOMED code validation, the structure opens
21+
the door to applying a wide range of expression rules in the future.
22+
23+
For example, when processing a list of items, this method helps determine
24+
which item(s) satisfy specific criteria based on the logic defined by the
25+
expression type and rule.
26+
"""
27+
if expression_type == "SNOMED" and expression_rule == "validate-code":
28+
if key_value_pair.get("code"):
29+
return is_valid_simple_snomed(key_value_pair["code"])
30+
31+
return True
32+
1333
# scan for a key name or a value
1434
def _scanValuesForMatch(self, parent, matchValue):
1535
try:
@@ -21,18 +41,21 @@ def _scanValuesForMatch(self, parent, matchValue):
2141
return False
2242

2343
# locate an index for an item in a list
24-
def _locateListId(self, parent, locator):
25-
fieldList = locator.split(":")
44+
def _locateListId(self, parent, locator, expression_type, expression_rule: str = ""):
45+
fieldList = locator.split(":", 1)
2646
nodeId = 0
2747
index = 0
2848
try:
2949
while index < len(parent):
30-
for key in parent[index]:
31-
if (parent[index][key] == fieldList[1]) or (key == fieldList[1]):
50+
for key, value in parent[index].items():
51+
if (
52+
(value == fieldList[1] or key == fieldList[1])
53+
and self._validate_expression_rule(expression_type, expression_rule, parent[index])
54+
):
3255
nodeId = index
3356
break
3457
else:
35-
if self._scanValuesForMatch(parent[index][key], fieldList[1]):
58+
if self._scanValuesForMatch(value, fieldList[1]):
3659
nodeId = index
3760
break
3861
index += 1
@@ -54,26 +77,26 @@ def _getNode(self, parent, child):
5477
return result
5578

5679
# locate a value for a key
57-
def _scanForValue(self, FHIRFields):
80+
def _scanForValue(self, FHIRFields, expression_type, expression_rule: str = ""):
5881
fieldList = FHIRFields.split("|")
5982
# get root field before we iterate
6083
rootfield = self.FHIRFile[fieldList[0]]
6184
del fieldList[0]
6285
try:
6386
for field in fieldList:
6487
if field.startswith("#"):
65-
rootfield = self._locateListId(rootfield, field) # check here for default index??
88+
rootfield = self._locateListId(rootfield, field, expression_type, expression_rule) # check here for default index??
6689
else:
6790
rootfield = self._getNode(rootfield, field)
6891
except:
6992
rootfield = ""
7093
return rootfield
7194

7295
# get the value for a key
73-
def getKeyValue(self, fieldName):
96+
def getKeyValue(self, fieldName, expression_type: str = "", expression_rule: str = ""):
7497
value = []
7598
try:
76-
responseValue = self._scanForValue(fieldName)
99+
responseValue = self._scanForValue(fieldName, expression_type, expression_rule)
77100
except:
78101
responseValue = ""
79102

delta_backend/src/utils.py

Lines changed: 20 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,20 @@
1+
2+
from stdnum.verhoeff import validate
3+
4+
def is_valid_simple_snomed(simple_snomed: str) -> bool:
5+
"""
6+
This utility is designed for reuse and should be packaged as part of a
7+
shared validation module or service.
8+
"""
9+
min_snomed_length = 6
10+
max_snomed_length = 18
11+
try:
12+
return (
13+
simple_snomed is not None
14+
and simple_snomed.isdigit()
15+
and min_snomed_length <= len(simple_snomed) <= max_snomed_length
16+
and validate(simple_snomed)
17+
and (simple_snomed[-3:-1] in ("00", "10"))
18+
)
19+
except:
20+
return False

delta_backend/tests/sample_data/__init__.py

Whitespace-only changes.

delta_backend/tests/sample_data/fhir_sample.json

Lines changed: 23 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -43,15 +43,25 @@
4343
{
4444
"url": "https://fhir.hl7.org.uk/StructureDefinition/Extension-UKCore-VaccinationProcedure",
4545
"valueCodeableConcept": {
46-
"coding": [
47-
{
48-
"system": "http://snomed.info/sct",
49-
"code": "13246810000001",
50-
"display": "Administration of first dose of severe acute respiratory syndrome coronavirus 2 vaccine (procedure)"
51-
}
52-
]
46+
"coding": [
47+
{
48+
"code": "956951000000105",
49+
"display": "Seasonal influenza vaccination (procedure)",
50+
"system": "http://snomed.info/test"
51+
},
52+
{
53+
"code": "956951000000104",
54+
"display": "Seasonal influenza vaccination (procedure)",
55+
"system": "http://snomed.info/sct"
56+
},
57+
{
58+
"code": "NEG",
59+
"display": "Seasonal influenza vaccination (procedure)",
60+
"system": "https://acme.lab/resultcodes"
61+
}
62+
]
63+
}
5364
}
54-
}
5565
],
5666
"identifier": [
5767
{
@@ -62,6 +72,11 @@
6272
"status": "completed",
6373
"vaccineCode": {
6474
"coding": [
75+
{
76+
"system": "http://snomed.info/sct",
77+
"code": "39114911000001104",
78+
"display": "COVID-19 Vaccine Vaxzevria (ChAdOx1 S [recombinant]) not less than 2.5x100,000,000 infectious units/0.5ml dose suspension for injection multidose vials (AstraZeneca UK Ltd) (product)"
79+
},
6580
{
6681
"system": "http://snomed.info/sct",
6782
"code": "39114911000001105",

0 commit comments

Comments
 (0)