Giter Club home page Giter Club logo

fhir-py's Introduction

build status codecov pypi Supported Python version

fhir-py

async/sync FHIR client for python3. This package provides an API for CRUD operations over FHIR resources

pip install fhirpy

or to install the latest dev version:

pip install git+https://github.com/beda-software/fhir-py.git

You can test this library by interactive FHIR course in the repository Aidbox/jupyter-course.

Getting started

Async example

import asyncio
from fhirpy import AsyncFHIRClient


async def main():
    # Create an instance
    client = AsyncFHIRClient(
        'http://fhir-server/',
        authorization='Bearer TOKEN',
    )

    # Search for patients
    resources = client.resources('Patient')  # Return lazy search set
    resources = resources.search(name='John').limit(10).sort('name')
    patients = await resources.fetch()  # Returns list of AsyncFHIRResource

    # Create Organization resource
    organization = client.resource(
        'Organization',
        name='beda.software',
        active=False
    )
    await organization.save()

    # Update (PATCH) organization. Resource support accessing its elements
    # both as attribute and as a dictionary keys
    if organization['active'] is False:
        organization.active = True
    await organization.save(fields=['active'])
    # `await organization.patch(active=True)` would do the same PATCH operation

    # Get patient resource by reference and delete
    patient_ref = client.reference('Patient', 'new_patient')
    # Get resource from this reference
    # (throw ResourceNotFound if no resource was found)
    patient_res = await patient_ref.to_resource()
    await patient_res.delete()

    # Iterate over search set
    org_resources = client.resources('Organization')
    # Lazy loading resources page by page with page count = 100
    async for org_resource in org_resources.limit(100):
        print(org_resource.serialize())


if __name__ == '__main__':
    loop = asyncio.get_event_loop()
    loop.run_until_complete(main())

Searchset examples

patients = client.resources('Patient')

patients.search(birthdate__gt='1944', birthdate__lt='1964')
# /Patient?birthdate=gt1944&birthdate=lt1964

patients.search(name__contains='John')
# /Patient?name:contains=John

patients.search(name=['John', 'Rivera'])
# /Patient?name=John&name=Rivera

patients.search(name='John,Eva')
# /Patient?name=John,Eva

patients.search(family__exact='Moore')
# /Patient?family:exact=Moore

patients.search(address_state='TX')
# /Patient?address-state=TX

patients.search(active=True, _id='id')
# /Patient?active=true&_id=id

patients.search(gender__not=['male', 'female'])
# /Patient?gender:not=male&gender:not=female

Chained parameters

patients.search(general_practitioner__Organization__name='Hospital')
# /Patient?general-practitioner:Organization.name=Hospital
patients.search(general_practitioner__name='Hospital')
# /Patient?general-practitioner.name=Hospital

Reference

practitioner = client.resources('Practitioner').search(_id='john-smith').first()
patients.search(general_practitioner=practitioner)
# /Patient?general-practitioner=Practitioner/john-smith

Date

import pytz
import datetime


patients.search(birthdate__lt=datetime.datetime.now(pytz.utc))
# /Patient?birthdate=lt2019-11-19T20:16:08Z

patients.search(birthdate__gt=datetime.datetime(2013, 10, 27, tzinfo=pytz.utc))
# /Patient?birthdate=gt2013-10-27T00:00:00Z

Modifiers

conditions = client.resources('Condition')

conditions.search(code__text='headache')
# /Condition?code:text=headache

conditions.search(code__in='http://acme.org/fhir/ValueSet/cardiac-conditions')
# /Condition?code:in=http://acme.org/fhir/ValueSet/cardiac-conditions

conditions.search(code__not_in='http://acme.org/fhir/ValueSet/cardiac-conditions')
# /Condition?code:not-in=http://acme.org/fhir/ValueSet/cardiac-conditions

conditions.search(code__below='126851005')
# /Condition?code:below=126851005

conditions.search(code__above='126851005')
# /Condition?code:above=126851005

Raw parameters

