Initial Release
A flexible Python-based strategy for customizing InterSystems IRIS FHIR Server behavior using decorators.
This project provides a bridge between the high-performance InterSystems IRIS FHIR Server and Python. It allows developers to customize FHIR operations (Create, Read, Update, Delete, Search) and implement business logic (Consent, Validation, OAuth) using familiar Python decorators.
on_before_) and post-processing (on_after_) hooks for all interactions.@fhir.on_before_create("Patient") to register handlers.$operations in Python.Clone the repository
git clone https://github.com/grongierisc/iris-fhir-python-strategy.git
cd iris-fhir-python-strategy
Start the containers
docker-compose up -d
Verify Installation
Access the FHIR metadata endpoint:
curl http://localhost:8083/fhir/r4/metadata
examples/custom_decorators.py (or create your own module).fhir registry from fhir_decorators.Validate resources before they are saved to the database.
from fhir_decorators import fhir@fhir.on_validate_resource("Patient")
def validate_patient(resource, is_in_transaction):
"""
Ensure specific rules for Patient resources.
Raises ValueError to reject the resource with a 400 Bad Request.
"""
# Rule: Check if 'identifier' exists
if "identifier" not in resource:
raise ValueError("Patient must have at least one identifier")# Rule: Check for forbidden names for name in resource.get("name", []): if name.get("family") == "Forbidden": raise ValueError("This family name is not allowed")
@fhir.on_validate_bundle
def validate_bundle(bundle, fhir_version):
"""
Apply rules to the entire bundle.
"""
if bundle.get("type") == "transaction":
if len(bundle.get("entry", [])) > 100:
raise ValueError("Transaction bundle too large (max 100 entries)")
Modify the incoming resource or metadata before the server processes it.
@fhir.on_before_create("Observation")
def enrich_observation(service, request, body, timeout):
"""
Automatically add a tag to all new Observations.
"""
meta = body.setdefault("meta", {})
tags = meta.setdefault("tag", [])
tags.append({
"system": "http://my-hospital.org/tags",
"code": "auto-generated",
"display": "Auto Generated"
})
Modify or filter the response after the database operation but before sending it to the client.
@fhir.on_after_read("Patient") def mask_patient_data(resource): """ Mask sensitive fields for non-admin users. Returns: True: Return the resource (modified or not). False: Hide the resource (client receives 404). """ # Assuming user context is stored globally or passed user_role = "user" # Replace with actual context retrieval logicif user_role != "admin": # Remove telecom info if "telecom" in resource: del resource["telecom"] # Obfuscate birth date if "birthDate" in resource: resource["birthDate"] = "1900-01-01" return True
Implement custom FHIR operations using Python functions.
@fhir.operation(name="echo", scope="Instance", resource_type="Patient") def echo_patient_operation(name, scope, body, service, request, response): """ Implements POST /Patient/{id}/$echo """ # Logic: Just reflect the input body and operation details response_payload = { "resourceType": "Parameters", "parameter": [ {"name": "operation", "valueString": name}, {"name": "received_body", "resource": body} ] }# Set the response payload # Note: 'response' is the IRIS response object wrapper response.Json = response_payload return response
Intercept search results to enforce fine-grained access control.
@fhir.on_after_search("Patient") def filter_search_results(result_set, resource_type): """ Iterate through search results and remove restricted items. 'result_set' is an iris.HS.FHIRServer.Util.SearchResult object. """ # Iterate over the result set result_set._SetIterator(0) while result_set._Next(): # Get resource content or ID resource_id = result_set._Get("ResourceId")# Example validation logic if resource_id.startswith("restricted-"): # Mark this row as deleted so it is excluded from the Bundle result_set.MarkAsDeleted() result_set._SaveRow()
Remove unsupported resources or add documentation.
@fhir.on_capability_statement def customize_metadata(capability_statement): """ Remove 'Account' resource from the metadata. """ rest_def = capability_statement['rest'][0] resources = rest_def['resource']# Filter out Account rest_def['resource'] = [r for r in resources if r['type'] != 'Account'] return capability_statement
For any given FHIR request, decorators execute in the following sequence:
@fhir.on_before_request: Global pre-processing (Logs, Auth).@fhir.on_before_create (or read/update/delete/search): Interaction-specific pre-processing.@fhir.on_after_create (or read/update/delete/search): Interaction-specific post-processing.@fhir.on_after_request: Global post-processing (Cleanup).graph LR
A[Global Before Request] --> B[Specific Before Interaction]
B --> C((Database))
C --> D[Specific After Interaction]
D --> E[Global After Request]
These hooks allow you to intercept and modify standard FHIR interactions.
Execution Order Rule: When multiple handlers are registered for the same interaction (e.g., global, specific, and wildcard), they execute in this strict order:
@fhir.hook() (No arguments)@fhir.hook("Patient")@fhir.hook("*")Runs before database operation. Signature: def handler(fhir_service, fhir_request, body, timeout):
@fhir.on_before_create(resource_type)@fhir.on_before_read(resource_type)@fhir.on_before_update(resource_type)@fhir.on_before_delete(resource_type)@fhir.on_before_search(resource_type)Runs after database operation.
@fhir.on_after_create(resource_type)
def handler(fhir_service, fhir_request, fhir_response, body):@fhir.on_after_update(resource_type)
def handler(fhir_service, fhir_request, fhir_response, body):@fhir.on_after_delete(resource_type)
def handler(fhir_service, fhir_request, fhir_response, body):@fhir.on_after_read(resource_type)
def handler(resource): -> Returns bool (False to hide/404)@fhir.on_after_search(resource_type)
def handler(result_set, resource_type):Note: resource_type is optional. If omitted, applies to all types.
@fhir.on_before_request
@fhir.on_before_{interaction}.def handler(fhir_service, fhir_request, body, timeout):@fhir.on_after_request
@fhir.on_after_{interaction}.def handler(fhir_service, fhir_request, fhir_response, body):@fhir.on_capability_statement
def handler(capability_statement): -> Returns dict@fhir.operation(name, scope, resource_type)
$diff).def handler(operation_name, operation_scope, body, fhir_service, fhir_request, fhir_response):@fhir.oauth_get_user_info
def handler(username, roles): -> Returns dict@fhir.oauth_get_introspection
def handler(): -> Returns dict@fhir.consent(resource_type)
def handler(resource): -> Returns bool@fhir.oauth_verify_resource_id(resource_type)
def handler(resource_type, resource_id, required_privilege): -> Returns bool@fhir.oauth_verify_resource_content(resource_type)
def handler(resource_dict, required_privilege, allow_shared): -> Returns bool@fhir.oauth_verify_search(resource_type)
def handler(resource_type, compartment_type, compartment_id, parameters, required_privilege): -> Returns bool@fhir.oauth_verify_system_level
def handler(): -> Returns bool@fhir.on_validate_resource(resource_type)
def handler(resource, is_in_transaction):@fhir.on_validate_bundle
def handler(bundle, fhir_version):The strategy is configured via environment variables in docker-compose.yml:
FHIR_CUSTOMIZATION_MODULE: The Python module to load (default: examples.custom_decorators).FHIR_CUSTOMIZATION_PATH: Path to the Python code (default: /irisdev/app/).When a request arrives (e.g., POST /Patient), IRIS calls Interactions.cls, which looks up the registered Python handler for on_before_create and executes it.
For detailed implementation of the ObjectScript to Python bridge, see src/cls/FHIR/Python/Interactions.cls and src/cls/FHIR/Python/Helper.cls.