ehr
EHR data resource protocol for step tasks.
Module
Functions
ehr_resource_identity
def ehr_resource_identity(ehr_config: EHRConfig) ‑> str:Return a stable identity string for the EHR ehr_config points at.
The EHR steps (ehr_patient_lister, ehr_query) carry no config field
naming the system they read, so their task_hash would otherwise be blind
to it — repointing a pod at a different EHR would serve the previous EHR's
cached rows. This string is folded into those steps' hashes; see
bitfount.flows.dag.hashing.
Four parts, because no one of them is sufficient:
providerselects the querier class inEHRDataResource.__init__, and so the shape of what lands in the cache.fhir_urlis the endpoint the FHIR R4 family actually queries — for those providersenterprise_urlis never set.enterprise_urlis the NextGen Enterprise endpoint.list_resource_idsselects the population: set, patient enumeration is scoped to those FHIRListresources' combined membership; unset, it is an unfiltered/Patientsearch.ehr_patient_listerhas no config at all, so without this a narrowed or widened cohort would reuse the previous cohort'sehr_patient_idsrows. Sorted, since the entries are a set of Lists whose declaration order does not change the population.
The two URLs are resolved exactly as EHRDataResource.__init__ resolves
them, defaults included, so a config that names an endpoint explicitly and
one that falls back to the default identify the same EHR.
Deliberately excluded: smart_on_fhir_url and
smart_on_fhir_resource_server_url. Both are auth endpoints — they decide
how the querier obtains a token, not which records come back — so folding
them in would re-partition the cache on a credentials move that changes no
data.
Arguments
ehr_config: The pod's EHR configuration.
Returns
A canonical provider|fhir_url|enterprise_url|list_resource_ids string.
Classes
EHRDataResource
class EHRDataResource( hub: BitfountHub, ehr_config: EHRConfig | None = None, ehr_secrets: RefreshableJWT | None = None,):Resource to allow connection to the EHR.
This is init with EHR configuration from the pod_config. Tasks can obtain a patient-specific querier either via Patient ID, or by providing first name + last name + DOB.
Construct the resource for a single EHR provider.
Picks one of two querier backends based on ehr_config:
NextGenEHRConfig selects the
NextGen path (which authenticates against the Hub session), any
other config whose provider ends in "r4" selects the
FHIR R4 path. Anything else raises ValueError.
Arguments
hub: Bitfount Hub instance, used by the NextGen path to obtain a SMART-on-FHIR session.ehr_config: Provider configuration loaded from the pod config.ehr_secrets: Externally-supplied JWT secrets for the FHIR R4 path. Not needed forSMARTBackendEHRConfig, which carries its own auth on the config object. May beNoneonly when the config is SMART Backend, or whenallow_no_ehr_secretsis set for a genuinely unauthenticated server.
Raises
ValueError: Ifehr_configisNone, ifbase_urlis missing on a FHIR R4 config, if the provider value is unrecognised, or if a FHIR R4 config has no usable credentials andallow_no_ehr_secretsis not set.
Methods
get_patient_info_by_id
def get_patient_info_by_id( self, patient_id: str, *, fetch_options: EHRFetchOptions = EHRFetchOptions(fetch_appointments=False, observation_codes=None, observation_categories=None),) ‑> EHRPatientResource:Returns EHR querier for this patient.
The querier class is selected by self.querier_type. Within each
class, the construction method is chosen by the patient details variant:
EHRIDPatientDetails builds via from_patient_id (no demographic
search), and NameDOBPatientDetails builds via from_patient_query.
Arguments
patient_id: The EHR system's patient ID.fetch_options: Forwarded to_get_patient_resource; seeEHRFetchOptions.
get_patient_info_by_name_dob
def get_patient_info_by_name_dob( self, given_name: str | None, family_name: str | None, patient_dob: str | date, *, fetch_options: EHRFetchOptions = EHRFetchOptions(fetch_appointments=False, observation_codes=None, observation_categories=None),) ‑> EHRPatientResource:Look up a single patient by demographic search.
Routes the call to the active querier backend
(NextGenPatientQuerier or FHIRR4PatientQuerier) and
normalises the response into an EHRPatientResource.
Arguments
given_name: Patient's first/given name.family_name: Patient's last/family name.patient_dob: Date of birth, either as adateor an ISO-format string the backend can parse.fetch_options: Forwarded to_get_patient_resource; seeEHRFetchOptions.
Returns
The materialised EHRPatientResource. Code lists may be
empty (or None on a GetPatientInfoError from the backend).
Raises
NoMatchingPatientError: Propagated from the backend when no patient matches the supplied demographics.ValueError: Ifself.querier_typeis set to an unsupported value.
get_patient_lister
def get_patient_lister(self) ‑> BaseEHRPatientLister:Build a paginated patient-ID lister for the active EHR backend.
Routes the call to the active lister backend
(NextGenPatientLister or FHIRR4PatientLister) via the
unified from_ehr_backend entrypoint — each implementation
cherry-picks the kwargs it needs and ignores the rest, mirroring
the dispatch shape of get_patient_info_by_name_dob.
EHRFetchOptions
class EHRFetchOptions( fetch_appointments: bool = False, observation_codes: Sequence[tuple[str | None, str]] | None = None, observation_categories: Sequence[str] | None = None,):Which extra per-patient EHR data to fetch, beyond the unfiltered code lists.
Condition/Procedure codes are always fetched unfiltered and matched against criteria locally afterwards — cheap, and every backend supports an unfiltered fetch. These three don't work that way, each for its own reason, which is why they must be requested up front rather than filtered afterwards:
fetch_appointmentsgates an extra round-trip (previous appointments/encounters, next appointment) most flows don't need.observation_codes/observation_categories: Observations must be searched by an explicit code or category — at least one major FHIR backend (Epic) rejects an Observation search with neither.
Variables
- static
fetch_appointments : bool
- static
observation_categories : collections.abc.Sequence[str] | None
- static
observation_codes : collections.abc.Sequence[tuple[str | None, str]] | None
EHRPatientResource
class EHRPatientResource(**data: Any):In-memory representation of a single patient pulled from the EHR.
Built by EHRDataResource._get_patient_resource from the raw
querier response, and consumed by _build_ehr_record when assembling
cache rows. patient_id is the EHR system's ID — it is not the same
as the imaging-side Patient ID column. condition_codes_json /
procedure_codes_json / observation_codes_json / medication_codes_json
/ allergy_codes_json default to None so that "lookup failed" (None)
stays distinguishable from "no codes for this patient" ([]).
observation_codes_json is only populated when observation_codes
and/or observation_categories was passed in — unlike
conditions/procedures/medications/allergies, Observations are fetched by
explicit code or category, not unfiltered; when both are passed, the two
fetches' results are merged and deduplicated. medication_codes_json and
allergy_codes_json are both fetched unfiltered, the same as
conditions/procedures. Every one of the five preserves None distinctly
from [] at this layer; see _get_patient_resource.
fetch_outcomes carries which FetchGroups that lookup reached and, for
those it did not, whether the endpoint is unsupported by the server or was
merely unreachable this time. FetchGroup.DEMOGRAPHICS never appears
there — it has no group-scope reason, so "were demographics fetched" is
answered by the lookup having succeeded at all. It is in-memory only: the
cache row builders copy the fields above across one at a time and do not
copy it, so a group outcome never lands in ehr_data. device_codes_json
is fetched unfiltered, the same as allergies, but has no date field of its
own (see DeviceCriterion).
Create a new model by parsing and validating input data from keyword arguments.
Raises [ValidationError][pydantic_core.ValidationError] if the input data cannot be
validated to form a valid model.
self is explicitly positional-only to allow self as a field name.
Variables
- static
allergy_codes_json : list[typing.Any] | None
- static
cell_numbers : list[str]
- static
condition_codes_json : list[dict[str, str | None | datetime.datetime | dict[str, str] | list[dict[str, typing.Any]]]] | None
- static
country : str | None
- static
date_of_birth : str | datetime.date | None
- static
device_codes_json : list[typing.Any] | None
- static
emails : list[str]
- static
family_name : str | None
- static
fetch_outcomes : tuple[GroupOutcome, ...]
- static
gender : str | None
- static
given_name : str | None
- static
home_numbers : list[str]
- static
mailing_address : str | None
- static
medical_record_number : list[str] | None
- static
medication_codes_json : list[typing.Any] | None
- static
model_config
- static
next_appointment : datetime.date | None
- static
observation_codes_json : list[dict[str, str | None | datetime.datetime | float | bool | int | dict[str, typing.Any] | list[dict[str, typing.Any]]]] | None
- static
patient_id : str
- static
postal_code : str | None
- static
previous_appointments : list[dict[str, str]] | None
- static
previous_encounters : list[dict[str, str]] | None
- static
procedure_codes_json : list[dict[str, str | None | datetime.datetime | dict[str, str] | list[dict[str, typing.Any]]]] | None
- static
unsupported_sources : frozenset[EHRFetchedSource]
QuerierType
class QuerierType(*args, **kwds):EHR Querier for use in algorithm.