Sometimes you can find that fhir-py does not implement some search parameters from the FHIR specification. In this case, you can use Raw() wrapper without any transformations

from fhirpy.base.searchset import Raw

patients = client.resources('Patient')
patients.search(Raw(**{'general-practitioner.name': 'Hospital'}))
# /Patient?general-practitioner.name=Hospital

Get resource by id

Use reference to get resource by id

patient = await client.reference('Patient', '1').to_resource()
# /Patient/1

Or use FHIR search API with .first() or .get() as described below.

Get exactly one resource

practitioners = client.resources('Practitioner')

try:
    await practitioners.search(active=True, _id='id').get()
    # /Practitioner?active=true&_id=id
except ResourceNotFound:
    pass
except MultipleResourcesFound:
    pass

Get first result

await practitioners.search(name='Jack').first()
# /Practitioner?name=Jack&_count=1

await patients.sort('active', '-birthdate').first()
# /Patient?_sort=active,-birthdate&_count=1

Get total count

await practitioners.search(active=True).count()

await patients.count()

Fetch one page

await practitioners.fetch()
# /Practitioner

await patients.elements('name', 'telecom').fetch()
# /Patient?_elements=resourceType,name,id,telecom

Fetch all resources on all pages

Keep in mind that this method as well as .fetch() doesn't return any included resources. Use fetch_raw() if you want to get all included resources.

# Returns a list of `Practitioner` resources
await practitioners.search(address_city='Krasnoyarsk').fetch_all()

await patients.fetch_all()

Page count (_count)

# Get 100 resources
await practitioners.limit(100).fetch()

Sort (_sort)

observations = client.resources('Observation')

observations.sort('status', '-date', 'category')
# /Observation?_sort=status,-date,category

Elements (_elements)

# Get only specified set of elements for each resource
patients.elements('identifier', 'active', 'link')
# /Patient?_elements=identifier,active,link

# Get all elements except specified set
practitioners.elements('address', 'telecom', exclude=True)

Include

result = await client.resources('EpisodeOfCare') \
    .include('EpisodeOfCare', 'patient').fetch_raw()
# /EpisodeOfCare?_include=EpisodeOfCare:patient
for entry in result.entry:
    print(entry.resource)

await client.resources('MedicationRequest') \
    .include('MedicationRequest', 'patient', target_resource_type='Patient') \
    .fetch_raw()
# /MedicationRequest?_include=MedicationRequest:patient:Patient

Modifier :iterate (or :recurse in some previous versions of FHIR)

# For FHIR version >= 3.5 we can also use modifier :iterate
await client.resources('MedicationRequest') \
    .include('MedicationDispense', 'prescription') \
    .include('MedicationRequest', 'performer', iterate=True) \
    .fetch_raw()
# /MedicationRequest?_include=MedicationDispense:prescription
#    &_include:iterate=MedicationRequest:performer

# For FHIR version 3.0-3.3 use modifier :recurse
await client.resources('MedicationDispense') \
    .include('MedicationRequest', 'prescriber', recursive=True) \
    .fetch_raw()
# /MedicationDispense?_include:recurse=MedicationRequest:prescriber

Wild card (any search parameter of type=reference be included)

await client.resources('Encounter').include('*') \
    .fetch_raw()
# /Encounter?_include=*

Revinclude

await practitioners.revinclude('Group', 'member').fetch_raw()
# /Practitioner?_revinclude=Group:member

or

await practitioners.include('Group', 'member', reverse=True).fetch_raw()
# /Practitioner?_revinclude=Group:member

Wild card (any search parameter of type=reference be included)

await client.resources('EpisodeOfCare').revinclude('*') \
    .fetch_raw()
# /EpisodeOfCare?_revinclude=*

Conditional operations

Conditional create

FHIR spec: Conditional create

For resource

# resource.create(search_params)
# executes POST /Patient?identifier=fhirpy

patient = client.resource("Patient",
    identifier=[{"system": "http://example.com/env", "value": "fhirpy"}],
    name=[{"text": "Mr. Smith"}],
)
await patient.create(identifier="other")

For SearchSet

