From c2ace76fb4fc4d2782d52fb98b1b8bb937fe09ea Mon Sep 17 00:00:00 2001 From: Robert Mitchell Date: Wed, 31 Jul 2024 09:01:22 -0700 Subject: [PATCH] Preserve trigger code elements within sections (#2005) * updated the loinc json object to include trigger code metadata; updated function and implementation * we're separating out trigger code data but not cleanly and not exactly right yet * updated the test function for loading json object * adding documentation to help guide further dev work * moved file name and added more details to notes * fixed typo * big refactor; if no params or sections_to_include are passed response is valid from schematron * testing out mermaid in the container readme * Update README.md * finally figured out solution to trigger code and clinical services filtering; valid output created * tests passing locally for test_refine.py * adding step to remove any section that isn't required * removed util functions we're no longer using; adding new clinical services dict test * test_refiner tests passing locally * remove _analyze_structure; as it's unused now * move _get_entries_for_section to tests/test_refine.py; as it's used only in tests now * adding missing docstrings * updated mermaid chart * remove Callable, args, kwargs * continue to remove Callable, args, kwargs * moved description to README, made correction in Dockerfile, and linked README to eICR-Notes * try adding fontawesome link * need space before fenced code div * no inline css so fontawesome icons won't work for mermaid on github it seems * make sure description is changed to README in main * this should hopefully get the integration test to pass --- containers/message-refiner/Dockerfile | 4 +- .../{description.md => README.md} | 51 + containers/message-refiner/app/main.py | 13 +- containers/message-refiner/app/refine.py | 660 +++-- containers/message-refiner/app/utils.py | 97 +- .../assets/refiner_details.json | 58 + .../assets/section_loincs.json | 83 - containers/message-refiner/eICR-Notes.md | 346 +++ containers/message-refiner/requirements.txt | 5 +- .../assets/message_refiner_test_eicr.xml | 2036 +++++++++++++++ .../assets/refined_message_condition_only.xml | 1057 +++++--- .../assets/refined_message_labs_reason.xml | 2324 ++++++++--------- .../assets/refined_message_no_parameters.xml | 668 +++++ ...ge_results_section_chlamydia_condition.xml | 700 +++++ .../refined_message_social_history_only.xml | 1322 +++++++--- .../message-refiner/tests/test_refine.py | 665 +++-- .../message-refiner/tests/test_refiner.py | 93 +- .../message-refiner/tests/test_utils.py | 102 +- 18 files changed, 7750 insertions(+), 2534 deletions(-) rename containers/message-refiner/{description.md => README.md} (64%) create mode 100644 containers/message-refiner/assets/refiner_details.json delete mode 100644 containers/message-refiner/assets/section_loincs.json create mode 100644 containers/message-refiner/eICR-Notes.md create mode 100644 containers/message-refiner/tests/assets/message_refiner_test_eicr.xml create mode 100644 containers/message-refiner/tests/assets/refined_message_no_parameters.xml create mode 100644 containers/message-refiner/tests/assets/refined_message_results_section_chlamydia_condition.xml diff --git a/containers/message-refiner/Dockerfile b/containers/message-refiner/Dockerfile index 9716b572bf..e2fbce9a25 100644 --- a/containers/message-refiner/Dockerfile +++ b/containers/message-refiner/Dockerfile @@ -7,7 +7,7 @@ RUN pip install -r requirements.txt COPY ./app /code/app COPY ./assets /code/assets -COPY ./description.md /code/description.md +COPY ./README.md /code/README.md EXPOSE 8080 -CMD uvicorn app.main:app --host 0.0.0.0 --port 8080 \ No newline at end of file +CMD uvicorn app.main:app --host 0.0.0.0 --port 8080 diff --git a/containers/message-refiner/description.md b/containers/message-refiner/README.md similarity index 64% rename from containers/message-refiner/description.md rename to containers/message-refiner/README.md index 271873f284..95f73b9a33 100644 --- a/containers/message-refiner/description.md +++ b/containers/message-refiner/README.md @@ -48,3 +48,54 @@ To build the Docker image for the message refiner from source instead of downloa ### The API When viewing these docs from the `/redoc` endpoint on a running instance of the message refiner or the PHDI website, detailed documentation on the API will be available below. + +### Architecture Diagram + +```mermaid +flowchart LR + +subgraph requests["Requests"] +direction TB +subgraph GET["fas:fa-download GET"] +hc["/\n(health check)"] +example["/example-collection\n(Example Requests)"] +end +subgraph PUT["fas:fa-upload PUT"] +ecr["/ecr\n(refine eICR)"] +end +end + +subgraph service[REST API Service] +direction TB +subgraph mr["fab:fa-docker container"] +refiner["fab:fa-python message-refiner
HTTP:8080/
"] +end +subgraph tcr["fab:fa-docker container"] +tcr-service["fab:fa-python trigger-code-reference
HTTP:8081/
"] <==> db["fas:fa-database SQLite DB"] +end +mr <==> |/get-value-sets| tcr +end + +subgraph response["Responses"] +subgraph JSON["fa:fa-file-alt JSON"] +rsp-hc["fa:fa-file-code OK fa:fa-thumbs-up"] +rsp-example["fa:fa-file-code Postman Collection"] +end +subgraph XML["fas:fa-chevron-left fas:fa-chevron-right XML"] +rsp-ecr["fas:fa-file-code Refined eICR"] +end +end + +hc -.-> mr -.-> rsp-hc +example --> mr --> rsp-example +ecr ===> mr ===> rsp-ecr + +``` + +### Additional notes on eICR Refinement + +For further details on `
`, ``, and `` elements, please see [eICR-Notes.md](eICR-Notes.md) for an explanation of trigger code ``s, which sections they're in, and the `` data that should be returned in the refined eICR output. + +``` + +``` diff --git a/containers/message-refiner/app/main.py b/containers/message-refiner/app/main.py index 4f725bcf6e..77a97bf781 100644 --- a/containers/message-refiner/app/main.py +++ b/containers/message-refiner/app/main.py @@ -15,7 +15,7 @@ from app.refine import refine from app.refine import validate_message from app.refine import validate_sections_to_include -from app.utils import create_clinical_xpaths +from app.utils import create_clinical_services_dict from app.utils import read_json_from_assets settings = get_settings() @@ -26,7 +26,7 @@ app = BaseService( service_name="Message Refiner", service_path="/message-refiner", - description_path=Path(__file__).parent.parent / "description.md", + description_path=Path(__file__).parent.parent / "README.md", include_health_check_endpoint=False, openapi_url="/message-refiner/openapi.json", ).start() @@ -156,10 +156,9 @@ async def refine_ecr( content=error_message, status_code=status.HTTP_422_UNPROCESSABLE_ENTITY ) - clinical_services_xpaths = None + clinical_services = None if conditions_to_include: responses = await get_clinical_services(conditions_to_include) - # confirm all API responses were 200 if set([response.status_code for response in responses]) != {200}: error_message = ";".join( [str(response) for response in responses if response.status_code != 200] @@ -168,9 +167,11 @@ async def refine_ecr( content=error_message, status_code=status.HTTP_502_BAD_GATEWAY ) clinical_services = [response.json() for response in responses] - clinical_services_xpaths = create_clinical_xpaths(clinical_services) - data = refine(validated_message, sections, clinical_services_xpaths) + # create a simple dictionary structure for refine.py to consume + clinical_services = create_clinical_services_dict(clinical_services) + + data = refine(validated_message, sections, clinical_services) return Response(content=data, media_type="application/xml") diff --git a/containers/message-refiner/app/refine.py b/containers/message-refiner/app/refine.py index bc26737655..abbfd9ee9e 100644 --- a/containers/message-refiner/app/refine.py +++ b/containers/message-refiner/app/refine.py @@ -1,27 +1,40 @@ -from lxml import etree as ET +from typing import Dict +from typing import List +from typing import Optional +from typing import Union -from app.utils import load_section_loincs +from lxml import etree + +# read json that contains details for refining and is the base of what drives `refine` from app.utils import read_json_from_assets -# LOINC codes for eICR sections our refiner API accepts -SECTION_LOINCS, SECTION_DETAILS = load_section_loincs( - read_json_from_assets("section_loincs.json") -) +REFINER_DETAILS = read_json_from_assets("refiner_details.json") + +# extract section LOINC codes from the REFINER_DETAILS dictionary +SECTION_LOINCS = list(REFINER_DETAILS["sections"].keys()) + +# ready to use list of all trigger code templateIds for simpler XPath query construction +TRIGGER_CODE_TEMPLATE_IDS = [ + "2.16.840.1.113883.10.20.15.2.3.5", + "2.16.840.1.113883.10.20.15.2.3.3", + "2.16.840.1.113883.10.20.15.2.3.4", + "2.16.840.1.113883.10.20.15.2.3.2", +] def validate_message(raw_message: str) -> tuple[bytes | None, str]: """ - Validates that an incoming XML message can be parsed by lxml's etree . + Validates that an incoming XML message can be parsed by lxml's etree. :param raw_message: The XML input. :return: The validation result as a string. """ error_message = "" try: - validated_message = ET.fromstring(raw_message) + validated_message = etree.fromstring(raw_message) return (validated_message, error_message) - except ET.XMLSyntaxError as e: - error_message = f"XMLSyntaxError: {e}" + except etree.XMLSyntaxError as error: + error_message = f"XMLSyntaxError: {error}" return (None, str(error_message)) @@ -31,7 +44,7 @@ def validate_sections_to_include(sections_to_include: str | None) -> tuple[list, of corresponding LOINC codes. :param sections_to_include: The sections to include in the refined message. - :raises ValueError: When at least one of the sections_to_inlcude is invalid. + :raises ValueError: When at least one of the sections_to_include is invalid. :return: A tuple that includes the sections to include in the refined message as a list of LOINC codes corresponding to the sections and an error message. If there is no error in validating the sections to include, the error message will be an empty @@ -52,262 +65,445 @@ def validate_sections_to_include(sections_to_include: str | None) -> tuple[list, def refine( - validated_message: bytes, - sections_to_include: list = None, - clinical_services: list = None, + validated_message: etree.Element, + sections_to_include: Optional[List[str]] = None, + clinical_services: Optional[Dict[str, List[str]]] = None, ) -> str: """ - Refines an incoming XML message based on the sections to include and/or - the clinical services found based on inputted section LOINC codes or - condition SNOMED codes. This will then loop through the dynamic XPaths to - create an XPath to refine the XML. - - :param validated_message: The XML input. - :param sections_to_include: The sections to include in the refined message. - :param clinical_services: clinical service XPaths to include in the - refined message. - :return: The refined message. + Refines an eICR XML document by processing its sections based on the provided parameters. + + Case 1: No parameters (only validated_message provided) + - Check for template IDs. + - If found, find matching observations, clean up entries, and update text. + - If not found, create a minimal section. + + Case 2: sections_to_include provided + - For sections in sections_to_include: + - Ignore processing (leave them as they are, unprocessed). + - For all other sections: + - Check for template IDs. + - If found, find matching observations, clean up entries, and update text. + - If not found, create a minimal section. + + Case 3: clinical_services provided + - Check for both template IDs and codes. + - If found, find matching observations, clean up entries, and update text. + - If not found, create a minimal section. + + Case 4: Both sections_to_include and clinical_services provided + - For sections in sections_to_include: + - Check for both template IDs and codes. + - If found, find matching observations, clean up entries, and update text. + - If not found, create a minimal section. + - For all other sections: + - Check for template IDs. + - If found, find matching observations, clean up entries, and update text. + - If not found, create a minimal section. + + :param validated_message: The eICR XML document to be refined. + :param sections_to_include: Optional list of section LOINC codes for the sections from + the ; passing only this parameter retains those section whereas + passing it with clinical_services focuses the search to the sections in this list. + :param clinical_services: Optional dictionary of clinical service codes to check within + sections from the Trigger Code Reference Service. + :return: The refined eICR XML document as a string. """ - header = _select_message_header(validated_message) + # dictionary that will hold the section processing instructions + # this is based on the combination of parameters passed to `refine` + # as well as deails from REFINER_DETAILS + section_processing = { + code: details for code, details in REFINER_DETAILS["sections"].items() + } + namespaces = {"hl7": "urn:hl7-org:v3"} - elements = [] - - # if no parameters are provided, return the header with all sections - if not sections_to_include and not clinical_services: - xpath_expression = "//*[local-name()='section']" - elements = validated_message.xpath(xpath_expression, namespaces=namespaces) - return _add_root_element(header, elements) - - # start with sections_to_include param - if sections_to_include: - sections_xpaths = " or ".join( - [f"@code='{section}'" for section in sections_to_include] - ) - sections_xpath_expression = ( - f"//*[local-name()='section'][hl7:code[{sections_xpaths}]]" + structured_body = validated_message.find(".//hl7:structuredBody", namespaces) + + # case 2: if only sections_to_include is provided, remove these sections from section_processing + if sections_to_include is not None and clinical_services is None: + section_processing = { + key: value + for key, value in section_processing.items() + if key not in sections_to_include + } + + # process sections + for code, details in section_processing.items(): + section = _get_section_by_code(structured_body, code) + if section is None: + continue # go to the next section if not found + + # case 4: search in sections_to_include for clinical_services; for sections + # not in sections_to_include, search for templateIds + if sections_to_include is not None and clinical_services is not None: + if code in sections_to_include: + combined_xpaths = _generate_combined_xpath( + template_ids=TRIGGER_CODE_TEMPLATE_IDS, + clinical_services_dict=clinical_services, + ) + clinical_services_codes = [ + code for codes in clinical_services.values() for code in codes + ] + _process_section( + section, + combined_xpaths, + namespaces, + TRIGGER_CODE_TEMPLATE_IDS, + clinical_services_codes, + ) + else: + combined_xpaths = _generate_combined_xpath( + template_ids=TRIGGER_CODE_TEMPLATE_IDS, + clinical_services_dict={}, + ) + _process_section( + section, combined_xpaths, namespaces, TRIGGER_CODE_TEMPLATE_IDS + ) + + # case 3: process all sections with clinical_services (no sections_to_include) + elif clinical_services is not None and sections_to_include is None: + combined_xpaths = _generate_combined_xpath( + template_ids=TRIGGER_CODE_TEMPLATE_IDS, + clinical_services_dict=clinical_services, + ) + clinical_services_codes = [ + code for codes in clinical_services.values() for code in codes + ] + _process_section( + section, + combined_xpaths, + namespaces, + TRIGGER_CODE_TEMPLATE_IDS, + clinical_services_codes, + ) + + # case 1: no parameters, process all sections normally + # case 2: process sections not in sections_to_include + else: + combined_xpaths = _generate_combined_xpath( + template_ids=TRIGGER_CODE_TEMPLATE_IDS, clinical_services_dict={} + ) + _process_section( + section, combined_xpaths, namespaces, TRIGGER_CODE_TEMPLATE_IDS + ) + + # TODO: there may be sections that are not standard but appear in an eICR that + # we could either decide to add to the refiner_details.json or use this code + # before returning the refined output that removes sections that are not required + for section in structured_body.findall(".//hl7:section", namespaces): + section_code = section.find(".//hl7:code", namespaces).get("code") + if section_code not in SECTION_LOINCS: + parent = section.getparent() + parent.remove(section) + + return etree.tostring(validated_message, encoding="unicode") + + +def _process_section( + section: etree.Element, + combined_xpaths: str, + namespaces: dict, + template_ids: List[str], + clinical_services_codes: Optional[List[str]] = None, +) -> None: + """ + Processes a section by checking for elements, finding observations, + cleaning up entries, and updating text. + + :param section: The section element to process. + :param combined_xpaths: The combined XPath expression for finding elements. + :param namespaces: The namespaces to use in XPath queries. + :param template_ids: The list of template IDs to check. + :param clinical_services_codes: Optional list of clinical service codes to check. + :return: None + """ + check_elements = _are_elements_present( + section, "templateId", template_ids, namespaces + ) + if clinical_services_codes: + check_elements |= _are_elements_present( + section, "code", clinical_services_codes, namespaces ) - # we need to find the condition codes in the clinical_services list and then - # extract the section LOINC codes - if clinical_services: - # join the xpath expressions together - services_xpath_expression = " | ".join(clinical_services) - # grab all of the elements with the clinical_service codes - service_elements = validated_message.xpath( - services_xpath_expression, namespaces=namespaces - ) - # initialize a dictionary to hold the section code and the element - services_section_and_elements = {} - for element in service_elements: - # the parent is the section element - parent = element.getparent() - # find the code element and extract the section LOINC - code_element = parent.find(".//hl7:code", namespaces=namespaces) - code = code_element.get("code") - # as we create the dictionary, we want to make sure the section key is unique - if code not in services_section_and_elements: - services_section_and_elements[code] = [] - # append () element to corresponding section key - services_section_and_elements[code].append(element) - - # if we only have sections_to_include then we need to create minimal sections - # for the sections not included - if sections_to_include and not clinical_services: - minimal_sections = _create_minimal_sections(sections_to_include) - xpath_expression = sections_xpath_expression - elements = validated_message.xpath(xpath_expression, namespaces=namespaces) - return _add_root_element(header, elements + minimal_sections) - - # if we only have clinical_services then we use the unique sections from the - # services_section_and_elements dictionary to include entries to sections + minimal sections - # if no sections_to_include are provided - if clinical_services and not sections_to_include: - elements = [] - for section_code, entries in services_section_and_elements.items(): - minimal_section = _create_minimal_section(section_code, empty_section=False) - for entry in entries: - minimal_section.append(entry) - elements.append(minimal_section) - minimal_sections = _create_minimal_sections( - sections_with_conditions=services_section_and_elements.keys(), - empty_section=True, - ) - return _add_root_element(header, elements + minimal_sections) - - # if we have both sections_to_include and clinical_services then we need to - # prioritize the clinical_services using the sections_to_include as a locus - if sections_to_include and clinical_services: - # check if there is match between sections_to_include and conditions we want to include - # if there is a match, these are the _only_ sections we want to include - matching_sections = set(sections_to_include) & set( - services_section_and_elements.keys() - ) - # if there is no match, we will respond with empty; minimal sections - if not matching_sections: - minimal_sections = _create_minimal_sections() - return _add_root_element(header, minimal_sections) - - elements = [] - for section_code in matching_sections: - minimal_section = _create_minimal_section(section_code, empty_section=False) - for entry in services_section_and_elements[section_code]: - minimal_section.append(entry) - elements.append(minimal_section) - - minimal_sections = _create_minimal_sections( - sections_with_conditions=matching_sections, - empty_section=True, - ) + if check_elements: + observations = _get_observations(section, combined_xpaths, namespaces) + if observations: + paths = [_find_path_to_entry(obs) for obs in observations] + _prune_unwanted_siblings(paths, observations) + _update_text_element(section, observations) + else: + _create_minimal_section(section) + else: + _create_minimal_section(section) - return _add_root_element(header, elements + minimal_sections) +def _generate_combined_xpath( + template_ids: List[str], clinical_services_dict: Dict[str, List[str]] +) -> str: + """ + Generate a combined XPath expression for templateIds and all codes across all systems, ensuring they are within 'observation' elements. + """ + xpath_conditions = [] + + # add templateId conditions within elements if needed + if template_ids: + template_id_conditions = [ + f'.//hl7:observation[hl7:templateId[@root="{tid}"]]' for tid in template_ids + ] + xpath_conditions.extend(template_id_conditions) + + # add code conditions within elements + for codes in clinical_services_dict.values(): + for code in codes: + code_conditions = f'.//hl7:observation[hl7:code[@code="{code}"]]' + xpath_conditions.append(code_conditions) + + # combine all conditions into a single XPath query using the union operator + combined_xpath = " | ".join(xpath_conditions) + return combined_xpath + + +def _get_section_by_code( + structured_body: etree.Element, + code: str, + namespaces: dict = {"hl7": "urn:hl7-org:v3"}, +) -> etree.Element: + """ + Gets a section of an eICR's by its LOINC code and returns the
element. -def _create_minimal_section(section_code: str, empty_section: bool) -> ET.Element: + :param structuredBody: The structuredBody element to search within. + :param code: LOINC code of the
to retrieve. + :param namespaces: The namespaces to use when searching for elements and defaults to 'hl7'. + :return: The
element of the section with the given LOINC code. """ - Helper function to create a minimal section element based on the LOINC section code. + xpath_query = f'.//hl7:section[hl7:code[@code="{code}"]]' + section = structured_body.xpath(xpath_query, namespaces=namespaces) + if section is not None and len(section) == 1: + return section[0] - :param section_code: The LOINC code of the section to create a minimal section for. - :param empty_section: Whether the section should be empty and include a nullFlavor attribute. - :return: A minimal section element or None if the section code is not recognized. + +def _get_observations( + section: etree.Element, + combined_xpath: str, + namespaces: dict = {"hl7": "urn:hl7-org:v3"}, +) -> List[etree.Element]: """ - if section_code not in SECTION_DETAILS: - return None + Get matching observations from a section or a callable returning a section based on combined XPath query. - display_name, template_root, template_extension, title_text = SECTION_DETAILS[ - section_code - ] + :param section: The
element of the section to retrieve observations from. + :param combined_xpath: this will be either code values from the TCR or templateId root values in one combined XPath. + :param namespaces: The namespaces to use when searching for elements and defaults to 'hl7'. + :return: A list of matching elements. + """ + # use a list to store the final list of matching observation elements + observations = [] + # use a set to store elements for uniqueness; trigger code data _may_ match clinical services + seen = set() + + # search once for matching elements using the combined XPath expression + matching_elements = section.xpath(combined_xpath, namespaces=namespaces) + for element in matching_elements: + if element not in seen: + seen.add(element) + observations.append(element) + + # TODO: we are not currently checking the codeSystemName at this time. this is because + # there is variation even within a single eICR in connection to the codeSystemName. + # you may see both "LOINC" and "loinc.org" as well as "SNOMED" and "SNOMED CT" in the + # same message. dynamically altering the XPath with variant names adds complexity and computation; + # we _can_ post filter, which i would suggest as a function that uses this one as its input. + # this is why there are two main transformations of the response from the TCR; one that is a dictionary + # of code systems and codes and another that is a combined XPath for all codes. this way we + # loop less, search less, and aim for simplicity + + return observations + + +def _are_elements_present( + section: etree.Element, + search_type: str, + search_values: List[str], + namespaces: dict = {"hl7": "urn:hl7-org:v3"}, +) -> bool: + """ + Checks if any of the specified elements are present in a section based on the search type and values. - section = ET.Element("section") - if empty_section: - section.set("nullFlavor", "NI") + :param section: The
element of the section to search within. + :param search_type: The type of search ('templateId' or 'code'). + :param search_values: The list of values to search for (template IDs or codes). + :param namespaces: The namespaces to use when searching for elements and defaults to 'hl7'. + :return: True if any of the specified elements are present, False otherwise. + """ + if search_type == "templateId": + xpath_queries = [ + f'.//hl7:templateId[@root="{value}"]' for value in search_values + ] + elif search_type == "code": + xpath_queries = [f'.//hl7:code[@code="{value}"]' for value in search_values] - templateId1 = ET.Element( - "templateId", root=template_root, extension=template_extension - ) - section.append(templateId1) + combined_xpath = " | ".join(xpath_queries) + return bool(section.xpath(combined_xpath, namespaces=namespaces)) - code = ET.Element( - "code", - code=section_code, - codeSystem="2.16.840.1.113883.6.1", - displayName=display_name, - ) - section.append(code) - title = ET.Element("title") - title.text = title_text - section.append(title) +def _find_path_to_entry(element: etree.Element) -> List[etree.Element]: + """ + Helper function to find the path from a given element to the parent element. + """ + path = [] + current_element = element + while current_element.tag != "{urn:hl7-org:v3}entry": + path.append(current_element) + current_element = current_element.getparent() + if current_element is None: + raise ValueError("Parent element not found.") + path.append(current_element) # Add the element + path.reverse() # Reverse to get the path from to the given element + return path + + +def _prune_unwanted_siblings( + paths: List[List[etree.Element]], desired_elements: List[etree.Element] +): + """ + Prunes unwanted siblings based on the desired elements. - text_elem = ET.Element("text") - if empty_section: - text_elem.text = "Removed via PRIME DIBBs Message Refiner API endpoint by Public Health Authority" - else: - text_elem.text = "Only entries that match the corresponding condition code were included in this section via PRIME DIBBs Message Refiner API endpoint by Public Health Authority" - section.append(text_elem) + :param paths: List of paths, where each path is a list of elements from to an . + :param desired_elements: List of desired elements to keep. + """ + # flatten the list of paths and remove duplicates + all_elements_to_keep = {elem for path in paths for elem in path} - return section + # iterate through all collected paths to prune siblings + for path in paths: + for element in path: + parent = element.getparent() + if parent is not None: + siblings = parent.findall(element.tag) + for sibling in siblings: + # only remove siblings that are not in the collected elements + if sibling not in all_elements_to_keep: + parent.remove(sibling) -def _create_minimal_sections( - sections_to_include: list = None, - sections_with_conditions: list = None, - empty_section: bool = True, -) -> list: +def _extract_observation_data( + observation: etree.Element, +) -> Dict[str, Union[str, bool]]: """ - Creates minimal sections for sections not included in sections_to_include. + Extracts relevant data from an observation element, including checking for trigger code template ID. - :param sections_to_include: The sections to include in the refined message. - :param sections_with_conditions: Sections that have condition-specific entries. - :param empty_section: Whether the sections should be empty and include a nullFlavor attribute. - :return: List of minimal section elements. + :param observation: The observation element. + :return: A dictionary with extracted data. """ - minimal_sections = [] - sections_to_exclude = ( - set(SECTION_DETAILS.keys()) - - set(sections_to_include or []) - - set(sections_with_conditions or []) + template_id_elements = observation.findall( + ".//hl7:templateId", namespaces={"hl7": "urn:hl7-org:v3"} ) + is_trigger_code = False + + for elem in template_id_elements: + root = elem.get("root") + if root in TRIGGER_CODE_TEMPLATE_IDS: + is_trigger_code = True + break + + data = { + "display_text": observation.find( + ".//hl7:code", namespaces={"hl7": "urn:hl7-org:v3"} + ).get("displayName"), + "code": observation.find( + ".//hl7:code", namespaces={"hl7": "urn:hl7-org:v3"} + ).get("code"), + "code_system": observation.find( + ".//hl7:code", namespaces={"hl7": "urn:hl7-org:v3"} + ).get("codeSystemName"), + "is_trigger_code": is_trigger_code, + } + return data - for section_code in sections_to_exclude: - minimal_section = _create_minimal_section(section_code, empty_section) - if minimal_section is not None: - minimal_sections.append(minimal_section) - return minimal_sections +def _create_or_update_text_element(observations: List[etree.Element]) -> etree.Element: + """ + Creates or updates a element with a table containing information from the given observations. -def _add_root_element(header: bytes, elements: list) -> str: + :param section: The section element to update. + :param observations: A list of observation elements. + :return: The created or updated element. """ - This helper function sets up and creates a new root element for the XML - by using a combination of a direct namespace uri and nsmap to ensure that - the default namespaces are set correctly. - :param header: The header section of the XML. - :param elements: List of refined elements found in XML. - :return: The full refined XML, formatted as a string. + text_element = etree.Element("{urn:hl7-org:v3}text") + title = etree.SubElement(text_element, "title") + title.text = "Output from CDC PRIME DIBBs `message-refiner` API by request of STLT" + + table_element = etree.SubElement(text_element, "table", border="1") + header_row = etree.SubElement(table_element, "tr") + headers = ["Display Text", "Code", "Code System", "Trigger Code Observation"] + + for header in headers: + th = etree.SubElement(header_row, "th") + th.text = header + + # add observation data to table + for observation in observations: + data = _extract_observation_data(observation) + row = etree.SubElement(table_element, "tr") + for key in headers[:-1]: # Exclude the last header as it's for the boolean flag + td = etree.SubElement(row, "td") + td.text = data[key.lower().replace(" ", "_")] + + # add boolean flag for trigger code observation + td = etree.SubElement(row, "td") + td.text = "TRUE" if data["is_trigger_code"] else "FALSE" + + return text_element + + +def _update_text_element( + section: etree.Element, observations: List[etree.Element] +) -> None: """ - namespace = "urn:hl7-org:v3" - nsmap = { - None: namespace, - "cda": namespace, - "sdtc": "urn:hl7-org:sdtc", - "xsi": "http://www.w3.org/2001/XMLSchema-instance", - } - # creating the root element with our uri namespace and nsmap - refined_message_root = ET.Element(f"{{{namespace}}}ClinicalDocument", nsmap=nsmap) - for h in header: - refined_message_root.append(h) - # creating the component element and structuredBody element with the same namespace - # and adding them to the new root - main_component = ET.SubElement(refined_message_root, f"{{{namespace}}}component") - structuredBody = ET.SubElement(main_component, f"{{{namespace}}}structuredBody") - - # Append the filtered elements to the new root and use the uri namespace - for element in elements: - section_component = ET.SubElement(structuredBody, f"{{{namespace}}}component") - section_component.append(element) - - # Create a new ElementTree with the result root - refined_message = ET.ElementTree(refined_message_root) - return ET.tostring(refined_message, encoding="unicode") - - -def _select_message_header(raw_message: bytes) -> bytes: + Updates the element of a section to include information from observations. + + :param section: The section element containing the element to update. + :param observations: A list of observation elements to include in the element. """ - Helper function that selects the header of an incoming message. + new_text_element = _create_or_update_text_element(observations) - :param raw_message: The XML input. - :return: The header section of the XML. + existing_text_element = section.find( + ".//hl7:text", namespaces={"hl7": "urn:hl7-org:v3"} + ) + + if existing_text_element is not None: + section.replace(existing_text_element, new_text_element) + else: + section.insert(0, new_text_element) + + +def _create_minimal_section(section: etree.Element) -> None: + """ + Creates a minimal section by updating the element, removing all elements, + and adding nullFlavor="NI" to the
element. + + :param section: The section element to update. """ - HEADER_SECTIONS = [ - "realmCode", - "typeId", - "templateId", - "id", - "code", - "title", - "effectiveTime", - "confidentialityCode", - "languageCode", - "setId", - "versionNumber", - "recordTarget", - "author", - "custodian", - "componentOf", - ] - - # Set up XPath expression namespaces = {"hl7": "urn:hl7-org:v3"} - xpath_expression = " | ".join( - [f"//hl7:ClinicalDocument/hl7:{section}" for section in HEADER_SECTIONS] + text_element = section.find(".//hl7:text", namespaces=namespaces) + + if text_element is None: + text_element = etree.Element("{urn:hl7-org:v3}text") + section.append(text_element) + + # update the element with the specific message + text_element.clear() + title_element = etree.SubElement(text_element, "title") + title_element.text = ( + "Output from CDC PRIME DIBBs `message-refiner` API by request of STLT" ) - # Use XPath to find elements matching the expression - elements = raw_message.xpath(xpath_expression, namespaces=namespaces) - # Create & set up a new root element for the refined XML - header = ET.Element(raw_message.tag) + table_element = etree.SubElement(text_element, "table", border="1") + tr_element = etree.SubElement(table_element, "tr") + td_element = etree.SubElement(tr_element, "td") + td_element.text = "Section details have been removed as requested" - # Append the filtered elements to the new root - for element in elements: - header.append(element) + # remove all elements + for entry in section.findall(".//hl7:entry", namespaces=namespaces): + section.remove(entry) - return header + # add nullFlavor="NI" to the
element + section.attrib["nullFlavor"] = "NI" diff --git a/containers/message-refiner/app/utils.py b/containers/message-refiner/app/utils.py index 25a8f6dec6..36f5590449 100644 --- a/containers/message-refiner/app/utils.py +++ b/containers/message-refiner/app/utils.py @@ -1,62 +1,9 @@ import json import pathlib +from typing import Dict from typing import List -def _generate_clinical_xpaths(system: str, codes: List[str]) -> List[str]: - """ - This is a small helper function that loops through codes to create a set of - xpaths that can be used in the refine step. - - :param system: This is the system type of the clinical service codes. - :param codes: This is a list of the clinical service codes for a specified - SNOMED code. - """ - """ - As of May 2024, these are the code systems used in clinical services - code_system - http://snomed.info/sct 28102 - http://loinc.org 9509 - http://hl7.org/fhir/sid/icd-10-cm 5892 - http://www.nlm.nih.gov/research/umls/rxnorm 468 - http://hl7.org/fhir/sid/cvx 2 - """ - system_dict = { - "http://hl7.org/fhir/sid/icd-10-cm": "ICD10", - "http://snomed.info/sct": "SNOMED CT", - "http://loinc.org": "loinc.org", - "http://www.nlm.nih.gov/research/umls/rxnorm": "?", # TODO - "http://hl7.org/fhir/sid/cvx": "?", # TODO - } - - # add condition to confirm if system in dict - if system not in system_dict.keys(): - raise KeyError(f"{system} not a recognized clinical service system.") - - # Loop through each code and create the XPath expressions - return [ - f".//*[local-name()='entry'][.//*[@code='{code}' and @codeSystemName='{system_dict.get(system)}']]" - for code in codes - ] - - -def create_clinical_xpaths(clinical_services_list: list[dict]) -> list[str]: - """ - This function loops through each of those clinical service codes and their - system to create a list of all possible xpath queries. - :param clinical_services_list: List of clinical_service dictionaries. - :return: List of xpath queries to check. - """ - clinical_services_xpaths = [] - for clinical_services in clinical_services_list: - for system, entries in clinical_services.items(): - for entry in entries: - system = entry.get("system") - xpaths = _generate_clinical_xpaths(system, entry.get("codes")) - clinical_services_xpaths.extend(xpaths) - return clinical_services_xpaths - - def read_json_from_assets(filename: str) -> dict: """ Reads a JSON file from the assets directory. @@ -79,11 +26,47 @@ def load_section_loincs(loinc_json: dict) -> tuple[list, dict]: section_list = list(loinc_json.keys()) # dictionary of the required eICR sections' - # LOINC code, displayName, templateId, extension, and title - # to be used to create minimal sections and to support validation + # LOINC section code, root templateId and extension, displayName, and title + # to be used to create minimal sections and trigger code templates to support validation section_details = { - loinc: details.get("minimal_fields") + loinc: { + "minimal_fields": details.get("minimal_fields"), + "trigger_code_template": details.get("trigger_code_template"), + } for loinc, details in loinc_json.items() if details.get("required") } return (section_list, section_details) + + +def create_clinical_services_dict( + clinical_services_list: List[Dict], +) -> Dict[str, List[str]]: + """ + Transform the original Trigger Code Reference API response to have keys as systems + and values as lists of codes, while ensuring the systems are recognized and using their + shorthand names so that we can both dynamicall construct XPaths and post-filter matches + to system name varients. + """ + system_dict = { + "http://hl7.org/fhir/sid/icd-10-cm": "icd10", + "http://snomed.info/sct": "snomed", + "http://loinc.org": "loinc", + "http://www.nlm.nih.gov/research/umls/rxnorm": "rxnorm", # TODO + "http://hl7.org/fhir/sid/cvx": "cvx", # TODO + } + + transformed_dict = {} + for clinical_services in clinical_services_list: + for service_type, entries in clinical_services.items(): + for entry in entries: + system = entry.get("system") + if system not in system_dict.keys(): + raise KeyError( + f"{system} not a recognized clinical service system." + ) + shorthand_system = system_dict[system] + if shorthand_system not in transformed_dict: + transformed_dict[shorthand_system] = [] + transformed_dict[shorthand_system].extend(entry.get("codes", [])) + return transformed_dict diff --git a/containers/message-refiner/assets/refiner_details.json b/containers/message-refiner/assets/refiner_details.json new file mode 100644 index 0000000000..65eb2161a5 --- /dev/null +++ b/containers/message-refiner/assets/refiner_details.json @@ -0,0 +1,58 @@ +{ + "sections": { + "46240-8": { + "required": true, + "display_name": "History of encounters", + "trigger_codes": { + "manually_triggered": { + "template_id_root": "2.16.840.1.113883.10.20.15.2.3.5" + }, + "problem_observation": { + "template_id_root": "2.16.840.1.113883.10.20.15.2.3.3" + } + } + }, + "10164-2": { + "required": true, + "display_name": "History of Present Illness" + }, + "11369-6": { + "required": false, + "display_name": "History of Immunizations" + }, + "29549-3": { + "required": true, + "display_name": "Medications Administered" + }, + "18776-5": { + "required": true, + "display_name": "Plan of Treatment", + "trigger_codes": { + "lab_test_order": { + "template_id_root": "2.16.840.1.113883.10.20.15.2.3.4" + } + } + }, + "11450-4": { + "required": true, + "display_name": "Problem List" + }, + "29299-5": { + "required": true, + "display_name": "Reason For Visit" + }, + "30954-2": { + "required": true, + "display_name": "Relevant diagnostic tests and/or laboratory data", + "trigger_codes": { + "result_observation": { + "template_id_root": "2.16.840.1.113883.10.20.15.2.3.2" + } + } + }, + "29762-2": { + "required": true, + "display_name": "Social History" + } + } +} diff --git a/containers/message-refiner/assets/section_loincs.json b/containers/message-refiner/assets/section_loincs.json deleted file mode 100644 index aa9f5d0e40..0000000000 --- a/containers/message-refiner/assets/section_loincs.json +++ /dev/null @@ -1,83 +0,0 @@ -{ - "46240-8": { - "minimal_fields": [ - "History of encounters", - "2.16.840.1.113883.10.20.22.2.22.1", - "2015-08-01", - "Encounters" - ], - "required": true - }, - "10164-2": { - "minimal_fields": [ - "History of Present Illness", - "2.16.840.1.113883.10.20.22.2.20", - "2015-08-01", - "History of Present Illness" - ], - "required": true - }, - "29549-3": { - "minimal_fields": [ - "Medications Administered", - "2.16.840.1.113883.10.20.22.2.38", - "2014-06-09", - "Medications Administered" - ], - "required": true - }, - "18776-5": { - "minimal_fields": [ - "Plan of Treatment", - "2.16.840.1.113883.10.20.22.2.10", - "2014-06-09", - "Plan of Treatment" - ], - "required": true - }, - "11450-4": { - "minimal_fields": [ - "Problem List", - "2.16.840.1.113883.10.20.22.2.5.1", - "2015-08-01", - "Problem List" - ], - "required": true - }, - "29299-5": { - "minimal_fields": [ - "Reason For Visit", - "2.16.840.1.113883.10.20.22.2.12", - "2015-08-01", - "Reason For Visit" - ], - "required": true - }, - "30954-2": { - "minimal_fields": [ - "Relevant diagnostic tests and/or laboratory data", - "2.16.840.1.113883.10.20.22.2.3.1", - "2015-08-01", - "Results" - ], - "required": true - }, - "29762-2": { - "minimal_fields": [ - "Social History", - "2.16.840.1.113883.10.20.22.2.17", - "2015-08-01", - "Social History" - ], - "required": true - }, - "11369-6": { - "minimal_fields": [ - "History of Immunizations", - "2.16.840.1.113883.10.20.22.4.52", - "2015-08-01", - "Immunizations" - ], - "required": false - } -} diff --git a/containers/message-refiner/eICR-Notes.md b/containers/message-refiner/eICR-Notes.md new file mode 100644 index 0000000000..3df77018fc --- /dev/null +++ b/containers/message-refiner/eICR-Notes.md @@ -0,0 +1,346 @@ +# eICR Specification Notes (CDA-phcaserpt-1.1.1) + +The purpose of this documentation is to focus on the contents of the `` and how the various section-level and entry-level components are composed to create the body of an eICR document. + +## Section-level vs entry-level components + +In volume 2 of the CDA-phcaserpt-1.1.1 specification there are tables that explain the hierarchical relationship that comprises an eICR document. There are specific blocks of elements that are named and these names have associated metadata that both signal and connect them to other nested named blocks of elements. You can think of these named blocks of elements almost like classes in object oriented programming but without inheritance in a strict sense. Rather than inheritance they signal the hierarchical relationship between the named block of elements. Each named block of elements is governed by a template that describes what metadata it must, should, or may contain as well as the order of these metadata. + +> the `root` value of a `templateId`, which is an OID, is the _best_ way to find a specific named block of elements within a CDA broadly and eICR specifically. Every single named block of elements has a unique `templateId` that can be queried via XPath. + +Section-level templates are higher up the hierarchy while entry-level templates are children of section-level templates. Some sections are **required** in order for an eICR document to be valid based on the associated schematron found on the spcification's [GitHub repositry](https://github.com/HL7/CDA-phcaserpt-1.1.1/tree/main/validation). Every single template has a unique id that is used to track and test each row of a template; these rules are called conf numbers (`CONF#`). When validating an eICR document against the schematron the messages coming back are based on the `CONF#` and whether or not this is a fatal error, an error, or a warning. Our goal in this work is to not introduce additional errors or warnings given that the eICR messages have likely already undergone validation on the AIMS platform. + +Additionally there are some section-level templates that have templates for both: + +- Entries optional, and; +- Entries required + +These signifiers do not necessarily mean that a section **must** be present in order for a message to be valid. For example, the Immunizations Section can be completely removed from an eICR document without affecting the schematron validation process whereas doing the same thing to the Encounters Section would result in an error. + +This might seem confusing, but the "entries required" vs "entries optional" deal with what is required by that specific `templateId`. One template does not require an `` (optional says `` _should_ be there but required says that it _shall_ be there). + +### Encounters Section (V3) + +Structure: + +- Encounters Section (V3) `
` ➡️ `` + - Encounter Activity (V3) `` ➡️ `` + - Encounter Diagnosis (V3) `` ➡️ `` + - Problem Observation (V3) `` ➡️ `` + - Initial Case Report Manual Initiation Reason Observation `` ➡️ `` + - Initial Case Report Trigger Code Problem Observation `` ➡️ `` + +### History of Present Illness Section + +> Doesn't contain any specific entry-level templates + +### Immunizations Section (V3) + +Structure: + +- Immunizations Section (V3) `
` ➡️ `` + - Immunization Activity (V3) `` ➡️ `` + - Immunization Medication Information (V2) `` + +### Medications Administered Section (V2) + +Structure: + +- Medications Administered Section (V2) `
` ➡️ `` + - Medication Activity (V2) `` ➡️ `` + - Medication Information (V2) `` + +### Plan of Treatment Section (V2) + +Structure: + +- Plan of Treatment Section (V2) `
` ➡️ `` + - Planned Observation (V2) `` ➡️ `` + - Initial Case Report Trigger Code Lab Test Order `` ➡️ `` + +### Problem Section (V3) + +Structure: + +- Problem Section (V3) `
` ➡️ `` + - Problem Concern Act (V3) `` ➡️ `` + - Problem Observation (V3) `` ➡️ `` + - Initial Case Report Trigger Code Problem Observation `` ➡️ `` + +### Reason for Visit Section + +> Doesn't contain any specific entry-level templates + +### Results Section (V3) + +Structure: + +- Results Section (V3) `
` ➡️ `` + - Result Organizer (V3) `` ➡️ `` + - Result Observation (V3) `` + - Initial Case Report Trigger Code Result Observation `` + +### Social History Section (V3) + +Structure: + +- Social History Section (V3) `
` ➡️ `` + - Social History Observation (V3) `` + - Pregnancy Observation `` ➡️ `` + - Estimated Date of Delivery `` + +## Trigger Code Templates + +### Manually triggered + +#### Section-level and entry-level components + +| Parent Section | LOINC code | +| ----------------------- | ---------- | +| Encounters Section (V3) | 46240-8 | + +- Encounters Section (V3) `
` ➡️ `` + - Encounter Activity (V3) `` ➡️ `` + - Encounter Diagnosis (V3) `` ➡️ `` + - Problem Observation (V3) `` ➡️ `` + - **Initial Case Report Manual Initiation Reason Observation `` ➡️ ``** + +Manually triggered eICRs have the following `templateId`: + +- `2.16.840.1.113883.10.20.15.2.3.5` + +You can see an example of this being used below in the Problem Observation (V3). The `templateId`'s `root` attribute contains the above OID as its value. + +#### Example from the sample files that ship with the spec: + +```xml + + + + + + + + + + + + + + + + Free text containing the reason for the manual eICR document + + + +``` + +#### How to return + +```xml + + + + + Data we want + + + + +``` + +### Lab test order + +| Parent Section | LOINC code | +| ------------------------------ | ---------- | +| Plan of Treatment Section (V2) | 18776-5 | + +- Plan of Treatment Section (V2) `
` ➡️ `` + - Planned Observation (V2) `` ➡️ `` + - **Initial Case Report Trigger Code Lab Test Order `` ➡️ ``** + +Lab test order triggered eICRs have the following `templateId`: + +- `2.16.840.1.113883.10.20.15.2.3.4` + +You can see an example of this being used below in the Planned Observation (V3). The `templateId`'s `root` attribute contains the above OID as its value. + +#### Example from the sample files that ship with the spec: + +```xml + + + + + + + + + + + + + + + + + +``` + +#### How to return + +```xml + + + Data we want + + +``` + +### Problem observation + +| Parent Section | LOINC code | +| ---------------------- | ---------- | +| Encounter Section (V3) | 46240-8 | +| Problem Section (V3) | 11450-4 | + +- Encounters Section (V3) `
` ➡️ `` + - Encounter Activity (V3) `` ➡️ `` + - Encounter Diagnosis (V3) `` ➡️ `` + - Problem Observation (V3) `` ➡️ `` + - **Initial Case Report Trigger Code Problem Observation `` ➡️ ``** + +**Or:** + +- Problem Section (V3) `
` ➡️ `` + - Problem Concern Act (V3) `` ➡️ `` + - Problem Observation (V3) `` ➡️ `` + - **Initial Case Report Trigger Code Problem Observation `` ➡️ ``** + +Problem observation triggered eICRs have the following `templateId`: + +- `2.16.840.1.113883.10.20.15.2.3.3` + +You can see an example of this being used below in the Problem Observation (V3). The `templateId`'s `root` attribute contains the above OID as its value. + +#### Example from the sample files that ship with the spec: + +```xml + + + + + + + + + + + + + + + + + + + + +``` + +#### How to return + +This will depend on the `typeCode` of the `` element. In the example above we have nested `` tags with a `typeCode="SUBJ"`, which means that as you continue to navigate towards the last child `` tag you end up with the subject (`SUBJ`) of the ``. This means that there may need to be either some cleaning out of additional `` blocks that are merely referenced (`typeCode="REFE"`) or a part of/component of (`typeCode="COMP"`) an `` rather than the main reason for the observation. + +```xml + + + + + + + Data we want + + + + + + +``` + +### Result observation + +| Parent Section | LOINC code | +| -------------------- | ---------- | +| Results Section (V3) | 30954-2 | + +- Results Section (V3) `
` ➡️ `` + - Result Organizer (V3) `` ➡️ `` + - Result Observation (V3) `` + - **Initial Case Report Trigger Code Result Observation ``** + +Result observation triggered eICRs have the following `templateId`: + +- `2.16.840.1.113883.10.20.15.2.3.2` + +You can see an example of this being used below in the Result Observation (V3). The `templateId`'s `root` attribute contains the above OID as its value. + +#### Example from the sample files that ship with the spec: + +```xml + + + + + + + + + + + + + + + + + + + + + +``` + +#### How to return + +This `` **must** contain what is called a "results organizer", which is the `` child immediately after the ``. The trigger code `` is then wrapped in a `` as a child of ``. + +```xml + + + + + Data we want + + + + +``` diff --git a/containers/message-refiner/requirements.txt b/containers/message-refiner/requirements.txt index fbff27381a..2eabb1dfa2 100644 --- a/containers/message-refiner/requirements.txt +++ b/containers/message-refiner/requirements.txt @@ -1,6 +1,7 @@ ../dibbs -pathlib httpx -requests lxml +pathlib python-multipart +requests +rich diff --git a/containers/message-refiner/tests/assets/message_refiner_test_eicr.xml b/containers/message-refiner/tests/assets/message_refiner_test_eicr.xml new file mode 100644 index 0000000000..57cb39e26d --- /dev/null +++ b/containers/message-refiner/tests/assets/message_refiner_test_eicr.xml @@ -0,0 +1,2036 @@ + + + + + + + + + + + + + + + + + + + + + + Initial Public Health Case Report + + + + + + + + + + + + + + 5101 Peachtree St NE + Atlanta + GA + 30302 + + Fulton County + US + + + + + + Jane + H + Stinn + + + + + + + + + + + + 1700 Dawes St + Lowell + MA + 02368 + US + + + + + + + + + Ruth + L + Stinn + + + + + + + + + + + + + + + + + + + + + + Salem Medical Center + + + 4646 Brown Rd + Salem + MA + 02368 + US + + + + + + + + + + + + + + + + + + + + + + + + + 2100 North Ave + Burlington + MA + 02368 + US + + + + + + + + + + + Michael + Coletta + M.D. + + + + + BMass Doctors + + + 2100 North Ave + Burlington + MA + 02368 + US + + + + + + + + + + + + + + 4646 Brown Rd + Salem + MA + 02368 + US + + + + + + Salem Medical Center + + + + + + + 4646 Brown Rd + Salem + MA + 02368 + US + + + + + + + + + + +
+ + + + + + Plan of Treatment + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
Initial Case Report Trigger Code Lab Test OrderTrigger CodeTrigger Code codeSystemRCTC OIDRCTC VersionOrdered Date
Zika virus envelope (E) gene [Presence] in Serum by Probe and target + amplification method80825-3LOINC2.16.840.1.114222.4.11.750819/05/2016NOV 8, 2016
+
+ + + + + + + + + + + + + + + + + +
+
+ + + +
+ + + + + + + + + + Encounters + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
EncounterDate(s)Location
Office outpatient visit 15 minutesNOV 7, 2016 + + + Urgent Care Center + + +
+ + + + + + + + + + + + + + + + + + + + + + + + + +
Encounter Diagnosis Type
Diagnosis
+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
Initial Case Report Trigger Code Problem Observation ProblemTrigger CodeTrigger Code codeSystemRCTC OIDRCTC VersionDate(s)
DiagnosisPertussis (disorder)27836007SNOMED CT2.16.840.1.114222.4.11.750819/05/2016NOV 7, 2016
+ + + + + + + + + + + + + + + + + + + + + + + +
Encounter Diagnosis Type
Diagnosis
+ + + + + + + + + + + + + + + + + + + + + + + + + + +
Problem TypeProblemDate(s)
DiagnosisA non-trigger code diagnosis (disorder)NOV 7, 2016
+
+
+
+
+
+
+
+
+
+
+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
+
+ + +
+ + + History of Present Illness + + Persistent Cough REPORTED starting on 2016/10/05
Whooping Respiration + not reported
Paroxysms Of Coughing REPORTED starting on 2016/11/04
+ Post-tussive vomiting not reported
+
+
+
+
+ + +
+ + + + + + Medications Administered + + + + + + + + + + + + + + + + + + + + + + + + + + + +
MedicationDoseDurationRoute
Azithromycin 500 MG Oral Tablet1 gNOV 7, 2016 11:60ORAL
+
+ + + + + + + + + + + + + + + + + + + + + + + +
+
+ + +
+ + + + + + + + + + Problems + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
ConcernConcern StatusDate(s)
ProblemactiveNOV 7, 2016
+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
Problem TypeProblemDate(s)
SymptomDark stools (finding)NOV 1, 2016
ComplaintParoxysmal cough (finding)NOV 4, 2016
+
+
+
+ + + + + + + + + + + + + + + + + + + + + + + + +
ConcernConcern StatusDate(s)
ProblemactiveMAY 23, 2016
+
+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
+
+ + +
+ + + Reason for Visit + + Dark stools + +
+
+ + +
+ + + + + + + + + + Results + + + + + + + + + + + + + + + + + + + + + + + + + + +
Results PanelDate(s)
CBC W Auto Differential panel in BloodNOV 7, 2016 to NOV 7, 2016
+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
TestOutcomeInterpretationDate(s)Reference RangeReference Range InterpretationReference Range Description
Hematocrit35.3 %LowNOV 7, 201634.9 % to 44.5 %LowLow
Lymphocytes [#/​volume] in Blood by Automated count5.2 10*3/uLHighNOV 7, 20161.0 10*3/uL to 4.8 10*3/uLNormal +
+
+
+
+ + + + + + + + + + + + + + + + + + + + + + + + + +
Results PanelDate(s)
Bordetella pertussis Ab [Units/volume] in SerumNOV 7, 2016 to NOV 7, 2016
+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
Initial Case Report Trigger Code Result Observation Trigger CodeTrigger Code codeSystemRCTC OIDRCTC VersionOutcomeInterpretationDate(s)Reference RangeReference Range Interpretation
Bordetella pertussis Ab [Units/volume] in Serum11585-7LOINC2.16.840.1.114222.4.11.750819/05/2016100 [iU]/mLHighNOV 7, 2016 to 45 [iU]/mLNormal
+
+
+
+ + + + + + + + + + + + + + + + + + + + + + + + + +
Results PanelDate(s)
Bordetella pertussis [Presence] in Throat by Organism specific cultureNOV 7, 2016 to NOV 7, 2016
+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
Initial Case Report Trigger Code Result Observation Trigger CodeTrigger Code codeSystemRCTC OIDRCTC VersionOutcomeTrigger CodeTrigger Code codeSystemRCTC OIDRCTC VersionInterpretationDate(s)
Bordetella pertussis [Presence] in Throat by Organism specific + culture548-8LOINC2.16.840.1.114222.4.11.750819/05/2016Bordetella pertussis (organism)5247005SNOMED CT2.16.840.1.114222.4.11.750819/05/2016AbnormalNOV 7, 2016
+
+
+
+ + + + + + + + + + + + + + + + + + + + + + + + + +
Results PanelDate(s)
SARS-like Coronavirus N gene [Presence] in + Unspecified specimen by NAA with probe + detection +
+ + + *** In the table below, row entries + with values under RCTC columns triggered this + Electronic Initial Case Report (eICR) + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
TestLab Test RCTC OID ***Lab Test RCTC Version ***OutcomeDate(s)Lab Result RCTC OID ***Lab Result RCTC Version ***
SARS-like Coronavirus N gene [Presence] in + Unspecified specimen by NAA with probe + detection2.16.840.1.114222.4.11.750820200429Detected (qualifier value)05/13/2020 + +
+
+
+
+ + + + + + + + + + + + + + + + + + + + + + + + + +
Lab Test NameLab Test Result ValueLab Test Result Date
+ 2020-05-13T08:30:00-07:00 + + Neisseria gonorrhoeae DNA [Presence] in Nasopharynx + by NAA with probe detection + + Detected +
+ 2020-05-13T08:30:00-07:00 + + Chlamydia trachomatis rRNA [Presence] in Vaginal + fluid by NAA with probe detection + + Not Detected +
+ 2020-05-14T08:30:00-07:00 + + SARS-like Coronavirus N gene [Presence] in + Unspecified specimen by NAA with probe detection + + Not Detected +
+
+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + Low + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
+
+ + +
+ + + + + + Social History + + + + + + + + + + + + + + + + + + + + + + + + + +
Birth SexValueDate
Sex Assigned At BirthFemaleNOV 24, 1974
+ + + + + + + + + + + + + + + + + + + + + + + + +
Social History Observation TypeValueDate(s)
Employment detailCenters for Disease Control 1, Peachtree St South Atlanta, GA, 404-564-7865JAN 1, 1993
+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
Travel History: Date(s)NotesLocation
1999 to 2007Spent 8 years in the UK during the BSE outbreak +
In the 3 weeks before NOV 9, 2016 + + + Brazil + +
APR 29, 2016 to OCT 30, 2016 + + + Nadi, FJ + +
MAY 6, 2016 to MAY 15, 2016 + + + Montreal, QC, CA + +
JUL 13, 2016 to JUL 15, 2016 + + + Sydney, NSW, AU + +
OCT 22, 2016 to OCT 30, 2016 + + + 1170 N Rancho Robles Rd Oracle, AZ8562, US + +
+ + + + + + + + + + + + + + + + + + + + + + +
Pregnancy StatusDate(s)
+ Not PregnantJAN 1, 2016 to NOV 7, 2016
+
+ + + + + + + + + + + + + + + + + + + + + + + + + + + Centers for Disease Control 1, Peachtree St South Atlanta, GA, + 404-564-7865 + + + + + + + + + + Spent 8 years in the UK during the BSE outbreak + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + FJ + Nadi + + + + + + + + + + + + + + + + + + + + + + CA + Montreal + QC + + + + + + + + + + + + + + + + + + + + + AU + Sydney + NSW + + + + + + + + + + + + + + + + + + + + + 1170 N Rancho Robles Rd + Oracle + AZ + 8562 + US + + + + + + + + + + + + + + + + + + + + + +
+
+
+
+
diff --git a/containers/message-refiner/tests/assets/refined_message_condition_only.xml b/containers/message-refiner/tests/assets/refined_message_condition_only.xml index 1616c5e150..b47fe22d63 100644 --- a/containers/message-refiner/tests/assets/refined_message_condition_only.xml +++ b/containers/message-refiner/tests/assets/refined_message_condition_only.xml @@ -1,312 +1,747 @@ - - - - - - - - - Initial Public Health Case Report - - - - - - - - - - 1050 CARPENTER ST - EDWARDS - CA - 93523-2800 - US - - - - - - ABEL - CASTILLO - - - - - - - - - - - - - - - - - - - PRM- Palmdale Regional Medical Center - - - 38600 Medical Center Drive - Palmdale - CA - 93551 - USA - - - - - - - - - - - - - - - - - - - - - - - - - - - ED Billing - Discharge - Physician - - - - - PRM- Palmdale Regional Medical Center - - - 38600 Medical Center Drive - Palmdale - CA - 93551 - USA - - - - - - - - - 38600 Medical Center Drive - Palmdale - CA - 93551 - US - - - - - Scott - A - Wang - - - - - - - - - - - - - - - - - Physician - Ed staff - - - - - - - - - - - - - 38600 Medical Center Drive - Palmdale - CA - 93551 - USA - - - - - PRM- Palmdale Regional Medical Center - - - 38600 Medical Center Drive - Palmdale - CA - 93551 - USA - - - - - - - - - -
- - - Results - Only entries that match the corresponding condition code were included in this section via PRIME DIBBs Message Refiner API endpoint by Public Health Authority - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
-
- -
- - - Medications Administered - Removed via PRIME DIBBs Message Refiner API endpoint by Public Health Authority -
-
- -
- - - Problem List - Removed via PRIME DIBBs Message Refiner API endpoint by Public Health Authority -
-
- -
- - - Plan of Treatment - Removed via PRIME DIBBs Message Refiner API endpoint by Public Health Authority -
-
- -
- - - Encounters - Removed via PRIME DIBBs Message Refiner API endpoint by Public Health Authority -
-
- -
- - - Social History - Removed via PRIME DIBBs Message Refiner API endpoint by Public Health Authority -
-
- -
- - - History of Present Illness - Removed via PRIME DIBBs Message Refiner API endpoint by Public Health Authority -
-
- -
- - - Reason For Visit - Removed via PRIME DIBBs Message Refiner API endpoint by Public Health Authority -
-
-
-
+ + + + + + + + + + + + + + + + Initial Public Health Case Report + + + + + + + + + + + + + + 5101 Peachtree St NE + Atlanta + GA + 30302 + + Fulton County + US + + + + + + Jane + H + Stinn + + + + + + + + + + + + 1700 Dawes St + Lowell + MA + 02368 + US + + + + + + + + + Ruth + L + Stinn + + + + + + + + + + + + + + + + + + + + + + Salem Medical Center + + + 4646 Brown Rd + Salem + MA + 02368 + US + + + + + + + + + + + + + + + + + + + + + + + + 2100 North Ave + Burlington + MA + 02368 + US + + + + + + + + + + + Michael + Coletta + M.D. + + + + + BMass Doctors + + + 2100 North Ave + Burlington + MA + 02368 + US + + + + + + + + + + + + + + 4646 Brown Rd + Salem + MA + 02368 + US + + + + + + Salem Medical Center + + + + + + + 4646 Brown Rd + Salem + MA + 02368 + US + + + + + + + + + + +
+ + + + + + Plan of Treatment + + Output from CDC PRIME DIBBs `message-refiner` API by request of STLT + + + + + + + + + + + + + +
Display TextCodeCode SystemTrigger Code Observation
Zika virus envelope (E) gene [Presence] in Serum by Probe and target + amplification method80825-3LOINCTRUE
+
+ + + + + + + + + + + + + + + + + +
+
+ + +
+ + + + + + + + + + Encounters + + Output from CDC PRIME DIBBs `message-refiner` API by request of STLT + + + + + + + + + + + + + +
Display TextCodeCode SystemTrigger Code Observation
Diagnosis29308-4LOINCTRUE
+
+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
+
+ + +
+ + + History of Present Illness + + Output from CDC PRIME DIBBs `message-refiner` API by request of STLT + + + + +
Section details have been removed as requested
+
+
+
+ + +
+ + + + + + Medications Administered + + Output from CDC PRIME DIBBs `message-refiner` API by request of STLT + + + + +
Section details have been removed as requested
+
+
+
+ + +
+ + + + + + + + + + Problems + + Output from CDC PRIME DIBBs `message-refiner` API by request of STLT + + + + +
Section details have been removed as requested
+
+
+
+ + +
+ + + Reason for Visit + + Output from CDC PRIME DIBBs `message-refiner` API by request of STLT + + + + +
Section details have been removed as requested
+
+
+
+ + +
+ + + + + + + + + + Results + + Output from CDC PRIME DIBBs `message-refiner` API by request of STLT + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
Display TextCodeCode SystemTrigger Code Observation
Bordetella pertussis Ab [Units/volume] in Serum11585-7LOINCTRUE
Bordetella pertussis [Presence] in Throat by Organism specific culture548-8LOINCTRUE
Chlamydia trachomatis rRNA [Presence] in Vaginal fluid by NAA with probe + detection53926-2loinc.orgFALSE
SARS-like Coronavirus N gene [Presence] in Unspecified specimen by NAA with + probe detection94310-0LOINCTRUE
+
+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
+
+ + +
+ + + + + + Social History + + Output from CDC PRIME DIBBs `message-refiner` API by request of STLT + + + + +
Section details have been removed as requested
+
+
+
+
+
diff --git a/containers/message-refiner/tests/assets/refined_message_labs_reason.xml b/containers/message-refiner/tests/assets/refined_message_labs_reason.xml index 9d261dc7a9..0867f618b5 100644 --- a/containers/message-refiner/tests/assets/refined_message_labs_reason.xml +++ b/containers/message-refiner/tests/assets/refined_message_labs_reason.xml @@ -1,1213 +1,1113 @@ - - - - - - - - - Initial Public Health Case Report - - - - - - - - - - 1050 CARPENTER ST - EDWARDS - CA - 93523-2800 - US - - - - - - ABEL - CASTILLO - - - - - - - - - - - - - - - - - - - PRM- Palmdale Regional Medical Center - - - 38600 Medical Center Drive - Palmdale - CA - 93551 - USA - - - - - - - - - - - - - - - - - - - - - - - - - - - ED Billing - Discharge - Physician - - - - - PRM- Palmdale Regional Medical Center - - - 38600 Medical Center Drive - Palmdale - CA - 93551 - USA - - - - - - - - - 38600 Medical Center Drive - Palmdale - CA - 93551 - US - - - - - Scott - A - Wang - - - - - - - - - - - - - - - - - Physician - Ed staff - - - - - - - - - - - - - 38600 Medical Center Drive - Palmdale - CA - 93551 - USA - - - - - PRM- Palmdale Regional Medical Center - - - 38600 Medical Center Drive - Palmdale - CA - 93551 - USA - - - - - - - - - -
- - - - RESULTS - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
Lab Test NameLab Test Result ValueLab Test Result Date
- Ur Mucous - - NONE /LPF - - - 2022-05-13T07:47:00.000Z -
- Ur Squam Epithelial - - - 1|http://unitsofmeasure.org|/HPF - - - 2022-05-13T07:47:00.000Z -
- Ur WBC - - - 1|http://unitsofmeasure.org|/HPF - - - 2022-05-13T07:47:00.000Z -
- Ur RBC - - - 2|http://unitsofmeasure.org|/HPF - - - 2022-05-13T07:47:00.000Z -
- Ur Bacteria - - None Seen /HPF - - - 2022-05-13T07:47:00.000Z -
- Symptomatic as defined by CDC? - - No - - - 2022-05-13T07:43:00.000Z -
- In ICU? - - No - - - 2022-05-13T07:43:00.000Z -
- Group care resident? - - No - - - 2022-05-13T07:43:00.000Z -
- First test? - - UNK - - - 2022-05-13T07:43:00.000Z -
- Employed in healthcare? - - No - - - 2022-05-13T07:43:00.000Z -
- Date of onset? - - UNK - - - 2022-05-13T07:43:00.000Z -
- SARS-CoV-2 Interp: - - Unknown - - - 2022-05-13T07:43:00.000Z -
- SARS-CoV-2 PCR - - Negative - - - 2022-05-13T07:43:00.000Z -
- Flu B PCR - - Not Detected - - - 2022-05-13T07:43:00.000Z -
- Flu A PCR - - Not Detected - - - 2022-05-13T07:43:00.000Z -
- Ur pH - - 6.0 - - - 2022-05-13T06:52:00.000Z -
- Ur Spec Grav - - 1.020 mg/dL - - - 2022-05-13T06:52:00.000Z -
- Ur Urobilinogen - - 0.2 EU/dL - - - 2022-05-13T06:52:00.000Z -
- Ur Blood - - Trace-lysed - - - 2022-05-13T06:52:00.000Z -
- Ur Bili - - Negative - - - 2022-05-13T06:52:00.000Z -
- Ur Leuk Est - - Negative - - - 2022-05-13T06:52:00.000Z -
- Ur Protein - - Negative mg/dL - - - 2022-05-13T06:52:00.000Z -
- Ur Nitrite - - Negative - - - 2022-05-13T06:52:00.000Z -
- Ur Ketones - - Negative mg/dL - - - 2022-05-13T06:52:00.000Z -
- UR Glucose - - Negative mg/dL - - - 2022-05-13T06:52:00.000Z -
-
- - - - - - - - - - - - - - - - - - - - NONE /LPF - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - UNK - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - UNK - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - Trace-lysed - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
-
- -
- - - Reason For Visit - - - - - - - - - - - - -
text
- FEVER -
-
-
-
- -
- - - Encounters - Removed via PRIME DIBBs Message Refiner API endpoint by Public Health Authority -
-
- -
- - - Medications Administered - Removed via PRIME DIBBs Message Refiner API endpoint by Public Health Authority -
-
- -
- - - Problem List - Removed via PRIME DIBBs Message Refiner API endpoint by Public Health Authority -
-
- -
- - - Plan of Treatment - Removed via PRIME DIBBs Message Refiner API endpoint by Public Health Authority -
-
- -
- - - Social History - Removed via PRIME DIBBs Message Refiner API endpoint by Public Health Authority -
-
- -
- - - History of Present Illness - Removed via PRIME DIBBs Message Refiner API endpoint by Public Health Authority -
-
-
-
+ + + + + + + + + + + + + + + + Initial Public Health Case Report + + + + + + + + + + + + + + 5101 Peachtree St NE + Atlanta + GA + 30302 + + Fulton County + US + + + + + + Jane + H + Stinn + + + + + + + + + + + + 1700 Dawes St + Lowell + MA + 02368 + US + + + + + + + + + Ruth + L + Stinn + + + + + + + + + + + + + + + + + + + + + + Salem Medical Center + + + 4646 Brown Rd + Salem + MA + 02368 + US + + + + + + + + + + + + + + + + + + + + + + + + 2100 North Ave + Burlington + MA + 02368 + US + + + + + + + + + + + Michael + Coletta + M.D. + + + + + BMass Doctors + + + 2100 North Ave + Burlington + MA + 02368 + US + + + + + + + + + + + + + + 4646 Brown Rd + Salem + MA + 02368 + US + + + + + + Salem Medical Center + + + + + + + 4646 Brown Rd + Salem + MA + 02368 + US + + + + + + + + + + +
+ + + + + + Plan of Treatment + + Output from CDC PRIME DIBBs `message-refiner` API by request of STLT + + + + + + + + + + + + + +
Display TextCodeCode SystemTrigger Code Observation
Zika virus envelope (E) gene [Presence] in Serum by Probe and target amplification method80825-3LOINCTRUE
+
+ + + + + + + + + + + + + + + + + +
+
+ + +
+ + + + + + + + + + Encounters + + Output from CDC PRIME DIBBs `message-refiner` API by request of STLT + + + + + + + + + + + + + +
Display TextCodeCode SystemTrigger Code Observation
Diagnosis29308-4LOINCTRUE
+
+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
+
+ + +
+ + + History of Present Illness + + Output from CDC PRIME DIBBs `message-refiner` API by request of STLT + + + + +
Section details have been removed as requested
+
+
+
+ + +
+ + + + + + Medications Administered + + Output from CDC PRIME DIBBs `message-refiner` API by request of STLT + + + + +
Section details have been removed as requested
+
+
+
+ + +
+ + + + + + + + + + Problems + + Output from CDC PRIME DIBBs `message-refiner` API by request of STLT + + + + +
Section details have been removed as requested
+
+
+
+ + +
+ + + Reason for Visit + + Dark stools + +
+
+ + +
+ + + + + + + + + + Results + + + + + + + + + + + + + + + + + + + + + + + + + + +
Results PanelDate(s)
CBC W Auto Differential panel in BloodNOV 7, 2016 to NOV 7, 2016
+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
TestOutcomeInterpretationDate(s)Reference RangeReference Range InterpretationReference Range Description
Hematocrit35.3 %LowNOV 7, 201634.9 % to 44.5 %LowLow
Lymphocytes [#/​volume] in Blood by Automated count5.2 10*3/uLHighNOV 7, 20161.0 10*3/uL to 4.8 10*3/uLNormal +
+
+
+
+ + + + + + + + + + + + + + + + + + + + + + + + + +
Results PanelDate(s)
Bordetella pertussis Ab [Units/volume] in SerumNOV 7, 2016 to NOV 7, 2016
+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
Initial Case Report Trigger Code Result Observation Trigger CodeTrigger Code codeSystemRCTC OIDRCTC VersionOutcomeInterpretationDate(s)Reference RangeReference Range Interpretation
Bordetella pertussis Ab [Units/volume] in Serum11585-7LOINC2.16.840.1.114222.4.11.750819/05/2016100 [iU]/mLHighNOV 7, 2016 to 45 [iU]/mLNormal
+
+
+
+ + + + + + + + + + + + + + + + + + + + + + + + + +
Results PanelDate(s)
Bordetella pertussis [Presence] in Throat by Organism specific cultureNOV 7, 2016 to NOV 7, 2016
+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
Initial Case Report Trigger Code Result Observation Trigger CodeTrigger Code codeSystemRCTC OIDRCTC VersionOutcomeTrigger CodeTrigger Code codeSystemRCTC OIDRCTC VersionInterpretationDate(s)
Bordetella pertussis [Presence] in Throat by Organism specific + culture548-8LOINC2.16.840.1.114222.4.11.750819/05/2016Bordetella pertussis (organism)5247005SNOMED CT2.16.840.1.114222.4.11.750819/05/2016AbnormalNOV 7, 2016
+
+
+
+ + + + + + + + + + + + + + + + + + + + + + + + + +
Results PanelDate(s)
SARS-like Coronavirus N gene [Presence] in + Unspecified specimen by NAA with probe + detection +
+ + + *** In the table below, row entries + with values under RCTC columns triggered this + Electronic Initial Case Report (eICR) + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
TestLab Test RCTC OID ***Lab Test RCTC Version ***OutcomeDate(s)Lab Result RCTC OID ***Lab Result RCTC Version ***
SARS-like Coronavirus N gene [Presence] in + Unspecified specimen by NAA with probe + detection2.16.840.1.114222.4.11.750820200429Detected (qualifier value)05/13/2020 + +
+
+
+
+ + + + + + + + + + + + + + + + + + + + + + + + + +
Lab Test NameLab Test Result ValueLab Test Result Date
+ 2020-05-13T08:30:00-07:00 + + Neisseria gonorrhoeae DNA [Presence] in Nasopharynx + by NAA with probe detection + + Detected +
+ 2020-05-13T08:30:00-07:00 + + Chlamydia trachomatis rRNA [Presence] in Vaginal + fluid by NAA with probe detection + + Not Detected +
+ 2020-05-14T08:30:00-07:00 + + SARS-like Coronavirus N gene [Presence] in + Unspecified specimen by NAA with probe detection + + Not Detected +
+
+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + Low + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
+
+ + +
+ + + + + + Social History + + Output from CDC PRIME DIBBs `message-refiner` API by request of STLT + + + + +
Section details have been removed as requested
+
+
+
+
+
diff --git a/containers/message-refiner/tests/assets/refined_message_no_parameters.xml b/containers/message-refiner/tests/assets/refined_message_no_parameters.xml new file mode 100644 index 0000000000..630d02ca64 --- /dev/null +++ b/containers/message-refiner/tests/assets/refined_message_no_parameters.xml @@ -0,0 +1,668 @@ + + + + + + + + + + + + + + + + Initial Public Health Case Report + + + + + + + + + + + + + + 5101 Peachtree St NE + Atlanta + GA + 30302 + + Fulton County + US + + + + + + Jane + H + Stinn + + + + + + + + + + + + 1700 Dawes St + Lowell + MA + 02368 + US + + + + + + + + + Ruth + L + Stinn + + + + + + + + + + + + + + + + + + + + + + Salem Medical Center + + + 4646 Brown Rd + Salem + MA + 02368 + US + + + + + + + + + + + + + + + + + + + + + + + + 2100 North Ave + Burlington + MA + 02368 + US + + + + + + + + + + + Michael + Coletta + M.D. + + + + + BMass Doctors + + + 2100 North Ave + Burlington + MA + 02368 + US + + + + + + + + + + + + + + 4646 Brown Rd + Salem + MA + 02368 + US + + + + + + Salem Medical Center + + + + + + + 4646 Brown Rd + Salem + MA + 02368 + US + + + + + + + + + + +
+ + + + + + Plan of Treatment + + Output from CDC PRIME DIBBs `message-refiner` API by request of STLT + + + + + + + + + + + + + +
Display TextCodeCode SystemTrigger Code Observation
Zika virus envelope (E) gene [Presence] in Serum by Probe and target amplification method80825-3LOINCTRUE
+
+ + + + + + + + + + + + + + + + + +
+
+ + +
+ + + + + + + + + + Encounters + + Output from CDC PRIME DIBBs `message-refiner` API by request of STLT + + + + + + + + + + + + + +
Display TextCodeCode SystemTrigger Code Observation
Diagnosis29308-4LOINCTRUE
+
+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
+
+ + +
+ + + History of Present Illness + + Output from CDC PRIME DIBBs `message-refiner` API by request of STLT + + + + +
Section details have been removed as requested
+
+
+
+ + +
+ + + + + + Medications Administered + + Output from CDC PRIME DIBBs `message-refiner` API by request of STLT + + + + +
Section details have been removed as requested
+
+
+
+ + +
+ + + + + + + + + + Problems + + Output from CDC PRIME DIBBs `message-refiner` API by request of STLT + + + + +
Section details have been removed as requested
+
+
+
+ + +
+ + + Reason for Visit + + Output from CDC PRIME DIBBs `message-refiner` API by request of STLT + + + + +
Section details have been removed as requested
+
+
+
+ + +
+ + + + + + + + + + Results + + Output from CDC PRIME DIBBs `message-refiner` API by request of STLT + + + + + + + + + + + + + + + + + + + + + + + + + +
Display TextCodeCode SystemTrigger Code Observation
Bordetella pertussis Ab [Units/volume] in Serum11585-7LOINCTRUE
Bordetella pertussis [Presence] in Throat by Organism specific culture548-8LOINCTRUE
SARS-like Coronavirus N gene [Presence] in Unspecified specimen by NAA with probe detection94310-0LOINCTRUE
+
+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
+
+ + +
+ + + + + + Social History + + Output from CDC PRIME DIBBs `message-refiner` API by request of STLT + + + + +
Section details have been removed as requested
+
+
+
+
+
+
diff --git a/containers/message-refiner/tests/assets/refined_message_results_section_chlamydia_condition.xml b/containers/message-refiner/tests/assets/refined_message_results_section_chlamydia_condition.xml new file mode 100644 index 0000000000..3c85803610 --- /dev/null +++ b/containers/message-refiner/tests/assets/refined_message_results_section_chlamydia_condition.xml @@ -0,0 +1,700 @@ + + + + + + + + + + + + + + + + Initial Public Health Case Report + + + + + + + + + + + + + + 5101 Peachtree St NE + Atlanta + GA + 30302 + + Fulton County + US + + + + + + Jane + H + Stinn + + + + + + + + + + + + 1700 Dawes St + Lowell + MA + 02368 + US + + + + + + + + + Ruth + L + Stinn + + + + + + + + + + + + + + + + + + + + + + Salem Medical Center + + + 4646 Brown Rd + Salem + MA + 02368 + US + + + + + + + + + + + + + + + + + + + + + + + + 2100 North Ave + Burlington + MA + 02368 + US + + + + + + + + + + + Michael + Coletta + M.D. + + + + + BMass Doctors + + + 2100 North Ave + Burlington + MA + 02368 + US + + + + + + + + + + + + + + 4646 Brown Rd + Salem + MA + 02368 + US + + + + + + Salem Medical Center + + + + + + + 4646 Brown Rd + Salem + MA + 02368 + US + + + + + + + + + + +
+ + + + + + Plan of Treatment + + Output from CDC PRIME DIBBs `message-refiner` API by request of STLT + + + + + + + + + + + + + +
Display TextCodeCode SystemTrigger Code Observation
Zika virus envelope (E) gene [Presence] in Serum by Probe and target amplification method80825-3LOINCTRUE
+
+ + + + + + + + + + + + + + + + + +
+
+ + +
+ + + + + + + + + + Encounters + + Output from CDC PRIME DIBBs `message-refiner` API by request of STLT + + + + + + + + + + + + + +
Display TextCodeCode SystemTrigger Code Observation
Diagnosis29308-4LOINCTRUE
+
+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
+
+ + +
+ + + History of Present Illness + + Output from CDC PRIME DIBBs `message-refiner` API by request of STLT + + + + +
Section details have been removed as requested
+
+
+
+ + +
+ + + + + + Medications Administered + + Output from CDC PRIME DIBBs `message-refiner` API by request of STLT + + + + +
Section details have been removed as requested
+
+
+
+ + +
+ + + + + + + + + + Problems + + Output from CDC PRIME DIBBs `message-refiner` API by request of STLT + + + + +
Section details have been removed as requested
+
+
+
+ + +
+ + + Reason for Visit + + Output from CDC PRIME DIBBs `message-refiner` API by request of STLT + + + + +
Section details have been removed as requested
+
+
+
+ + +
+ + + + + + + + + + Results + + Output from CDC PRIME DIBBs `message-refiner` API by request of STLT + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
Display TextCodeCode SystemTrigger Code Observation
Bordetella pertussis Ab [Units/volume] in Serum11585-7LOINCTRUE
Bordetella pertussis [Presence] in Throat by Organism specific culture548-8LOINCTRUE
Chlamydia trachomatis rRNA [Presence] in Vaginal fluid by NAA with probe detection53926-2loinc.orgFALSE
SARS-like Coronavirus N gene [Presence] in Unspecified specimen by NAA with probe detection94310-0LOINCTRUE
+
+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
+
+ + +
+ + + + + + Social History + + Output from CDC PRIME DIBBs `message-refiner` API by request of STLT + + + + +
Section details have been removed as requested
+
+
+
+
+
+
\ No newline at end of file diff --git a/containers/message-refiner/tests/assets/refined_message_social_history_only.xml b/containers/message-refiner/tests/assets/refined_message_social_history_only.xml index fdfa88ed2c..923ec14898 100644 --- a/containers/message-refiner/tests/assets/refined_message_social_history_only.xml +++ b/containers/message-refiner/tests/assets/refined_message_social_history_only.xml @@ -1,329 +1,995 @@ - - - - - - - - - Initial Public Health Case Report - - - - - - - - - - 1050 CARPENTER ST - EDWARDS - CA - 93523-2800 - US - - - - - - ABEL - CASTILLO - - - - - - - - - - - - - - - - - - - PRM- Palmdale Regional Medical Center - - - 38600 Medical Center Drive - Palmdale - CA - 93551 - USA - - - - - - - - - - - - - - - - - - - - - - - - - - - ED Billing - Discharge - Physician - - - - - PRM- Palmdale Regional Medical Center - - - 38600 Medical Center Drive - Palmdale - CA - 93551 - USA - - - - - - - - - 38600 Medical Center Drive - Palmdale - CA - 93551 - US - - - - - Scott - A - Wang - - - - - - - - - - - - - - - - - Physician - Ed staff - - - - - - - - - - - - - 38600 Medical Center Drive - Palmdale - CA - 93551 - USA - - - - - PRM- Palmdale Regional Medical Center - - - 38600 Medical Center Drive - Palmdale - CA - 93551 - USA - - - - - - - - - -
- - - - SOCIAL HISTORY - - - - - - - - - - - - - - - - - - - - - - -
Social History ObservationSocial History Observation Result
- Birth Sex - - M -
- Travel History - - COVID- 19 Testing - Reason-Screening -
- Travel History - - Pregnancy Status-Not - applicable (qualifier value) -
-
- - - - - - - - - - - - - - - COVID- 19 Testing Reason-Screening - - - - - - - - - - Pregnancy Status-Not applicable (qualifier value) - - - - -
-
- -
- - - Encounters - Removed via PRIME DIBBs Message Refiner API endpoint by Public Health Authority -
-
- -
- - - History of Present Illness - Removed via PRIME DIBBs Message Refiner API endpoint by Public Health Authority -
-
- -
- - - Medications Administered - Removed via PRIME DIBBs Message Refiner API endpoint by Public Health Authority -
-
- -
- - - Problem List - Removed via PRIME DIBBs Message Refiner API endpoint by Public Health Authority -
-
- -
- - - Plan of Treatment - Removed via PRIME DIBBs Message Refiner API endpoint by Public Health Authority -
-
- -
- - - Results - Removed via PRIME DIBBs Message Refiner API endpoint by Public Health Authority -
-
- -
- - - Reason For Visit - Removed via PRIME DIBBs Message Refiner API endpoint by Public Health Authority -
-
-
-
+ + + + + + + + + + + + + + + + Initial Public Health Case Report + + + + + + + + + + + + + + 5101 Peachtree St NE + Atlanta + GA + 30302 + + Fulton County + US + + + + + + Jane + H + Stinn + + + + + + + + + + + + 1700 Dawes St + Lowell + MA + 02368 + US + + + + + + + + + Ruth + L + Stinn + + + + + + + + + + + + + + + + + + + + + + Salem Medical Center + + + 4646 Brown Rd + Salem + MA + 02368 + US + + + + + + + + + + + + + + + + + + + + + + + + 2100 North Ave + Burlington + MA + 02368 + US + + + + + + + + + + + Michael + Coletta + M.D. + + + + + BMass Doctors + + + 2100 North Ave + Burlington + MA + 02368 + US + + + + + + + + + + + + + + 4646 Brown Rd + Salem + MA + 02368 + US + + + + + + Salem Medical Center + + + + + + + 4646 Brown Rd + Salem + MA + 02368 + US + + + + + + + + + + +
+ + + + + + Plan of Treatment + + Output from CDC PRIME DIBBs `message-refiner` API by request of STLT + + + + + + + + + + + + + +
Display TextCodeCode SystemTrigger Code Observation
Zika virus envelope (E) gene [Presence] in Serum by Probe and target amplification method80825-3LOINCTRUE
+
+ + + + + + + + + + + + + + + + + +
+
+ + +
+ + + + + + + + + + Encounters + + Output from CDC PRIME DIBBs `message-refiner` API by request of STLT + + + + + + + + + + + + + +
Display TextCodeCode SystemTrigger Code Observation
Diagnosis29308-4LOINCTRUE
+
+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
+
+ + +
+ + + History of Present Illness + + Output from CDC PRIME DIBBs `message-refiner` API by request of STLT + + + + +
Section details have been removed as requested
+
+
+
+ + +
+ + + + + + Medications Administered + + Output from CDC PRIME DIBBs `message-refiner` API by request of STLT + + + + +
Section details have been removed as requested
+
+
+
+ + +
+ + + + + + + + + + Problems + + Output from CDC PRIME DIBBs `message-refiner` API by request of STLT + + + + +
Section details have been removed as requested
+
+
+
+ + +
+ + + Reason for Visit + + Output from CDC PRIME DIBBs `message-refiner` API by request of STLT + + + + +
Section details have been removed as requested
+
+
+
+ + +
+ + + + + + + + + + Results + + Output from CDC PRIME DIBBs `message-refiner` API by request of STLT + + + + + + + + + + + + + + + + + + + + + + + + + +
Display TextCodeCode SystemTrigger Code Observation
Bordetella pertussis Ab [Units/volume] in Serum11585-7LOINCTRUE
Bordetella pertussis [Presence] in Throat by Organism specific culture548-8LOINCTRUE
SARS-like Coronavirus N gene [Presence] in Unspecified specimen by NAA with probe detection94310-0LOINCTRUE
+
+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
+
+ + +
+ + + + + + Social History + + + + + + + + + + + + + + + + + + + + + + + + + +
Birth SexValueDate
Sex Assigned At BirthFemaleNOV 24, 1974
+ + + + + + + + + + + + + + + + + + + + + + + + +
Social History Observation TypeValueDate(s)
Employment detailCenters for Disease Control 1, Peachtree St South Atlanta, GA, 404-564-7865JAN 1, 1993
+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
Travel History: Date(s)NotesLocation
1999 to 2007Spent 8 years in the UK during the BSE outbreak +
In the 3 weeks before NOV 9, 2016 + + + Brazil + +
APR 29, 2016 to OCT 30, 2016 + + + Nadi, FJ + +
MAY 6, 2016 to MAY 15, 2016 + + + Montreal, QC, CA + +
JUL 13, 2016 to JUL 15, 2016 + + + Sydney, NSW, AU + +
OCT 22, 2016 to OCT 30, 2016 + + + 1170 N Rancho Robles Rd Oracle, AZ8562, US + +
+ + + + + + + + + + + + + + + + + + + + + + +
Pregnancy StatusDate(s)
+ Not + Pregnant + JAN 1, 2016 to NOV 7, 2016
+
+ + + + + + + + + + + + + + + + + + + + + + + + + + + + Centers for Disease Control 1, Peachtree St South Atlanta, GA, + 404-564-7865 + + + + + + + + + + Spent 8 years in the UK during the BSE outbreak + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + FJ + Nadi + + + + + + + + + + + + + + + + + + + + + + CA + Montreal + QC + + + + + + + + + + + + + + + + + + + + + AU + Sydney + NSW + + + + + + + + + + + + + + + + + + + + + 1170 N Rancho Robles Rd + Oracle + AZ + 8562 + US + + + + + + + + + + + + + + + + + + + + + +
+
+
+
diff --git a/containers/message-refiner/tests/test_refine.py b/containers/message-refiner/tests/test_refine.py index d041fcf05c..052f26a31e 100644 --- a/containers/message-refiner/tests/test_refine.py +++ b/containers/message-refiner/tests/test_refine.py @@ -1,21 +1,34 @@ import pathlib -import re +from typing import List import pytest -from app.refine import _add_root_element +from app.refine import _are_elements_present from app.refine import _create_minimal_section -from app.refine import _create_minimal_sections -from app.refine import _select_message_header +from app.refine import _create_or_update_text_element +from app.refine import _extract_observation_data +from app.refine import _find_path_to_entry +from app.refine import _generate_combined_xpath +from app.refine import _get_observations +from app.refine import _get_section_by_code +from app.refine import _process_section +from app.refine import _prune_unwanted_siblings +from app.refine import _update_text_element from app.refine import refine from app.refine import validate_message from app.refine import validate_sections_to_include -from app.utils import _generate_clinical_xpaths -from app.utils import load_section_loincs -from app.utils import read_json_from_assets -from lxml import etree as ET +from lxml import etree +TRIGGER_CODE_TEMPLATE_IDS = [ + "2.16.840.1.113883.10.20.15.2.3.5", + "2.16.840.1.113883.10.20.15.2.3.3", + "2.16.840.1.113883.10.20.15.2.3.4", + "2.16.840.1.113883.10.20.15.2.3.2", +] -def parse_file_from_test_assets(filename: str) -> ET.ElementTree: +NAMESPACES = {"hl7": "urn:hl7-org:v3"} + + +def parse_file_from_test_assets(filename: str) -> etree.ElementTree: """ Parses a file from the assets directory into an ElementTree. @@ -25,8 +38,8 @@ def parse_file_from_test_assets(filename: str) -> ET.ElementTree: with open( (pathlib.Path(__file__).parent.parent / "tests" / "assets" / filename), "r" ) as file: - parser = ET.XMLParser(remove_blank_text=True) - tree = ET.parse(file, parser) + parser = etree.XMLParser(remove_blank_text=True) + tree = etree.parse(file, parser) return tree @@ -44,27 +57,42 @@ def read_file_from_test_assets(filename: str) -> str: return file.read() -test_eICR_xml = read_file_from_test_assets("CDA_eICR.xml") -refined_test_eICR_social_history_only = parse_file_from_test_assets( - "refined_message_social_history_only.xml" -) -refined_test_eICR_labs_reason = parse_file_from_test_assets( - "refined_message_labs_reason.xml" -) +def _get_entries_for_section( + section: etree.Element, + namespaces: dict = {"hl7": "urn:hl7-org:v3"}, +) -> List[etree.Element]: + """ + Gets the entries of a section of an eICR and returns a list of elements. -refined_test_condition_only = parse_file_from_test_assets( - "refined_message_condition_only.xml" -) + :param section: The
element of the section to retrieve entries from or a function that returns the section. + :param namespaces: The namespaces to use when searching for elements and defaults to 'hl7'. + :return: A list of elements of the entries in the section. + """ + entries = section.xpath(".//hl7:entry", namespaces=namespaces) + if entries is not None: + return entries -refined_test_conditon_and_labs = parse_file_from_test_assets( - "refined_message_condition_and_lab_section.xml" -) -refined_test_no_relevant_section_data = parse_file_from_test_assets( - "refined_message_with_condition_and_section_with_no_condition_info.xml" -) +def normalize_xml(xml_string): + parser = etree.XMLParser(remove_blank_text=True) + tree = etree.XML(xml_string, parser) + return etree.tostring(tree, pretty_print=True).decode() -test_header = parse_file_from_test_assets("test_header.xml") + +test_file = read_file_from_test_assets("CDA_eICR.xml") +test_message = parse_file_from_test_assets("message_refiner_test_eicr.xml") +test_clinical_document = test_message.getroot() +test_structured_body = test_clinical_document.find( + ".//{urn:hl7-org:v3}structuredBody", NAMESPACES +) +encounters_section = _get_section_by_code(test_structured_body, "46240-8") +results_section = _get_section_by_code(test_structured_body, "30954-2") +social_history_section = _get_section_by_code(test_structured_body, "29762-2") +chlymidia_xpath = './/hl7:observation[hl7:templateId[@root="2.16.840.1.113883.10.20.15.2.3.2"]] | .//hl7:observation[hl7:code[@code="53926-2"]]' +chlamydia_observations = _get_observations( + section=results_section, combined_xpath=chlymidia_xpath +) +chlamydia_observation = chlamydia_observations[2] @pytest.mark.parametrize( @@ -93,7 +121,7 @@ def read_file_from_test_assets(filename: str) -> str: ], ) def test_validate_sections_to_include(test_data, expected_result): - # # Test cases: single and multiple sections_to_include + # Test cases: single and multiple sections_to_include if test_data != "blah blah blah" and test_data is not None: actual_response = validate_sections_to_include(test_data) assert actual_response == expected_result @@ -110,185 +138,456 @@ def test_validate_sections_to_include(test_data, expected_result): assert actual_response[1] != "" -def test_refine(): - raw_message = ET.fromstring(test_eICR_xml) - # Test case: Refine for only social history - expected_message = refined_test_eICR_social_history_only - sections_to_include = ["29762-2"] - refined_message = refine(raw_message, sections_to_include) - - actual_flattened = [i.tag for i in ET.fromstring(refined_message).iter()] - expected_flattened = [i.tag for i in expected_message.iter()] +def test_validate_message(): + # Test case: valid XML + raw_message = test_file + actual_response, error_message = validate_message(raw_message) + actual_flattened = [i.tag for i in actual_response.iter()] + expected_flattened = [i.tag for i in etree.fromstring(raw_message).iter()] assert actual_flattened == expected_flattened + assert error_message == "" - # Test case: Refine for labs/diagnostics and reason for visit - expected_message = refined_test_eICR_labs_reason - sections_to_include = ["30954-2", "29299-5"] - raw_message = ET.fromstring(test_eICR_xml) - refined_message = refine(raw_message, sections_to_include) + # Test case: invalid XML + raw_message = "this is not a valid XML" + actual_response, error_message = validate_message(raw_message) + assert actual_response is None + assert "XMLSyntaxError" in error_message - actual_flattened = [i.tag for i in ET.fromstring(refined_message).iter()] - expected_flattened = [i.tag for i in expected_message.iter()] - assert actual_flattened == expected_flattened - # Test case: Refine for condition only - expected_message = refined_test_condition_only - raw_message = ET.fromstring(test_eICR_xml) - system = "http://loinc.org" - codes = ["76078-5", "76080-1"] - mock_clinical_service_xpaths = _generate_clinical_xpaths(system, codes) - refined_message = refine( - raw_message, - sections_to_include=None, - clinical_services=mock_clinical_service_xpaths, - ) - actual_flattened = [i.tag for i in ET.fromstring(refined_message).iter()] - expected_flattened = [i.tag for i in expected_message.iter()] - assert actual_flattened == expected_flattened +def test_generate_combined_xpath(): + template_ids = ["2.16.840.1.113883.10.20.15.2.3.2"] + clinical_services_dict = { + "loinc": ["95423-0", "96764-6"], + "snomed": ["138389411000119105", "1119302008"], + } - # Test case: Refine for condition and labs/diagnostics section - expected_message = refined_test_conditon_and_labs - raw_message = ET.fromstring(test_eICR_xml) - sections_to_include = ["30954-2"] - refined_message = refine( - raw_message, - sections_to_include=sections_to_include, - clinical_services=mock_clinical_service_xpaths, + expected_xpath = ( + './/hl7:observation[hl7:templateId[@root="2.16.840.1.113883.10.20.15.2.3.2"]] | ' + './/hl7:observation[hl7:code[@code="95423-0"]] | ' + './/hl7:observation[hl7:code[@code="96764-6"]] | ' + './/hl7:observation[hl7:code[@code="138389411000119105"]] | ' + './/hl7:observation[hl7:code[@code="1119302008"]]' ) - actual_flattened = [i.tag for i in ET.fromstring(refined_message).iter()] - expected_flattened = [i.tag for i in expected_message.iter()] - assert actual_flattened == expected_flattened + output = _generate_combined_xpath(template_ids, clinical_services_dict) + assert output == expected_xpath -def test_select_header(): - raw_message = ET.fromstring(test_eICR_xml) - actual_header = _select_message_header(raw_message) - expected_header = test_header - actual_flattened = [i.tag for i in actual_header.iter()] - expected_flattened = [i.tag for i in expected_header.iter()] - assert actual_flattened == expected_flattened +@pytest.mark.parametrize( + "test_section_code, expected_section_code", + [ + ("46240-8", "46240-8"), + ("30954-2", "30954-2"), + ("29762-2", "29762-2"), + ("12345", None), + ], +) +def test_get_section_by_code( + test_section_code, + expected_section_code, +): + test_section = _get_section_by_code(test_structured_body, test_section_code) + if test_section is not None: + assert test_section is not None + assert test_section.tag.endswith("section") + assert ( + test_section.xpath("./hl7:code/@code", namespaces=NAMESPACES)[0] + == expected_section_code + ) + else: + assert test_section is None -def test_add_root_element(): - raw_message = ET.fromstring(test_eICR_xml) - header = _select_message_header(raw_message) - elements = raw_message.xpath( - "//*[local-name()='section']", namespaces={"hl7": "urn:hl7-org:v3"} - ) - result = _add_root_element(header, elements) - # TODO: I could only get this to work with regex - assert re.sub(r"\s+", "", result) == re.sub(r"\s+", "", test_eICR_xml) +@pytest.mark.parametrize( + "section, expected_length", + [(encounters_section, 1), (results_section, 7), (social_history_section, 9)], +) +def test_get_entries_for_section(section, expected_length): + entries = _get_entries_for_section(section, NAMESPACES) + assert len(entries) == expected_length -def test_validate_message(): - # Test case: valid XML - raw_message = test_eICR_xml - actual_response, error_message = validate_message(raw_message) - actual_flattened = [i.tag for i in actual_response.iter()] - expected_flattened = [i.tag for i in ET.fromstring(raw_message).iter()] - assert actual_flattened == expected_flattened - assert error_message == "" + # check that all returned elements are elements + for entry in entries: + assert entry.tag.endswith("entry") - # Test case: invalid XML - raw_message = "this is not a valid XML" - actual_response, error_message = validate_message(raw_message) - assert actual_response is None - assert "XMLSyntaxError" in error_message +@pytest.mark.parametrize( + "section, combined_xpath, expected_length", + [ + ( + results_section, + _generate_combined_xpath( + template_ids=TRIGGER_CODE_TEMPLATE_IDS, clinical_services_dict={} + ), + 3, + ), + (results_section, chlymidia_xpath, 4), + ], +) +def test_get_observations(section, combined_xpath, expected_length): + observations = _get_observations(section, combined_xpath, NAMESPACES) + assert len(observations) == expected_length -_, SECTION_DETAILS = load_section_loincs(read_json_from_assets("section_loincs.json")) + # check that all returned elements are elements + for obs in observations: + assert obs.tag.endswith("observation") -def test_create_minimal_section(): - # Test case: Valid section code, not empty - section_code = "29762-2" - empty_section = False - result = _create_minimal_section(section_code, empty_section) - assert result is not None - assert result.tag == "section" - assert result.find("templateId").get("root") == "2.16.840.1.113883.10.20.22.2.17" - assert result.find("templateId").get("extension") == "2015-08-01" - assert result.find("code").get("code") == section_code - assert result.find("code").get("displayName") == "Social History" - assert result.find("title").text == "Social History" - assert "nullFlavor" not in result.attrib +@pytest.mark.parametrize( + "section, search_type, search_values, expected_result", + [ + ( + results_section, + "templateId", + ["2.16.840.1.113883.10.20.15.2.3.2", "non-existent-templateId"], + True, + ), + (results_section, "templateId", ["non-existent-templateId"], False), + (results_section, "code", ["53926-2", "non-existent-code"], True), + (results_section, "code", ["non-existent-code"], False), + ], +) +def test_are_elements_present(section, search_type, search_values, expected_result): assert ( - "Only entries that match the corresponding condition code were included" - in result.find("text").text + _are_elements_present(section, search_type, search_values, NAMESPACES) + == expected_result ) - # Test case: Valid section code, empty - empty_section = True - result = _create_minimal_section(section_code, empty_section) - assert result is not None - assert result.tag == "section" - assert result.get("nullFlavor") == "NI" - assert ( - "Removed via PRIME DIBBs Message Refiner API endpoint" - in result.find("text").text - ) - # Test case: Invalid section code - section_code = "invalid-code" - result = _create_minimal_section(section_code, empty_section) - assert result is None - - -def test_create_minimal_sections(): - # Test case: No sections to include or conditions - result = _create_minimal_sections() - assert len(result) == len(SECTION_DETAILS) # all minimal - assert all([section.tag == "section" for section in result]) - assert all([section.get("nullFlavor") == "NI" for section in result]) - - # Test case: Sections to include - sections_to_include = ["29762-2"] - result = _create_minimal_sections(sections_to_include=sections_to_include) - expected_length = len(SECTION_DETAILS) - len(sections_to_include) - assert len(result) == expected_length # Excluded section should be minimal - - # Check that excluded sections are in the result - excluded_sections = set(SECTION_DETAILS.keys()) - set(sections_to_include) - assert all( - [section.find("code").get("code") in excluded_sections for section in result] - ) +@pytest.mark.parametrize( + "observation, expected_path", + [ + ( + chlamydia_observation, + [ + chlamydia_observations[2] + .getparent() + .getparent() + .getparent(), # + chlamydia_observations[2].getparent().getparent(), # + chlamydia_observations[2].getparent(), # + chlamydia_observations[2], # + ], + ) + ], +) +def test_find_path_to_entry(observation, expected_path): + path = _find_path_to_entry(observation) + assert path == expected_path + assert len(path) == len(expected_path) - # Test case: Sections with conditions - sections_with_conditions = ["30954-2"] - result = _create_minimal_sections(sections_with_conditions=sections_with_conditions) - expected_length = len(SECTION_DETAILS) - len(sections_with_conditions) - assert len(result) == expected_length # Excluded section should be minimal - # Check that excluded sections are in the result - excluded_sections = set(SECTION_DETAILS.keys()) - set(sections_with_conditions) - assert all( - [section.find("code").get("code") in excluded_sections for section in result] - ) +@pytest.mark.parametrize( + "xml_content, xpath, expected_entry_count", + [ + ( + """ +
+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
+ """, + './/hl7:observation[hl7:code/@code="12345-6" or hl7:code/@code="67890-1"]', + 2, + ), + # future test cases here if needed + ], +) +def test_prune_unwanted_siblings(xml_content, xpath, expected_entry_count): + element = etree.fromstring(xml_content) - # Test case: Sections to include and sections with conditions - sections_to_include = ["29762-2"] - sections_with_conditions = ["30954-2"] - result = _create_minimal_sections( - sections_to_include=sections_to_include, - sections_with_conditions=sections_with_conditions, - ) - expected_length = ( - len(SECTION_DETAILS) - len(sections_to_include) - len(sections_with_conditions) + # find matching observations + matching_observations = _get_observations(element, xpath, NAMESPACES) + + # collect paths + paths = [_find_path_to_entry(obs) for obs in matching_observations] + + # prune unwanted siblings + _prune_unwanted_siblings(paths, matching_observations) + + # check the number of remaining elements + remaining_entries = _get_entries_for_section(element) + assert len(remaining_entries) == expected_entry_count + + +@pytest.mark.parametrize( + "observation, expected_data", + [ + ( + chlamydia_observations[0], + { + "display_text": "Bordetella pertussis Ab [Units/volume] in Serum", + "code": "11585-7", + "code_system": "LOINC", + "is_trigger_code": True, + }, + ), + ( + chlamydia_observations[1], + { + "display_text": "Bordetella pertussis [Presence] in Throat by Organism specific culture", + "code": "548-8", + "code_system": "LOINC", + "is_trigger_code": True, + }, + ), + ( + chlamydia_observations[2], + { + "display_text": "Chlamydia trachomatis rRNA [Presence] in Vaginal fluid by NAA with probe detection", + "code": "53926-2", + "code_system": "loinc.org", + "is_trigger_code": False, + }, + ), + ( + chlamydia_observations[3], + { + "display_text": "SARS-like Coronavirus N gene [Presence] in Unspecified specimen by NAA with probe detection", + "code": "94310-0", + "code_system": "LOINC", + "is_trigger_code": True, + }, + ), + ], +) +def test_extract_observation_data(observation, expected_data): + data = _extract_observation_data(observation) + assert data == expected_data + + +@pytest.mark.parametrize( + "observations, expected_text_xml", + [ + ( + chlamydia_observations, + b'Output from CDC PRIME DIBBs `message-refiner` API by request of STLT
Display TextCodeCode SystemTrigger Code Observation
Bordetella pertussis Ab [Units/volume] in Serum11585-7LOINCTRUE
Bordetella pertussis [Presence] in Throat by Organism specific culture548-8LOINCTRUE
Chlamydia trachomatis rRNA [Presence] in Vaginal fluid by NAA with probe detection53926-2loinc.orgFALSE
SARS-like Coronavirus N gene [Presence] in Unspecified specimen by NAA with probe detection94310-0LOINCTRUE
', + ) + ], +) +def test_create_or_update_text_element(observations, expected_text_xml): + text_element = _create_or_update_text_element(observations) + actual_string = etree.tostring(text_element) + assert actual_string == expected_text_xml + + +@pytest.mark.parametrize( + "section_xml, observations_xml, expected_text_xml", + [ + ( + """
""", + [ + """ + + + + """, + """ + + + """, + ], + b""" + Output from CDC PRIME DIBBs `message-refiner` API by request of STLT + + + + + + + + + + + + + + + + + + + +
Display TextCodeCode SystemTrigger Code Observation
Test Display 112345-6LOINCTRUE
Test Display 267890-1LOINCFALSE
+
""", + ) + ], +) +def test_update_text_element(section_xml, observations_xml, expected_text_xml): + section = etree.fromstring(section_xml) + observations = [etree.fromstring(obs_xml) for obs_xml in observations_xml] + + _update_text_element(section, observations) + + text_element = section.find(".//{urn:hl7-org:v3}text") + actual_string = etree.tostring(text_element, pretty_print=True).decode() + expected_string = etree.tostring( + etree.fromstring(expected_text_xml), pretty_print=True + ).decode() + + assert normalize_xml(actual_string) == normalize_xml(expected_string) + + +@pytest.mark.parametrize( + "section_xml, expected_section_xml", + [ + ( + """
+ + + + + + Results + +
+
+ + +
""", + b"""
+ + + + + + Results + + Output from CDC PRIME DIBBs `message-refiner` API by request of STLT + + + + +
Section details have been removed as requested
+
+
""", + ) + ], +) +def test_create_minimal_section(section_xml, expected_section_xml): + section = etree.fromstring(section_xml) + + _create_minimal_section(section) + + actual_string = etree.tostring(section, pretty_print=True).decode() + expected_string = etree.tostring( + etree.fromstring(expected_section_xml), pretty_print=True + ).decode() + + assert normalize_xml(actual_string) == normalize_xml(expected_string) + + +@pytest.mark.parametrize( + "combined_xpaths, clinical_services_codes, expected_in_results", + [ + # test case 1: Process section with a match (Chlamydia code) + ( + _generate_combined_xpath( + template_ids=TRIGGER_CODE_TEMPLATE_IDS, + clinical_services_dict={"loinc": ["53926-2"]}, + ), + ["53926-2"], + True, + ), + # test case 2: Process section without a match (Zika code) + ( + _generate_combined_xpath( + template_ids=TRIGGER_CODE_TEMPLATE_IDS, + clinical_services_dict={"loinc": ["85622-9"]}, + ), + ["85622-9"], + False, + ), + ], +) +def test_process_section(combined_xpaths, clinical_services_codes, expected_in_results): + _process_section( + section=results_section, + combined_xpaths=combined_xpaths, + namespaces=NAMESPACES, + template_ids=TRIGGER_CODE_TEMPLATE_IDS, + clinical_services_codes=clinical_services_codes, ) - assert len(result) == expected_length # Excluded sections should be minimal + if expected_in_results: + assert _are_elements_present(results_section, "code", ["53926-2"], NAMESPACES) + else: + assert not _are_elements_present( + results_section, "code", ["53926-2"], NAMESPACES + ) - # Check that excluded sections are in the result - excluded_sections = ( - set(SECTION_DETAILS.keys()) - - set(sections_to_include) - - set(sections_with_conditions) + +@pytest.mark.parametrize( + "sections_to_include, conditions_to_include, expected_in_results", + [ + # test case 1: refine with no parameters + (None, None, False), + # test case 1: refine with clinical services + (None, {"loinc": ["53926-2"]}, True), + # test case 2: refine with wrong sections_to_include for code + (["29762-2"], {"loinc": ["53926-2"]}, False), + # test case 3: refine with correct sections_to_include for code + (["30954-2"], {"loinc": ["53926-2"]}, True), + ], +) +def test_refine(sections_to_include, conditions_to_include, expected_in_results): + fresh_test_data = parse_file_from_test_assets("message_refiner_test_eicr.xml") + fresh_clinical_document = fresh_test_data.getroot() + + sections = None + if sections_to_include: + sections = sections_to_include + + clinical_services = None + if conditions_to_include: + clinical_services = conditions_to_include + + test_refined_output = refine( + validated_message=fresh_clinical_document, + sections_to_include=sections, + clinical_services=clinical_services, ) - assert all( - [section.find("code").get("code") in excluded_sections for section in result] + test_refined_document = etree.fromstring(test_refined_output) + test_refined_structured_body = test_refined_document.find( + ".//{urn:hl7-org:v3}structuredBody", NAMESPACES ) - - # Test case: Empty sections - result = _create_minimal_sections(empty_section=False) - assert len(result) == len(SECTION_DETAILS) # Should create minimal sections for all - assert all([section.tag == "section" for section in result]) - assert all(["nullFlavor" not in section.attrib for section in result]) + test_refined_results_section = _get_section_by_code( + test_refined_structured_body, "30954-2" + ) + if expected_in_results: + assert _are_elements_present( + test_refined_results_section, "code", ["53926-2"], NAMESPACES + ) + else: + assert not _are_elements_present( + test_refined_results_section, "code", ["53926-2"], NAMESPACES + ) diff --git a/containers/message-refiner/tests/test_refiner.py b/containers/message-refiner/tests/test_refiner.py index e80c07f5f4..98b6d0fd1d 100644 --- a/containers/message-refiner/tests/test_refiner.py +++ b/containers/message-refiner/tests/test_refiner.py @@ -8,12 +8,12 @@ from app.main import app from app.main import get_clinical_services from fastapi.testclient import TestClient -from lxml import etree as ET +from lxml import etree client = TestClient(app) -def parse_file_from_test_assets(filename: str) -> ET.ElementTree: +def parse_file_from_test_assets(filename: str) -> etree.ElementTree: """ Parses a file from the assets directory into an ElementTree. @@ -23,8 +23,8 @@ def parse_file_from_test_assets(filename: str) -> ET.ElementTree: with open( (pathlib.Path(__file__).parent.parent / "tests" / "assets" / filename), "r" ) as file: - parser = ET.XMLParser(remove_blank_text=True) - tree = ET.parse(file, parser) + parser = etree.XMLParser(remove_blank_text=True) + tree = etree.parse(file, parser) return tree @@ -42,10 +42,16 @@ def read_file_from_test_assets(filename: str) -> str: return file.read() -test_eICR_xml = read_file_from_test_assets("CDA_eICR.xml") +test_eICR_xml = read_file_from_test_assets("message_refiner_test_eicr.xml") + +refined_test_no_parameters = parse_file_from_test_assets( + "refined_message_no_parameters.xml" +) + refined_test_eICR_social_history_only = parse_file_from_test_assets( "refined_message_social_history_only.xml" ) + refined_test_eICR_labs_reason = parse_file_from_test_assets( "refined_message_labs_reason.xml" ) @@ -54,19 +60,11 @@ def read_file_from_test_assets(filename: str) -> str: "refined_message_condition_only.xml" ) -refined_test_conditon_and_labs = parse_file_from_test_assets( - "refined_message_condition_and_lab_section.xml" +refined_test_results_chlamydia_condition = parse_file_from_test_assets( + "refined_message_results_section_chlamydia_condition.xml" ) -refined_test_no_relevant_section_data = parse_file_from_test_assets( - "refined_message_with_condition_and_section_with_no_condition_info.xml" -) - -test_header = parse_file_from_test_assets("test_header.xml") - -mock_tcr_response = { - "lrtc": [{"codes": ["76078-5", "76080-1"], "system": "http://loinc.org"}] -} +mock_tcr_response = {"lrtc": [{"codes": ["53926-2"], "system": "http://loinc.org"}]} def test_health_check(): @@ -95,14 +93,14 @@ def test_openapi(): def test_ecr_refiner(): # Test case: sections_to_include = None - expected_response = parse_file_from_test_assets("CDA_eICR.xml") + expected_response = refined_test_no_parameters content = test_eICR_xml sections_to_include = None endpoint = "/ecr/" actual_response = client.post(endpoint, content=content) assert actual_response.status_code == 200 - actual_flattened = [i.tag for i in ET.fromstring(actual_response.content).iter()] + actual_flattened = [i.tag for i in etree.fromstring(actual_response.content).iter()] expected_flattened = [i.tag for i in expected_response.iter()] assert actual_flattened == expected_flattened @@ -114,7 +112,7 @@ def test_ecr_refiner(): actual_response = client.post(endpoint, content=content) assert actual_response.status_code == 200 - actual_flattened = [i.tag for i in ET.fromstring(actual_response.content).iter()] + actual_flattened = [i.tag for i in etree.fromstring(actual_response.content).iter()] expected_flattened = [i.tag for i in expected_response.iter()] assert actual_flattened == expected_flattened @@ -125,7 +123,7 @@ def test_ecr_refiner(): endpoint = f"/ecr/?sections_to_include={sections_to_include}" actual_response = client.post(endpoint, content=content) assert actual_response.status_code == 200 - actual_flattened = [i.tag for i in ET.fromstring(actual_response.content).iter()] + actual_flattened = [i.tag for i in etree.fromstring(actual_response.content).iter()] expected_flattened = [i.tag for i in expected_response.iter()] assert actual_flattened == expected_flattened @@ -156,62 +154,81 @@ async def test_ecr_refiner_conditions(mock_get): mock_response.json.return_value = mock_tcr_response mock_get.return_value = mock_response - # Test conditions only + # Test chlamydia condition only expected_response = refined_test_condition_only content = test_eICR_xml - conditions_to_include = "6142004" + conditions_to_include = "240589008" endpoint = f"/ecr/?conditions_to_include={conditions_to_include}" actual_response = client.post(endpoint, content=content) assert actual_response.status_code == 200 actual_flattened = [ - i.tag for i in ET.fromstring(actual_response.content.decode()).iter() + i.tag + for i in etree.fromstring(actual_response.content.decode()).iter() + if isinstance(i, etree._Element) + ] + expected_flattened = [ + i.tag for i in expected_response.iter() if isinstance(i, etree._Element) ] - expected_flattened = [i.tag for i in expected_response.iter()] assert actual_flattened == expected_flattened + actual_elements = [ i.tag.split("}")[-1] - for i in ET.fromstring(actual_response.content.decode()).iter() + for i in etree.fromstring(actual_response.content.decode()).iter() + if isinstance(i, etree._Element) and isinstance(i.tag, str) ] assert "ClinicalDocument" in actual_elements - # Test conditions and relevant labs section - expected_response = refined_test_conditon_and_labs + # Test results section with chlamydia condition + expected_response = refined_test_results_chlamydia_condition content = test_eICR_xml - conditions_to_include = "6142004" + conditions_to_include = "240589008" sections_to_include = "30954-2" endpoint = f"/ecr/?sections_to_include={sections_to_include}&conditions_to_include={conditions_to_include}" actual_response = client.post(endpoint, content=content) assert actual_response.status_code == 200 actual_flattened = [ - i.tag for i in ET.fromstring(actual_response.content.decode()).iter() + i.tag + for i in etree.fromstring(actual_response.content.decode()).iter() + if isinstance(i, etree._Element) + ] + expected_flattened = [ + i.tag for i in expected_response.iter() if isinstance(i, etree._Element) ] - expected_flattened = [i.tag for i in expected_response.iter()] assert actual_flattened == expected_flattened + actual_elements = [ i.tag.split("}")[-1] - for i in ET.fromstring(actual_response.content.decode()).iter() + for i in etree.fromstring(actual_response.content.decode()).iter() + if isinstance(i, etree._Element) and isinstance(i.tag, str) ] assert "ClinicalDocument" in actual_elements # Test conditions, history of hospitalization section without relevant data - expected_response = refined_test_no_relevant_section_data + # this will process in the same way as if no parameters were passed + expected_response = refined_test_no_parameters content = test_eICR_xml - conditions_to_include = "6142004" + conditions_to_include = "240589008" sections_to_include = "46240-8" endpoint = f"/ecr/?sections_to_include={sections_to_include}&conditions_to_include={conditions_to_include}" actual_response = client.post(endpoint, content=content) assert actual_response.status_code == 200 actual_flattened = [ - i.tag for i in ET.fromstring(actual_response.content.decode()).iter() + i.tag + for i in etree.fromstring(actual_response.content.decode()).iter() + if isinstance(i, etree._Element) + ] + expected_flattened = [ + i.tag for i in expected_response.iter() if isinstance(i, etree._Element) ] - expected_flattened = [i.tag for i in expected_response.iter()] assert actual_flattened == expected_flattened + actual_elements = [ i.tag.split("}")[-1] - for i in ET.fromstring(actual_response.content.decode()).iter() + for i in etree.fromstring(actual_response.content.decode()).iter() + if isinstance(i, etree._Element) and isinstance(i.tag, str) ] assert "ClinicalDocument" in actual_elements @@ -222,11 +239,11 @@ async def test_get_clinical_services(mock_get): mock_response = Mock() mock_response.status_code = 200 mock_response.json.return_value = { - "lrtc": [{"codes": ["76078-5", "76080-1"], "system": "http://loinc.org"}] + "lrtc": [{"codes": ["53926-2"], "system": "http://loinc.org"}] } mock_get.return_value = mock_response - condition_codes = "6142004" + condition_codes = "240589008" clinical_services = await get_clinical_services(condition_codes) expected_result = [mock_response] assert clinical_services == expected_result diff --git a/containers/message-refiner/tests/test_utils.py b/containers/message-refiner/tests/test_utils.py index 91ec503dfe..69ece3cd20 100644 --- a/containers/message-refiner/tests/test_utils.py +++ b/containers/message-refiner/tests/test_utils.py @@ -1,86 +1,28 @@ -import pytest -from app.utils import _generate_clinical_xpaths -from app.utils import create_clinical_xpaths -from app.utils import load_section_loincs +from app.utils import create_clinical_services_dict -def test_generate_clinical_xpaths(): - """ - Confirms that xpaths can be generated for sample codes. - """ - system = "http://loinc.org" - codes = ["76078-5", "76080-1"] - expected_output = [ - ".//*[local-name()='entry'][.//*[@code='76078-5' and @codeSystemName='loinc.org']]", - ".//*[local-name()='entry'][.//*[@code='76080-1' and @codeSystemName='loinc.org']]", - ] - output = _generate_clinical_xpaths(system, codes) - assert output == expected_output - - -def test_generate_clinical_xpaths_unknown_system(): - """ - Confirms error is generated if system is not recognized. - """ - system = "http://unknown.org" - codes = ["A01", "B02"] - with pytest.raises(KeyError) as exc_info: - _generate_clinical_xpaths(system, codes) - assert ( - str(exc_info.value) - == "'http://unknown.org not a recognized clinical service system.'" - ) - - -def test_create_clinical_xpaths(): - """ - Confirms dynamic xpaths generated from clinical_service list - """ +def test_create_clinical_services_dict(): clinical_services_list = [ - {"lrtc": [{"codes": ["76078-5", "76080-1"], "system": "http://loinc.org"}]} - ] - expected_xpaths = [ - ".//*[local-name()='entry'][.//*[@code='76078-5' and @codeSystemName='loinc.org']]", - ".//*[local-name()='entry'][.//*[@code='76080-1' and @codeSystemName='loinc.org']]", + { + "dxtc": [ + { + "codes": ["U07.1", "U07.2"], + "system": "http://hl7.org/fhir/sid/icd-10-cm", + }, + { + "codes": ["138389411000119105", "1119302008"], + "system": "http://snomed.info/sct", + }, + ], + "lrtc": [{"codes": ["95423-0", "96764-6"], "system": "http://loinc.org"}], + } ] - actual_xpaths = create_clinical_xpaths(clinical_services_list) - assert actual_xpaths == expected_xpaths - -def test_load_section_loincs(): - """ - Confirms that a dictionary of loinc data can be transformed into a list - and that a dictionary of required sections can be generated. - """ - loinc_json = { - "29762-2": { - "minimal_fields": [ - "Social History", - "2.16.840.1.113883.10.20.22.2.17", - "2015-08-01", - "Social History", - ], - "required": True, - }, - "11369-6": { - "minimal_fields": [ - "History of Immunizations", - "2.16.840.1.113883.10.20.22.4.52", - "2015-08-01", - "Immunizations", - ], - "required": False, - }, + expected_output = { + "icd10": ["U07.1", "U07.2"], + "snomed": ["138389411000119105", "1119302008"], + "loinc": ["95423-0", "96764-6"], } - expected_section_loincs = ["29762-2", "11369-6"] - expected_section_details = { - "29762-2": [ - "Social History", - "2.16.840.1.113883.10.20.22.2.17", - "2015-08-01", - "Social History", - ] - } - section_loincs, section_details = load_section_loincs(loinc_json) - assert section_loincs == expected_section_loincs - assert section_details == expected_section_details + + output = create_clinical_services_dict(clinical_services_list) + assert output == expected_output