fhir_kindling/fhir_server/transactions.py
from enum import Enum
from typing import List, Union
from fhir.resources import FHIRAbstractModel, construct_fhir_element
from fhir.resources.bundle import Bundle, BundleEntry, BundleEntryRequest
from fhir.resources.reference import Reference
from fhir.resources.resource import Resource
class TransactionMethod(str, Enum):
GET = "GET"
POST = "POST"
PUT = "PUT"
DELETE = "DELETE"
class TransactionType(str, Enum):
TRANSACTION = "transaction"
BATCH = "batch"
def make_transaction_bundle(
transaction_type: TransactionType = TransactionType.TRANSACTION,
method: Union[TransactionMethod, str] = TransactionMethod.POST,
resources: Union[List[Resource], List[dict]] = None,
references: Union[List[Reference], List[str]] = None,
) -> Bundle:
"""
Create a transaction bundle based on the resources, references, transaction type, and method.
Args:
transaction_type:
method:
resources:
references:
Returns:
"""
# validate input parameters
_validate_transaction_input(method, references, resources)
bundle = Bundle.construct()
bundle.type = transaction_type.value
if resources:
if not (
isinstance(resources[0], FHIRAbstractModel)
or isinstance(resources[0], dict)
):
raise ValueError(
"Resources must be a list of FHIR resources or a list of dicts"
)
entries = [
make_transaction_entry(method, resource=resource) for resource in resources
]
else:
entries = []
for reference in references:
if isinstance(reference, Reference):
reference = reference.reference
entries.append(make_transaction_entry(method, url=reference))
bundle.entry = entries
return Bundle(**bundle.dict(exclude_none=True))
def _validate_transaction_input(
method: Union[TransactionMethod, str],
references: Union[List[Reference], List[str]],
resources: Union[List[Resource], List[dict]],
):
if isinstance(method, str):
method = TransactionMethod(method)
if not resources and not references:
raise ValueError("Either resources or references must be provided")
if resources and references:
raise ValueError("Only one of resources or references can be provided")
if method in [TransactionMethod.POST, TransactionMethod.PUT]:
# if method is POST or PUT, resources must be provided
if not resources:
raise ValueError("Resources must be provided for POST and PUT transactions")
elif method == TransactionMethod.GET:
if not references:
raise ValueError("References must be provided for GET transactions")
def make_transaction_entry(
method: Union[TransactionMethod, str],
url: str = None,
resource: Union[Resource, dict] = None,
) -> BundleEntry:
"""Create a transaction entry for a bundle based on the method, url, and resource.
If only a resource is provided, the url will be constructed from the resource otherwise the given url will be used.
Args:
method: the method to use for the transaction one of GET, POST, PUT, DELETE
url: optional relative url to use for the transaction
resource: optional FHIR resource to use for the transaction
Returns:
The transaction entry
"""
if isinstance(method, str):
method = TransactionMethod(method)
if not url and not resource:
raise ValueError("Either url or resource must be provided.")
entry = BundleEntry.construct()
if resource:
# construct the fhir element if the resource is a dict
if isinstance(resource, dict):
try:
resource = construct_fhir_element(
resource.get("resourceType"), resource
)
except Exception as e:
raise ValueError(
f"Unable to construct resource from dict: {resource} \n{e}"
)
# remove the id if the method is POST (create)
if method == TransactionMethod.PUT and not resource.id:
raise ValueError("PUT requires a resource with an id")
if method == TransactionMethod.POST:
if hasattr(resource, "id"):
del resource.id
_add_resource_to_entry(entry, resource)
url = _get_transaction_url_for_method(method, url, resource)
entry.request = BundleEntryRequest(**{"method": method.value, "url": url})
return entry
def _get_transaction_url_for_method(
method: TransactionMethod, url: str = None, resource: Resource = None
) -> str:
"""
Get the url for a transaction entry based on the method, url, and resource.
Args:
method:
url:
resource:
Returns:
"""
if method == TransactionMethod.GET:
if not url:
url = resource.relative_path()
elif method == TransactionMethod.PUT:
if not resource:
raise ValueError("PUT requires a resource")
url = resource.relative_path()
elif method == TransactionMethod.DELETE:
if not url:
url = resource.relative_path()
else:
if not url:
url = resource.get_resource_type()
return url
def _add_resource_to_entry(
entry: BundleEntry, resource: Union[FHIRAbstractModel, dict]
):
if isinstance(resource, FHIRAbstractModel):
entry.resource = resource
elif isinstance(resource, dict):
entry.resource = construct_fhir_element(resource.get("resourceType"), resource)
else:
raise ValueError("Resource must be a FHIR resource or a dict")