# searchset.get_or_create(resource)
# executes POST /Patient?identifier=fhirpy

patient, created = await client.resources("Patient").search(identifier="fhirpy").get_or_create(patient_to_save)

# no match -> created is True
# one match -> created is False, return existing resource
# multiple matches -> 412 'MultipleResourcesFound'

Conditional update

FHIR spec: Conditional update

# resource, created: bool = searchset.patch(resource)
# executes PUT /Patient?identifier=fhirpy

patient_to_update = client.resource("Patient", 
                                    identifier=[{"system": "http://example.com/env", "value": "fhirpy"}],
                                    active=False)
new_patient, created = await client.resources("Patient").search(identifier="fhirpy").update(patient_to_update)

# no match -> created is True
# one match -> created is False, the matched resource is overwritten
# multiple matches -> 412 'MultipleResourcesFound'

Conditional patch

FHIR spec: Conditional patch

# patched_resource = searchset.patch(resource)
# executes PATCH /Patient?identifier=fhirpy

patient_to_patch = client.resource("Patient", 
                                    identifier=[{"system": "http://example.com/env", "value": "fhirpy"}], 
                                    name=[{"text": "Mr. Smith"}])
patched_patient = await client.resources("Patient").search(identifier="fhirpy").patch(patient_to_patch)

# no match -> 404 'ResourceNotFound'
# multiple matches -> 412 'MultipleResourcesFound'

Conditional delete

FHIR spec: Conditional delete

response_data, status_code = await self.client.resources("Patient").search(identifier="abc").delete()

# no match -> status_code = 204 'No Content'
# one match -> status_code = 200 'OK'
# multiple matches -> status_code = 412 'MultipleResourcesFound' (implementation specific)

Resource and helper methods

Validate resource using operation $validate

try:
    await client.resource('Patient', birthDate='date', custom_prop='123', telecom=True) \
        .is_valid(raise_exception=True)
except OperationOutcome as e:
    print('Error: {}'.format(e))

patient = client.resource('Patient', birthDate='1998-01-01')
if (await patient.is_valid()):
    pass

Accessing resource attributes

patient = await client.resources('Patient').first()

# Work with the resource as a dictionary
patient_family = patient['name'][0]['family']

# Or access value by an attribute
patient_given_name = patient.name[0].given[0]

Static type checking with mypy and fhir-py-types

from fhir_py_types.generated.resources import Patient

patient: Patient = await client.resources('Patient').first()

# Works only with dictionary-like resource access
patient_family = patient['name'][0]['family1']
# 'TypedDict "HumanName" has no key "family1" note: Did you mean "family"?'

Check fhir-py-types for more details on generating type definitions from FHIR StructureDefintion

get_by_path(path, default=None)

patient_postal = patient.get_by_path(['resource', 'address', 0, 'postalCode'])

# get_by_path can be also used on any nested attribute
patient_name = patient.name[0]
patient_fullname = '{} {}'.format(
    patient_name.get_by_path(['given', 0]),
    patient_name.get_by_path(['family'])
)

# Get identifier value by specified system or empty string
uid = patient.get_by_path([
        'resource', 'identifier',
        {'system':'http://example.com/identifier/uid'},
        'value'
    ], '')

# Get base value amount or 0
invoice = await client.resources('Invoice').first()
base_value = invoice.get_by_path([
    'totalPriceComponent',
    {'type': 'base'},
    'amount', 'value'], 0)

set_by_path(obj, path, value)

resource = {
    "name": [{"given": ["Firstname"], "family": "Lastname"}],
}

set_by_path(resource, ["name", 0, "given", 0], "FirstnameUpdated")

# resource
# {"name": [{"given": ["FirstnameUpdated"], "family": "Lastname"}]}

serialize()

# Returns resources as dict
patient = await client.reference('Patient', '1').to_resource()
patient.serialize()
# Or 
await client.reference('Patient', '1').to_resource().serialize()
# {'resourceType': 'Patient', 'id': '1', 'meta': {'versionId': '1', 'lastUpdated': '2021-11-13T11:50:24.685719Z'}, ...}

