Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
3 changes: 3 additions & 0 deletions mypy.ini
Original file line number Diff line number Diff line change
Expand Up @@ -44,3 +44,6 @@ warn_unreachable = False

[mypy-*.factories]
disallow_untyped_calls = False

[mypy-*.fhir.tests.*]
disable_error_code = index,union-attr,arg-type
28 changes: 22 additions & 6 deletions opal/services/fhir/fhir.py
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,7 @@

import datetime as dt
from datetime import datetime
from typing import Any
from typing import Any, cast

import structlog
from authlib.integrations.requests_client import OAuth2Session
Expand Down Expand Up @@ -147,7 +147,9 @@ def patient_conditions(self, uuid: str) -> list[Condition]:
_clean_coding(coding)

conditions_bundle = Bundle.model_validate(data)
return [condition.resource for condition in conditions_bundle.entry or []]
return [
cast('Condition', condition.resource) for condition in conditions_bundle.entry or [] if condition.resource
]

def patient_medication_requests(self, uuid: str) -> list[MedicationRequest]:
"""
Expand All @@ -168,7 +170,11 @@ def patient_medication_requests(self, uuid: str) -> list[MedicationRequest]:

medications_bundle = Bundle.model_validate(data)

return [medication.resource for medication in medications_bundle.entry or []]
return [
cast('MedicationRequest', medication.resource)
for medication in medications_bundle.entry or []
if medication.resource
]

def patient_allergies(self, uuid: str) -> list[AllergyIntolerance]:
"""
Expand All @@ -195,7 +201,9 @@ def patient_allergies(self, uuid: str) -> list[AllergyIntolerance]:
_clean_coding(coding)

allergies_bundle = Bundle.model_validate(data)
return [allergy.resource for allergy in allergies_bundle.entry or []]
return [
cast('AllergyIntolerance', allergy.resource) for allergy in allergies_bundle.entry or [] if allergy.resource
]

def patient_immunizations(self, uuid: str) -> list[Immunization]:
"""
Expand Down Expand Up @@ -225,7 +233,11 @@ def patient_immunizations(self, uuid: str) -> list[Immunization]:

immunizations_bundle = Bundle.model_validate(data)

return [immunization.resource for immunization in immunizations_bundle.entry or []]
return [
cast('Immunization', immunization.resource)
for immunization in immunizations_bundle.entry or []
if immunization.resource
]

def patient_observations(self, uuid: str) -> list[Observation]:
"""
Expand All @@ -245,4 +257,8 @@ def patient_observations(self, uuid: str) -> list[Observation]:
data = response.json()

