Skip to content

Commit

Permalink
TCRS now finds Conditions on its own. (#2238)
Browse files Browse the repository at this point in the history
* TCRS now finds condtions on its own.

* Remove old validator

* Remove unit test for removed validator

* Look for conditions in all resources

* Fix unit test

---------

Co-authored-by: Josh Nygaard <[email protected]>
  • Loading branch information
JNygaard-Skylight and Josh Nygaard authored Jul 26, 2024
1 parent 741ab2c commit b7417f6
Show file tree
Hide file tree
Showing 8 changed files with 75 additions and 62 deletions.
13 changes: 13 additions & 0 deletions .vscode/launch.json
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,19 @@
"cwd": "${workspaceFolder}/containers/orchestration",
"justMyCode": false,
"python": "${workspaceFolder}/containers/orchestration/.venv/bin/python"
},
{
"name": "Python Debugger: Trigger Code Reference",
"type": "debugpy",
"request": "launch",
"program": "${workspaceFolder}/containers/trigger-code-reference/app/main.py",
"console": "integratedTerminal",
"cwd": "${workspaceFolder}/containers/trigger-code-reference",
"justMyCode": false,
"python": "${workspaceFolder}/containers/trigger-code-reference/.venv/bin/python",
"env": {
"PYTHONPATH": "${workspaceFolder}/containers/trigger-code-reference"
}
}
]
}
Original file line number Diff line number Diff line change
Expand Up @@ -23,12 +23,7 @@
{
"name": "stamped_ecr",
"service": "trigger_code_reference",
"endpoint": "/stamp-condition-extensions",
"params": {
"conditions": [
"840539006"
]
}
"endpoint": "/stamp-condition-extensions"
},
{
"name": "message_parser_values",
Expand Down
32 changes: 23 additions & 9 deletions containers/trigger-code-reference/app/main.py
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,7 @@
from app.models import InsertConditionInput
from app.utils import _find_codes_by_resource_type
from app.utils import _stamp_resource_with_code_extension
from app.utils import find_conditions
from app.utils import get_clean_snomed_code
from app.utils import get_clinical_services_dict
from app.utils import get_clinical_services_list
Expand Down Expand Up @@ -61,21 +62,22 @@ async def stamp_condition_extensions(
) -> Response:
"""
Extends the resources of a supplied FHIR bundle with extension tags
related to one or more supplied conditions. For each condition in the
given list of conditions, each resource in the bundle is appended with
an extension structure indicating which SNOMED condition code the
related to one or more supplied conditions. For each condition found in
the bundle, each resource in the bundle is appended with an extension
structure indicating which SNOMED condition code the
resource is linked to.
:param input: A request formatted as an InsertConditionInput, containing a
FHIR bundle whose resources to extend and one or more SNOMED condition
code strings to extend by.
FHIR bundle whose resources to extend.
:return: HTTP Response containing the bundle with resources extended by
any linked conditions.
"""
# Collate all clinical services for each of the supplied conditions to
# extend, collected by service type
# Collate all clinical services for each of the found conditions to extend,
# collected by service type
stamp_codes_to_service_codes = {}
for cond in input.conditions:
conditions = find_conditions(input.bundle)

for cond in conditions:
cond_list = get_clinical_services_list([cond])
cond_dict = get_clinical_services_dict(cond_list)
stamp_codes_to_service_codes[cond] = cond_dict
Expand All @@ -92,7 +94,7 @@ async def stamp_condition_extensions(
continue

# Want to check each queried condition for extension codes
for cond in input.conditions:
for cond in conditions:
# Only need a single instance of service type lookup to contain
# the resource's code
should_stamp = False
Expand Down Expand Up @@ -161,3 +163,15 @@ async def get_value_sets_for_condition(
clinical_services_list, filter_clinical_services
)
return values


# This block is only executed if the script is run directly, for local development and debugging.
if "__main__" == __name__:
import uvicorn

uvicorn.run(
app="app.main:app",
host="0.0.0.0",
port=8080,
reload=True,
)
29 changes: 1 addition & 28 deletions containers/trigger-code-reference/app/models.py
Original file line number Diff line number Diff line change
@@ -1,8 +1,5 @@
from typing import List

from pydantic import BaseModel
from pydantic import Field
from pydantic import root_validator


class InsertConditionInput(BaseModel):
Expand All @@ -12,31 +9,7 @@ class InsertConditionInput(BaseModel):

bundle: dict = Field(
description="The FHIR bundle to modify. Each resource in the bundle related "
"to one or more of the conditions in the other supplied parameter will have "
"to one or more of the conditions found in the bundle will have "
"an extension added to the resource noting the SNOMED code relating to the "
"associated condition(s)."
)
conditions: List[str] = Field(
description="The list of SNOMED codes to insert as extensions into any "
"associated resources in the supplied FHIR bundle."
)

@root_validator
def require_non_empty_condition_list(cls, values):
"""
Ensures that the supplied conditions list parameter is both a list
and has at least one element by which to extend the supplied FHIR
bundle.
:param cls: The InsertConditionInput class.
:param values: The condition list supplied by the caller.
:raises ValueError: Errors when supplied condition list contains no
elements, since this can't be used to extend a bundle.
:return: The endpoint's values, if the condition list is valid.
"""
if len(values.get("conditions")) == 0:
raise ValueError(
"Supplied list of SNOMED conditions must contain "
"one or more elements; given list was empty."
)
return values
32 changes: 32 additions & 0 deletions containers/trigger-code-reference/app/utils.py
Original file line number Diff line number Diff line change
Expand Up @@ -197,3 +197,35 @@ def read_json_from_assets(filename: str) -> dict:
:return: A dictionary containing the contents of the file.
"""
return json.load(open((Path(__file__).parent.parent / "assets" / filename)))


def find_conditions(bundle: dict) -> set[str]:
"""
Finds conditions in a bundle of resources.
:param bundle: The bundle of resources to search.
:return: A set of SNOMED codes representing the conditions found.
"""
CONDITION_CODE = "64572001"
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
Original file line number Diff line number Diff line change
Expand Up @@ -3,9 +3,6 @@
"summary": "Stamping a FHIR bundle consisting of an eICR merged with its RR",
"description": "This is an example bundle demonstrating condition-stamping on an input eICR that's checking for COVID-related condition codes.",
"value": {
"conditions": [
"840539006"
],
"bundle": {
"resourceType": "Bundle",
"type": "batch",
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -25,7 +25,7 @@ def test_openapi():
@pytest.mark.integration
def test_tcr_stamping(setup, fhir_bundle):
reportable_condition_code = "840539006"
request = {"bundle": fhir_bundle, "conditions": [reportable_condition_code]}
request = {"bundle": fhir_bundle}
stamp_response = httpx.post(STAMP_ENDPOINT, json=request)
assert stamp_response.status_code == 200

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -77,17 +77,6 @@ def test_get_value_sets_for_condition(mock_db):
assert response.json() == expected_result


def test_stamp_conditions_bad_input():
input = {"bundle": {"test_key": "test_val"}, "conditions": []}
response = client.post("/stamp-condition-extensions", json=input)
assert response.status_code == 422
assert (
response.json()["detail"][0]["msg"]
== "Supplied list of SNOMED conditions "
+ "must contain one or more elements; given list was empty."
)


# Note: This function is defined in utils, but we mock it in the namespace
# coming from main because that's where the endpoint is invoking it from
@patch("app.main.get_clinical_services_list")
Expand Down Expand Up @@ -143,25 +132,25 @@ def test_stamp_condition_extensions(patched_get_services_list):
("dxtc", "8971234987123", "code-sys-2"),
("lotc", "72391283|8916394-2|24", "code-sys-1"),
]
input = {"bundle": message, "conditions": ["99999-9"]}
input = {"bundle": message}
response = client.post("/stamp-condition-extensions", json=input)
assert response.status_code == 200
stamped_message = response.json()["extended_bundle"]

# Check observation: diagnostic code value came back successful
found_matching_extension = _check_for_stamped_resource_in_bundle(
stamped_message, "99999-9", "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, "99999-9", "Condition"
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, "99999-9", "Immunization"
stamped_message, "840539006", "Immunization"
)
assert found_matching_extension

0 comments on commit b7417f6

Please sign in to comment.