Reference

Main class structure

Both async and sync clients have identical sets of classes and methods.

Sync Async
Client SyncFHIRClient AsyncFHIRClient
SearchSet SyncFHIRSearchSet AsyncFHIRSearchSet
Resource SyncFHIRResource AsyncFHIRResource
Reference SyncFHIRReference AsyncFHIRReference

Acync client (based on aiohttp) โ€“ AsyncFHIRClient

Import library:

from fhirpy import AsyncFHIRClient

To create AsyncFHIRClient instance use:

AsyncFHIRClient(url, authorization='', extra_headers={})

Returns an instance of the connection to the server which provides:

  • .reference(resource_type, id, reference, **kwargs) - returns AsyncFHIRReference to the resource
  • .resource(resource_type, **kwargs) - returns AsyncFHIRResource which described below
  • .resources(resource_type) - returns AsyncFHIRSearchSet
  • .execute(path, method='post', data=None, params=None) - returns a result of FHIR operation

Aiohttp request parameters

Sometimes you need more control over the way http request is made and provide additional aiohttp session's request parameters like ssl, proxy, cookies, timeout etc. It's possible by providing aiohttp_config dict for AsyncFHIRClient:

client = AsyncFHIRClient(
    FHIR_SERVER_URL,
    aiohttp_config={
        "ssl": ssl.create_default_context(),
        "timeout": aiohttp.ClientTimeout(total=100),
    }
)

Be careful and don't override other request values like params, json, data, auth, because it'll interfere with the way fhir-py works and lead to an incorrect behavior.

AsyncFHIRResource

provides:

  • .serialize() - serializes resource
  • .get_by_path(path, default=None) โ€“ gets the value at path of resource
  • async .save(fields=[]) - creates or updates or patches (with fields=[...]) resource instance
  • async .update() - overrides resource instance
  • async .patch(**kwargs) - patches resource instance
  • async .delete() - deletes resource instance
  • async .refresh() - reloads resource from a server
  • async .to_reference(**kwargs) - returns AsyncFHIRReference for this resource
  • async .execute(operation, method='post', data=None, params=None) - returns a result of FHIR operation on the resource

AsyncFHIRReference

provides:

  • async .to_resource() - returns AsyncFHIRResource for this reference
  • async .execute(operation, method='post', data=None, params=None) - returns a result of FHIR operation on the resource

AsyncFHIRSearchSet

provides:

  • .search(param=value)
  • .limit(count)
  • .sort(*args)
  • .elements(*args, exclude=False)
  • .include(resource_type, attr=None, recursive=False, iterate=False)
  • .revinclude(resource_type, attr=None, recursive=False, iterate=False)
  • .has(*args, **kwargs)
  • async .fetch() - makes query to the server and returns a list of Resource filtered by resource type
  • async .fetch_all() - makes query to the server and returns a full list of Resource filtered by resource type
  • async .fetch_raw() - makes query to the server and returns a raw Bundle Resource
  • async .first() - returns Resource or None
  • async .get(id=None) - returns Resource or raises ResourceNotFound when no resource found or MultipleResourcesFound when more than one resource found (parameter 'id' is deprecated)
  • async .count() - makes query to the server and returns the total number of resources that match the SearchSet
  • async .get_or_create(resource) - conditional create
  • async .update(resource) - conditional update
  • async .patch(resource) - conditional patch

Sync client (based on requests) โ€“ SyncFHIRClient

Import library:

from fhirpy import SyncFHIRClient

To create SyncFHIRClient instance use:

SyncFHIRClient(url, authorization='', extra_headers={})

Returns an instance of the connection to the server which provides:

  • .reference(resource_type, id, reference, **kwargs) - returns SyncFHIRReference to the resource
  • .resource(resource_type, **kwargs) - returns SyncFHIRResource which described below
  • .resources(resource_type) - returns SyncFHIRSearchSet

Requests request parameters

Pass requests_config parameter to SyncFHIRClient if you want to provide additional parameters for a request like verify, cert, timeout etc.

