Initial Release
With InterSystems IRIS FHIR Server you can build a Strategy to customize the behavior of the server (see documentation for more details).
This repository contains a Python Strategy that can be used as a starting point to build your own Strategy in python.
This demo strategy provides the following features:
Account
resourceObservation
resource
Observation
resource is returnedObservation
resource is not returnedgit clone git@github.com:grongierisc/iris-fhir-python-strategy.git
docker-compose build
docker-compose up -d
GET http://localhost:8083/fhir/r4/metadata
Accept: application/json+fhir
The Account
resource should not be present in the Capability Statement.
GET http://localhost:8083/fhir/r4/Account
Accept: application/json+fhir
returns :
{
"resourceType": "OperationOutcome",
"issue": [
{
"severity": "error",
"code": "not-supported",
"diagnostics": "ResourceNotSupported",
"details": {
"text": "Resource type 'Account' is not supported."
}
}
]
}
GET http://localhost:8089/fhir/r4/Patient/3/$everything
Content-Type: application/json+fhir
Accept: application/json+fhir
returns :
{
"resourceType": "Bundle",
"id": "feaa09c0-1cb7-11ee-b77a-0242c0a88002",
"type": "searchset",
"timestamp": "2023-07-07T11:07:49Z",
"total": 0,
"link": [
{
"relation": "self",
"url": "http://localhost:8083/fhir/r4/Observation?patient=178"
}
]
}
GET http://localhost:8089/fhir/r4/Patient/3/$everything
Content-Type: application/json+fhir
Accept: application/json+fhir
Authorization: Basic U3VwZXJVc2VyOlNZUw==
returns :
{
"resourceType": "Bundle",
"id": "953a1b06-1cb7-11ee-b77b-0242c0a88002",
"type": "searchset",
"timestamp": "2023-07-07T11:08:04Z",
"total": 100,
"link": [
{
"relation": "self",
"url": "http://localhost:8083/fhir/r4/Observation?patient=178"
}
],
"entry": [
{
"fullUrl": "http://localhost:8083/fhir/r4/Observation/277",
"resource": {
"resourceType": "Observation",
"id": "277",
"status": "final",
"category": [
...
],
}
},
...
]
}
More details on a next section about Consent.
The consent management system is simulated by the consent
method in the CustomInteraction
class in the custom
module.
The consent
method returns True
if the user has sufficient rights to access the resource, False
otherwise.
def consent(self, resource_type, user, roles):
#Example consent logic - only allow users with the role '%All' to see
#Observation resources.
if resource_type == 'Observation':
if '%All' in roles:
return True
else:
return False
else:
return True
The consent
function is part of the CustomInteraction
.
The CustomInteraction
class is an implementation of the Interaction
class.
The Interaction
class is an Abstract class that must be implemented by the Strategy. It part of the FhirInteraction
module.
class Interaction(object): __metaclass__ = abc.ABCMeta
@abc.abstractmethod def on_before_request(self, fhir_service:'iris.HS.FHIRServer.API.Service', fhir_request:'iris.FHIRServer.API.Data.Request', body:dict, timeout:int): """ on_before_request is called before the request is sent to the server. param fhir_service: the fhir service object iris.HS.FHIRServer.API.Service param fhir_request: the fhir request object iris.FHIRServer.API.Data.Request param timeout: the timeout in seconds return: None """ @abc.abstractmethod def on_after_request(self, fhir_service:'iris.HS.FHIRServer.API.Service', fhir_request:'iris.FHIRServer.API.Data.Request', fhir_response:'iris.FHIRServer.API.Data.Response', body:dict): """ on_after_request is called after the request is sent to the server. param fhir_service: the fhir service object iris.HS.FHIRServer.API.Service param fhir_request: the fhir request object iris.FHIRServer.API.Data.Request param fhir_response: the fhir response object iris.FHIRServer.API.Data.Response return: None """ @abc.abstractmethod def post_process_read(self, fhir_object:dict) -> bool: """ post_process_read is called after the read operation is done. param fhir_object: the fhir object return: True the resource should be returned to the client, False otherwise """ @abc.abstractmethod def post_process_search(self, rs:'iris.HS.FHIRServer.Util.SearchResult', resource_type:str): """ post_process_search is called after the search operation is done. param rs: the search result iris.HS.FHIRServer.Util.SearchResult param resource_type: the resource type return: None """
The CustomInteraction
class is an implementation of the Interaction
class.
class CustomInteraction(Interaction):
def on_before_request(self, fhir_service, fhir_request, body, timeout): #Extract the user and roles for this request #so consent can be evaluated. self.requesting_user = fhir_request.Username self.requesting_roles = fhir_request.Roles def on_after_request(self, fhir_service, fhir_request, fhir_response, body): #Clear the user and roles between requests. self.requesting_user = "" self.requesting_roles = "" def post_process_read(self, fhir_object): #Evaluate consent based on the resource and user/roles. #Returning 0 indicates this resource shouldn't be displayed - a 404 Not Found #will be returned to the user. return self.consent(fhir_object['resourceType'], self.requesting_user, self.requesting_roles) def post_process_search(self, rs, resource_type): #Iterate through each resource in the search set and evaluate #consent based on the resource and user/roles. #Each row marked as deleted and saved will be excluded from the Bundle. rs._SetIterator(0) while rs._Next(): if not self.consent(rs.ResourceType, self.requesting_user, self.requesting_roles): #Mark the row as deleted and save it. rs.MarkAsDeleted() rs._SaveRow() def consent(self, resource_type, user, roles): #Example consent logic - only allow users with the role '%All' to see #Observation resources. if resource_type == 'Observation': if '%All' in roles: return True else: return False else: return True
You can modify the custom
module to implement your own consent logic.
All modifications to the custom
module will be directly reflected in the FHIR Server.
Other behaviors can be implemented by overriding the Interaction
classes.
IRIS FHIR Server provides a default CapabilityStatement based on the Implementation Guide guiven at installation time.
More information how to customize the CapabilityStatement can be found at FHIR CapabilityStatement.
For this example, the Implementation Guide is raw FHIR R4.
To customize the CapabilityStatement, you can modify the custom
module.
The CustomStrategy
class is an implementation of the Strategy
class.
The Strategy
class is an Abstract class that must be implemented by the Strategy. It part of the FhirInteraction
module.
class Strategy(object): __metaclass__ = abc.ABCMeta
@abc.abstractmethod def on_get_capability_statement(self,capability_statement:dict)-> dict: """ on_after_get_capability_statement is called after the capability statement is retrieved. param capability_statement: the capability statement return: None """
The on_get_capability_statement
method is called after the CapabilityStatement is generated.
The CustomStrategy
class is an implementation of the Strategy
class.
class CustomStrategy(Strategy):
def on_get_capability_statement(self, capability_statement): # Example : del resources Account capability_statement['rest'][0]['resource'] = [resource for resource in capability_statement['rest'][0]['resource'] if resource['type'] != 'Account'] return capability_statement
You can modify the custom
module to implement your own Custom CapabilityStatement.
To apply the changes, you need to update the fhir server configuration.
cd /irisdev/app/src/python
/usr/irissys/bin/irispython
>>> import custom
>>> custom.set_capability_statement()
First of all, we have to understand how IRIS FHIR Server works.
Every IRIS FHIR Server implements a Strategy
.
A Strategy
is a set of two classes :
Superclass | Subclass Parameters |
---|---|
HS.FHIRServer.API.InteractionsStrategy | StrategyKey — Specifies a unique identifier for the InteractionsStrategy.InteractionsClass — Specifies the name of your Interactions subclass. |
HS.FHIRServer.API.RepoManager | StrategyClass — Specifies the name of your InteractionsStrategy subclass.StrategyKey — Specifies a unique identifier for the InteractionsStrategy. Must match the StrategyKey parameter in the InteractionsStrategy subclass. |
Both classes are Abstract
classes.
HS.FHIRServer.API.InteractionsStrategy
is an Abstract
class that must be implemented to customize the behavior of the FHIR Server.HS.FHIRServer.API.RepoManager
is an Abstract
class that must be implemented to customize the storage of the FHIR Server.For our example, we will only focus on the HS.FHIRServer.API.InteractionsStrategy
class even if the HS.FHIRServer.API.RepoManager
class is also implemented and mandatory to customize the FHIR Server.
The HS.FHIRServer.API.RepoManager
class is implemented by HS.FHIRServer.Storage.Json.RepoManager
class, which is the default implementation of the FHIR Server.
All source code can be found in the src
folder.
The src
folder contains the following folders :
python
: contains the python codecls
: contains the ObjectScript code that is used to call the python codeIn this proof of concept, we will only be interested in how to implement a Strategy
in Python, not how to implement a RepoManager
.
To implement a Strategy
you need to create at least two classes :
HS.FHIRServer.API.InteractionsStrategy
classHS.FHIRServer.API.Interactions
classHS.FHIRServer.API.InteractionsStrategy
class aim to customize the behavior of the FHIR Server by overriding the following methods :
GetMetadataResource
: called to get the metadata of the FHIR Server
HS.FHIRServer.API.InteractionsStrategy
has also two parameters :
StrategyKey
: a unique identifier for the InteractionsStrategyInteractionsClass
: the name of your Interactions subclassHS.FHIRServer.API.Interactions
class aim to customize the behavior of the FHIR Server by overriding the following methods :
OnBeforeRequest
: called before the request is sent to the serverOnAfterRequest
: called after the request is sent to the serverPostProcessRead
: called after the read operation is donePostProcessSearch
: called after the search operation is doneRead
: called to read a resourceAdd
: called to add a resourceUpdate
: called to update a resourceDelete
: called to delete a resourceWe implement HS.FHIRServer.API.Interactions
class in the src/cls/FHIR/Python/Interactions.cls
class.
Class FHIR.Python.Interactions Extends (HS.FHIRServer.Storage.Json.Interactions, FHIR.Python.Helper) {
Parameter OAuth2TokenHandlerClass As %String = "FHIR.Python.OAuth2Token";
Method %OnNew(pStrategy As HS.FHIRServer.Storage.Json.InteractionsStrategy) As %Status
{
// %OnNew is called when the object is created.
// The pStrategy parameter is the strategy object that created this object.
// The default implementation does nothing
// Frist set the python path from an env var
set ..PythonPath = $system.Util.GetEnviron("INTERACTION_PATH")
// Then set the python class name from the env var
set ..PythonClassname = $system.Util.GetEnviron("INTERACTION_CLASS")
// Then set the python module name from the env var
set ..PythonModule = $system.Util.GetEnviron("INTERACTION_MODULE")if (..PythonPath = "") || (..PythonClassname = "") || (..PythonModule = "") { //quit ##super(pStrategy) set ..PythonPath = "/irisdev/app/src/python/" set ..PythonClassname = "CustomInteraction" set ..PythonModule = "custom" } // Then set the python class do ..SetPythonPath(..PythonPath) set ..PythonClass = ##class(FHIR.Python.Interactions).GetPythonInstance(..PythonModule, ..PythonClassname) quit ##super(pStrategy)
}
Method OnBeforeRequest(
pFHIRService As HS.FHIRServer.API.Service,
pFHIRRequest As HS.FHIRServer.API.Data.Request,
pTimeout As %Integer)
{
// OnBeforeRequest is called before each request is processed.
if $ISOBJECT(..PythonClass) {
set body = ##class(%SYS.Python).None()
if pFHIRRequest.Json '= "" {
set jsonLib = ##class(%SYS.Python).Import("json")
set body = jsonLib.loads(pFHIRRequest.Json.%ToJSON())
}
do ..PythonClass."on_before_request"(pFHIRService, pFHIRRequest, body, pTimeout)
}
}Method OnAfterRequest(
pFHIRService As HS.FHIRServer.API.Service,
pFHIRRequest As HS.FHIRServer.API.Data.Request,
pFHIRResponse As HS.FHIRServer.API.Data.Response)
{
// OnAfterRequest is called after each request is processed.
if $ISOBJECT(..PythonClass) {
set body = ##class(%SYS.Python).None()
if pFHIRResponse.Json '= "" {
set jsonLib = ##class(%SYS.Python).Import("json")
set body = jsonLib.loads(pFHIRResponse.Json.%ToJSON())
}
do ..PythonClass."on_after_request"(pFHIRService, pFHIRRequest, pFHIRResponse, body)
}
}Method PostProcessRead(pResourceObject As %DynamicObject) As %Boolean
{
// PostProcessRead is called after a resource is read from the database.
// Return 1 to indicate that the resource should be included in the response.
// Return 0 to indicate that the resource should be excluded from the response.
if $ISOBJECT(..PythonClass) {
if pResourceObject '= "" {
set jsonLib = ##class(%SYS.Python).Import("json")
set body = jsonLib.loads(pResourceObject.%ToJSON())
}
return ..PythonClass."post_process_read"(body)
}
quit 1
}Method PostProcessSearch(
pRS As HS.FHIRServer.Util.SearchResult,
pResourceType As %String) As %Status
{
// PostProcessSearch is called after a search is performed.
// Return $$$OK to indicate that the search was successful.
// Return an error code to indicate that the search failed.
if $ISOBJECT(..PythonClass) {
return ..PythonClass."post_process_search"(pRS, pResourceType)
}
quit $$$OK
}Method Read(
pResourceType As %String,
pResourceId As %String,
pVersionId As %String = "") As %DynamicObject
{
return ##super(pResourceType, pResourceId, pVersionId)
}Method Add(
pResourceObj As %DynamicObject,
pResourceIdToAssign As %String = "",
pHttpMethod = "POST") As %String
{
return ##super(pResourceObj, pResourceIdToAssign, pHttpMethod)
}/// Returns VersionId for the "deleted" version
Method Delete(
pResourceType As %String,
pResourceId As %String) As %String
{
return ##super(pResourceType, pResourceId)
}Method Update(pResourceObj As %DynamicObject) As %String
{
return ##super(pResourceObj)
}
}
The FHIR.Python.Interactions
class inherits from HS.FHIRServer.Storage.Json.Interactions
class and FHIR.Python.Helper
class.
The HS.FHIRServer.Storage.Json.Interactions
class is the default implementation of the FHIR Server.
The FHIR.Python.Helper
class aim to help to call Python code from ObjectScript.
The FHIR.Python.Interactions
class overrides the following methods :
%OnNew
: called when the object is created
%OnNew
method of the parent classMethod %OnNew(pStrategy As HS.FHIRServer.Storage.Json.InteractionsStrategy) As %Status { // First set the python path from an env var set ..PythonPath = $system.Util.GetEnviron("INTERACTION_PATH") // Then set the python class name from the env var set ..PythonClassname = $system.Util.GetEnviron("INTERACTION_CLASS") // Then set the python module name from the env var set ..PythonModule = $system.Util.GetEnviron("INTERACTION_MODULE")
if (..PythonPath = "") || (..PythonClassname = "") || (..PythonModule = "") { // use default values set ..PythonPath = "/irisdev/app/src/python/" set ..PythonClassname = "CustomInteraction" set ..PythonModule = "custom" } // Then set the python class do ..SetPythonPath(..PythonPath) set ..PythonClass = ..GetPythonInstance(..PythonModule, ..PythonClassname) quit ##super(pStrategy)
}
OnBeforeRequest
: called before the request is sent to the server
on_before_request
method of the python classHS.FHIRServer.API.Service
object, the HS.FHIRServer.API.Data.Request
object, the body of the request and the timeoutMethod OnBeforeRequest(
pFHIRService As HS.FHIRServer.API.Service,
pFHIRRequest As HS.FHIRServer.API.Data.Request,
pTimeout As %Integer)
{
// OnBeforeRequest is called before each request is processed.
if $ISOBJECT(..PythonClass) {
set body = ##class(%SYS.Python).None()
if pFHIRRequest.Json '= "" {
set jsonLib = ##class(%SYS.Python).Import("json")
set body = jsonLib.loads(pFHIRRequest.Json.%ToJSON())
}
do ..PythonClass."on_before_request"(pFHIRService, pFHIRRequest, body, pTimeout)
}
}
OnAfterRequest
: called after the request is sent to the server
on_after_request
method of the python classHS.FHIRServer.API.Service
object, the HS.FHIRServer.API.Data.Request
object, the HS.FHIRServer.API.Data.Response
object and the body of the responseMethod OnAfterRequest(
pFHIRService As HS.FHIRServer.API.Service,
pFHIRRequest As HS.FHIRServer.API.Data.Request,
pFHIRResponse As HS.FHIRServer.API.Data.Response)
{
// OnAfterRequest is called after each request is processed.
if $ISOBJECT(..PythonClass) {
set body = ##class(%SYS.Python).None()
if pFHIRResponse.Json '= "" {
set jsonLib = ##class(%SYS.Python).Import("json")
set body = jsonLib.loads(pFHIRResponse.Json.%ToJSON())
}
do ..PythonClass."on_after_request"(pFHIRService, pFHIRRequest, pFHIRResponse, body)
}
}
FHIR.Python.Interactions
class calls the on_before_request
, on_after_request
, … methods of the python class.
Here is the abstract python class :
import abc import iris
class Interaction(object):
metaclass = abc.ABCMeta@abc.abstractmethod def on_before_request(self, fhir_service:'iris.HS.FHIRServer.API.Service', fhir_request:'iris.HS.FHIRServer.API.Data.Request', body:dict, timeout:int): """ on_before_request is called before the request is sent to the server. param fhir_service: the fhir service object iris.HS.FHIRServer.API.Service param fhir_request: the fhir request object iris.FHIRServer.API.Data.Request param timeout: the timeout in seconds return: None """ @abc.abstractmethod def on_after_request(self, fhir_service:'iris.HS.FHIRServer.API.Service', fhir_request:'iris.HS.FHIRServer.API.Data.Request', fhir_response:'iris.HS.FHIRServer.API.Data.Response', body:dict): """ on_after_request is called after the request is sent to the server. param fhir_service: the fhir service object iris.HS.FHIRServer.API.Service param fhir_request: the fhir request object iris.FHIRServer.API.Data.Request param fhir_response: the fhir response object iris.FHIRServer.API.Data.Response return: None """ @abc.abstractmethod def post_process_read(self, fhir_object:dict) -> bool: """ post_process_read is called after the read operation is done. param fhir_object: the fhir object return: True the resource should be returned to the client, False otherwise """ @abc.abstractmethod def post_process_search(self, rs:'iris.HS.FHIRServer.Util.SearchResult', resource_type:str): """ post_process_search is called after the search operation is done. param rs: the search result iris.HS.FHIRServer.Util.SearchResult param resource_type: the resource type return: None """
from FhirInteraction import Interaction
class CustomInteraction(Interaction):
def on_before_request(self, fhir_service, fhir_request, body, timeout): #Extract the user and roles for this request #so consent can be evaluated. self.requesting_user = fhir_request.Username self.requesting_roles = fhir_request.Roles def on_after_request(self, fhir_service, fhir_request, fhir_response, body): #Clear the user and roles between requests. self.requesting_user = "" self.requesting_roles = "" def post_process_read(self, fhir_object): #Evaluate consent based on the resource and user/roles. #Returning 0 indicates this resource shouldn't be displayed - a 404 Not Found #will be returned to the user. return self.consent(fhir_object['resourceType'], self.requesting_user, self.requesting_roles) def post_process_search(self, rs, resource_type): #Iterate through each resource in the search set and evaluate #consent based on the resource and user/roles. #Each row marked as deleted and saved will be excluded from the Bundle. rs._SetIterator(0) while rs._Next(): if not self.consent(rs.ResourceType, self.requesting_user, self.requesting_roles): #Mark the row as deleted and save it. rs.MarkAsDeleted() rs._SaveRow() def consent(self, resource_type, user, roles): #Example consent logic - only allow users with the role '%All' to see #Observation resources. if resource_type == 'Observation': if '%All' in roles: return True else: return False else: return True
The FHIR.Python.Interactions
class is a wrapper to call the python class.
IRIS abstracts classes are implemented to wrap python abstract classes 🥳.
That help us to keep python code and ObjectScript code separated and for so benefit from the best of both worlds.