FHIR resource handling
Create, search and update Patient, Observation and other FHIR resources from Python services.
With Zato, you can work with any FHIR server from Python services, and this tutorial will show you how - creating, finding and updating resources like Patient or Observation.
Setting up Zato
- Install Zato through Docker, then go to http://localhost:8183 in your browser and log into the Dashboard
- Have the address of a FHIR server at hand - any R4 server works, and the connection created below will point at it
Connect to a FHIR server
Python services reach FHIR servers through outgoing connections - named once in the Dashboard and available in code as self.fhir.
With the Dashboard open, go to Connections ▹ Outgoing ▹ HL7 ▹ FHIR, click Create a new connection and fill in the form:
- Name: FHIR.Sample
- Address: your FHIR server's address, e.g.
https://fhir.example.com - Security: No security definition
- Click OK
The connection is ready the moment you click OK - no server restarts, and the name is all your code will ever reference.
A server that requires credentials takes a Basic Auth or OAuth definition in the Security field - Zato attaches the credentials to every request, and OAuth tokens are acquired and refreshed automatically.
Create and read resources
With the Dashboard open, go to the IDE, create a file called fhir_api.py, paste the code below and click Deploy:
# -*- coding: utf-8 -*-
# Zato
from zato.server.service import Service
# #############################################################################
# #############################################################################
class CreatePatient(Service):
""" Creates a new patient and reads it back from the FHIR server.
"""
name = 'fhir-api.create-patient'
def handle(self) -> 'None':
# The connection's name is all it takes to obtain a client ..
client = self.fhir['FHIR.Sample']
# .. a resource behaves like an object and a dict alike ..
patient_name = [{'family': 'Chalmers', 'given': ['Peter']}]
patient = client.resource('Patient', name=patient_name)
patient.birthDate = '1974-12-25'
patient['gender'] = 'male'
# .. and one save stores it on the server.
patient.save()
self.logger.info(f'Created Patient/{patient.id}')
# Read it back by type and the ID the server assigned
found = client.get('Patient', patient.id)
family_name = found['name'][0]['family']
self.logger.info(f'Family name is {family_name}')
Invoke the service from the IDE and the log shows both halves - the ID the server assigned during the save and the data read back with it:
You can click below to see the resource as the server now stores it - the ID and the version metadata are the server's own additions:
The patient on the serverJSON
telecom entry to the patient before saving - patient.telecom = [{'system': 'phone', 'value': '555-0101', 'use': 'home'}] - redeploy and confirm it comes back in the read.Search for resources
An ID lets you read one resource, but most questions start without one - which observations are final, whose are they, since when? Searches built with .resources answer them - each call refines the query and nothing reaches the server until you fetch the results:
# -*- coding: utf-8 -*-
# Zato
from zato.server.service import Service
# ##############################################################################
# ##############################################################################
class FindObservations(Service):
""" Searches a FHIR server for matching observations.
"""
name = 'fhir-api.find-observations'
def handle(self) -> 'None':
client = self.fhir['FHIR.Sample']
# Everything about observations starts here ..
observations = client.resources('Observation')
# .. final observations for one patient, from 2024 on, newest first ..
result = observations.search(
subject='Patient/544ccba4010d4458a3cc31d75b828412',
status='final',
date__ge='2024-01-01',
).sort('-date').limit(10)
# .. and only now is the server invoked.
for observation in result.fetch():
code = observation['code']
self.logger.info(f'Observation: {code}')
With a few observations for this patient already on the server, the log reads:
INFO - Observation: {'coding': [{'system': 'http://loinc.org', 'code': '8867-4', 'display': 'Heart rate'}]}
INFO - Observation: {'coding': [{'system': 'http://loinc.org', 'code': '8480-6', 'display': 'Systolic blood pressure'}]}
INFO - Observation: {'coding': [{'system': 'http://loinc.org', 'code': '8462-4', 'display': 'Diastolic blood pressure'}]}
Operators like __ge above mirror what the FHIR specification allows in search URLs - date__ge='2024-01-01' becomes date=ge2024-01-01 on the wire. There are several ways to fetch the results, depending on how much data you expect:
# One page of results, as configured with .limit
found = result.fetch()
# All the results - pagination is followed for you
found_all = result.fetch_all()
# The first match or None
observation = result.first()
# Only the number of matches, without any resources
match_count = observations.search(status='final').count()
client.resources('Patient').search(name='Chalmers') - and log how many the server has with .count().Update and delete resources
A saved resource can be modified and saved again, which sends the full resource back to the server. To send only selected fields, call .patch with just those fields:
# Read the resource first.
patient = client.get('Patient', '544ccba4010d4458a3cc31d75b828412')
# Change a field and store the whole resource ..
patient.birthDate = '1974-12-26'
patient.save()
# .. or send only one field, leaving the rest untouched.
patient.patch(birthDate='1974-12-27')
If another system may have modified the resource in the meantime, .refresh re-reads it from the server, and .delete removes it:
Follow references between resources
FHIR resources point at each other through references - an Observation refers to the Patient it was recorded for through its subject field. A reference found in one resource can be turned into the full resource it points to:
# Find an observation ..
observation = client.resources('Observation').search(status='final').first()
# .. its subject is a reference, e.g. Patient/544ccba4010d4458a3cc31d75b828412 ..
subject = observation['subject']
# .. and this fetches the actual Patient from the server.
patient = subject.to_resource()
family_name = patient['name'][0]['family']
self.logger.info(f'Patient is {family_name}')
The log confirms the reference led back to the patient created earlier:
Going the other way, pass one resource inside another and the reference is built for you:
appointment = client.resource('Appointment')
appointment.status = 'booked'
appointment.participant = [{'actor': patient, 'status': 'accepted'}]
appointment.start = '2027-01-11T11:11:11.111+00:00'
appointment.end = '2027-01-11T12:11:11.111+00:00'
appointment.save()
Read nested data with paths
FHIR resources are deeply nested - a patient's family name lives under name[0].family and a phone number under whichever telecom entry has the right system. Path access condenses such lookups into single calls:
patient = client.get('Patient', '544ccba4010d4458a3cc31d75b828412')
# The first given name of the first name entry
given_name = patient.get_by_path('name.0.given.0')
# The home phone number, wherever it is in the telecom list -
# a dict selects the first list element whose fields match it.
phone = patient.get_by_path(['telecom', {'system': 'phone', 'use': 'home'}, 'value'])
# If anything along the path does not exist, the default is returned.
maiden_name = patient.get_by_path('name.1.family', '(none)')
The same works for plain dicts through get_path and set_path, e.g. when the data comes from a request payload rather than a FHIR server:
# Zato
from zato.fhir.path_access import get_path, set_path
data = {'name': [{'family': 'Smith', 'given': ['John']}]}
family = get_path(data, 'name[0].family')
_ = set_path(data, 'name[0].family', 'Jones')
get_by_path in one call each and log them.Convert HL7 v2 to FHIR
Resources do not always start out as FHIR - clinical feeds arrive as HL7 v2, and one call maps a whole parsed message, msg below, to a transaction bundle:
# The same conversion as pretty-printed JSON, e.g. for logging
bundle_json = msg.to_fhir_json(indent=2)
# Stored atomically - all the resources or none
client.execute('', method='post', data=bundle.to_dict())
When single fields need your own decisions, typed classes map by hand - HL7 fields in, a Patient out:
# Zato
from zato.fhir import Patient
patient = Patient()
patient.identifier = [{'value': msg.pid.patient_identifier_list[0].id_number}]
patient.name = [{'family': msg.pid.patient_name.family_name}]
Both paths are covered in their own chapters, linked below.
That's all there is to it
You create a connection once and every service can create, find and update resources on the server it points at. The chapters below cover the details whenever you need more.
What you built
- An outgoing FHIR connection configured once in the Dashboard and available in every service as
self.fhir - Create and read -
client.resource,.save, andclient.getfor direct reads by ID - Searches - parameters, operators, sorting and pagination handled for you
- Updates and deletes -
.save,.patch,.refreshand.delete - References - from an Observation to its Patient with
.to_resource() - Path access - nested values in one call with
get_by_path,get_pathandset_path - HL7 v2 conversion - whole messages into bundles with
msg.to_fhir(), single fields with typed classes
What next
- Connect your AI to ask more questions about Zato and to build your interfaces
- Point FHIR.Sample at your own server and invoke the services again - the code does not change
- Route incoming HL7 v2 traffic into everything this page built with the HL7 MLLP tutorial
What else you can work with
| Feature | What it does |
|---|---|
| FHIR connections | Everything self.fhir accepts, pooling and connection options |
| Working with resources | Resource classes, fields and validation in depth |
| Searches and bundles | Search parameters, result bundles and pagination |
| Path access | Nested reads and writes in single calls |
| Extensions | Read and write the fields the base specification does not define |
| Security | Basic Auth, OAuth and TLS for FHIR servers |
| Automatic HL7 v2 conversion | Everything msg.to_fhir produces and how to customize it |
| Manual transformation | Field-by-field mapping with typed classes and validation |
| Mapping tables | Which HL7 v2 segment becomes which FHIR resource |
Schedule a meaningful demo
Book a demo with an expert who will help you build meaningful systems that match your ambitions
"We evaluated 12 integration platforms and Zato was the only one to score 100%."
Philip Zuñiga, Assistant Professor, University of the Philippines