client = SyncFHIRClient(
    FHIR_SERVER_URL,
    requests_config={
        "verify": False,
        "allow_redirects": True,
        "timeout": 60,
    }
)

Be careful and don't override other request values like params, json, data, headers, which may interfere with the way fhir-py works and lead to an incorrect behavior.

SyncFHIRResource

The same as AsyncFHIRResource but with sync methods

SyncFHIRReference

provides: The same as AsyncFHIRReference but with sync methods

SyncFHIRSearchSet

The same as AsyncFHIRSearchSet but with sync methods

Run integration tests

(need some test FHIR server to run with, e.g. https://docs.aidbox.app/installation/setup-aidbox.dev)

  1. Clone this repository: https://github.com/beda-software/fhir-py.git

  2. Go to fhir-py folder and install dev dependencies:

cd fhir-py
pip install -r requirements.txt

If you've already installed fhir-py library and want to test the last changes, reinstall it by running python setup.py install (or uninstall pip uninstall fhirpy)

  1. Provide ENV variables FHIR_SERVER_URL and FHIR_SERVER_AUTHORIZATION, or edit tests/config.py

  2. Run pytest

If you've found any bugs or think that some part of fhir-py is not compatible with FHIR spec, feel free to create an issue/pull request.

fhir-py's People

Contributors

adam-nym avatar bsvogler avatar dependabot[bot] avatar dmitryashutov avatar grongierisc avatar ir4y avatar m0rl avatar mkizesov avatar motey avatar null-none avatar pavlushkin avatar projkov avatar ruscoder avatar woodsjs avatar

Stargazers

 avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar

Watchers

 avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar

fhir-py's Issues

Lazy search sets

In #35 we've done lazy fetching using __iter__/__aiter__. I think In 2.0.0 we can remove fetch/fetch_all/fetch_raw due to __iter__/__aiter__ (always returns all results) and some methods such as to_list() and to_bundle()

Add license to repo

I've started adding issues and pull requests, though the project's license file is empty. While the beda software site speaks to a commitment to open source, it would make folks, including myself, more comfortable contributing if the licensing was clearly stated in the repo.

https://choosealicense.com/no-permission/

searchset.py not in 1.0.0?

The documentation mentions you can use Raw Parameters using the following:

from fhirpy.base.searchset import Raw

patients = client.resources('Patient')
patients.search(Raw('general-practitioner.name=Hospital'))
# /Patient?general-practitioner.name=Hospital

However, after downloading the source code for 1.0.0 it appears that searchset.py was not included in the release. Is this a mistake?

Add support for Q() queries in .search()

It will be very useful to combine complex queries using Q() like it made in Django.

Examples:
OR

client.resources('Slot').search(Q(schedule='id1') | Q(schedule='id2'))

transforms to =>

client.resources('Schedule').search(actor='id1,id2')

and generates =>

?schedule=id1,id2

AND

client.resources('Schedule').search(Q(actor='id1') & Q(actor='id2'))

transforms to =>

client.resources('Schedule').search(actor=['id1', 'id2'])

and generates =>

?actor=id1&actor=id2

Add API for calling resource operations

/Questionnaire/<id>/$populate
/Valuest/<id>/$expand
/Valuest/$expand

$ operation should be a string parameter and accept any string. It should work on both levels.

How to Handle List of Searches?

For example, if I have a list of Patient Id's I want to look up, how can I iterate the list and make an asynchronous request for each one? Here is what I tried so far:

import asyncio
from fhirpy import AsyncFHIRClient

async def main():
    # Create an instance
    client = AsyncFHIRClient(
        'https://myfhirendpoint.com',
        fhir_version='4.0.0',
    )
    client.schema = None

    pat_ids = [
        "patient1",
        "patient2",
        "patient3",
        "patient4",
        "patient5",
    ]

    resources = client.resources('Patient')
    tasks = [
        resources.search(_id=pat_id).limit(10)
        for pat_id in pat_ids
    ]

    patients = [await task.fetch() for task in tasks]
    return patients

if __name__ == '__main__':
    loop = asyncio.get_event_loop()
    loop.run_until_complete(main())

The above works, but I don't think it is executing asynchronously. What is the proper way to do this using fhirpy?

Error in V1.0.0 of fhirpy

Hi
I created a repl.it program using fhirpy. You can test it here
https://repl.it/@fhirinterm/TryFhirPy

It just runs this:

import asyncio
from fhirpy import AsyncFHIRClient

async def main():
# Create an instance
client = AsyncFHIRClient(
'http://test.fhir.org/r4',
fhir_version='4.0.0',
authorization='Bearer TOKEN',
)

# Iterate over search set
org_resources = client.resources('Patient')
async for org_resource in org_resources:
    print(org_resource.serialize())

if name == 'main':
loop = asyncio.get_event_loop()
loop.run_until_complete(main())

I got this error message:

Traceback (most recent call last):
File "main.py", line 22, in
loop.run_until_complete(main())
File "/usr/local/lib/python3.7/asyncio/base_events.py", line 579, in run_until_complete
return future.result()
File "main.py", line 16, in main
async for org_resource in org_resources:
File "/home/runner/.local/share/virtualenvs/python3/lib/python3.7/site-packages/fhirpy/base/lib.py", line496, in aiter
items = await iterable_coroutine
File "/home/runner/.local/share/virtualenvs/python3/lib/python3.7/site-packages/fhirpy/base/lib.py", line522, in fetch
resource = self._perform_resource(data, skip_caching)
File "/home/runner/.local/share/virtualenvs/python3/lib/python3.7/site-packages/fhirpy/base/lib.py", line282, in _perform_resource
resource = self.client.resource(resource_type, **data)
File "/home/runner/.local/share/virtualenvs/python3/lib/python3.7/site-packages/fhirpy/base/lib.py", line90, in resource
return self.resource_class(self, resource_type=resource_type, **kwargs)
File "/home/runner/.local/share/virtualenvs/python3/lib/python3.7/site-packages/fhirpy/base/lib.py", line709, in init
super(BaseResource, self).init(client, **converted_kwargs)
File "/home/runner/.local/share/virtualenvs/python3/lib/python3.7/site-packages/fhirpy/base/lib.py", line597, in init
self._raise_error_if_invalid_keys(kwargs.keys())
File "/home/runner/.local/share/virtualenvs/python3/lib/python3.7/site-packages/fhirpy/base/lib.py", line684, in _raise_error_if_invalid_keys
key, ', '.join(root_attrs)
KeyError: 'Invalid key _birthDate. Possible keys are multipleBirthInteger, text, active, deceasedBoolean, identifier, multipleBirthBoolean, language, extension, contained, address, gender, modifierExtension, id, communication, managingOrganization, resourceType, maritalStatus, photo, telecom, birthDate, name, generalPractitioner, meta, contact, implicitRules, deceasedDateTime, link'

Interestingly, if you replace 'Patient' with 'Organization'
It works perfectly
KInd regards
DK

on 200 response JSON is not being properly decoded

When making a get call on multiple end points, both open (http://hapi.fhir.org/baseDstu3) and EMR Vendor based (Epic 2018, Epic August 2018) receive error

the JSON object must be str, not 'bytes'

This is caused by a missing .decode() call on r.content, here's the broken piece of code. Notice the 200 call is the only one without a .decode()

        if 200 <= r.status_code < 300:
            return json.loads(r.content) if r.content else None

        if r.status_code == 404:
            raise FHIRResourceNotFound(r.content.decode())

        raise FHIROperationOutcome(r.content.decode())

This simple example shows the issue

from fhirpy import FHIRClient

client = FHIRClient(url='http://hapi.fhir.org/baseDstu3')
resources = client.resources('Practitioner').get(id=1700173)

I have a fix in place I'm using currently and will submit a pull request.

Add support for _history

We should add a new SearchSet (e.g. HistorySearchSet) and provide methods .history() for Resource/Reference, SearchSet and Client.

It should be something like that:

client.history('User') - returns HistorySearchSet for /User/_history/
client.history('User', 'id') - returns HistorySearchSet for /User/id/_history/

clients.resources('User').history() - returns HistorySearchSet for /User/_history/
clients.resources('User').history('id') - returns HistorySearchSet for /User/id/_history/

And resource/reference methods:

resource = client.resource('User').get('id')
resource.history() - returns HistorySearchSet for /User/id/_history/

And HistorySearchSet should provide:

history = client.history('User', 'id')

history.fetch()/history.fetch_all() - executes and returns list of resources
history.count() - executes and returns history count
history.limit()/history.page() - the same as in the SearchSet
history.at('date')/history.since('date') - returns new HistorySearchSet
history.get('vid') - executes and returns /User/id/_history/vid

And also we can support some useful helpers:

resource.history().get_previous() - returns the previous version of the resource if possible or None

fetch_all() Creating Infinite Loop.

Not sure if this problem is specific to the FHIR Server I am pulling from, but supplying a page number will always return a bundle with resources. The current fetch_all() code uses this:

while True:
    new_resources = await self.page(page).fetch()
        if not new_resources:
            break

Since my server always returns something regardless of the page number, it gets stuck in the loop above. The way I've gotten around this in the past is to refer to the next link in the Bundle .

I suppose a quick and dirty solution would be something like the following:

async def fetch(self):
    bundle_data = await self.client._fetch_resource(
        self.resource_type, self.params
    )
    bundle_resource_type = bundle_data.get('resourceType', None)

    ...
    
    next_link = None
    for link in bundle_data.get('link', []):
        if link['relation'] == 'next':
            next_link = link['url']
    
    return resources, next_link

async def fetch_all(self):
    page = 1
    resources = []

    while True:
        new_resources, next_link = await self.page(page).fetch()
        resources.extend(new_resources)
        
        if not next_link:
            break
            
        page += 1

    return resources

to_resource() error "Unknown search parameter 'id'"

I am trying to resolve references from a resource, with AsyncReference.to_resource()
This seems to fail.

example

import asyncio
from fhirpy import AsyncFHIRClient
from py2neo import Graph, Node, Relationship


async def main():
    # Create an instance
    client = AsyncFHIRClient(
        "http://hapi.fhir.org/baseR4",
        # "http://localhost:8080/fhir",
        authorization="Bearer TOKEN",
    )

    obser_client = client.resources("Observation")  # Return lazy search set
    obser_query = obser_client.search().limit(5)
    observations = await obser_query.fetch()
    for obser in observations:
        if "subject" in obser:
            # This will fail
            patient = await obser.subject.to_resource()


if __name__ == "__main__":
    loop = asyncio.get_event_loop()
    loop.run_until_complete(main())

It looks like a typo on line

self.resource_type).search(id=self.id).get()

because when changing the line

self.resource_type).search(id=self.id).get()

