Skip to content

Commit 2b09a1c

Browse files
JNygaard-SkylightJoshua Nygaard
andauthored
eicr metadata (#338)
## Description Add a property to eICRProcessor to get the eICR metadata: The eICR ID and vender. ## Related Issues Closes #328 --------- Co-authored-by: Joshua Nygaard <jnygaard@mac.myfiosgateway.com>
1 parent ef341ba commit 2b09a1c

File tree

5 files changed

+85
-10
lines changed

5 files changed

+85
-10
lines changed

packages/shared-models/src/shared_models/__init__.py

Lines changed: 13 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,19 @@
44
from pydantic import ConfigDict
55

66

7+
class CdaInstanceIdentifier(BaseModel):
8+
"""CDA Instance Identifier (II) data type.
9+
10+
https://build.fhir.org/ig/HL7/CDA-core-2.0/StructureDefinition-II.html
11+
"""
12+
13+
null_flavor: str | None = None
14+
assigning_authority_name: str | None = None
15+
displayable: bool | None = None
16+
root: str | None = None
17+
extension: str | None = None
18+
19+
720
class DataField(StrEnum):
821
"""Enum for eICR data fields relevant to the TTC module."""
922

packages/text-to-code/src/text_to_code/models/eicr.py

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,7 @@
11
from enum import StrEnum
22

33
from pydantic import BaseModel
4+
from shared_models import CdaInstanceIdentifier
45

56

67
class LabXPaths(StrEnum):
@@ -19,3 +20,10 @@ class Candidate(BaseModel):
1920
value: str
2021
xpath: LabXPaths
2122
system: str | None = None
23+
24+
25+
class Metadata(BaseModel):
26+
"""Model representing metadata about the eICR."""
27+
28+
eicr_id: CdaInstanceIdentifier
29+
eicr_vendor: str | None = None

packages/text-to-code/src/text_to_code/services/eicr_processor.py

Lines changed: 32 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,8 +1,10 @@
11
from lxml import etree
22
from lxml.etree import Element
3+
from shared_models import CdaInstanceIdentifier
34
from shared_models import DataField
45

56
from text_to_code.models import Candidate
7+
from text_to_code.models.eicr import Metadata
68
from text_to_code.services.utils import get_config_for_data_field
79

810

@@ -102,6 +104,36 @@ def _extract_text_from_element(self, element: Element) -> str:
102104

103105
return " ".join(filter(None, text_parts))
104106

107+
@property
108+
def eicr_metadata(self) -> Metadata:
109+
"""Get the eICR ID from the XML."""
110+
id_element = self._xml_root.find(".//id")
111+
if id_element is None or id_element.get("nullFlavor") is not None:
112+
raise ValueError("No ID element found in eICR XML.")
113+
instance_identifer = CdaInstanceIdentifier(
114+
root=id_element.get("root"),
115+
extension=id_element.get("extension"),
116+
assigning_authority_name=id_element.get("assigningAuthorityName"),
117+
displayable=_to_bool(id_element.get("displayable")),
118+
null_flavor=id_element.get("nullFlavor"),
119+
)
120+
vendor = self._xml_root.find(
121+
".//author/assignedAuthor/assignedAuthoringDevice/softwareName"
122+
)
123+
return Metadata(
124+
eicr_id=instance_identifer, eicr_vendor=vendor.text if vendor is not None else None
125+
)
126+
127+
128+
def _to_bool(value: str | None) -> bool | None:
129+
if value is None:
130+
return None
131+
if value.lower() == "true":
132+
return True
133+
if value.lower() == "false":
134+
return False
135+
return None
136+
105137

106138
def _create_xml_tree(xml: str) -> Element:
107139
"""Remove all namespaces from an XML tree."""

packages/text-to-code/tests/assets/basic_test_eicr.xml

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,7 @@
33
xmlns:voc="http://www.lantanagroup.com/voc"
44
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
55
xsi:schemaLocation="urn:hl7-org:v3 ../../schema/infrastructure/cda/CDA_SDTC.xsd">
6+
<id root="c8516bdc-8bb2-40aa-8dae-20a77546488f" />
67
<component>
78
<structuredBody>
89
<component>
@@ -22,4 +23,11 @@
2223
</component>
2324
</structuredBody>
2425
</component>
26+
<author>
27+
<assignedAuthor>
28+
<assignedAuthoringDevice>
29+
<softwareName>Test eCR Vendor Name</softwareName>
30+
</assignedAuthoringDevice>
31+
</assignedAuthor>
32+
</author>
2533
</ClinicalDocument>

packages/text-to-code/tests/unit/test_eicr_processor.py

Lines changed: 24 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -2,9 +2,11 @@
22

33
import pytest
44
from lxml.etree import XMLSyntaxError
5+
from shared_models import CdaInstanceIdentifier
56
from shared_models import DataField
67
from text_to_code.models import Candidate
78
from text_to_code.models import LabXPaths
9+
from text_to_code.models.eicr import Metadata
810
from text_to_code.services.eicr_processor import EicrProcessor
911

1012
EXAMPLE_EICRS_DIRECTORY = Path(__file__).parent.parent / "assets"
@@ -40,28 +42,40 @@ def test_bad_eicr(self):
4042

4143
class TestBasicEicrProcessor:
4244
@pytest.fixture(scope="class")
43-
def result(self) -> list[Candidate]:
45+
def eicr_processor(self) -> EicrProcessor:
4446
eicr_path = EXAMPLE_EICRS_DIRECTORY / "basic_test_eicr.xml"
4547
with eicr_path.open() as f:
4648
eicr_output = f.read()
4749

48-
return EicrProcessor(eicr_output).get_text_candidates(
49-
BASE_XPATH, DataField.LAB_TEST_NAME_RESULTED
50-
)
50+
return EicrProcessor(eicr_output)
51+
52+
@pytest.fixture(scope="class")
53+
def eicr_metadata(self, eicr_processor: EicrProcessor) -> Metadata:
54+
return eicr_processor.eicr_metadata
55+
56+
@pytest.fixture(scope="class")
57+
def candidates(self, eicr_processor: EicrProcessor) -> list[Candidate]:
58+
return eicr_processor.get_text_candidates(BASE_XPATH, DataField.LAB_TEST_NAME_RESULTED)
5159

52-
def test_attribute_candidate(self, result: list[Candidate]):
53-
assert result[0] == Candidate(
60+
def test_attribute_candidate(self, candidates: list[Candidate]):
61+
assert candidates[0] == Candidate(
5462
value="A custom code in display name.", xpath=LabXPaths.CODE_DISPLAY_NAME
5563
)
5664

57-
def test_text_candidate(self, result: list[Candidate]):
58-
assert result[1] == Candidate(
65+
def test_text_candidate(self, candidates: list[Candidate]):
66+
assert candidates[1] == Candidate(
5967
value="A custom code in original text.", xpath=LabXPaths.CODE_ORIGINAL_TEXT
6068
)
6169

62-
def test_candidate_count(self, result: list[Candidate]):
70+
def test_candidate_count(self, candidates: list[Candidate]):
6371
expected = 2
64-
assert len(result) == expected
72+
assert len(candidates) == expected
73+
74+
def test_metadata(self, eicr_metadata: Metadata):
75+
assert eicr_metadata == Metadata(
76+
eicr_id=CdaInstanceIdentifier(root="c8516bdc-8bb2-40aa-8dae-20a77546488f"),
77+
eicr_vendor="Test eCR Vendor Name",
78+
)
6579

6680

6781
class TestReferences:

0 commit comments

Comments
 (0)