observations_bundle = Bundle.model_validate(data)
return [observation.resource for observation in observations_bundle.entry or []]
return [
cast('Observation', observation.resource)
for observation in observations_bundle.entry or []
if observation.resource
]
169 changes: 86 additions & 83 deletions opal/services/fhir/ips.py
Original file line number Diff line number Diff line change
Expand Up @@ -58,12 +58,16 @@ def build_patient_summary( # noqa: PLR0913, PLR0917
vital_signs = [
observation
for observation in observations_with_category
if observation.category[0].coding[0].code == 'vital-signs'
if observation.category
and observation.category[0].coding
and observation.category[0].coding[0].code == 'vital-signs'
]
labs = [
observation
for observation in observations_with_category
if observation.category[0].coding[0].code == 'laboratory'
if observation.category
and observation.category[0].coding
and observation.category[0].coding[0].code == 'laboratory'
]

# Translates static strings in the right language for the bundle
Expand All @@ -86,102 +90,101 @@ def build_patient_summary( # noqa: PLR0913, PLR0917
date=dt.datetime.now(tz=dt.UTC).replace(microsecond=0),
title=_('International Patient Summary as of {date}').format(date=last_updated),
subject=Reference(reference=f'urn:uuid:{patient.id}'),
section=[
CompositionSection(
title=_('Active Problems'),
code=CodeableConcept(
coding=[Coding(system='http://loinc.org', code='11450-4', display='Problem list Reported')]
),
entry=[
Reference(reference=f'urn:uuid:{condition.id}')
for condition in conditions
if condition.clinicalStatus.coding[0].code == 'active'
],
)
composition.section = [
CompositionSection(
title=_('Active Problems'),
code=CodeableConcept(
coding=[Coding(system='http://loinc.org', code='11450-4', display='Problem list Reported')]
),
CompositionSection(
title=_('Past Medical History'),
code=CodeableConcept(
coding=[
Coding(system='http://loinc.org', code='11348-0', display='History of Past illness note')
]
),
entry=[
Reference(reference=f'urn:uuid:{condition.id}')
for condition in conditions
if condition.clinicalStatus.coding[0].code != 'active'
],
entry=[
Reference(reference=f'urn:uuid:{condition.id}')
for condition in conditions
if condition.clinicalStatus
and condition.clinicalStatus.coding
and condition.clinicalStatus.coding[0].code == 'active'
],
),
CompositionSection(
title=_('Past Medical History'),
code=CodeableConcept(
coding=[Coding(system='http://loinc.org', code='11348-0', display='History of Past illness note')]
),
CompositionSection(
title=_('Medication'),
code=CodeableConcept(
coding=[
Coding(
system='http://loinc.org', code='10160-0', display='History of Medication use Narrative'
)
],
),
entry=[
Reference(reference=f'urn:uuid:{medication_request.id}')
for medication_request in medication_requests
entry=[
Reference(reference=f'urn:uuid:{condition.id}')
for condition in conditions
if condition.clinicalStatus
and condition.clinicalStatus.coding
and condition.clinicalStatus.coding[0].code != 'active'
],
),
CompositionSection(
title=_('Medication'),
code=CodeableConcept(
coding=[
Coding(system='http://loinc.org', code='10160-0', display='History of Medication use Narrative')
],
),
CompositionSection(
title=_('Allergies and Intolerances'),
code=CodeableConcept(
coding=[
Coding(
system='http://loinc.org',
code='48765-2',
display='Allergies and adverse reactions Document',
)
]
),
entry=[Reference(reference=f'urn:uuid:{allergy.id}') for allergy in allergies],
entry=[
Reference(reference=f'urn:uuid:{medication_request.id}')
for medication_request in medication_requests
],
),
CompositionSection(
title=_('Allergies and Intolerances'),
code=CodeableConcept(
coding=[
Coding(
system='http://loinc.org',
code='48765-2',
display='Allergies and adverse reactions Document',
)
]
),
CompositionSection(
title=_('Vital Signs'),
code=CodeableConcept(
coding=[Coding(system='http://loinc.org', code='8716-3', display='Vital signs note')]
),
entry=[Reference(reference=f'urn:uuid:{vital_sign.id}') for vital_sign in vital_signs],
entry=[Reference(reference=f'urn:uuid:{allergy.id}') for allergy in allergies],
),
CompositionSection(
title=_('Vital Signs'),
code=CodeableConcept(
coding=[Coding(system='http://loinc.org', code='8716-3', display='Vital signs note')]
),
CompositionSection(
title=_('Laboratory Results'),
code=CodeableConcept(
coding=[
Coding(
system='http://loinc.org',
code='30954-2',
display='Relevant diagnostic tests/laboratory data note',
)
]
),
entry=[Reference(reference=f'urn:uuid:{lab.id}') for lab in labs],
entry=[Reference(reference=f'urn:uuid:{vital_sign.id}') for vital_sign in vital_signs],
),
CompositionSection(
title=_('Laboratory Results'),
code=CodeableConcept(
coding=[
Coding(
system='http://loinc.org',
code='30954-2',
display='Relevant diagnostic tests/laboratory data note',
)
]
),
CompositionSection(
title=_('Immunizations'),
code=CodeableConcept(
coding=[
Coding(system='http://loinc.org', code='11369-6', display='History of Immunization note')
]
),
entry=[Reference(reference=f'urn:uuid:{immunization.id}') for immunization in immunizations],
entry=[Reference(reference=f'urn:uuid:{lab.id}') for lab in labs],
),
CompositionSection(
title=_('Immunizations'),
code=CodeableConcept(
coding=[Coding(system='http://loinc.org', code='11369-6', display='History of Immunization note')]
),
],
)
entry=[Reference(reference=f'urn:uuid:{immunization.id}') for immunization in immunizations],
),
]

ips = Bundle(
identifier={'system': 'urn:oid:2.16.724.4.8.10.200.10', 'value': f'{uuid.uuid4()}'},
type='document',
language=ips_language,
timestamp=dt.datetime.now(tz=dt.UTC).replace(microsecond=0),
entry=[
BundleEntry(resource=composition, fullUrl=f'urn:uuid:{composition.id}'),
BundleEntry(resource=patient, fullUrl=f'urn:uuid:{patient.id}'),
BundleEntry(resource=generator, fullUrl=f'urn:uuid:{generator.id}'),
],
)

ips.entry = [
BundleEntry(resource=composition, fullUrl=f'urn:uuid:{composition.id}'),
BundleEntry(resource=patient, fullUrl=f'urn:uuid:{patient.id}'),
BundleEntry(resource=generator, fullUrl=f'urn:uuid:{generator.id}'),
]

ips.entry.extend(
BundleEntry(resource=condition, fullUrl=f'urn:uuid:{condition.id}') for condition in conditions
)
Expand All @@ -200,7 +203,7 @@ def build_patient_summary( # noqa: PLR0913, PLR0917

# add narrative for empty entries
for section in composition.section:
if not section.entry:
if not section.entry and section.title:
no_information = _("There is no information available about the subject's {category}.").format(
category=section.title.lower()
)
Expand Down
14 changes: 7 additions & 7 deletions opal/services/fhir/tests/test_ips.py
Original file line number Diff line number Diff line change
Expand Up @@ -64,7 +64,7 @@ def _prepare_build_patient_summary() -> tuple[
immunizations,
)

return (summary, patient, conditions, medication_requests, allergies, observations, immunizations)
return (summary, patient, conditions, medication_requests, allergies, observations, immunizations) # type: ignore[return-value]


def test_build_patient_summary() -> None:
Expand Down Expand Up @@ -140,7 +140,7 @@ def test_build_patient_summary_composition() -> None:
# The section's resources are referenced
if resources:
assert len(section.entry) == len(resources)
for entry, resource in zip(section.entry, resources, strict=True):
for entry, resource in zip(section.entry, resources, strict=True): # type:ignore[call-overload]
assert entry.reference == f'urn:uuid:{resource.id}', (
f'Section {section.title} has an incorrect resource reference'
)
Expand Down Expand Up @@ -206,11 +206,11 @@ def test_build_patient_summary_resources_included() -> None:
resource_ids = {entry.resource.id for entry in summary.entry[3:]}

expected_ids: set[str] = set()
expected_ids.update(condition.id for condition in conditions)
expected_ids.update(medication_request.id for medication_request in medication_requests)
expected_ids.update(allergy.id for allergy in allergies)
expected_ids.update(observation.id for observation in observations if observation.category)
expected_ids.update(immunization.id for immunization in immunizations)
expected_ids.update(condition.id for condition in conditions) # type: ignore[misc]
expected_ids.update(medication_request.id for medication_request in medication_requests) # type: ignore[misc]
expected_ids.update(allergy.id for allergy in allergies) # type: ignore[misc]
expected_ids.update(observation.id for observation in observations if observation.category) # type: ignore[misc]
expected_ids.update(immunization.id for immunization in immunizations) # type: ignore[misc]

assert resource_ids == expected_ids

Expand Down
12 changes: 9 additions & 3 deletions opal/services/fhir/utils.py
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,6 @@

import logging
import secrets
import uuid

import structlog
from authlib.oauth2 import OAuth2Error
Expand Down Expand Up @@ -48,7 +47,7 @@ def jwe_sh_link_encrypt(data: str) -> tuple[str, bytes]:

def retrieve_patient_summary(
oauth_url: str, fhir_url: str, client_id: str, private_key: str, identifier: str
) -> tuple[str, uuid.UUID]:
) -> tuple[str, str]:
"""
Retrieve patient data and build a patient summary in IPS format for a patient identified by their identifier.

Expand Down Expand Up @@ -83,6 +82,10 @@ def retrieve_patient_summary(

patient = fhir.find_patient(identifier)
patient_uuid = patient.id

if not patient_uuid:
raise FHIRDataRetrievalError(f'Patient with identifier {identifier} has no UUID')

conditions = fhir.patient_conditions(patient_uuid)
medication_requests = fhir.patient_medication_requests(patient_uuid)
allergies = fhir.patient_allergies(patient_uuid)
Expand Down Expand Up @@ -113,4 +116,7 @@ def retrieve_patient_summary(

LOGGER.debug('Successfully built IPS bundle for patient with UUID %s', patient_uuid)

return ips_bundle.model_dump_json(indent=2), ips_bundle.identifier.value
# we know that there is an identifier because it is set in the build_patient_summary function
ips_uuid: str = ips_bundle.identifier.value # type: ignore[assignment,union-attr]

return ips_bundle.model_dump_json(indent=2), ips_uuid
5 changes: 4 additions & 1 deletion pyproject.toml
Original file line number Diff line number Diff line change
Expand Up @@ -31,7 +31,7 @@ dependencies = [
"django-tables2==2.8.0",
"djangorestframework==3.16.1",
"drf-spectacular==0.29.0",
"fhir-resources==8.0.0",
"fhir-resources==8.1.0",
"fpdf2==2.8.5",
"hl7apy==1.3.5",
# required for image export in plotly
Expand Down Expand Up @@ -88,3 +88,6 @@ docs = [
"mkdocs-material==9.7.1",
"mkdocstrings[python-legacy]==0.30.1",
]

[tool.uv.sources]
fhir-resources = { git = "https://github.com/nazrulworld/fhir.resources.git", rev = "9a2eec1774913dbd89816e9a1cfcda22cdd4388e" }
10 changes: 3 additions & 7 deletions uv.lock

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

Loading