to

self.resource_type).search(_id=self.id).get()

it seems to work.
Im just starting to work with fhir, and do not have a good overview of the subject matter.
Do I use the function in a wrong way?
Is the fhir test server data corrupt?
If i found a bug, i can provide a merge request.

Setup fhir-py using base-fhir-py

  • Create classes FHIRClient, FHIRResource, FHIRReference, FHIRSearchSet based on classes from base-fhir-py
  • Override FHIRReference.id, FHIRReference.resource_type, FHIRReference.reference
  • Add validation using FHIRResource.raise_error_if_invalid_keys FHIRReference.raise_error_if_invalid_keys

Move common functional to base-fhir-py

  • Rename FHIRClient -> Client, FHIRResource -> Resource, FHIRBaseResource -> AbstractResource, FHIRReference -> Reference, FHIRSearchSet -> SearchSet
  • Make them abstract
  • In Client specify searchset_class, resource_class, reference_class and use them in public API (resource, resources, reference)
  • Remove load_schema, validation, fhir_version, schema, root_keys make raise_error_if_invalid_keys public and empty by default
  • Make Reference.resource_type, Reference.id, Reference.reference abstract
  • Make AbstractResource.is_reference non static, abstract and move into Resource
  • In SearchSet.clone use self.class to instantiate a copy

