Welcome to fhirbug’s documentation!¶
Fhirbug intends to be a full-featured FHIR server for python >= 3.6. It has been designed to be easy to set up and configure and be flexible when it comes to the rest of tools it is combined with, like web frameworks and database interfaces. In most simple cases, very little code has to be written apart from field mappings.
Fhirbug is still under development! The API may still change at any time, it probably contains heaps of bugs and has never been tested in production. If you are interested in making it better, you are very welcome to contribute!
What fhirbug does:
- It provides the ability to create “real time” transformations between your ORM models to valid FHIR resources through an extensive mapping API.
- Has been designed to work with existing data-sets and database schemas but can of course be used with its own database.
- It’s compatible with the SQLAlchemy, DjangoORM and PyMODM ORMs, so if you can describe your database in one of them, you are good to go. It should also be pretty easy to extend to support any other ORM, feel free to submit a pull request!
- Handles many of the FHIR REST operations and searches like creating and updating resources, performing advanced queries such as reverse includes, paginated bundles, contained or referenced resources, and more.
- Provides the ability to audit each request, at the granularity that you desire, all the way down to limiting which of the attributes for each model should be accessible for each user.
What fhirbug does not do:
- Provide a ready to use solution for a Fhir server. Fhirbug is a framework, we want things to be as easy as possible but you will still have to write code.
- Contain a web server. Fhirbug takes over once there is a request string and request body and returns a json object. You have to handle the actual requests and responses.
- Handle authentication and authorization. It supports it, but you must write the implementation.
- A ton of smaller stuff, which you can find in the Roadmap_.
Quickstart¶
Contents:
This section contains a brief example of creating a simple application using fhirbug. It’s goal is to give the reader a general idea of how fhirbug works, not to provide them with in-depth knowledge about it.
For a more detailed guide check out the Overview and the API docs.
Preparation¶
In this example we will use an sqlite3 database with SQLAlchemy and flask. The first is in the standard library, you can install SQLAlchemy and flask using pip:
$ pip install sqlalchemy flask
Let’s say we have a very simple database schema, for now only containing a table for Patients and one for hospital admissions. The SQLAlchemy models look like this:
from sqlalchemy.ext.declarative import declarative_base
from sqlalchemy import Column, DateTime, Integer, String, ForeignKey
from sqlalchemy.orm import relationship
Base = declarative_base()
class PatientModel(Base):
__tablename__ = 'patients'
patient_id = Column(Integer, primary_key=True)
first_name = Column(String)
last_name = Column(String)
dob = Column(DateTime) # date of birth
gender = Column(Integer) # 0: female, 1: male, 2: other, 3: unknown
class AdmissionModel(Base):
__tablename__ = 'admissions'
id = Column(Integer, primary_key=True)
status = Column(String) # 'a': active, 'c': closed
patient_id = Column(Integer, ForeignKey('patients.patient_id'))
date_start = Column(DateTime) # date and time of admission
date_end = Column(DateTime) # date and time of release
patient = relationship("patientmodel", back_populates="admissions")
To create the database and tables, open an interactive python shell and type the following:
>>> from sqlalchemy import create_engine
>>> from models import Base
>>> engine = create_engine('sqlite:///:memory:')
>>> Base.metadata.create_all(engine)
Creating your first Mappings¶
We will start by creating mappings between our Patient and Admission models and the Patient and Encounter FHIR resources. In our simple example the mapping we want to create looks something like this:
DB column | FHIR attribute | notes |
---|---|---|
patient_id | id | read-only |
first_name, last_name | name | first and last name must be combined into a HumanName resource |
dob | birthDate | must be converted to type FHIRDate |
gender | gender | values must be translated between the two systems (eg: 0 -> ‘female’) |
Mapping in fhirbug is pretty straightforward. All we need to do is:
- Subclass the model class, inheriting from FhirBaseModel
- Add a member class called FhirMap
- Inside it, add class attributes using the names of the fhir attributes of the resource you are setting up.
- Use Attributes to describe how the conversion between db columns and FHIR attributes should happen
Since we are using SQLAlchemy, we will use the fhirbug.db.backends.SQLAlchemy
module, and more specifically inherit our Mappings from
fhirbug.db.backends.SQLAlchemy.models.FhirBaseModel
So, we start describing our mapping for the Patient resource from the id field which is the simplest:
Warning
Fhirbug needs to know which ORM the mappings we create are for. Therefore, before importing FhirBaseModel, we must have configured the fhirbug settings. If you write the following code in an interactive session instead of a file, you will get an error unless you configure fhirbug first. To do so, just paste the code described below.
from models import Patient as PatientModel
from fhirbug.db.backends.SQLAlchemy.models import FhirBaseModel
from fhirbug.models.attributes import Attribute
class Patient(PatientModel, FhirBaseModel):
class FhirMap:
id = Attribute('patient_id')
Note
The fact that we named the mapper class Patient is important, since when fhirbug looks for a mapper, it looks by default for a class with the same name as the fhir resource.
By passing the column name as a string to the Attribute
we tell fhirbug
that the id attribute of the Patient FHIR resource should be retrieved from the
patient_id
column.
For the birthDate
attribute we get the information from a single database column,
but it must be converted to and from a FHIR DateTime datatype. So, we will use the
DateAttribute
helper and let it handle
conversions automatically.
We will also add the name attribute, using the NameAttribute
helper. We tell it that we get and set the family name from the column last_name
and
the given name from first_name
from models import Patient as PatientModel
from fhirbug.db.backends.SQLAlchemy.models import FhirBaseModel
from fhirbug.models.attributes import Attribute, DateAttribute, NameAttribute
class Patient(PatientModel, FhirBaseModel):
class FhirMap:
id = Attribute('patient_id')
birthDate = DateAttribute('dob')
name = NameAttribute(family_getter='last_name',
family_setter='last_name',
given_getter='first_name',
given_setter='first_name')
Letting the magic happen¶
Let’s test what we have so far. First, we must provide fhirbug with some basic configuration:
>>> from fhirbug.config import settings
>>> settings.configure({
... 'DB_BACKEND': 'SQLAlchemy',
... 'SQLALCHEMY_CONFIG': {
... 'URI': 'sqlite:///:memory:'
... }
... })
Now, we import or mapper class and create an item just as we would if it were a simple SQLAlchemy model:
>>> from datetime import datetime
>>> from mappings import Patient
>>> patient = Patient(dob=datetime(1980, 11, 11),
... first_name='Alice',
... last_name='Alison')
This patient
object we have created here is a classic SQLAlchemy model.
We can save it, delete it, change values for its columns, etc. But it has
also been enhanced by fhirbug.
Here’s some stuff that we can do with it:
>>> to_fhir = patient.to_fhir()
>>> to_fhir.as_json()
{
'birthDate': '1980-11-11T00:00:00',
'name': [{'family': 'Alison', 'given': ['Alice']}],
'resourceType': 'Patient'
}
The same way that all model attributes are accessible from the patient
instance,
all FHIR attributes are accessible from patient.Fhir
:
>>> patient.Fhir.name
<fhirbug.Fhir.Resources.humanname.HumanName at 0x7fc62e1cbcf8>
>>> patient.Fhir.name.as_json()
{'family': 'Alison', 'given': ['Alice']}
>>> patient.Fhir.name.family
'Alison'
>>> patient.Fhir.name.given
['Alice']
If you set an attribute on the FHIR resource:
>>> patient.Fhir.name.family = 'Walker'
The change is applied to the actual database model!
>>> patient.last_name
'Walker'
>>> patient.Fhir.birthDate = datetime(1970, 11, 11)
>>> patient.dob
datetime.datetime(1970, 11, 11, 0, 0)
Handling requests¶
We will finish this quick introduction to fhirbug with a look on how requests are handled. First, let’s create a couple more entries:
>>> from datetime import datetime
>>> from fhirbug.config import settings
>>> settings.configure({
... 'DB_BACKEND': 'SQLAlchemy',
... 'SQLALCHEMY_CONFIG': {
... 'URI': 'sqlite:///:memory:'
... }
... })
>>> from fhirbug.db.backends.SQLAlchemy.base import session
>>> from mappings import Patient
>>> session.add_all([
... Patient(first_name='Some', last_name='Guy', dob=datetime(1990, 10, 10)),
... Patient(first_name='Someone', last_name='Else', dob=datetime(1993, 12, 18)),
... Patient(first_name='Not', last_name='Me', dob=datetime(1985, 6, 6)),
... ])
>>> session.commit()
Great! Now we can simulate some requests. The mapper class we defined earlier is enough for us to get some nice FHIR functionality like searches.
Let’s start by asking for all Patient entries:
>>> from fhirbug.server.requestparser import parse_url
>>> query = parse_url('Patient')
>>> Patient.get(query, strict=False)
{
"entry": [
{
"resource": {
"birthDate": "1990-10-10T00:00:00",
"name": [{"family": "Guy", "given": ["Some"]}],
"resourceType": "Patient",
}
},
{
"resource": {
"birthDate": "1993-12-18T00:00:00",
"name": [{"family": "Else", "given": ["Someone"]}],
"resourceType": "Patient",
}
},
{
"resource": {
"birthDate": "1985-06-06T00:00:00",
"name": [{"family": "Me", "given": ["Not"]}],
"resourceType": "Patient",
}
},
],
"resourceType": "Bundle",
"total": 3,
"type": "searchset",
}
We get a proper Bundle Resource containing all of our Patient records!
Advanced Queries¶
This quick guide is almost over, but before that let us see some more things Fhirbug can do. We start by asking only one result per page.
>>> query = parse_url('Patient?_count=1')
>>> Patient.get(query, strict=False)
{
"entry": [
{
"resource": {
"birthDate": "1990-10-10T00:00:00",
"name": [{"family": "Guy", "given": ["Some"]}],
"resourceType": "Patient",
}
}
],
"link": [
{"relation": "next", "url": "Patient/?_count=1&search-offset=2"},
{"relation": "previous", "url": "Patient/?_count=1&search-offset=1"},
],
"resourceType": "Bundle",
"total": 4,
"type": "searchset",
}
Notice how when defining our mappings we declared birthDate
as a
DateAttribute
and name as a NameAttribute
? This allows us to
use several automations that Fhirbug provides like advanced searches:
>>> query = parse_url('Patient?birthDate=gt1990&given:contains=one')
>>> Patient.get(query, strict=False)
{
"entry": [
{
"resource": {
"birthDate": "1993-12-18T00:00:00",
"name": [{"family": "Else", "given": ["Someone"]}],
"resourceType": "Patient",
}
}
],
"resourceType": "Bundle",
"total": 1,
"type": "searchset",
}
Here, we ask for all Patients
that were born after 1990-01-01 and whose given
name contains one
.
Further Reading¶
You can dive into the actual documentation starting at the Overview or read the docs for the API.
Overview¶
Creating Mappings¶
Fhirbug offers a simple declarative way to map your database tables to Fhir Resources. You need to have created models for your tables using one of the supported ORMs.
Let’s see an example using SQLAlchemy. Suppose we have this model of our database table where patient personal infrormation is stored.
(Note that we have named the model Patient. This allows Fhirbug to match it to the corresponding resource automatically. If we wanted to give it a different name, we would then have to define __Resource__ = ‘Patient’ after the __tablename__)
from sqlalchemy import Column, Integer, String
class Patient(Base):
__tablename__ = "PatientEntries"
id = Column(Integer, primary_key=True)
name_first = Column(String)
name_last = Column(String)
gender = Column(Integer) # 0: unknown, 1:female, 2:male
ssn = Column(Integer)
To map this table to the Patient resource, we will make it inherit it fhirbug.db.backends.SQLAlchemy.FhirBaseModel
instead of Base.
Then we add a class named FhirMap as a member and add all fhir fields we want to support using Attributes
:
Note
You do not need to put your FhirMap in the same class as your models. You could just as well extend it in a second class while using FhirBaseModel as a mixin.
from sqlalchemy import Column, Integer, String
from fhirbug.db.backends.SQLAlchemy import FhirBaseModel
from fhirbug.models import Attribute, NameAttribute
from fhirbug.db.backends.SQLAlchemy.searches import NumericSearch
class Patient(FhirBaseModel):
__tablename__ = "PatientEntries"
pat_id = Column(Integer, primary_key=True)
name_first = Column(String)
name_last = Column(String)
gender = Column(Integer) # 0: unknown, 1:female, 2:male, 3:other
ssn = Column(Integer)
@property
def get_gender(self):
genders = ['unknown', 'female', 'male', 'other']
return genders[self.gender]
@set_gender.setter
def set_gender(self, value):
genders = {'unknown': 0, 'female': 1, 'male': 2, 'other': 3}
self.gender = genders[value]
class FhirMap:
id = Attribute('pat_id', searcher=NumericSearch('pid'))
name = NameAttribute(given_getter='name_first', family_getter='name_last')
def get_name(instance):
gender = Attribute('get_gender', 'set_gender')
FHIR Resources¶
Contents:
Fhirbug uses fhir-parser to automatically parse the Fhir specification and generate classes for resources based on resource definitions. It’s an excellent tool that downloads the Resource Definition files from the official website of FHIR and generates classes automatically. For more details, check out the project’s repository.
Fhirbug comes with pre-generated classes for all FHIR Resources, which live
inside fhirbug.Fhir.resources
. You can generate your own resource classes
based on a subset or extension of the default resource definitions but this is
not currently covered by this documentation.
Uses of FHIR Resources in Fhirbug¶
As return values of mapper.to_fhir()
¶
FHIR Resource classes are used when a mapper instance is converted to a FHIR
Resource using .to_fhir()
.
Supposing we have defined a mapper for the Location resource, we could see the following:
>>> Location
mappings.Location
>>> location = Location.query.first()
<mappings.Location>
>>> location.to_fhir()
<fhirbug.Fhir.Resources.patient.Patient>
As values for mapper Attributes¶
FHIR Resources are also used as values for mapper attributes that are either references to other Resources, Backbone Elements or complex datatypes.
For example, let’s return back to the Location example. As we can see in the FHIR specification, the Location.address attribute is of type Address. This would mean something like this:
>>> location.Fhir.address
<fhirbug.Fhir.Resources.address.Address>
>>> location.Fhir.address.as_json()
{
'use': 'work',
'type': 'physical',
[...]
}
>>> location.Fhir.address.use
'work'
Creating resources¶
You will be wanting to use the Resource classes to create return values for your mapper attributes.
The default way for creating resource instances is by passing a json object to the constructor:
>>> from fhirbug.Fhir.resources import Observation
>>> o = Observation({
... 'id': '2',
... 'status': 'final',
... 'code': {'coding': [{'code': '5', 'system': 'test'}]},
... })
As you can see, this may get a but verbose so there are several shortcuts to help with that.
Resource instances can be created:
by passing a dict with the proper json structure as we already saw
by passing the same values as keyword arguments:
>>> o = Observation( ... id='2', status='final', code={'coding': [{'code': '5', 'system': 'test'}]} ... )when an attribute’s type is a Backbone Element or a complex type, we can pass a resource:
>>> from fhirbug.Fhir.resources import CodeableConcept >>> test_code = CodeableConcept(coding=[{'code': '5', 'system': 'test'}]) >>> o = Observation(id='2', status='final', code=test_code)When an attribute has a cardinality larger than one, that is its values are part of an array, but we only want to pass one value, we can skip the array:
>>> test_code = CodeableConcept(coding={'code': '5', 'system': 'test'}) >>> o = Observation(id='2', status='final', code=test_code)
Fhirbug tries to make it as easy to create resources as possible by providing several shortcuts with the base contructor.
Ignore missing required Attributes¶
If you try to initialize a resource without providing a value for a required attribute you will get an error:
>>> o = Observation(id='2', status='final')
FHIRValidationError: {root}:
'Non-optional property "code" on <fhirbug.Fhir.Resources.observation.Observation object>
is missing'
You can suppress errors into warnings by passing the strict=False
argument:
>>> o = Observation(id='2', status='final', strict=False)
Fhirbug will display a warning but it will not complain again if you try to save or serve the instance. It’s up to you make sure that your data is well defined.
The base Resource class¶
Tis is the abstract class used as a base to provide common functionality to all produced Resource classes. It has been modified in order to provide a convenient API for Creating resources.
-
class
fhirbug.Fhir.base.fhirabstractbase.
FHIRAbstractBase
¶ -
FHIRAbstractBase.
__init__
(jsondict=None, strict=True, **kwargs)¶ Initializer. If strict is true, raises on errors, otherwise uses logger.warning().
Raises: FHIRValidationError on validation errors, unless strict is False
Parameters: - jsondict (dict) – A JSON dictionary or array or an (array of) instance(s) of an other resource to use for initialization
- strict (bool) – If True (the default), invalid variables will raise a TypeError
- kwargs – Instead of a JSON dict, parameters can also be passed as keyword arguments
Parameters that are not defined in FHIR for this resource are ignored.
-
FHIRAbstractBase.
as_json
()¶ Serializes to JSON by inspecting elementProperties() and creating a JSON dictionary of all registered properties. Checks:
- whether required properties are not None (and lists not empty)
- whether not-None properties are of the correct type
Raises: FHIRValidationError if properties have the wrong type or if required properties are empty Returns: A validated dict object that can be JSON serialized
-
FHIRAbstractBase.
elementProperties
()¶ Returns a list of tuples, one tuple for each property that should be serialized, as: (“name”, “json_name”, type, is_list, “of_many”, not_optional)
-
FHIRAbstractBase.
mandatoryFields
()¶ Returns a list of properties that are marked as mandatory / not_optional.
-
FHIRAbstractBase.
owningResource
()¶ Walks the owner hierarchy and returns the next parent that is a DomainResource instance.
-
FHIRAbstractBase.
owningBundle
()¶ Walks the owner hierarchy and returns the next parent that is a Bundle instance.
-
Auditing¶
With Fhirbug you can audit requests on three levels:
- Request level: Allow or disallow the specific operation on the specific resource, and
- Resource level: Allow or disallow access to each individual resource and/or limit access to each of its attributes.
- Attribute level: Allow or disallow access to each individual attribute for each resource.
Warning
The Auditing API is still undergoing heavy changes and is even more unstable than the rest of the project. Use it at your own risk!
Auditing at the request level¶
All you need to do do in order to implement request-level auditing in Fhribug
is to provide the built-in fhirbug.server.requesthandlers
with an extra
method called audit_request
.
This method should accept a single positional parameter, a FhirRequestQuery
and should return an
AuditEvent
. If the outcome attribute
of the returned AuditEvent
is “0” (the code for “Success”), the request
is processed normally.
from fhirbug.server.requesthandlers import GetRequestHandler
from fhirbug.Fhir.resources import AuditEvent
class CustomGetRequestHandler(GetRequestHandler):
def audit_request(self, query):
return AuditEvent(outcome="0", strict=False)
The simplest possible auditing handler, one that approves all requests.
In any other case, the request fails with status code 403
,
and returns an OperationOutcome resource containing the outcomeDesc
of the AuditEvent
. This way you can return information about the reasons for failure.
from fhirbug.server.requesthandlers import GetRequestHandler
from fhirbug.Fhir.resources import AuditEvent
class CustomGetRequestHandler(GetRequestHandler):
def audit_request(self, query):
if "_history" in query.modifiers:
if is_authorized(query.context.user):
return AuditEvent(outcome="0", strict=False)
else:
return AuditEvent(
outcome="8",
outcomeDesc="Unauthorized accounts can not access resource history.",
strict=False
)
Note
Notice how we passed strict=False
to the AuditEvent constructor?
That’s because without it, it would not allow us to create an AuditEvent resource
without filling in all its required fields.
However, since we do not store it in this example and instead just use it to communicate with the rest of the application, there is no need to let it validate our resource.
Since Fhirbug does not care about your web server implementation, or your
authentication mechanism, you need to collect and provide the information neccessary for authenticationg the request to the audit_request
method.
Fhirbug’s suggestion is passing this information through the query.context
object, by providing query_context
when calling the request handler’s handle
method.
Auditing at the resource level¶
Controlling access to the entire resource¶
In order to implement auditing at the resource level, give your mapper models one or more of the
methods audit_read
, audit_create
, audit_update
, audit_delete
.
The signature for these methods is the same as the one for request handlers we saw above.
They accept a single parameter holding a FhirRequestQuery
and
should return an AuditEvent
, whose
outcome
should be "0"
for success and anything else for failure.
class Patient(FhirBaseModel):
# Database field definitions go here
def audit_read(self, query):
return AuditEvent(outcome="0", strict=False)
class FhirMap:
# Fhirbug Attributes go here
You can use Mixins to let resources share common auditing methods:
class OnlyForAdmins: def audit_read(self, query): # Assuming you have passed the appropriate query cintext to the request handler isSuperUser = query.context.User.is_superuser return ( AuditEvent(outcome="0", strict=False) if isSuperUser else AuditEvent( outcome="4", outcomeDesc="Only admins can access this resource", strict=False, ) ) class AuditRequest(OnlyForAdmins, FhirBaseModel): # Mapping goes here class OperationOutcome(OnlyForAdmins, FhirBaseModel): # Mapping goes here ...
Controlling access to specific attributes¶
If you want more refined control over which attributes can be changed and displayed, during the
execution of one of the above audit_*
methods, you can call self.protect_attributes(*attrs*)
and /or
self.hide_attributes(*attrs*)
inside them.
In both cases, *attrs*
should be an iterable that contains a list of attribute names that should be protected or hidden.
protect_attributes()¶
The list of attributes passed to protect_attributes
will be marked as protected for the duration of this request
and will not be allowed to change
hide_attributes()¶
The list of attributes passed to hide_attributes
will be marked as hidden for the current request.
This means that in case of a POST or PUT request they may be changed but they will not
be included in the response.
For example if we wanted to hide patient contact information from unauthorized users, we could do the following:
class Patient(FhirBaseModel):
# Database field definitions go here
def audit_read(self, query):
if not is_authorized(query.context.user):
self.hide_attributes(['contact'])
return AuditEvent(outcome="0", strict=False)
class FhirMap:
# Fhirbug Attributes go here
Similarly, if we wanted to only prevent unauthorized users from changing the Identifiers
of Patients we would use protect_attributes
:
class Patient(FhirBaseModel):
# Database field definitions go here
def audit_update(self, query):
if not is_authorized(query.context.user):
self.protect_attributes = ['identifier']
return AuditEvent(outcome="0", strict=False)
class FhirMap:
# Fhirbug Attributes go here
Auditing at the attribute level¶
Warning
This feature is more experimental than the rest. If you intend to use it be aware of the complications that may rise because you are inside a desciptor getter (For example trying to get the specific attribute’s value would result in an infinte loop)
When declaring attributes, you can provide a function to the audit_set
and audit_get
keyword arguments. These functions accept three positional arguments:
The first is the instance of the Attribute descriptor, the second, query
being the FhirRequestQuery
for this request and the third being the attribute’s name
It should return True
if access to the attribute
is allowed, or False
otherwise.
It’s also possible to deny the entire request by throwing an
AuthorizationError
-
audit_get
(descriptor, query, attribute_name) → boolean¶ Parameters: - query (FhirRequestQuery) – The
FhirRequestQuery
object for this request - attribute_name (str) – The name this attribute has been assigned to
Returns: True if access to this attribute is allowed, False otherwise
Return type: boolean
- query (FhirRequestQuery) – The
-
audit_set
(descriptor, query, attribute_name) → boolean¶ Parameters: - query (FhirRequestQuery) – The
FhirRequestQuery
object for this request - attribute_name (str) – The name this attribute has been assigned to
Returns: True if changing this attribute is allowed, False otherwise
Return type: boolean
- query (FhirRequestQuery) – The
Logging¶
Fhirbug’s RequestHandlers
all have a
method called log_request
that is called whenever a request is done being proccessed
with several information about the request.
By default, this method returns an AuditEvent
FHIR resource instance populated
with available information about the request.
Enhancing or persisting the default handler¶
Enhancing the generated AuditEvents with extra information about the request
and Persisiting them is pretty simple. Just use custom RequestHandlers
and override the log_request
method:
from fhirbug.Fhir.resources import AuditEventEntity
from fhirbug.config import import_models
class EnhancedLoggingMixin:
def log_request(self, *args, **kwargs):
audit_event = super(EnhancedLoggingMixin, self).log_request(*args, **kwargs)
context = kwargs["query"].context
user = context.user
# We populate the entity field with info about the user
audit_event.entity = [
AuditEventEntity({
"type": {"display": "Person"},
"name": user.username,
"description": user.userid,
})
]
return audit_event
class PersistentLoggingMixin:
def log_request(self, *args, **kwargs):
audit_event = super(PersistentLoggingMixin, self).log_request(*args, **kwargs)
models = import_models()
AuditEvent = getattr(models, 'AuditEvent')
audit_event_model = AuditEvent.create_from_resource(audit_event)
return audit_event
# Create the handler
class CustomGetRequestHandler(
PersistentLoggingMixin, EnhancedLoggingMixin, GetRequestHandler
):
pass
Note
In order to have access to the user instance we assume you have passed a query context to the request handler’s handle method containing the necessary info
Note
Note that the order in which we pass the mixins to the custom handler class
is important. Python applies mixins from right to left, meaning
PersistentLoggingMixin
’s super()
method will call
EnhancedLoggingMixin
’s log_request
and EnhancedLoggingMixin
’s
super()
method will call GetRequestHandler
’s
So, we expect the AuditEvent that is persisted by the
PersistentLoggingMixin
to contain information about the user because
it is comes before EnhancedLoggingMixin
in the class definition
Creating a custom log handler¶
If you don’t want to use fhirbug’s default log handling and want to implement
something your self, the process is pretty much the same. You implement your own
log_request
method and process the information that is passed to it by
fhirbug any way you want. Essentially the only difference with the examples above
is that you do not call super()
inside your custom log function.
The signature of the log_request
function is the following:
-
AbstractRequestHandler.
log_request
(self, url, query, status, method, resource=None, OperationOutcome=None, request_body=None, time=datetime.now())[source] Create an AuditEvent resource that contains details about the request.
Parameters: - url (string) – The initial url that was requested
- query (FhirRequestQuery) – The FhirRequestQuery that was generated
- status (int) – The status code that was returned
- method (string) – The request method
- resource (FhirResource) – A Fhir resource, possibly a bundle, of the resources that were accessed or modified during the request
- OperationOutcome (OperationOutcome) – An OperationOutcome related to the requset
- request_body – The body of the request
- time (datetime) – The time the request occured
Here’s an example where we use python’s built-in logging module:
from datetme import datetime
from logging import getLogger
logger = getLogger(__name__)
class CustomGetRequestHandler(GetRequestHandler):
def log_request(self, url, status, method, *args, **kwargs):
logger.info("%s: %s %s %s" % (datetime.now(), method, url, status))
API¶
Attributes¶
-
class
fhirbug.models.attributes.
Attribute
(getter=None, setter=None, searcher=None, search_regex=None, audit_get=None, audit_set=None)[source]¶ The base class for declaring db to fhir mappings. Accepts three positional arguments, a getter, a setter and a searcher.
The getter parameter can be a string, a tuple, a callable or type const.
- Using a string:
>>> from types import SimpleNamespace as SN >>> class Bla: ... _model = SN(column_name=12) ... p = Attribute('column_name') ... >>> b = Bla() >>> b.p 12
- Strings can also be properties:
>>> class Model: ... column_name = property(lambda x: 13) >>> class Bla: ... _model = Model() ... p = Attribute('column_name') ... >>> b = Bla() >>> b.p 13
- Callables will be called:
>>> class Bla: ... _model = SN(column_name=12) ... def get_col(self): ... return 'test' ... p = Attribute(get_col) ... >>> b = Bla() >>> b.p 'test'
- As a shortcut, a tuple (col_name, callable) can be passed. The result will be callable(_model.col_name)
>>> import datetime >>> class Bla: ... _model = SN(date='2012') ... p = Attribute(('date', int)) ... >>> b = Bla() >>> b.p 2012
The setter parameter can be a string, a tuple, a callable or type const.
- Using a string:
>>> class Bla: ... _model = SN(date='2012') ... p = Attribute(setter='date') ... >>> b = Bla() >>> b.p = '2013' >>> b._model.date '2013'
- Again, the string can point to a property with a setter:
>>> class Model: ... b = 12 ... def set_b(self, value): ... self.b = value ... column_name = property(lambda self: self.b, set_b) >>> class Bla: ... _model = Model() ... p = Attribute(getter='column_name', setter='column_name') ... >>> b = Bla() >>> b.p = 13 >>> b.p == b._model.b == 13 True
- Callables will be called:
>>> class Bla: ... _model = SN(column_name=12) ... def set_col(self, value): ... self._model.column_name = value ... p = Attribute(setter=set_col) ... >>> b = Bla() >>> b.p = 'test' >>> b._model.column_name 'test'
- Two-tuples contain a column name and a callable or const. Set the column to the result of the callable or const
>>> def add(column, value): ... return column + value
>>> class Bla: ... _model = SN(column_name=12) ... p = Attribute(setter=('column_name', add)) ... >>> b = Bla() >>> b.p = 3 >>> b._model.column_name 15
-
class
fhirbug.models.attributes.
BooleanAttribute
(*args, save_true_as=1, save_false_as=0, default=None, truthy_values=['true', 'True', 1, '1'], falsy_values=['false', 'False', '0', 0], **kwargs)[source]¶ Used for attributes representing boolean types.
truthy_values
andfalsy_values
are used to determine which possible values from the database we should consider as True and False. Values that are not in any of the lists are mapped todefault
and if that is None, a MappingValidationError is thrown.Parameters: - save_true_as – How do we save True in the database
- save_false_as – How do we save Fasle in the database
- deafult – If we read a value that is not in
truthy_values
orfalsy_values
, it will default to ths value. - truthy_values (list) – Which values, when read from the database should be mapped to True
- falsy_values (list) – Which values, when read from the database should be mapped to False
-
class
fhirbug.models.attributes.
EmbeddedAttribute
(*args, type=None, **kwargs)[source]¶ An attribute representing a BackboneElement that is described by a model and is stored using an ORM relationship, usually a ForeignKeyField or an embedded mongo document.
-
class
fhirbug.models.attributes.
NameAttribute
(family_getter=None, given_getter=None, family_setter=None, given_setter=None, join_given_names=False, pass_given_names=False, getter=None, setter=None, searcher=None, given_join_separator=' ', audit_get=None, audit_set=None)[source]¶ NameAttribute is for used on fields that represnt a HumanName resource. The parameters can be any of the valid getter and setter types for simple
Attribute
Parameters: - family_getter – A getter type parameter for the family name.
- given_getter – A getter type parameter for the given name
- family_setter – A setter type parameter for the family name
- given_setter – A getter type parameter for the given name
-
class
fhirbug.models.attributes.
ReferenceAttribute
(cls, id, name, setter=None, force_display=False, searcher=None)[source]¶ A Reference to some other Resource that may be contained.
-
fhirbug.models.attributes.
audited
(func)[source]¶ A decorator that adds auditing functionality to the
__get__
and__set__
methods of descriptor Attributes. Attribute auditors, depending on the result of the audit, can returnTrue
, meaning access to the attribute has been granted orFalse
, meaning access has been denied but execution should continue normally. If execution should stop and an error returned to the requester, it should raise an exception.
Mixins¶
-
class
fhirbug.models.mixins.
FhirAbstractBaseMixin
[source]¶ Adds additional fhir related functionality to all models. Most importantly, it provides the
.to_fhir()
method that handles the transformation from an SQLAlchemy model to a Fhir resource. User-defined models subclassing this class must implement aFhirMap
nested class.-
classmethod
from_resource
(resource, query=None)[source]¶ Creates and saves a new row from a Fhir.Resource object
-
get_params_dict
(resource, elements=None)[source]¶ Return a dictionary of all valid values this instance can provide for a resource of the type
resource
.Parameters: resource – The class of the resource we wish to create Returns: A dictionary to be used as an argument to initialize a resource instance
-
get_rev_includes
(query)[source]¶ Read the _revincludes that were asked for in the request, query the database to retrieve them and add them to the initial resources
contained
field.Parameters: query ( fhirbug.server.requestparser.FhirRequestQuery
) – AFhirRequestQuery
object holding the current request.Returns: None
-
hide_attributes
(attribute_names=[])[source]¶ Accepts a list of attribute names and marks them as hidden, meaning they will not be included in json representations of this item. Subsequent calls replace the previous attribute list.
Parameters: attribute_names (list) – A list of Fhir attribute names to set as hidden
-
protect_attributes
(attribute_names=[])[source]¶ Accepts a list of attribute names and protects them for the duration of the current operation. Protected attributes can not be changed when creating or editing a resource. Subsequent calls replace the previous attribute list.
Parameters: attribute_names (list) – A list of Fhir attribute names to set as protected
-
classmethod
-
class
fhirbug.models.mixins.
FhirBaseModelMixin
[source]¶ -
Fhir
¶ Wrapper property that initializes an instance of FhirMap.
-
classmethod
get_searcher
(query_string)[source]¶ Return the first search function that matches the provided query string
Parameters: query_string (string) – A query string that is matched against registered field names or regular expressions by existing searchers Returns: function
-
-
fhirbug.models.mixins.
get_pagination_info
(query)[source]¶ Reads item count and offset from the provided
FhirRequestQuery
instance, or the application settings. It makes count obay MAX_BUNDLE_SIZE and calculates which page we are on.Parameters: query (FhirRequestQuery) – The FhirRequestQuery object for this request. Returns: (page, count, prev_offset, next_offset) The number of the page we are on, how many items we should show, the offset of the next page and the offset of the previous page. Return type: (int, int, int, int)
fhirbug.server¶
Request Parsing¶
-
class
fhirbug.server.requestparser.
FhirRequestQuery
(resource, resourceId=None, operation=None, operationId=None, modifiers={}, search_params={}, body=None, request=None)[source]¶ Represents parsed parameters from requests.
-
modifiers
= None¶ Dictionary. Keys are modifier names and values are the provided values. Holds search parameters that start with an underscore. For example
Patient/123?_format=json
would have a modifiers value of{'_format': 'json'}
-
operationId
= None¶ Extra parameters passed after the operation. For example if
Patient/123/_history/2
was requested,operation
would be_history
andoperationId
would be2
-
resource
= None¶ A string containing the name of the requested Resource. eg:
'Procedure'
-
resourceId
= None¶ The id of the requested resource if a specific resource was requested else
None
-
search_params
= None¶ Dictionary. Keys are parameter names and values are the provided values. Holds search parameters that are not modifiers For example
Patient/123?_format=json
would have a modifiers value of{'_format': 'json'}
-
-
fhirbug.server.requestparser.
generate_query_string
(query)[source]¶ Convert a
FhirRequestQuery
back to a query string.
-
fhirbug.server.requestparser.
parse_url
(url)[source]¶ Parse an http request string and produce an option dict.
>>> p = parse_url('Patient/123/$validate?_format=json') >>> p.resource 'Patient' >>> p.resourceId '123' >>> p.operation '$validate' >>> p.modifiers {'_format': ['json']} >>> p.search_params {}
Parameters: url – a string containing the path of the request. It should not contain the server path. For example: Patients/123?name:contains=Jo Returns: A FhirRequestQuery
object
-
fhirbug.server.requestparser.
split_join
(lst)[source]¶ Accepts a list of comma separated strings, splits them and joins them in a new list
>>> split_join(['a,b,c', 'd', 'e,f']) ['a', 'b', 'c', 'd', 'e', 'f']
-
fhirbug.server.requestparser.
validate_params
(params)[source]¶ Validate a parameter dictionary. If the parameters are invalid, raise a QueryValidationError with the details.
Parameters: params – Parameter dictionary produced by parse_url Returns: Raises: fhirbug.exceptions.QueryValidationError
Request Handling¶
-
class
fhirbug.server.requesthandlers.
AbstractRequestHandler
[source]¶ Base class for request handlers
-
log_request
(url, query, status, method, resource=None, OperationOutcome=None, request_body=None, time=datetime.datetime(2020, 1, 4, 11, 45, 39, 553753))[source]¶ Create an AuditEvent resource that contains details about the request.
Parameters: - url (string) – The initial url that was requested
- query (FhirRequestQuery) – The FhirRequestQuery that was generated
- status (int) – The status code that was returned
- method (string) – The request method
- resource (FhirResource) – A Fhir resource, possibly a bundle, of the resources that were accessed or modified during the request
- OperationOutcome (OperationOutcome) – An OperationOutcome related to the requset
- request_body – The body of the request
- time (datetime) – The time the request occured
-
-
class
fhirbug.server.requesthandlers.
DeleteRequestHandler
[source]¶ Receive a request url and the request body of a DELETE request and handle it. This includes parsing the string into a
fhirbug.server.requestparser.FhirRequestQuery
, finding the model for the requested resource and deleting it. It returns a tuple (response json, status code). If an error occurs during the process, an OperationOutcome is returned.Parameters: url (string) – a string containing the path of the request. It should not contain the server path. For example: Patients/123?name:contains=Jo Returns: A tuple (response_json, status code)
, where response_json may be the requested resource, a Bundle or an OperationOutcome in case of an error.Return type: tuple -
log_request
(url, query, status, method, resource=None, OperationOutcome=None, request_body=None, time=datetime.datetime(2020, 1, 4, 11, 45, 39, 553753))¶ Create an AuditEvent resource that contains details about the request.
Parameters: - url (string) – The initial url that was requested
- query (FhirRequestQuery) – The FhirRequestQuery that was generated
- status (int) – The status code that was returned
- method (string) – The request method
- resource (FhirResource) – A Fhir resource, possibly a bundle, of the resources that were accessed or modified during the request
- OperationOutcome (OperationOutcome) – An OperationOutcome related to the requset
- request_body – The body of the request
- time (datetime) – The time the request occured
-
-
class
fhirbug.server.requesthandlers.
GetRequestHandler
[source]¶ Receive a request url as a string and handle it. This includes parsing the string into a
fhirbug.server.requestparser.FhirRequestQuery
, finding the model for the requested resource and calling Resource.get on it. It returns a tuple (response json, status code). If an error occurs during the process, an OperationOutcome is returned.Parameters: url – a string containing the path of the request. It should not contain the server path. For example: Patients/123?name:contains=Jo Returns: A tuple (response json, status code)
where response_json may be the requested resource, a Bundle or an OperationOutcome in case of an error.Return type: tuple -
log_request
(url, query, status, method, resource=None, OperationOutcome=None, request_body=None, time=datetime.datetime(2020, 1, 4, 11, 45, 39, 553753))¶ Create an AuditEvent resource that contains details about the request.
Parameters: - url (string) – The initial url that was requested
- query (FhirRequestQuery) – The FhirRequestQuery that was generated
- status (int) – The status code that was returned
- method (string) – The request method
- resource (FhirResource) – A Fhir resource, possibly a bundle, of the resources that were accessed or modified during the request
- OperationOutcome (OperationOutcome) – An OperationOutcome related to the requset
- request_body – The body of the request
- time (datetime) – The time the request occured
-
-
class
fhirbug.server.requesthandlers.
PostRequestHandler
[source]¶ Receive a request url and the request body of a POST request and handle it. This includes parsing the string into a
fhirbug.server.requestparser.FhirRequestQuery
, finding the model for the requested resource and creating a new instance. It returns a tuple (response json, status code). If an error occurs during the process, an OperationOutcome is returned.Parameters: - url (string) – a string containing the path of the request. It should not contain the server path. For example: Patients/123?name:contains=Jo
- body (dict) – a dictionary containing all data that was sent with the request
Returns: A tuple
(response_json, status code)
, where response_json may be the requested resource, a Bundle or an OperationOutcome in case of an error.Return type: tuple
-
log_request
(url, query, status, method, resource=None, OperationOutcome=None, request_body=None, time=datetime.datetime(2020, 1, 4, 11, 45, 39, 553753))¶ Create an AuditEvent resource that contains details about the request.
Parameters: - url (string) – The initial url that was requested
- query (FhirRequestQuery) – The FhirRequestQuery that was generated
- status (int) – The status code that was returned
- method (string) – The request method
- resource (FhirResource) – A Fhir resource, possibly a bundle, of the resources that were accessed or modified during the request
- OperationOutcome (OperationOutcome) – An OperationOutcome related to the requset
- request_body – The body of the request
- time (datetime) – The time the request occured
-
class
fhirbug.server.requesthandlers.
PutRequestHandler
[source]¶ Receive a request url and the request body of a POST request and handle it. This includes parsing the string into a
fhirbug.server.requestparser.FhirRequestQuery
, finding the model for the requested resource and creating a new instance. It returns a tuple (response json, status code). If an error occurs during the process, an OperationOutcome is returned.Parameters: - url (string) – a string containing the path of the request. It should not contain the server path. For example: Patients/123?name:contains=Jo
- body (dict) – a dictionary containing all data that was sent with the request
Returns: A tuple
(response_json, status code)
, where response_json may be the requested resource, a Bundle or an OperationOutcome in case of an error.Return type: tuple
-
log_request
(url, query, status, method, resource=None, OperationOutcome=None, request_body=None, time=datetime.datetime(2020, 1, 4, 11, 45, 39, 553753))¶ Create an AuditEvent resource that contains details about the request.
Parameters: - url (string) – The initial url that was requested
- query (FhirRequestQuery) – The FhirRequestQuery that was generated
- status (int) – The status code that was returned
- method (string) – The request method
- resource (FhirResource) – A Fhir resource, possibly a bundle, of the resources that were accessed or modified during the request
- OperationOutcome (OperationOutcome) – An OperationOutcome related to the requset
- request_body – The body of the request
- time (datetime) – The time the request occured
fhirbug.exceptions¶
-
exception
fhirbug.exceptions.
AuthorizationError
(auditEvent, query=None)[source]¶ The request could not be authorized.
-
auditEvent
= None¶ This exception carries an auditEvent resource describing why authorization failed It can be thrown anywhere in a mappings
.get()
method.
-
with_traceback
()¶ Exception.with_traceback(tb) – set self.__traceback__ to tb and return self.
-
-
exception
fhirbug.exceptions.
ConfigurationError
[source]¶ Something is wrong with the settings
-
with_traceback
()¶ Exception.with_traceback(tb) – set self.__traceback__ to tb and return self.
-
-
exception
fhirbug.exceptions.
DoesNotExistError
(pk=None, resource_type=None)[source]¶ A http request query was malformed or not understood by the server
-
with_traceback
()¶ Exception.with_traceback(tb) – set self.__traceback__ to tb and return self.
-
-
exception
fhirbug.exceptions.
InvalidOperationError
[source]¶ The requested opertion is not valid
-
with_traceback
()¶ Exception.with_traceback(tb) – set self.__traceback__ to tb and return self.
-
-
exception
fhirbug.exceptions.
MappingException
[source]¶ A fhir mapping received data that was not correct
-
with_traceback
()¶ Exception.with_traceback(tb) – set self.__traceback__ to tb and return self.
-
-
exception
fhirbug.exceptions.
MappingValidationError
[source]¶ A fhir mapping has been set up wrong
-
with_traceback
()¶ Exception.with_traceback(tb) – set self.__traceback__ to tb and return self.
-
-
exception
fhirbug.exceptions.
OperationError
(severity='error', code='exception', diagnostics='', status_code=500)[source]¶ An exception that happens during a requested operation that should be returned as an OperationOutcome to the user.
-
to_fhir
()[source]¶ Express the exception as an OperationOutcome resource. This allows us to catch it and immediately return it to the user.
-
with_traceback
()¶ Exception.with_traceback(tb) – set self.__traceback__ to tb and return self.
-
Examples¶
PyMODM Autogenerated¶
This is an example implementation of a fhirbug server using mongodb via PyMODM.
Warning
This is meant as a demo implementation aimed to demonstrate some of the capabilities of fhirbug. IT IS NOT A PRODUCTION READY SERVER, please do not use it as one!
It uses models generated from the FHIR specification files so it covers pretty much every possible resource and can populate the database using FHIR’s official examples.
Installation¶
Running the example server locally¶
The only requirement besides the python packages is mongodb. If you are on Linux, installing it is probably as simple as:
$ sudo apt install mongodb-server
or equivalent. See detailed instructions for your platform here.
All python packages required are in requirements.txt, so, in a Python >= 3.6 environment, do:
$ pip install -r requirements.txt
and you should be set to go.
Starting the development server¶
In order to run the server locally for testing/debugging run:
$ python examples/pymodm_autogenerated/flask_app.py
from the project’s root directory.
Deploying using gunicorn¶
If you intend to put heavier load on the server,it is recommended to run it using a wsgi server. Fhirbug comes with gunicorn included in it’s requirements but you can use any alternative as long as it can serve a flak app (so all of them).
So, if you haven’t installed gunicorn from the server’s requirements.txt, do:
$ pip install gunicorn
and then:
$ cd examples/pymodm_autogenerated
$ gunicorn --bind 0.0.0.0:5001 flask_app:app
It is recommended to serve the app behind a proxy server like nginx. Have a look at https://gunicorn.org/#deployment for more details.
Deploying via docker¶
First, make sure you have Docker and Docker Compose installed.
Then, simply clone the git repository and start the instances:
$ git clone https://github.com/zensoup/fhirbug.git
$ cd fhirbug/examples/pymodm_autogenerated
$ sudo docker-compose up
By default, this will create one docker instance for mongodb and one for python. It will then download the example files from fhir.org and populate the database with the example resources. Finally, it will start serving the application, binding to port 5000.
If you do not want to generate example resources you can edit the DockerFile before
running docker-compose up
.
If you want to acces the mongo database from the host machine for debugging purposes
edit docker-compose.yml
to contain the following:
version: '3'
services:
web:
build: .
ports:
- "5000:5000"
depends_on:
- mongo
mongo:
image: "mongo:4"
ports:
- "27018:27017"
Then, you can connect to the db from the command line:
$ mongo mongodb://localhost:27018
Generating Example data¶
The demo server comes with a couple of scripts that download the FHIR specification files from http://hl7.org/fhir/ and populates the database with the examples included within it.
Note
If you are developing on fhirbug please note that these files are also used to generate the fixtures for some of fhirbug’s tests. So you should be aware that you may see failing tests if you change these files.
Downloading the specification files¶
To download the specification files fron the hl7 archive can either use:
$ python examples/pymodm_autogenerated/tools/download_examples.py examples/pymodm_autogenerated/test_cache
or:
$ cd examples/pymodm_autogenerated/
$ python tools/download_examples.py
This will create a folder called test_cache
inside the dem server’s root directory.
Using your own sample data¶
You can feed the fixture generation script any valid FHIR JSON formatted data and
it will seed the database using them, as long as you place inside the test_cache
folder. For the script to recogninse them as seed data, they must fit the glob pattern
*example*.json
. So any set of files like example-1.json
, example-2.json
, etc
would be recognized and parsed for use as seed data.
Populating the database¶
Once you have a cache folder with the seed data you want to use, run:
$ python examples/pymodm_autogenerated/generate_examples.py
This script will read the database configuration in examples/pymodm_autogenerated/settings.py
and use that caonnection to write the documents.
Warning
This script drops the database before starting so you will loose any existing documents in that database.
Generating the Models¶
Note
These models have already been generated and live in
examples/pymodm_autogenerated/mappings.py
. This section only
applies if you want to customize the way models are generated.
The script in examples/pymodm_autogenerated/tools/generate_pymodm_schema.py
goes through all of fhirbug’s FHIR resource classes and creates code for the
corresponding fhirbug Mappings.
You can call it simply by calling:
$ python examples/pymodm_autogenerated/tools/generate_pymodm_schema.py <output_path>
By default this will create a file in examples/pymodm_autogenerated
called
mappings.py
. You can pass a path to the script to override this behavior.
Limitations¶
There is currently a bug in pymodm that does not allow cyclic references between models. This means that some resource attributes have not been included in the generated models.
Namely, the attributes that are missing from the generated models are:
CodeSystemConcept.concept CompositionSection.section ConsentProvision.provision ContractTerm.group ExampleScenarioProcessStep.process ExampleScenarioProcessStepAlternative.step Extension.extension FHIRReference.identifier GraphDefinitionLinkTarget.link ImplementationGuideDefinitionPage.page MedicinalProductAuthorizationProcedure.application MedicinalProductPackagedPackageItem.packageItem OperationDefinitionParameter.part ParametersParameter.part QuestionnaireItem.item QuestionnaireResponseItem.item QuestionnaireResponseItemAnswer.item RequestGroupAction.action resource.action StructureMapGroupRule.rule SubstanceSpecificationName.synonym SubstanceSpecificationName.translation ValueSetExpansionContains.containsplus all of the value attributes of the Extension resource:
valueAddress, valueAge, valueAnnotation, valueAttachment, valueCodeableConcept, valueCoding, valueContactDetail, valueContactPoint, valueContributor, valueCount, valueDataRequirement, valueDistance, valueDosage, valueDuration, valueExpression, valueHumanName, valueIdentifier, valueMoney, valueParameterDefinition, valuePeriod, valueQuantity, valueRange, valueRatio, valueReference, valueRelatedArtifact, valueSampledData, valueSignature, valueTiming, valueTriggerDefinition, valueUsageContext,