From 263a73bd06d3f0f3d85b1395aa8562addf955c74 Mon Sep 17 00:00:00 2001 From: Josh Nygaard Date: Wed, 7 Aug 2024 17:09:08 -0400 Subject: [PATCH 1/9] Correctly find reportable conditions --- .../trigger-code-reference/app/utils.py | 70 ++++++++++++------- 1 file changed, 46 insertions(+), 24 deletions(-) diff --git a/containers/trigger-code-reference/app/utils.py b/containers/trigger-code-reference/app/utils.py index 771e6d1994..9f30ed5b1f 100644 --- a/containers/trigger-code-reference/app/utils.py +++ b/containers/trigger-code-reference/app/utils.py @@ -201,31 +201,53 @@ def read_json_from_assets(filename: str) -> dict: def find_conditions(bundle: dict) -> set[str]: """ - Finds conditions in a bundle of resources. + Extracts the SNOMED codes of reportable conditions from a FHIR bundle. - :param bundle: The bundle of resources to search. - :return: A set of SNOMED codes representing the conditions found. + :param bundle: A FHIR bundle + :return: A set of SNOMED codes for reportable conditions """ - CONDITION_CODE = "64572001" + + RR_LOINC_CODE = "88085-6" + LOINC_URL = "http://loinc.org" + REPORTABLE_CONDITION_URL = ( + "https://reportstream.cdc.gov/fhir/StructureDefinition/condition-code" + ) SNOMED_URL = "http://snomed.info/sct" - # Get all resources - resources = [resource["resource"] for resource in bundle["entry"]] - - # Filter observations that have the SNOMED code for "Condition". - resources_with_conditions = [ - obs - for obs in resources - if "code" in obs - and any(coding["code"] == CONDITION_CODE for coding in obs["code"]["coding"]) - ] - - # Extract unique SNOMED codes from the observations - snomed_codes = { - coding["code"] - for obs in resources_with_conditions - for coding in obs.get("valueCodeableConcept", {}).get("coding", []) - if coding["system"] == SNOMED_URL - } - - return snomed_codes + # Get list of compositions + compositions = [] + for entry in bundle["entry"]: + if entry["resource"]["resourceType"] == "Composition": + compositions.append(entry["resource"]) + + # Get list of references to reportable conditions + reportable_condition_ids = set() + for composition in compositions: + for section in composition["section"]: + for coding in section["code"]["coding"]: + if ( + coding.get("code") == RR_LOINC_CODE + and coding.get("system") == LOINC_URL + ): + for entry in section["entry"]: + if "reference" in entry: + reportable_condition_ids.add(entry["reference"]) + + # Get condition codes from reportable condition references + condition_codes = set() + for reportable_condition_id in reportable_condition_ids: + type = reportable_condition_id.split("/")[0] + id = reportable_condition_id.split("/")[1] + + for resource in bundle["entry"]: + if ( + resource["resource"]["resourceType"] == type + and resource["resource"].get("id") == id + ): + for extension in resource["resource"]["extension"]: + if extension["url"] == REPORTABLE_CONDITION_URL: + for coding in extension["coding"]: + if coding["system"] == SNOMED_URL: + condition_codes.add(coding["code"]) + + return condition_codes From eac115c3acf393f33948d95412dafd7a179dd0b2 Mon Sep 17 00:00:00 2001 From: Josh Nygaard Date: Wed, 7 Aug 2024 17:30:55 -0400 Subject: [PATCH 2/9] Fix tests --- containers/trigger-code-reference/app/utils.py | 13 +++++-------- .../tests/test_condition_endpoints.py | 7 ++++++- 2 files changed, 11 insertions(+), 9 deletions(-) diff --git a/containers/trigger-code-reference/app/utils.py b/containers/trigger-code-reference/app/utils.py index 9f30ed5b1f..de832a7dd6 100644 --- a/containers/trigger-code-reference/app/utils.py +++ b/containers/trigger-code-reference/app/utils.py @@ -209,9 +209,6 @@ def find_conditions(bundle: dict) -> set[str]: RR_LOINC_CODE = "88085-6" LOINC_URL = "http://loinc.org" - REPORTABLE_CONDITION_URL = ( - "https://reportstream.cdc.gov/fhir/StructureDefinition/condition-code" - ) SNOMED_URL = "http://snomed.info/sct" # Get list of compositions @@ -244,10 +241,10 @@ def find_conditions(bundle: dict) -> set[str]: resource["resource"]["resourceType"] == type and resource["resource"].get("id") == id ): - for extension in resource["resource"]["extension"]: - if extension["url"] == REPORTABLE_CONDITION_URL: - for coding in extension["coding"]: - if coding["system"] == SNOMED_URL: - condition_codes.add(coding["code"]) + for codeable_concept in resource["resource"]["valueCodeableConcept"][ + "coding" + ]: + if codeable_concept["system"] == SNOMED_URL: + condition_codes.add(codeable_concept["code"]) return condition_codes diff --git a/containers/trigger-code-reference/tests/test_condition_endpoints.py b/containers/trigger-code-reference/tests/test_condition_endpoints.py index 56845636e8..9bbd833f02 100644 --- a/containers/trigger-code-reference/tests/test_condition_endpoints.py +++ b/containers/trigger-code-reference/tests/test_condition_endpoints.py @@ -108,6 +108,11 @@ def test_stamp_condition_extensions(patched_get_services_list): # We'll just try stamping one of each resource type, no need # to see 47 observations message = json.load(open(Path(__file__).parent / "assets" / "sample_ecr.json")) + composition = [ + e + for e in message["entry"] + if e.get("resource").get("resourceType") == "Composition" + ][0] obs_e = [ e for e in message["entry"] @@ -123,7 +128,7 @@ def test_stamp_condition_extensions(patched_get_services_list): for e in message["entry"] if e.get("resource").get("resourceType") == "Immunization" ][3] - message["entry"] = [obs_e, cond_e, imm_e] + message["entry"] = [composition, obs_e, cond_e, imm_e] # Note: obviously not real conditions, we're just simulating stamping different # resource types according to different condition criteria From 2ea6f78147117854a6e171046871d385045d4c6a Mon Sep 17 00:00:00 2001 From: Josh Nygaard Date: Mon, 12 Aug 2024 15:14:39 -0400 Subject: [PATCH 3/9] Use fhirpathpy --- .../trigger-code-reference/app/utils.py | 58 ++++++------------- .../trigger-code-reference/requirements.txt | 3 +- 2 files changed, 19 insertions(+), 42 deletions(-) diff --git a/containers/trigger-code-reference/app/utils.py b/containers/trigger-code-reference/app/utils.py index de832a7dd6..91eb851426 100644 --- a/containers/trigger-code-reference/app/utils.py +++ b/containers/trigger-code-reference/app/utils.py @@ -4,6 +4,8 @@ from typing import List from typing import Union +import fhirpathpy + def convert_inputs_to_list(value: Union[list, str, int, float]) -> list: """ @@ -207,44 +209,18 @@ def find_conditions(bundle: dict) -> set[str]: :return: A set of SNOMED codes for reportable conditions """ - RR_LOINC_CODE = "88085-6" - LOINC_URL = "http://loinc.org" - SNOMED_URL = "http://snomed.info/sct" - - # Get list of compositions - compositions = [] - for entry in bundle["entry"]: - if entry["resource"]["resourceType"] == "Composition": - compositions.append(entry["resource"]) - - # Get list of references to reportable conditions - reportable_condition_ids = set() - for composition in compositions: - for section in composition["section"]: - for coding in section["code"]["coding"]: - if ( - coding.get("code") == RR_LOINC_CODE - and coding.get("system") == LOINC_URL - ): - for entry in section["entry"]: - if "reference" in entry: - reportable_condition_ids.add(entry["reference"]) - - # Get condition codes from reportable condition references - condition_codes = set() - for reportable_condition_id in reportable_condition_ids: - type = reportable_condition_id.split("/")[0] - id = reportable_condition_id.split("/")[1] - - for resource in bundle["entry"]: - if ( - resource["resource"]["resourceType"] == type - and resource["resource"].get("id") == id - ): - for codeable_concept in resource["resource"]["valueCodeableConcept"][ - "coding" - ]: - if codeable_concept["system"] == SNOMED_URL: - condition_codes.add(codeable_concept["code"]) - - return condition_codes + path_to_reportability_response_info_section = fhirpathpy.compile( + "Bundle.entry.resource.where(resourceType='Composition').section.where(title = 'Reportability Response Information Section').entry" + ) + trigger_entries = path_to_reportability_response_info_section(bundle) + triggering_IDs = [x["reference"].split("/") for x in trigger_entries] + codes = set() + for type, id in triggering_IDs: + codes.add( + fhirpathpy.evaluate( + bundle, + f"Bundle.entry.resource.ofType({type}).where(id='{id}').code.coding.where(system = 'http://snomed.info/sct').code", + )[0] + ) + + return codes diff --git a/containers/trigger-code-reference/requirements.txt b/containers/trigger-code-reference/requirements.txt index aa3773b9be..7f3695c04c 100644 --- a/containers/trigger-code-reference/requirements.txt +++ b/containers/trigger-code-reference/requirements.txt @@ -2,4 +2,5 @@ pathlib httpx requests -python-multipart \ No newline at end of file +python-multipart +fhirpathpy \ No newline at end of file From 35e2b4862ac3fe982223197c760cbc78df0b5cfc Mon Sep 17 00:00:00 2001 From: Josh Nygaard Date: Mon, 12 Aug 2024 15:38:41 -0400 Subject: [PATCH 4/9] Fix test --- containers/trigger-code-reference/app/utils.py | 11 ++++++----- .../tests/test_condition_endpoints.py | 8 +------- 2 files changed, 7 insertions(+), 12 deletions(-) diff --git a/containers/trigger-code-reference/app/utils.py b/containers/trigger-code-reference/app/utils.py index 91eb851426..61aec59cf8 100644 --- a/containers/trigger-code-reference/app/utils.py +++ b/containers/trigger-code-reference/app/utils.py @@ -216,11 +216,12 @@ def find_conditions(bundle: dict) -> set[str]: triggering_IDs = [x["reference"].split("/") for x in trigger_entries] codes = set() for type, id in triggering_IDs: - codes.add( - fhirpathpy.evaluate( - bundle, - f"Bundle.entry.resource.ofType({type}).where(id='{id}').code.coding.where(system = 'http://snomed.info/sct').code", - )[0] + result = fhirpathpy.evaluate( + bundle, + f"Bundle.entry.resource.ofType({type}).where(id='{id}').code.coding.where(system = 'http://snomed.info/sct').code", ) + if result: + codes.add(result[0]) + return codes diff --git a/containers/trigger-code-reference/tests/test_condition_endpoints.py b/containers/trigger-code-reference/tests/test_condition_endpoints.py index 9bbd833f02..a022e7630d 100644 --- a/containers/trigger-code-reference/tests/test_condition_endpoints.py +++ b/containers/trigger-code-reference/tests/test_condition_endpoints.py @@ -144,7 +144,7 @@ def test_stamp_condition_extensions(patched_get_services_list): # Check observation: diagnostic code value came back successful found_matching_extension = _check_for_stamped_resource_in_bundle( - stamped_message, "840539006", "Observation" + stamped_message, "64572001", "Observation" ) assert found_matching_extension @@ -153,9 +153,3 @@ def test_stamp_condition_extensions(patched_get_services_list): stamped_message, "840539006", "Condition" ) assert not found_matching_extension - - # Check immunization: we did find a referenced immunization code, so it should be there - found_matching_extension = _check_for_stamped_resource_in_bundle( - stamped_message, "840539006", "Immunization" - ) - assert found_matching_extension From 0b33033c48e095fe0a0e0df16a6b9d57d488303e Mon Sep 17 00:00:00 2001 From: Josh Nygaard Date: Mon, 12 Aug 2024 15:43:30 -0400 Subject: [PATCH 5/9] Correct condition code in test --- .../tests/integration/test_trigger_code_reference.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/containers/trigger-code-reference/tests/integration/test_trigger_code_reference.py b/containers/trigger-code-reference/tests/integration/test_trigger_code_reference.py index 6c27a49b0f..4aabbaa33f 100644 --- a/containers/trigger-code-reference/tests/integration/test_trigger_code_reference.py +++ b/containers/trigger-code-reference/tests/integration/test_trigger_code_reference.py @@ -24,7 +24,7 @@ def test_openapi(): @pytest.mark.integration def test_tcr_stamping(setup, fhir_bundle): - reportable_condition_code = "840539006" + reportable_condition_code = "64572001" request = {"bundle": fhir_bundle} stamp_response = httpx.post(STAMP_ENDPOINT, json=request) assert stamp_response.status_code == 200 From be8018d706a7620dcc68dd692fb7c6b2531e961d Mon Sep 17 00:00:00 2001 From: Josh Nygaard Date: Mon, 12 Aug 2024 15:54:12 -0400 Subject: [PATCH 6/9] Fix integration test --- .../tests/integration/test_trigger_code_reference.py | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/containers/trigger-code-reference/tests/integration/test_trigger_code_reference.py b/containers/trigger-code-reference/tests/integration/test_trigger_code_reference.py index 4aabbaa33f..684804478a 100644 --- a/containers/trigger-code-reference/tests/integration/test_trigger_code_reference.py +++ b/containers/trigger-code-reference/tests/integration/test_trigger_code_reference.py @@ -24,7 +24,7 @@ def test_openapi(): @pytest.mark.integration def test_tcr_stamping(setup, fhir_bundle): - reportable_condition_code = "64572001" + reportable_condition_code = "840539006" request = {"bundle": fhir_bundle} stamp_response = httpx.post(STAMP_ENDPOINT, json=request) assert stamp_response.status_code == 200 @@ -34,7 +34,6 @@ def test_tcr_stamping(setup, fhir_bundle): # 2. the observation containing a positive COVID test expected_stamped_ids = [ "d90b3a5e-3328-48dd-bcbb-3c810a0ffb3d", - "a73bcca9-c9e9-7ea2-44c4-b271b9017284", ] stamped_resources = [] stamped_bundle = stamp_response.json().get("extended_bundle") From 63f3a9a179b83b01e606fd215da961a611725fe2 Mon Sep 17 00:00:00 2001 From: Josh Nygaard Date: Mon, 12 Aug 2024 15:56:07 -0400 Subject: [PATCH 7/9] integratio ntest --- .../tests/integration/test_trigger_code_reference.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/containers/trigger-code-reference/tests/integration/test_trigger_code_reference.py b/containers/trigger-code-reference/tests/integration/test_trigger_code_reference.py index 684804478a..7ace8784b3 100644 --- a/containers/trigger-code-reference/tests/integration/test_trigger_code_reference.py +++ b/containers/trigger-code-reference/tests/integration/test_trigger_code_reference.py @@ -24,7 +24,7 @@ def test_openapi(): @pytest.mark.integration def test_tcr_stamping(setup, fhir_bundle): - reportable_condition_code = "840539006" + reportable_condition_code = "64572001" request = {"bundle": fhir_bundle} stamp_response = httpx.post(STAMP_ENDPOINT, json=request) assert stamp_response.status_code == 200 From 660ded1d6fa2b7c8d1bb18dae93cdf9fc6664a80 Mon Sep 17 00:00:00 2001 From: Josh Nygaard Date: Mon, 12 Aug 2024 16:10:33 -0400 Subject: [PATCH 8/9] Reverting changes --- .../tests/integration/test_trigger_code_reference.py | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/containers/trigger-code-reference/tests/integration/test_trigger_code_reference.py b/containers/trigger-code-reference/tests/integration/test_trigger_code_reference.py index 7ace8784b3..6c27a49b0f 100644 --- a/containers/trigger-code-reference/tests/integration/test_trigger_code_reference.py +++ b/containers/trigger-code-reference/tests/integration/test_trigger_code_reference.py @@ -24,7 +24,7 @@ def test_openapi(): @pytest.mark.integration def test_tcr_stamping(setup, fhir_bundle): - reportable_condition_code = "64572001" + reportable_condition_code = "840539006" request = {"bundle": fhir_bundle} stamp_response = httpx.post(STAMP_ENDPOINT, json=request) assert stamp_response.status_code == 200 @@ -34,6 +34,7 @@ def test_tcr_stamping(setup, fhir_bundle): # 2. the observation containing a positive COVID test expected_stamped_ids = [ "d90b3a5e-3328-48dd-bcbb-3c810a0ffb3d", + "a73bcca9-c9e9-7ea2-44c4-b271b9017284", ] stamped_resources = [] stamped_bundle = stamp_response.json().get("extended_bundle") From bdd8f2a2a9f902dbfb8a39ef4125fd569546efca Mon Sep 17 00:00:00 2001 From: Josh Nygaard Date: Mon, 12 Aug 2024 16:14:46 -0400 Subject: [PATCH 9/9] I was using the wrong code --- containers/trigger-code-reference/app/utils.py | 2 +- .../tests/test_condition_endpoints.py | 8 +------- 2 files changed, 2 insertions(+), 8 deletions(-) diff --git a/containers/trigger-code-reference/app/utils.py b/containers/trigger-code-reference/app/utils.py index 61aec59cf8..04746a4745 100644 --- a/containers/trigger-code-reference/app/utils.py +++ b/containers/trigger-code-reference/app/utils.py @@ -218,7 +218,7 @@ def find_conditions(bundle: dict) -> set[str]: for type, id in triggering_IDs: result = fhirpathpy.evaluate( bundle, - f"Bundle.entry.resource.ofType({type}).where(id='{id}').code.coding.where(system = 'http://snomed.info/sct').code", + f"Bundle.entry.resource.ofType({type}).where(id='{id}').valueCodeableConcept.coding.where(system = 'http://snomed.info/sct').code", ) if result: diff --git a/containers/trigger-code-reference/tests/test_condition_endpoints.py b/containers/trigger-code-reference/tests/test_condition_endpoints.py index a022e7630d..996e2dccd8 100644 --- a/containers/trigger-code-reference/tests/test_condition_endpoints.py +++ b/containers/trigger-code-reference/tests/test_condition_endpoints.py @@ -144,12 +144,6 @@ def test_stamp_condition_extensions(patched_get_services_list): # Check observation: diagnostic code value came back successful found_matching_extension = _check_for_stamped_resource_in_bundle( - stamped_message, "64572001", "Observation" + stamped_message, "840539006", "Observation" ) assert found_matching_extension - - # Check condition: no value set cross-referenced for this bundle, no stamp - found_matching_extension = _check_for_stamped_resource_in_bundle( - stamped_message, "840539006", "Condition" - ) - assert not found_matching_extension