Add better support for related resources via include/revinclude

When we use revinclude or include we have a bundle with multiple types of resources, but fetch/fetch_all returns only main resource.

We can add fetch_raw/fetch_all_raw that should always return a dict with fetched resources.

Example:

client.resources('Schedule').revinclude('Slot', 'schedule').fetch_all_raw()

returns

{'Slot': [SlotResource, SlotResource, ...],
'Schedule': [ScheduleResource, ScheduleResource, ...],}

And

client.resources('Schedule').fetch_all_raw()

returns

{'Schedule': [ScheduleResource, ScheduleResource, ...],}

TBD

Make fetch/fetch_all lazy

Sometimes we need to iterate over thousands of entries and I think it will be very useful if we can have fetch_all() as a generator. Now if we call fetch_all for thousands of entries we firstly wait until all entries are fetched.

Consider implementing lazy fetch for searchset iterating.

Recursively remove all null values

Example:

p = client.resource('Patient', managingOrganization=[...])

p['managingOrganization'] = None
p.save()

should remove managingOrganization key for POST/PUT query.
For PATCH - TBD, but it hasn't implemented yet

Transform FHIRResource/FHIRReference in search lookups

We should transform resource/reference instance in search lookups, for example:

practitioner = clients.resources('Practitioner').first()

# This line should be transformed to
client.resources('Schedule').search(actor=practitioner)

# Something like that
client.resources('Schedule').search(actor=practitioner.reference)

get should accept any search parameters

Now it accepts only id. get should accept any search parameters and assert that exactly one result was returned. Otherwise, an exception should be fired.

No "extra headers" can be added to requests

Several EHR vendors, including Epic, require headers that cannot be passed in, or make no sense to be passed in, with the current state of the object constructor. These are typically used for things like client IDs and additional authorization for access.

I have a solution currently implemented and being used with a large EHR installation. I add an extra_headers attribute to the object, defaulted to None. When not None, the extra_headers attribute is unpacked into the headers variable.

If you can please assign this bug to me, I will submit a pull request for the solution within the next few days.

Implement fetch_all() using pagination

  1. Create alias for execute - fetch()
  2. Create fetch_all() which fetches all instances with pagination
client.resources('Patient').limit(10).fetch_all()

Possible implementation:

def fetch_all(self):
    resources = []
    page = 0
    while True:
        new_resources = self.page(page).fetch()
        if not new_resources:
            break
        resources.extend(new_resources)
        page += 1
    
    return new_resources

Recommend Projects

  • React photo React

    A declarative, efficient, and flexible JavaScript library for building user interfaces.

  • Vue.js photo Vue.js

    ๐Ÿ–– Vue.js is a progressive, incrementally-adoptable JavaScript framework for building UI on the web.

  • Typescript photo Typescript

    TypeScript is a superset of JavaScript that compiles to clean JavaScript output.

  • TensorFlow photo TensorFlow

    An Open Source Machine Learning Framework for Everyone

  • Django photo Django

    The Web framework for perfectionists with deadlines.

  • D3 photo D3

    Bring data to life with SVG, Canvas and HTML. ๐Ÿ“Š๐Ÿ“ˆ๐ŸŽ‰

Recommend Topics

  • javascript

    JavaScript (JS) is a lightweight interpreted programming language with first-class functions.

  • web

    Some thing interesting about web. New door for the world.

  • server

    A server is a program made to process requests and deliver data to clients.

  • Machine learning

    Machine learning is a way of modeling and interpreting data that allows a piece of software to respond intelligently.

  • Game

    Some thing interesting about game, make everyone happy.

Recommend Org

  • Facebook photo Facebook

    We are working to build community through open source technology. NB: members must have two-factor auth.

  • Microsoft photo Microsoft

    Open source projects and samples from Microsoft.

  • Google photo Google

    Google โค๏ธ Open Source for everyone.

  • D3 photo D3

    Data-Driven Documents codes.