diff --git a/README.md b/README.md index 120e462f..48cbbf7a 100644 --- a/README.md +++ b/README.md @@ -191,7 +191,7 @@ print(f"FHIR conditions: {result.fhir.problem_list}") # Auto-converted to FHIR ```python from healthchain.gateway import HealthChainAPI, FHIRGateway -from fhir.resources.patient import Patient +from healthchain.fhir.r4b import Patient # Create healthcare application app = HealthChainAPI(title="Multi-EHR Patient Data") diff --git a/cookbook/multi_ehr_data_aggregation.py b/cookbook/multi_ehr_data_aggregation.py index 16fb55f6..85050829 100644 --- a/cookbook/multi_ehr_data_aggregation.py +++ b/cookbook/multi_ehr_data_aggregation.py @@ -20,9 +20,7 @@ from dotenv import load_dotenv -from fhir.resources.bundle import Bundle -from fhir.resources.condition import Condition -from fhir.resources.annotation import Annotation +from healthchain.fhir.r4b import Bundle, Condition, Annotation from healthchain.gateway import FHIRGateway, HealthChainAPI from healthchain.gateway.clients.fhir.base import FHIRAuthConfig diff --git a/cookbook/sepsis_fhir_batch.py b/cookbook/sepsis_fhir_batch.py index 3c41dcb7..231406c7 100644 --- a/cookbook/sepsis_fhir_batch.py +++ b/cookbook/sepsis_fhir_batch.py @@ -21,9 +21,7 @@ import joblib import logging from dotenv import load_dotenv -from fhir.resources.patient import Patient -from fhir.resources.observation import Observation -from fhir.resources.riskassessment import RiskAssessment +from healthchain.fhir.r4b import Patient, Observation, RiskAssessment from healthchain.gateway import HealthChainAPI, FHIRGateway from healthchain.gateway.clients.fhir.base import FHIRAuthConfig diff --git a/docs/cookbook/clinical_coding.md b/docs/cookbook/clinical_coding.md index 129afad8..aa58598a 100644 --- a/docs/cookbook/clinical_coding.md +++ b/docs/cookbook/clinical_coding.md @@ -55,11 +55,9 @@ First we'll need to convert the incoming CDA XML to FHIR. The [CdaAdapter](../re ```python from healthchain.io import CdaAdapter -from healthchain.engine import create_interop -# Create an interop engine with default configuration -interop_engine = create_interop() -cda_adapter = CdaAdapter(engine=interop_engine) +# Create CDA adapter +cda_adapter = CdaAdapter() # Parse the CDA document to a Document object doc = cda_adapter.parse(request) @@ -150,7 +148,7 @@ Use `.add_source` to register a FHIR endpoint you want to connect to with its co ```python from healthchain.gateway import FHIRGateway -from healthchain.gateway.clients.fhir.base import FHIRAuthConfig +from healthchain.gateway.clients import FHIRAuthConfig from dotenv import load_dotenv load_dotenv() @@ -193,7 +191,7 @@ def ai_coding_workflow(request: CdaRequest): condition, source="epic-notereader", tag_code="cdi" ) # Send to external FHIR server via gateway - fhir_gateway.create(condition, source="billing") + fhir_gateway.create(condition, source="medplum") # Return processed CDA response to the legacy system cda_response = cda_adapter.format(doc) @@ -224,7 +222,7 @@ from healthchain.sandbox import SandboxClient # Create sandbox client for SOAP/CDA testing client = SandboxClient( - url="http://localhost:8000/notereader/ProcessDocument", + url="http://localhost:8000/notereader/?wsdl", workflow="sign-note-inpatient", protocol="soap" ) @@ -242,14 +240,9 @@ client.load_from_path("./data/notereader_cda.xml") Now for the moment of truth! Start your service and run the sandbox to see the complete workflow in action. ```python -import uvicorn import threading -# Start the API server in a separate thread -def start_api(): - uvicorn.run(app, port=8000) - -api_thread = threading.Thread(target=start_api, daemon=True) +api_thread = threading.Thread(target=app.run, daemon=True) api_thread.start() # Send requests and save responses with sandbox client @@ -472,3 +465,4 @@ A clinical coding service that bridges legacy CDA systems with modern FHIR infra - **Add validation**: Implement FHIR resource validation before sending to external servers. - **Expand to other workflows**: Adapt the pattern for lab results, medications, or radiology reports. - **Build on it**: Use the extracted conditions in the [Data Aggregation example](./multi_ehr_aggregation.md) to combine with other FHIR sources. + - **Go to production**: Scaffold a project with `healthchain new` and run with `healthchain serve` — see [From cookbook to service](./index.md#from-cookbook-to-service). diff --git a/docs/cookbook/discharge_summarizer.md b/docs/cookbook/discharge_summarizer.md index 8fd510d2..02f5ae58 100644 --- a/docs/cookbook/discharge_summarizer.md +++ b/docs/cookbook/discharge_summarizer.md @@ -124,7 +124,7 @@ from healthchain.models import CDSRequest, CDSResponse cds_service = CDSHooksService() # Define the CDS service function -@cds_service.hook("encounter-discharge", id="discharge-summary") +@cds_service.hook("encounter-discharge", id="discharge-summarizer") def handle_discharge_summary(request: CDSRequest) -> CDSResponse: """Process discharge summaries with AI""" # Parse CDS request to internal Document format @@ -183,14 +183,9 @@ client.load_free_text( Put it all together and run both the service and sandbox client: ```python -import uvicorn import threading -# Start the API server in a separate thread -def start_api(): - uvicorn.run(app, port=8000) - -api_thread = threading.Thread(target=start_api, daemon=True) +api_thread = threading.Thread(target=app.run, daemon=True) api_thread.start() # Send requests and save responses with sandbox client @@ -203,7 +198,7 @@ client.save_results("./output/") Once running, your service will be available at: - **Service discovery**: `http://localhost:8000/cds-services` - - **Discharge summary endpoint**: `http://localhost:8000/cds-services/discharge-summary` + - **Discharge summary endpoint**: `http://localhost:8000/cds/cds-services/discharge-summarizer` ??? example "Example CDS Response" @@ -257,3 +252,4 @@ A CDS Hooks service for discharge workflows that integrates seamlessly with EHR - **Add validation**: Implement checks for required discharge elements (medications, follow-ups, equipment). - **Multi-card support**: Expand to generate separate cards for different discharge aspects (medication reconciliation, transportation, follow-up scheduling). - **Integrate with workflows**: Deploy to Epic App Orchard or Cerner Code Console for production EHR integration. + - **Go to production**: Scaffold a project with `healthchain new` and run with `healthchain serve` — see [From cookbook to service](./index.md#from-cookbook-to-service). diff --git a/docs/cookbook/format_conversion.md b/docs/cookbook/format_conversion.md index 5dd3b364..1848db3b 100644 --- a/docs/cookbook/format_conversion.md +++ b/docs/cookbook/format_conversion.md @@ -94,8 +94,7 @@ for resource in fhir_resources: Generate a CDA document from FHIR resources: ```python -from fhir.resources.condition import Condition -from fhir.resources.patient import Patient +from healthchain.fhir.r4b import Condition, Patient # Create FHIR resources patient = Patient( @@ -171,8 +170,7 @@ for resource in fhir_resources: Generate an HL7v2 message from FHIR resources: ```python -from fhir.resources.patient import Patient -from fhir.resources.encounter import Encounter +from healthchain.fhir.r4b import Patient, Encounter patient = Patient( resourceType="Patient", diff --git a/docs/cookbook/index.md b/docs/cookbook/index.md index 0791585f..da7eb9cb 100644 --- a/docs/cookbook/index.md +++ b/docs/cookbook/index.md @@ -105,5 +105,65 @@ Hands-on, production-ready examples for building healthcare AI applications with --- -!!! tip "What next?" - See the source code for each recipe, experiment with the sandboxes, and adapt the patterns for your projects! +## From cookbook to service + +Cookbooks are standalone scripts — run them directly to explore and experiment. When you're ready to build a proper service, scaffold a project and move your logic in: + +```bash +# 1. Run a cookbook locally +python cookbook/sepsis_cds_hooks.py + +# 2. Scaffold a project +healthchain new my-sepsis-service -t cds-hooks +cd my-sepsis-service + +# 3. Move your hook logic into app.py, then run with config +healthchain serve +``` + +`app.run()` (used in cookbooks) is a convenience wrapper — equivalent to running uvicorn directly. `healthchain serve` reads `healthchain.yaml` for port, TLS, and deployment settings, and prints a startup banner so you can see what's active at a glance. + +**What moves from your script into `healthchain.yaml`:** + +```python +# cookbook — everything hardcoded in Python +gateway = FHIRGateway() +gateway.add_source("medplum", FHIRAuthConfig.from_env("MEDPLUM").to_connection_string()) + +llm = ChatAnthropic(model="claude-opus-4-6", max_tokens=512) + +app = HealthChainAPI(title="My App", port=8000, service_type="fhir-gateway") +``` + +```yaml +# healthchain.yaml — port, sources, and LLM provider declared here +service: + type: fhir-gateway + port: 8000 + +sources: + medplum: + env_prefix: MEDPLUM # credentials stay in .env + +llm: + provider: anthropic + model: claude-opus-4-6 + max_tokens: 512 +``` + +```python +# app.py — load from config instead +from healthchain.config.appconfig import AppConfig +from healthchain.gateway import FHIRGateway, HealthChainAPI + +config = AppConfig.load() +gateway = FHIRGateway.from_config(config) +llm = config.llm.to_langchain() + +app = HealthChainAPI(title="My App") +``` + +Credentials (API keys, client secrets) always stay in `.env` — never in `healthchain.yaml`. + +!!! tip "Configuration reference" + See the [configuration reference](../reference/config.md) for all available settings — security, compliance, eval, and more. diff --git a/docs/cookbook/ml_model_deployment.md b/docs/cookbook/ml_model_deployment.md index 8d3fde0d..91f7efdd 100644 --- a/docs/cookbook/ml_model_deployment.md +++ b/docs/cookbook/ml_model_deployment.md @@ -355,8 +355,7 @@ Query patients from FHIR server → Run predictions → Write RiskAssessment bac Configure the [FHIRGateway](../reference/gateway/fhir_gateway.md) with your FHIR source: ```python -from fhir.resources.patient import Patient -from fhir.resources.observation import Observation +from healthchain.fhir.r4b import Patient, Observation from healthchain.gateway import FHIRGateway from healthchain.gateway.clients.fhir.base import FHIRAuthConfig from healthchain.fhir import merge_bundles @@ -513,5 +512,5 @@ Both patterns: - **Add more features**: Extend `sepsis_vitals.yaml` with lab values, medications, or other Observations - **Add more FHIR sources**: The gateway supports multiple sources—see the cookbook script for Epic sandbox configuration, or the [FHIR Sandbox Setup guide](./setup_fhir_sandboxes.md) - **Automate batch runs**: Schedule screening jobs with cron, Airflow, or cloud schedulers; or use [FHIR Subscriptions](https://www.hl7.org/fhir/subscription.html) to trigger on new ICU admissions ([PRs welcome!](https://github.com/dotimplement/HealthChain/pulls)) - - **Combine patterns**: Use batch screening to identify high-risk patients, then enable CDS - alerts for those patients + - **Combine patterns**: Use batch screening to identify high-risk patients, then enable CDS alerts for those patients + - **Go to production**: Scaffold a project with `healthchain new` and run with `healthchain serve` — see [From cookbook to service](./index.md#from-cookbook-to-service). diff --git a/docs/cookbook/multi_ehr_aggregation.md b/docs/cookbook/multi_ehr_aggregation.md index 2e03800d..0603b08c 100644 --- a/docs/cookbook/multi_ehr_aggregation.md +++ b/docs/cookbook/multi_ehr_aggregation.md @@ -123,13 +123,17 @@ def get_unified_patient(patient_id: str, sources: List[str]) -> Bundle: Register the gateway with [HealthChainAPI](../reference/gateway/api.md) to create REST endpoints. ```python -import uvicorn from healthchain.gateway import HealthChainAPI -app = HealthChainAPI() +app = HealthChainAPI( + title="Multi-EHR Data Aggregation", + description="Aggregate patient data from multiple FHIR sources", + port=8888, + service_type="fhir-gateway", +) app.register_gateway(gateway, path="/fhir") -uvicorn.run(app) +app.run() ``` !!! tip "FHIR Endpoints Provided by the Service" @@ -405,3 +409,4 @@ A production-ready data aggregation service with: - **Expand resource types**: Change `Condition` to `MedicationStatement`, `Observation`, or `Procedure` to aggregate different data. - **Add processing**: Extend the pipeline with terminology mapping, entity extraction, or quality checks. - **Build on it**: Use aggregated data in the [Clinical Coding tutorial](./clinical_coding.md) or feed it to your LLM application. + - **Go to production**: Scaffold a project with `healthchain new` and run with `healthchain serve` — see [From cookbook to service](./index.md#from-cookbook-to-service). diff --git a/docs/quickstart.md b/docs/quickstart.md index 30b3f2bd..492da870 100644 --- a/docs/quickstart.md +++ b/docs/quickstart.md @@ -18,7 +18,7 @@ The [**HealthChainAPI**](./reference/gateway/api.md) provides a unified interfac ```python from healthchain.gateway import HealthChainAPI, FHIRGateway -from fhir.resources.patient import Patient +from healthchain.fhir.r4b import Patient # Create your healthcare application app = HealthChainAPI(title="My Healthcare AI App") diff --git a/docs/reference/concepts.md b/docs/reference/concepts.md index 0dea45cb..58c6dd87 100644 --- a/docs/reference/concepts.md +++ b/docs/reference/concepts.md @@ -15,7 +15,7 @@ The [**HealthChainAPI**](./gateway/api.md) provides a unified interface for conn ```python from healthchain.gateway import HealthChainAPI, FHIRGateway -from fhir.resources.patient import Patient +from healthchain.fhir.r4b import Patient # Create your healthcare application app = HealthChainAPI(title="My Healthcare AI App") diff --git a/docs/reference/config.md b/docs/reference/config.md index 0fef4eba..6e6ebf8e 100644 --- a/docs/reference/config.md +++ b/docs/reference/config.md @@ -39,6 +39,17 @@ eval: site: name: "" environment: development + +# FHIR data sources — credentials stay in .env +# sources: +# medplum: +# env_prefix: MEDPLUM + +# LLM provider for LangChain-based pipelines +# llm: +# provider: anthropic +# model: claude-opus-4-6 +# max_tokens: 512 ``` --- @@ -116,3 +127,59 @@ The `card_feedback` event closes the evaluation loop — it provides implicit gr |-------|------|---------|-------------| | `name` | string | `""` | Hospital or organisation name — displayed in `healthchain status` | | `environment` | string | `development` | Deployment environment — `development`, `staging`, or `production` | + +--- + +## `sources` + +Declare FHIR data sources here. Credentials stay in environment variables — only source names and env prefixes are stored in config. + +```yaml +sources: + medplum: + env_prefix: MEDPLUM # reads MEDPLUM_CLIENT_ID, MEDPLUM_BASE_URL, etc. + epic: + env_prefix: EPIC +``` + +| Field | Type | Default | Description | +|-------|------|---------|-------------| +| `` | object | — | Arbitrary source name used in `gateway.search(..., source="")` | +| `.env_prefix` | string | — | Prefix for env vars: `{PREFIX}_CLIENT_ID`, `{PREFIX}_CLIENT_SECRET`, `{PREFIX}_BASE_URL`, `{PREFIX}_TOKEN_URL` | + +With sources declared, use `FHIRGateway.from_config()` instead of `gateway.add_source()`: + +```python +from healthchain.gateway import FHIRGateway +from healthchain.config.appconfig import AppConfig + +gateway = FHIRGateway.from_config(AppConfig.load()) +``` + +--- + +## `llm` + +LLM provider settings for LangChain-based pipelines. API key is read from the standard environment variable for each provider (`ANTHROPIC_API_KEY`, `OPENAI_API_KEY`, etc.). + +```yaml +llm: + provider: anthropic + model: claude-opus-4-6 + max_tokens: 512 +``` + +| Field | Type | Default | Description | +|-------|------|---------|-------------| +| `provider` | string | `anthropic` | LLM provider — `anthropic`, `openai`, or `google` | +| `model` | string | `claude-opus-4-6` | Model ID passed to the LangChain chat model | +| `max_tokens` | int | `512` | Maximum tokens for model response | + +Use `llm.to_langchain()` to instantiate the configured model: + +```python +from healthchain.config.appconfig import AppConfig + +config = AppConfig.load() +llm = config.llm.to_langchain() # returns ChatAnthropic / ChatOpenAI / ChatGoogleGenerativeAI +``` diff --git a/docs/reference/gateway/fhir_gateway.md b/docs/reference/gateway/fhir_gateway.md index 32f81e10..0de30f3d 100644 --- a/docs/reference/gateway/fhir_gateway.md +++ b/docs/reference/gateway/fhir_gateway.md @@ -22,7 +22,7 @@ Both handle the complexity of managing multiple FHIR clients and provide a consi ```python from healthchain.gateway import FHIRGateway -from fhir.resources.patient import Patient +from healthchain.fhir.r4b import Patient gateway = FHIRGateway() @@ -44,7 +44,7 @@ with gateway: import asyncio from healthchain.gateway import AsyncFHIRGateway -from fhir.resources.patient import Patient +from healthchain.fhir.r4b import Patient gateway = AsyncFHIRGateway() @@ -148,8 +148,7 @@ fhir://hostname:port/path?param1=value1¶m2=value2 ### Create Resources ```python -from fhir.resources.patient import Patient -from fhir.resources.humanname import HumanName +from healthchain.fhir.r4b import Patient, HumanName # Create a new patient patient = Patient( @@ -165,7 +164,7 @@ print(f"Created patient with ID: {created_patient.id}") ### Read Resources ```python -from fhir.resources.patient import Patient +from healthchain.fhir.r4b import Patient # Read a specific patient (Derrick Lin, Epic Sandbox) patient = gateway.read( @@ -179,7 +178,7 @@ patient = gateway.read( === "Sync" ```python - from fhir.resources.patient import Patient + from healthchain.fhir.r4b import Patient # Read, modify, and update (sync) patient = gateway.read(Patient, "123", "medplum") @@ -189,7 +188,7 @@ patient = gateway.read( === "Async" ```python - from fhir.resources.patient import Patient + from healthchain.fhir.r4b import Patient # Read, modify, and update (async) patient = await gateway.read(Patient, "123", "medplum") @@ -206,7 +205,7 @@ patient = gateway.read( ### Delete Resources ```python -from fhir.resources.patient import Patient +from healthchain.fhir.r4b import Patient # Delete a patient success = gateway.delete(Patient, "123", "medplum") @@ -219,8 +218,7 @@ if success: ### Basic Search ```python -from fhir.resources.patient import Patient -from fhir.resources.bundle import Bundle +from healthchain.fhir.r4b import Patient, Bundle # Search by name search_params = {"family": "Smith", "given": "John"} @@ -234,7 +232,7 @@ for entry in results.entry: ### Advanced Search ```python -from fhir.resources.patient import Patient +from healthchain.fhir.r4b import Patient # Complex search with multiple parameters search_params = { @@ -255,8 +253,7 @@ Transform handlers allow you to create custom API endpoints that process and enh === "Sync" ```python - from fhir.resources.patient import Patient - from fhir.resources.observation import Observation + from healthchain.fhir.r4b import Patient, Observation @fhir_gateway.transform(Patient) def get_enhanced_patient_summary(id: str, source: str = None) -> Patient: @@ -291,8 +288,7 @@ Transform handlers allow you to create custom API endpoints that process and enh === "Async" ```python - from fhir.resources.patient import Patient - from fhir.resources.observation import Observation + from healthchain.fhir.r4b import Patient, Observation @fhir_gateway.transform(Patient) async def get_enhanced_patient_summary(id: str, source: str = None) -> Patient: @@ -328,8 +324,7 @@ Aggregate handlers allow you to combine data from multiple FHIR sources into a s === "Sync" ```python - from fhir.resources.observation import Observation - from fhir.resources.bundle import Bundle + from healthchain.fhir.r4b import Observation, Bundle @gateway.aggregate(Observation) def aggregate_vitals(patient_id: str, sources: list = None) -> Bundle: @@ -357,8 +352,7 @@ Aggregate handlers allow you to combine data from multiple FHIR sources into a s === "Async" ```python - from fhir.resources.observation import Observation - from fhir.resources.bundle import Bundle + from healthchain.fhir.r4b import Observation, Bundle @gateway.aggregate(Observation) async def aggregate_vitals(patient_id: str, sources: list = None) -> Bundle: diff --git a/docs/reference/gateway/gateway.md b/docs/reference/gateway/gateway.md index 388efe92..8693363f 100644 --- a/docs/reference/gateway/gateway.md +++ b/docs/reference/gateway/gateway.md @@ -33,7 +33,7 @@ The Gateway handles the complex parts of healthcare integration: === "Sync" ```python from healthchain.gateway import HealthChainAPI, FHIRGateway - from fhir.resources.patient import Patient + from healthchain.fhir.r4b import Patient # Create the application app = HealthChainAPI() @@ -59,7 +59,7 @@ The Gateway handles the complex parts of healthcare integration: === "Async" ```python from healthchain.gateway import HealthChainAPI, AsyncFHIRGateway - from fhir.resources.patient import Patient + from healthchain.fhir.r4b import Patient # Create the application app = HealthChainAPI() diff --git a/docs/reference/interop/generators.md b/docs/reference/interop/generators.md index 8d49d17d..ee2a36b0 100644 --- a/docs/reference/interop/generators.md +++ b/docs/reference/interop/generators.md @@ -203,7 +203,7 @@ from healthchain.interop.template_registry import TemplateRegistry from healthchain.interop.generators import BaseGenerator from typing import List -from fhir.resources.resource import Resource +from fhir.resources.R4B.resource import Resource class CustomGenerator(BaseGenerator): diff --git a/docs/reference/utilities/fhir_helpers.md b/docs/reference/utilities/fhir_helpers.md index 40cedbca..5a7b90fe 100644 --- a/docs/reference/utilities/fhir_helpers.md +++ b/docs/reference/utilities/fhir_helpers.md @@ -4,80 +4,34 @@ The `fhir` module provides a set of helper functions to make it easier for you t ## FHIR Version Support -HealthChain supports multiple FHIR versions: **R5** (default), **R4B**, and **STU3**. All resource creation and helper functions accept an optional `version` parameter. - -### Supported Versions - -| Version | Description | Package Path | -|---------|-------------|--------------| -| **R5** | FHIR Release 5 (default) | `fhir.resources.*` | -| **R4B** | FHIR R4B (Ballot) | `fhir.resources.R4B.*` | -| **STU3** | FHIR STU3 | `fhir.resources.STU3.*` | - -### Basic Usage +HealthChain uses **R4B by default** — this matches most production EHRs. For most use cases, import from `healthchain.fhir.r4b` and don't think about versions: ```python -from healthchain.fhir import ( - FHIRVersion, - get_fhir_resource, - set_default_version, - fhir_version_context, - convert_resource, - create_condition, -) - -# Get a resource class for a specific version -Patient_R4B = get_fhir_resource("Patient", "R4B") -Patient_R5 = get_fhir_resource("Patient", FHIRVersion.R5) - -# Create resources with a specific version -condition_r4b = create_condition( - subject="Patient/123", - code="38341003", - display="Hypertension", - version="R4B" # Creates R4B Condition -) - -# Set the default version for the session -set_default_version("R4B") - -# Use context manager for temporary version changes -with fhir_version_context("STU3"): - # All resources created here use STU3 - condition = create_condition(subject="Patient/123", code="123") +from healthchain.fhir.r4b import Patient, Condition, Bundle ``` -### Version Conversion - -Convert resources between FHIR versions using `convert_resource()`: +FHIR resource classes are provided by the [`fhir.resources`](https://github.com/nazrulworld/fhir.resources) library. R4B is a minor ballot update to R4 with no breaking changes to core resources — it is compatible with R4 FHIR servers in practice. -```python -from healthchain.fhir import get_fhir_resource, convert_resource - -# Create an R5 Patient -Patient_R5 = get_fhir_resource("Patient") -patient_r5 = Patient_R5(id="test-123", gender="male") - -# Convert to R4B -patient_r4b = convert_resource(patient_r5, "R4B") -print(patient_r4b.__class__.__module__) # fhir.resources.R4B.patient -``` - -!!! warning "Version Conversion Limitations" - The `convert_resource()` function uses a serialize/deserialize approach. Field mappings between FHIR versions may not be 1:1 - some fields may be added, removed, or renamed between versions. Complex resources with version-specific fields may require manual handling. +!!! note "Scope of version utilities" + The version utilities below apply to dynamic resource loading and cross-version conversion. The `create_*` helpers always produce R4B resources. The FHIR gateway client always deserializes server responses as R4B. -### Version Detection +### Explicit version control -Detect the FHIR version of an existing resource: +For cases where you need to load or convert resources in a specific version: ```python -from healthchain.fhir import get_resource_version, get_fhir_resource +from healthchain.fhir import get_fhir_resource, fhir_version_context, convert_resource +# Get a resource class for a specific version Patient_R4B = get_fhir_resource("Patient", "R4B") -patient = Patient_R4B(id="123") +Patient_R5 = get_fhir_resource("Patient", "R5") + +# Temporarily switch the default version for get_fhir_resource calls +with fhir_version_context("STU3"): + PatientSTU3 = get_fhir_resource("Patient") -version = get_resource_version(patient) -print(version) # FHIRVersion.R4B +# Convert between versions (serialize/deserialize — not lossless across major field renames) +patient_r5 = convert_resource(patient_r4b, "R5") ``` ### API Reference @@ -85,12 +39,10 @@ print(version) # FHIRVersion.R4B | Function | Description | |----------|-------------| | `get_fhir_resource(name, version)` | Get a resource class for a specific version | -| `get_default_version()` | Get the current default FHIR version | -| `set_default_version(version)` | Set the global default FHIR version | -| `reset_default_version()` | Reset to library default (R5) | -| `fhir_version_context(version)` | Context manager for temporary version changes | -| `convert_resource(resource, version)` | Convert a resource to a different version | -| `get_resource_version(resource)` | Detect the version of an existing resource | +| `get_default_version()` | Returns the current default version (R4B) | +| `fhir_version_context(version)` | Context manager for temporarily switching the default version | +| `convert_resource(resource, version)` | Convert a resource to a different version (best-effort, not lossless) | +| `get_resource_version(resource)` | Detect the FHIR version of an existing resource | --- @@ -172,14 +124,6 @@ condition = create_condition( system="http://snomed.info/sct", ) -# Create an R4B condition -condition_r4b = create_condition( - subject="Patient/123", - code="38341003", - display="Hypertension", - version="R4B", # Optional: specify FHIR version -) - # Output the created resource print(condition.model_dump()) ``` @@ -244,14 +188,12 @@ print(medication.model_dump()) "resourceType": "MedicationStatement", "id": "hc-86a26eba-63f9-4017-b7b2-5b36f9bad5f1", "status": "recorded", - "medication": { - "concept": { - "coding": [{ - "system": "http://www.nlm.nih.gov/research/umls/rxnorm", - "code": "1049221", - "display": "Acetaminophen 325 MG Oral Tablet" - }] - } + "medicationCodeableConcept": { + "coding": [{ + "system": "http://www.nlm.nih.gov/research/umls/rxnorm", + "code": "1049221", + "display": "Acetaminophen 325 MG Oral Tablet" + }] }, "subject": { "reference": "Patient/123" @@ -362,12 +304,12 @@ print(doc_ref.model_dump()) ## Utilities -### set_problem_list_item_category() +### set_condition_category() Sets the category of a [**Condition**](https://www.hl7.org/fhir/condition.html) resource to "`problem-list-item`". ```python -from healthchain.fhir import set_problem_list_item_category, create_condition +from healthchain.fhir import set_condition_category, create_condition # Create a condition and set it as a problem list item problem_list_item = create_condition( @@ -376,7 +318,7 @@ problem_list_item = create_condition( display="Hypertension" ) -set_problem_list_item_category(problem_list_item) +set_condition_category(problem_list_item) # Output the modified resource print(problem_list_item.model_dump()) @@ -556,7 +498,7 @@ from healthchain.fhir import get_resources conditions = get_resources(bundle, "Condition") # Or using the resource type directly -from fhir.resources.condition import Condition +from healthchain.fhir.r4b import Condition conditions = get_resources(bundle, Condition) for condition in conditions: diff --git a/docs/tutorials/clinicalflow/fhir-basics.md b/docs/tutorials/clinicalflow/fhir-basics.md index 8d25c052..db4fd4e6 100644 --- a/docs/tutorials/clinicalflow/fhir-basics.md +++ b/docs/tutorials/clinicalflow/fhir-basics.md @@ -72,7 +72,7 @@ HealthChain provides utilities to work with FHIR resources easily: ```python from healthchain.fhir import create_condition, create_patient -from fhir.resources.patient import Patient +from healthchain.fhir.r4b import Patient # Create a patient with basic demographics # Note: create_patient generates an auto-prefixed ID (e.g., "hc-abc123") @@ -97,7 +97,7 @@ print(f"With condition: {condition.code.coding[0].display}") ??? info "FHIR Versions in HealthChain" - HealthChain uses **FHIR R5** as the default version. However, **STU3** and **R4B** are also supported for compatibility with different EHR systems. + HealthChain uses **FHIR R4B** as the default version, matching most production EHRs (Epic, Cerner, etc.). **R5** and **STU3** are also supported. You can specify the version when working with FHIR resources, and HealthChain provides utilities for converting between versions when needed. @@ -108,7 +108,7 @@ print(f"With condition: {condition.code.coding[0].display}") When an EHR sends patient context, it often comes as a **Bundle** - a collection of related resources: ```python -from fhir.resources.bundle import Bundle +from healthchain.fhir.r4b import Bundle # A bundle might contain a patient, their conditions, and medications bundle_data = { diff --git a/healthchain/configs/templates/cda_fhir/medication_statement.liquid b/healthchain/configs/templates/cda_fhir/medication_statement.liquid index 9d31f1ab..dda04ca1 100644 --- a/healthchain/configs/templates/cda_fhir/medication_statement.liquid +++ b/healthchain/configs/templates/cda_fhir/medication_statement.liquid @@ -2,14 +2,12 @@ "resourceType": "MedicationStatement", {% assign substance_admin = entry.substanceAdministration %} "status": "{{ substance_admin.statusCode['@code'] | map_status: 'cda_to_fhir' }}", - "medication": { - "concept": { - "coding": [{ - "system": "{{ substance_admin.consumable.manufacturedProduct.manufacturedMaterial.code['@codeSystem'] | map_system: 'cda_to_fhir' }}", - "code": "{{ substance_admin.consumable.manufacturedProduct.manufacturedMaterial.code['@code'] }}", - "display": "{{ substance_admin.consumable.manufacturedProduct.manufacturedMaterial.code['@displayName'] }}" - }] - } + "medicationCodeableConcept": { + "coding": [{ + "system": "{{ substance_admin.consumable.manufacturedProduct.manufacturedMaterial.code['@codeSystem'] | map_system: 'cda_to_fhir' }}", + "code": "{{ substance_admin.consumable.manufacturedProduct.manufacturedMaterial.code['@code'] }}", + "display": "{{ substance_admin.consumable.manufacturedProduct.manufacturedMaterial.code['@displayName'] }}" + }] } {% comment %}Process effectiveTime and extract period/timing information if exists{% endcomment %} diff --git a/healthchain/configs/templates/fhir_cda/medication_entry.liquid b/healthchain/configs/templates/fhir_cda/medication_entry.liquid index 65f00954..29051495 100644 --- a/healthchain/configs/templates/fhir_cda/medication_entry.liquid +++ b/healthchain/configs/templates/fhir_cda/medication_entry.liquid @@ -71,9 +71,9 @@ ], "manufacturedMaterial": { "code": { - "@code": "{{ resource.medication.concept.coding[0].code }}", - "@codeSystem": "{{ resource.medication.concept.coding[0].system | map_system: 'fhir_to_cda' }}", - "@displayName": "{{ resource.medication.concept.coding[0].display }}", + "@code": "{{ resource.medicationCodeableConcept.coding[0].code }}", + "@codeSystem": "{{ resource.medicationCodeableConcept.coding[0].system | map_system: 'fhir_to_cda' }}", + "@displayName": "{{ resource.medicationCodeableConcept.coding[0].display }}", "originalText": { "reference": {"@value": "{{ text_reference_name }}"} } diff --git a/healthchain/fhir/__init__.py b/healthchain/fhir/__init__.py index da59f240..565d6431 100644 --- a/healthchain/fhir/__init__.py +++ b/healthchain/fhir/__init__.py @@ -1,11 +1,11 @@ """FHIR utilities for HealthChain.""" +from healthchain.fhir import r4b + from healthchain.fhir.version import ( FHIRVersion, get_fhir_resource, get_default_version, - set_default_version, - reset_default_version, fhir_version_context, convert_resource, get_resource_version, @@ -64,12 +64,12 @@ ) __all__ = [ + # R4B re-export module + "r4b", # Version management "FHIRVersion", "get_fhir_resource", "get_default_version", - "set_default_version", - "reset_default_version", "fhir_version_context", "convert_resource", "get_resource_version", diff --git a/healthchain/fhir/bundlehelpers.py b/healthchain/fhir/bundlehelpers.py index 2bfbda7b..35cc7dcf 100644 --- a/healthchain/fhir/bundlehelpers.py +++ b/healthchain/fhir/bundlehelpers.py @@ -8,13 +8,10 @@ - extract_*(): extract resources from a bundle """ -from typing import List, Type, TypeVar, Optional, Union, TYPE_CHECKING -from fhir.resources.bundle import Bundle, BundleEntry +from typing import List, Type, TypeVar, Optional, Union +from fhir.resources.R4B.bundle import Bundle, BundleEntry from fhir.resources.resource import Resource -if TYPE_CHECKING: - from healthchain.fhir.version import FHIRVersion - T = TypeVar("T", bound=Resource) @@ -48,22 +45,19 @@ def add_resource( def get_resource_type( resource_type: Union[str, Type[Resource]], - version: Optional[Union["FHIRVersion", str]] = None, ) -> Type[Resource]: """Get the resource type class from string or type. Args: resource_type: String name of the resource type (e.g. "Condition") or the type itself - version: Optional FHIR version (e.g., "R4B", "STU3", or FHIRVersion enum). - If None, uses the current default version. Returns: - The resource type class for the specified version + The R4B resource type class Raises: ValueError: If the resource type is not supported or cannot be imported """ - if isinstance(resource_type, type) and issubclass(resource_type, Resource): + if isinstance(resource_type, type): return resource_type if not isinstance(resource_type, str): @@ -71,10 +65,9 @@ def get_resource_type( f"Resource type must be a string or Resource class, got {type(resource_type)}" ) - # Use version manager for dynamic import with version support from healthchain.fhir.version import get_fhir_resource - return get_fhir_resource(resource_type, version) + return get_fhir_resource(resource_type) def get_resources( @@ -100,11 +93,19 @@ def get_resources( >>> from fhir.resources.condition import Condition >>> conditions = get_resources(bundle, Condition) """ + if isinstance(resource_type, str): + type_name = resource_type + return [ + entry.resource + for entry in (bundle.entry or []) + if entry.resource is not None and type(entry.resource).__name__ == type_name + ] type_class = get_resource_type(resource_type) + type_name = type_class.__name__ return [ entry.resource for entry in (bundle.entry or []) - if isinstance(entry.resource, type_class) + if entry.resource is not None and entry.resource.__class__.__name__ == type_name ] @@ -136,24 +137,44 @@ def set_resources( >>> from fhir.resources.condition import Condition >>> set_resources(bundle, [condition1, condition2], Condition) """ - type_class = get_resource_type(resource_type) - # Remove existing resources of this type if replace=True if replace: - bundle.entry = [ - entry - for entry in (bundle.entry or []) - if not isinstance(entry.resource, type_class) - ] - - # Add new resources - for resource in resources: - if not isinstance(resource, type_class): - raise ValueError( - f"Resource must be of type {type_class.__name__}, " - f"got {type(resource).__name__}" - ) - add_resource(bundle, resource) + if isinstance(resource_type, str): + type_name = resource_type + bundle.entry = [ + entry + for entry in (bundle.entry or []) + if entry.resource is None or type(entry.resource).__name__ != type_name + ] + else: + type_class = get_resource_type(resource_type) + type_name_cls = type_class.__name__ + bundle.entry = [ + entry + for entry in (bundle.entry or []) + if entry.resource is None + or entry.resource.__class__.__name__ != type_name_cls + ] + + # Add new resources, validating type + if isinstance(resource_type, str): + type_name = resource_type + for resource in resources: + if type(resource).__name__ != type_name: + raise ValueError( + f"Resource must be of type {type_name}, " + f"got {type(resource).__name__}" + ) + add_resource(bundle, resource) + else: + type_class = get_resource_type(resource_type) + for resource in resources: + if resource.__class__.__name__ != type_class.__name__: + raise ValueError( + f"Resource must be of type {type_class.__name__}, " + f"got {type(resource).__name__}" + ) + add_resource(bundle, resource) def merge_bundles( @@ -242,17 +263,26 @@ def extract_resources( if not bundle or not bundle.entry: return [] - type_class = get_resource_type(resource_type) - extracted: List[Resource] = [] remaining_entries: List[BundleEntry] = [] - for entry in bundle.entry: - resource = entry.resource - if isinstance(resource, type_class): - extracted.append(resource) - continue - remaining_entries.append(entry) + if isinstance(resource_type, str): + type_name = resource_type + for entry in bundle.entry: + resource = entry.resource + if resource is not None and type(resource).__name__ == type_name: + extracted.append(resource) + continue + remaining_entries.append(entry) + else: + type_class = get_resource_type(resource_type) + type_name_cls = type_class.__name__ + for entry in bundle.entry: + resource = entry.resource + if resource is not None and resource.__class__.__name__ == type_name_cls: + extracted.append(resource) + continue + remaining_entries.append(entry) bundle.entry = remaining_entries return extracted diff --git a/healthchain/fhir/dataframe.py b/healthchain/fhir/dataframe.py index 0c9bfdb2..29c1551c 100644 --- a/healthchain/fhir/dataframe.py +++ b/healthchain/fhir/dataframe.py @@ -11,7 +11,7 @@ from typing import Any, Dict, List, Union, Optional, Literal from collections import defaultdict -from fhir.resources.bundle import Bundle +from fhir.resources.R4B.bundle import Bundle from pydantic import BaseModel, field_validator, ConfigDict from healthchain.fhir.utilities import ( @@ -33,7 +33,7 @@ "Observation": { "handler": "_flatten_observations", "description": "Clinical observations (vitals, labs)", - "output_columns": "Dynamic based on observation codes", + "output_columns": "Dynamic: obs_{code}_{display}", "options": ["aggregation"], }, "Condition": { @@ -408,7 +408,7 @@ def bundle_to_dataframe( # Get handler function by name handler_name = handler_info["handler"] - handler = globals()[handler_name] + handler = _HANDLERS[handler_name] # Call handler with standardized signature features = handler(resources, config) @@ -504,8 +504,8 @@ def _flatten_observations( values = [item["value"] for item in obs_list] display = obs_list[0]["display"] - # Create column name: code_display - col_name = f"{code}_{display.replace(' ', '_')}" + # Create column name: obs_code_display + col_name = f"obs_{code}_{display.replace(' ', '_')}" # Aggregate values if aggregation == "mean": @@ -576,11 +576,7 @@ def _flatten_medications( features = {} for med in medications: - medication = _get_field(med, "medication") - if not medication: - continue - - med_concept = _get_field(medication, "concept") + med_concept = _get_field(med, "medicationCodeableConcept") if not med_concept: continue @@ -598,3 +594,12 @@ def _flatten_medications( features[col_name] = 1 return features + + +# Populated after all handler functions are defined. +_HANDLERS = { + "_flatten_patient": _flatten_patient, + "_flatten_observations": _flatten_observations, + "_flatten_conditions": _flatten_conditions, + "_flatten_medications": _flatten_medications, +} diff --git a/healthchain/fhir/elementhelpers.py b/healthchain/fhir/elementhelpers.py index 9f86811f..b431d269 100644 --- a/healthchain/fhir/elementhelpers.py +++ b/healthchain/fhir/elementhelpers.py @@ -8,10 +8,11 @@ import base64 import datetime -from typing import Optional, List, Dict, Any, Union, TYPE_CHECKING +from typing import Optional, List, Dict, Any -if TYPE_CHECKING: - from healthchain.fhir.version import FHIRVersion +from fhir.resources.R4B.codeableconcept import CodeableConcept +from fhir.resources.R4B.coding import Coding +from fhir.resources.R4B.attachment import Attachment logger = logging.getLogger(__name__) @@ -20,7 +21,6 @@ def create_single_codeable_concept( code: str, display: Optional[str] = None, system: Optional[str] = "http://snomed.info/sct", - version: Optional[Union["FHIRVersion", str]] = None, ) -> Any: """ Create a minimal FHIR CodeableConcept with a single coding. @@ -29,16 +29,10 @@ def create_single_codeable_concept( code: REQUIRED. The code value from the code system display: The display name for the code system: The code system (default: SNOMED CT) - version: FHIR version to use (e.g., "R4B", "STU3"). Defaults to current default. Returns: CodeableConcept: A FHIR CodeableConcept resource with a single coding """ - from healthchain.fhir.version import get_fhir_resource - - CodeableConcept = get_fhir_resource("CodeableConcept", version) - Coding = get_fhir_resource("Coding", version) - return CodeableConcept(coding=[Coding(system=system, code=code, display=display)]) @@ -47,7 +41,6 @@ def create_single_reaction( display: Optional[str] = None, system: Optional[str] = "http://snomed.info/sct", severity: Optional[str] = None, - version: Optional[Union["FHIRVersion", str]] = None, ) -> List[Dict[str, Any]]: """Create a minimal FHIR Reaction with a single coding. @@ -60,26 +53,17 @@ def create_single_reaction( display: The display name for the manifestation code system: The code system for the manifestation code (default: SNOMED CT) severity: The severity of the reaction (mild, moderate, severe) - version: FHIR version to use (e.g., "R4B", "STU3"). Defaults to current default. Returns: A list containing a single FHIR Reaction dictionary with manifestation and severity fields """ - from healthchain.fhir.version import get_fhir_resource - - CodeableConcept = get_fhir_resource("CodeableConcept", version) - CodeableReference = get_fhir_resource("CodeableReference", version) - Coding = get_fhir_resource("Coding", version) + concept = CodeableConcept( + coding=[Coding(system=system, code=code, display=display)] + ) return [ { - "manifestation": [ - CodeableReference( - concept=CodeableConcept( - coding=[Coding(system=system, code=code, display=display)] - ) - ) - ], + "manifestation": [concept], "severity": severity, } ] @@ -90,7 +74,6 @@ def create_single_attachment( data: Optional[str] = None, url: Optional[str] = None, title: Optional[str] = "Attachment created by HealthChain", - version: Optional[Union["FHIRVersion", str]] = None, ) -> Any: """Create a minimal FHIR Attachment. @@ -102,15 +85,10 @@ def create_single_attachment( data: The actual data content to be base64 encoded url: The URL where the data can be found title: A title for the attachment (default: "Attachment created by HealthChain") - version: FHIR version to use (e.g., "R4B", "STU3"). Defaults to current default. Returns: Attachment: A FHIR Attachment resource with basic metadata and content """ - from healthchain.fhir.version import get_fhir_resource - - Attachment = get_fhir_resource("Attachment", version) - if not data and not url: logger.warning("No data or url provided for attachment") diff --git a/healthchain/fhir/r4b.py b/healthchain/fhir/r4b.py new file mode 100644 index 00000000..b5ee1c49 --- /dev/null +++ b/healthchain/fhir/r4b.py @@ -0,0 +1,75 @@ +"""Version-aware re-exports for FHIR R4B resources. + +Import FHIR R4B resource classes from here instead of fhir.resources directly: + + from healthchain.fhir.r4b import Condition, Patient, Appointment + +This ensures version consistency with HealthChain's FHIR version management. +""" + +from healthchain.fhir.version import get_fhir_resource as _get + + +def __getattr__(name: str): + if name.startswith("__"): + raise AttributeError(name) + try: + return _get(name, "R4B") + except ValueError: + raise AttributeError(f"module 'healthchain.fhir.r4b' has no attribute {name!r}") + + +__all__ = [ # noqa: F822 + # Clinical resources + "AllergyIntolerance", + "Appointment", + "CarePlan", + "CareTeam", + "Condition", + "DiagnosticReport", + "DocumentReference", + "Encounter", + "FamilyMemberHistory", + "Goal", + "Immunization", + "MedicationRequest", + "MedicationStatement", + "Observation", + "Procedure", + "Questionnaire", + "QuestionnaireResponse", + "RiskAssessment", + "ServiceRequest", + "Specimen", + # Administrative resources + "Device", + "Location", + "Organization", + "Patient", + "Practitioner", + "Task", + # Infrastructure resources + "Bundle", + "BundleEntry", + "CapabilityStatement", + "OperationOutcome", + "Provenance", + "ProvenanceAgent", + # Element types + "Address", + "Annotation", + "Attachment", + "CodeableConcept", + "Coding", + "ContactPoint", + "Dosage", + "Extension", + "HumanName", + "Identifier", + "Meta", + "Period", + "Quantity", + "Range", + "Reference", + "Timing", +] diff --git a/healthchain/fhir/r4b.pyi b/healthchain/fhir/r4b.pyi new file mode 100644 index 00000000..3310b6ca --- /dev/null +++ b/healthchain/fhir/r4b.pyi @@ -0,0 +1,57 @@ +from fhir.resources.R4B.address import Address as Address +from fhir.resources.R4B.allergyintolerance import ( + AllergyIntolerance as AllergyIntolerance, +) +from fhir.resources.R4B.annotation import Annotation as Annotation +from fhir.resources.R4B.appointment import Appointment as Appointment +from fhir.resources.R4B.attachment import Attachment as Attachment +from fhir.resources.R4B.bundle import Bundle as Bundle, BundleEntry as BundleEntry +from fhir.resources.R4B.capabilitystatement import ( + CapabilityStatement as CapabilityStatement, +) +from fhir.resources.R4B.careplan import CarePlan as CarePlan +from fhir.resources.R4B.careteam import CareTeam as CareTeam +from fhir.resources.R4B.codeableconcept import CodeableConcept as CodeableConcept +from fhir.resources.R4B.coding import Coding as Coding +from fhir.resources.R4B.condition import Condition as Condition +from fhir.resources.R4B.contactpoint import ContactPoint as ContactPoint +from fhir.resources.R4B.device import Device as Device +from fhir.resources.R4B.diagnosticreport import DiagnosticReport as DiagnosticReport +from fhir.resources.R4B.documentreference import DocumentReference as DocumentReference +from fhir.resources.R4B.dosage import Dosage as Dosage +from fhir.resources.R4B.encounter import Encounter as Encounter +from fhir.resources.R4B.extension import Extension as Extension +from fhir.resources.R4B.familymemberhistory import ( + FamilyMemberHistory as FamilyMemberHistory, +) +from fhir.resources.R4B.goal import Goal as Goal +from fhir.resources.R4B.humanname import HumanName as HumanName +from fhir.resources.R4B.identifier import Identifier as Identifier +from fhir.resources.R4B.immunization import Immunization as Immunization +from fhir.resources.R4B.location import Location as Location +from fhir.resources.R4B.medicationrequest import MedicationRequest as MedicationRequest +from fhir.resources.R4B.medicationstatement import ( + MedicationStatement as MedicationStatement, +) +from fhir.resources.R4B.meta import Meta as Meta +from fhir.resources.R4B.observation import Observation as Observation +from fhir.resources.R4B.operationoutcome import OperationOutcome as OperationOutcome +from fhir.resources.R4B.organization import Organization as Organization +from fhir.resources.R4B.patient import Patient as Patient +from fhir.resources.R4B.period import Period as Period +from fhir.resources.R4B.practitioner import Practitioner as Practitioner +from fhir.resources.R4B.procedure import Procedure as Procedure +from fhir.resources.R4B.provenance import Provenance as Provenance +from fhir.resources.R4B.provenance import ProvenanceAgent as ProvenanceAgent +from fhir.resources.R4B.quantity import Quantity as Quantity +from fhir.resources.R4B.questionnaire import Questionnaire as Questionnaire +from fhir.resources.R4B.questionnaireresponse import ( + QuestionnaireResponse as QuestionnaireResponse, +) +from fhir.resources.R4B.range import Range as Range +from fhir.resources.R4B.reference import Reference as Reference +from fhir.resources.R4B.riskassessment import RiskAssessment as RiskAssessment +from fhir.resources.R4B.servicerequest import ServiceRequest as ServiceRequest +from fhir.resources.R4B.specimen import Specimen as Specimen +from fhir.resources.R4B.task import Task as Task +from fhir.resources.R4B.timing import Timing as Timing diff --git a/healthchain/fhir/readers.py b/healthchain/fhir/readers.py index 00c2ad4f..0dac97a4 100644 --- a/healthchain/fhir/readers.py +++ b/healthchain/fhir/readers.py @@ -10,7 +10,7 @@ from typing import Optional, Dict, Any, List from fhir.resources.resource import Resource -from fhir.resources.documentreference import DocumentReference +from fhir.resources.R4B.documentreference import DocumentReference logger = logging.getLogger(__name__) @@ -65,7 +65,7 @@ def create_resource_from_dict( """ try: resource_module = importlib.import_module( - f"fhir.resources.{resource_type.lower()}" + f"fhir.resources.R4B.{resource_type.lower()}" ) resource_class = getattr(resource_module, resource_type) return resource_class(**resource_dict) @@ -126,8 +126,6 @@ def convert_prefetch_to_fhir_objects( >>> isinstance(fhir_objects["patient"], Patient) # True >>> isinstance(fhir_objects["condition"], Condition) # True """ - from fhir.resources import get_fhir_model_class - result: Dict[str, Resource] = {} for key, resource_data in prefetch_dict.items(): @@ -138,7 +136,10 @@ def convert_prefetch_to_fhir_objects( try: # Fix timezone-naive datetimes before validation fixed_data = _fix_timezone_naive_datetimes(resource_data) - resource_class = get_fhir_model_class(resource_type) + resource_module = importlib.import_module( + f"fhir.resources.R4B.{resource_type.lower()}" + ) + resource_class = getattr(resource_module, resource_type) result[key] = resource_class(**fixed_data) except Exception as e: logger.warning( diff --git a/healthchain/fhir/resourcehelpers.py b/healthchain/fhir/resourcehelpers.py index dcac457e..3407f704 100644 --- a/healthchain/fhir/resourcehelpers.py +++ b/healthchain/fhir/resourcehelpers.py @@ -13,12 +13,19 @@ import logging import datetime -from typing import List, Optional, Dict, Any, Union, TYPE_CHECKING - -# Keep static imports only for types that are always version-compatible -# and used in signatures/type hints -from fhir.resources.codeableconcept import CodeableConcept -from fhir.resources.reference import Reference +from typing import List, Optional, Dict, Any + +from fhir.resources.R4B.allergyintolerance import AllergyIntolerance +from fhir.resources.R4B.condition import Condition +from fhir.resources.R4B.documentreference import DocumentReference +from fhir.resources.R4B.identifier import Identifier +from fhir.resources.R4B.medicationstatement import MedicationStatement +from fhir.resources.R4B.observation import Observation +from fhir.resources.R4B.patient import Patient +from fhir.resources.R4B.quantity import Quantity +from fhir.resources.R4B.reference import Reference +from fhir.resources.R4B.riskassessment import RiskAssessment +from fhir.resources.R4B.codeableconcept import CodeableConcept from healthchain.fhir.elementhelpers import ( create_single_codeable_concept, @@ -26,9 +33,6 @@ ) from healthchain.fhir.utilities import _generate_id -if TYPE_CHECKING: - from healthchain.fhir.version import FHIRVersion - logger = logging.getLogger(__name__) @@ -38,8 +42,7 @@ def create_condition( code: Optional[str] = None, display: Optional[str] = None, system: Optional[str] = "http://snomed.info/sct", - version: Optional[Union["FHIRVersion", str]] = None, -) -> Any: +) -> Condition: """ Create a minimal active FHIR Condition. If you need to create a more complex condition, use the FHIR Condition resource directly. @@ -51,35 +54,25 @@ def create_condition( code: The condition code display: The display name for the condition system: The code system (default: SNOMED CT) - version: FHIR version to use (e.g., "R4B", "STU3"). Defaults to current default. Returns: Condition: A FHIR Condition resource with an auto-generated ID prefixed with 'hc-' """ - from healthchain.fhir.version import get_fhir_resource - - Condition = get_fhir_resource("Condition", version) - ReferenceClass = get_fhir_resource("Reference", version) - - if code: - condition_code = create_single_codeable_concept(code, display, system, version) - else: - condition_code = None + condition_code = ( + create_single_codeable_concept(code, display, system) if code else None + ) - condition = Condition( + return Condition( id=_generate_id(), - subject=ReferenceClass(reference=subject), + subject=Reference(reference=subject), clinicalStatus=create_single_codeable_concept( code=clinical_status, display=clinical_status.capitalize(), system="http://terminology.hl7.org/CodeSystem/condition-clinical", - version=version, ), code=condition_code, ) - return condition - def create_medication_statement( subject: str, @@ -87,8 +80,7 @@ def create_medication_statement( code: Optional[str] = None, display: Optional[str] = None, system: Optional[str] = "http://snomed.info/sct", - version: Optional[Union["FHIRVersion", str]] = None, -) -> Any: +) -> MedicationStatement: """ Create a minimal recorded FHIR MedicationStatement. If you need to create a more complex medication statement, use the FHIR MedicationStatement resource directly. @@ -100,40 +92,28 @@ def create_medication_statement( code: The medication code display: The display name for the medication system: The code system (default: SNOMED CT) - version: FHIR version to use (e.g., "R4B", "STU3"). Defaults to current default. Returns: MedicationStatement: A FHIR MedicationStatement resource with an auto-generated ID prefixed with 'hc-' """ - from healthchain.fhir.version import get_fhir_resource - - MedicationStatement = get_fhir_resource("MedicationStatement", version) - ReferenceClass = get_fhir_resource("Reference", version) - - if code: - medication_concept = create_single_codeable_concept( - code, display, system, version - ) - else: - medication_concept = None + medication_concept = ( + create_single_codeable_concept(code, display, system) if code else None + ) - medication = MedicationStatement( + return MedicationStatement( id=_generate_id(), - subject=ReferenceClass(reference=subject), + subject=Reference(reference=subject), status=status, - medication={"concept": medication_concept}, + medicationCodeableConcept=medication_concept, ) - return medication - def create_allergy_intolerance( patient: str, code: Optional[str] = None, display: Optional[str] = None, system: Optional[str] = "http://snomed.info/sct", - version: Optional[Union["FHIRVersion", str]] = None, -) -> Any: +) -> AllergyIntolerance: """ Create a minimal active FHIR AllergyIntolerance. If you need to create a more complex allergy intolerance, use the FHIR AllergyIntolerance resource directly. @@ -144,29 +124,20 @@ def create_allergy_intolerance( code: The allergen code display: The display name for the allergen system: The code system (default: SNOMED CT) - version: FHIR version to use (e.g., "R4B", "STU3"). Defaults to current default. Returns: AllergyIntolerance: A FHIR AllergyIntolerance resource with an auto-generated ID prefixed with 'hc-' """ - from healthchain.fhir.version import get_fhir_resource - - AllergyIntolerance = get_fhir_resource("AllergyIntolerance", version) - ReferenceClass = get_fhir_resource("Reference", version) - - if code: - allergy_code = create_single_codeable_concept(code, display, system, version) - else: - allergy_code = None + allergy_code = ( + create_single_codeable_concept(code, display, system) if code else None + ) - allergy = AllergyIntolerance( + return AllergyIntolerance( id=_generate_id(), - patient=ReferenceClass(reference=patient), + patient=Reference(reference=patient), code=allergy_code, ) - return allergy - def create_value_quantity_observation( code: str, @@ -177,8 +148,7 @@ def create_value_quantity_observation( system: str = "http://loinc.org", display: Optional[str] = None, effective_datetime: Optional[str] = None, - version: Optional[Union["FHIRVersion", str]] = None, -) -> Any: +) -> Observation: """ Create a minimal FHIR Observation for vital signs or laboratory values. If you need to create a more complex observation, use the FHIR Observation resource directly. @@ -193,29 +163,21 @@ def create_value_quantity_observation( display: The display name for the observation code effective_datetime: When the observation was made (ISO format). Uses current time if not provided. subject: Reference to the patient (e.g. "Patient/123") - version: FHIR version to use (e.g., "R4B", "STU3"). Defaults to current default. Returns: Observation: A FHIR Observation resource with an auto-generated ID prefixed with 'hc-' """ - from healthchain.fhir.version import get_fhir_resource - - Observation = get_fhir_resource("Observation", version) - ReferenceClass = get_fhir_resource("Reference", version) - Quantity = get_fhir_resource("Quantity", version) - if not effective_datetime: effective_datetime = datetime.datetime.now(datetime.timezone.utc).strftime( "%Y-%m-%dT%H:%M:%S%z" ) - subject_ref = None - if subject is not None: - subject_ref = ReferenceClass(reference=subject) - observation = Observation( + subject_ref = Reference(reference=subject) if subject is not None else None + + return Observation( id=_generate_id(), status=status, - code=create_single_codeable_concept(code, display, system, version), + code=create_single_codeable_concept(code, display, system), subject=subject_ref, effectiveDateTime=effective_datetime, valueQuantity=Quantity( @@ -223,16 +185,13 @@ def create_value_quantity_observation( ), ) - return observation - def create_patient( gender: Optional[str] = None, birth_date: Optional[str] = None, identifier: Optional[str] = None, identifier_system: Optional[str] = "http://hospital.example.org", - version: Optional[Union["FHIRVersion", str]] = None, -) -> Any: +) -> Patient: """ Create a minimal FHIR Patient resource with basic gender and birthdate If you need to create a more complex patient, use the FHIR Patient resource directly @@ -243,19 +202,11 @@ def create_patient( birth_date: Birth date in YYYY-MM-DD format identifier: Optional identifier value for the patient (e.g., MRN) identifier_system: The system for the identifier (default: "http://hospital.example.org") - version: FHIR version to use (e.g., "R4B", "STU3"). Defaults to current default. Returns: Patient: A FHIR Patient resource with an auto-generated ID prefixed with 'hc-' """ - from healthchain.fhir.version import get_fhir_resource - - Patient = get_fhir_resource("Patient", version) - Identifier = get_fhir_resource("Identifier", version) - - patient_id = _generate_id() - - patient_data: Dict[str, Any] = {"id": patient_id} + patient_data: Dict[str, Any] = {"id": _generate_id()} if birth_date: patient_data["birthDate"] = birth_date @@ -265,14 +216,10 @@ def create_patient( if identifier: patient_data["identifier"] = [ - Identifier( - system=identifier_system, - value=identifier, - ) + Identifier(system=identifier_system, value=identifier) ] - patient = Patient(**patient_data) - return patient + return Patient(**patient_data) def create_risk_assessment_from_prediction( @@ -283,8 +230,7 @@ def create_risk_assessment_from_prediction( basis: Optional[List[Reference]] = None, comment: Optional[str] = None, occurrence_datetime: Optional[str] = None, - version: Optional[Union["FHIRVersion", str]] = None, -) -> Any: +) -> RiskAssessment: """ Create a FHIR RiskAssessment from ML model prediction output. If you need to create a more complex risk assessment, use the FHIR RiskAssessment resource directly. @@ -301,7 +247,6 @@ def create_risk_assessment_from_prediction( basis: Optional list of References to observations or other resources used as input comment: Optional text comment about the assessment occurrence_datetime: When the assessment was made (ISO format). Uses current time if not provided. - version: FHIR version to use (e.g., "R4B", "STU3"). Defaults to current default. Returns: RiskAssessment: A FHIR RiskAssessment resource with an auto-generated ID prefixed with 'hc-' @@ -314,11 +259,6 @@ def create_risk_assessment_from_prediction( ... } >>> risk = create_risk_assessment("Patient/123", prediction) """ - from healthchain.fhir.version import get_fhir_resource - - RiskAssessment = get_fhir_resource("RiskAssessment", version) - ReferenceClass = get_fhir_resource("Reference", version) - if not occurrence_datetime: occurrence_datetime = datetime.datetime.now(datetime.timezone.utc).strftime( "%Y-%m-%dT%H:%M:%S%z" @@ -330,14 +270,11 @@ def create_risk_assessment_from_prediction( code=outcome["code"], display=outcome.get("display"), system=outcome.get("system", "http://snomed.info/sct"), - version=version, ) else: outcome_concept = outcome - prediction_data: Dict[str, Any] = { - "outcome": outcome_concept, - } + prediction_data: Dict[str, Any] = {"outcome": outcome_concept} if "probability" in prediction: prediction_data["probabilityDecimal"] = prediction["probability"] @@ -347,13 +284,12 @@ def create_risk_assessment_from_prediction( code=prediction["qualitative_risk"], display=prediction["qualitative_risk"].capitalize(), system="http://terminology.hl7.org/CodeSystem/risk-probability", - version=version, ) risk_assessment_data: Dict[str, Any] = { "id": _generate_id(), "status": status, - "subject": ReferenceClass(reference=subject), + "subject": Reference(reference=subject), "occurrenceDateTime": occurrence_datetime, "prediction": [prediction_data], } @@ -367,9 +303,7 @@ def create_risk_assessment_from_prediction( if comment: risk_assessment_data["note"] = [{"text": comment}] - risk_assessment = RiskAssessment(**risk_assessment_data) - - return risk_assessment + return RiskAssessment(**risk_assessment_data) def create_document_reference( @@ -379,8 +313,7 @@ def create_document_reference( status: str = "current", description: Optional[str] = "DocumentReference created by HealthChain", attachment_title: Optional[str] = "Attachment created by HealthChain", - version: Optional[Union["FHIRVersion", str]] = None, -) -> Any: +) -> DocumentReference: """ Create a minimal FHIR DocumentReference. If you need to create a more complex document reference, use the FHIR DocumentReference resource directly. @@ -393,16 +326,11 @@ def create_document_reference( status: REQUIRED. Status of the document reference (default: current) description: Description of the document reference attachment_title: Title for the document attachment - version: FHIR version to use (e.g., "R4B", "STU3"). Defaults to current default. Returns: DocumentReference: A FHIR DocumentReference resource with an auto-generated ID prefixed with 'hc-' """ - from healthchain.fhir.version import get_fhir_resource - - DocumentReference = get_fhir_resource("DocumentReference", version) - - document_reference = DocumentReference( + return DocumentReference( id=_generate_id(), status=status, date=datetime.datetime.now(datetime.timezone.utc).strftime( @@ -416,14 +344,11 @@ def create_document_reference( data=data, url=url, title=attachment_title, - version=version, ) } ], ) - return document_reference - def create_document_reference_content( attachment_data: Optional[str] = None, @@ -431,7 +356,6 @@ def create_document_reference_content( content_type: str = "text/plain", language: Optional[str] = "en-US", title: Optional[str] = None, - version: Optional[Union["FHIRVersion", str]] = None, **kwargs, ) -> Dict[str, Any]: """Create a FHIR DocumentReferenceContent object. @@ -446,7 +370,6 @@ def create_document_reference_content( content_type: MIME type (e.g., 'text/plain', 'text/html', 'application/pdf') (default: text/plain) language: Language code (default: en-US) title: Optional title for the content (default: "Attachment created by HealthChain") - version: FHIR version to use (e.g., "R4B", "STU3"). Defaults to current default. **kwargs: Additional DocumentReferenceContent fields (e.g., format, profile) Returns: @@ -487,12 +410,9 @@ def create_document_reference_content( data=attachment_data, url=url, title=title, - version=version, ) - content: Dict[str, Any] = { - "attachment": attachment, - } + content: Dict[str, Any] = {"attachment": attachment} if language: content["language"] = language @@ -503,17 +423,15 @@ def create_document_reference_content( def set_condition_category( - condition: Any, + condition: Condition, category: str, - version: Optional[Union["FHIRVersion", str]] = None, -) -> Any: +) -> Condition: """ Set the category of a FHIR Condition to either 'problem-list-item' or 'encounter-diagnosis'. Args: condition: The FHIR Condition resource to modify category: The category to set. Must be 'problem-list-item' or 'encounter-diagnosis'. - version: FHIR version to use. If None, attempts to detect from the condition resource. Returns: Condition: The modified FHIR Condition resource with the specified category set @@ -521,11 +439,11 @@ def set_condition_category( Raises: ValueError: If the category is not one of the allowed values. """ - from healthchain.fhir.version import get_resource_version + from healthchain.fhir.version import get_fhir_resource, get_resource_version - # Detect version from resource if not provided - if version is None: - version = get_resource_version(condition) + version = get_resource_version(condition) + CodeableConceptCls = get_fhir_resource("CodeableConcept", version) + CodingCls = get_fhir_resource("Coding", version) allowed_categories = { "problem-list-item": { @@ -544,11 +462,14 @@ def set_condition_category( cat_info = allowed_categories[category] condition.category = [ - create_single_codeable_concept( - code=cat_info["code"], - display=cat_info["display"], - system="http://terminology.hl7.org/CodeSystem/condition-category", - version=version, + CodeableConceptCls( + coding=[ + CodingCls( + system="http://terminology.hl7.org/CodeSystem/condition-category", + code=cat_info["code"], + display=cat_info["display"], + ) + ] ) ] return condition @@ -559,7 +480,6 @@ def add_provenance_metadata( source: str, tag_code: Optional[str] = None, tag_display: Optional[str] = None, - version: Optional[Union["FHIRVersion", str]] = None, ) -> Any: """Add provenance metadata to a FHIR resource. @@ -571,7 +491,6 @@ def add_provenance_metadata( source: Name of the source system (e.g., "epic", "cerner") tag_code: Optional tag code for processing operations (e.g., "aggregated", "deduplicated") tag_display: Optional display text for the tag - version: FHIR version to use. If None, attempts to detect from the resource. Returns: Resource: The resource with added provenance metadata @@ -582,29 +501,22 @@ def add_provenance_metadata( """ from healthchain.fhir.version import get_fhir_resource, get_resource_version - # Detect version from resource if not provided - if version is None: - version = get_resource_version(resource) - - Meta = get_fhir_resource("Meta", version) - Coding = get_fhir_resource("Coding", version) + version = get_resource_version(resource) + MetaCls = get_fhir_resource("Meta", version) + CodingCls = get_fhir_resource("Coding", version) if not resource.meta: - resource.meta = Meta() + resource.meta = MetaCls() - # Add source system identifier resource.meta.source = f"urn:healthchain:source:{source}" - - # Update timestamp resource.meta.lastUpdated = datetime.datetime.now(datetime.timezone.utc).isoformat() - # Add processing tag if provided if tag_code: if not resource.meta.tag: resource.meta.tag = [] resource.meta.tag.append( - Coding( + CodingCls( system="https://dotimplement.github.io/HealthChain/fhir/tags", code=tag_code, display=tag_display or tag_code, @@ -615,12 +527,11 @@ def add_provenance_metadata( def add_coding_to_codeable_concept( - codeable_concept: Any, + codeable_concept: CodeableConcept, code: str, system: str, display: Optional[str] = None, - version: Optional[Union["FHIRVersion", str]] = None, -) -> Any: +) -> CodeableConcept: """Add a coding to an existing CodeableConcept. Useful for adding standardized codes (e.g., SNOMED CT) to resources that already @@ -631,7 +542,6 @@ def add_coding_to_codeable_concept( code: The code value from the code system system: The code system URI display: Optional display text for the code - version: FHIR version to use. If None, attempts to detect from the CodeableConcept. Returns: CodeableConcept: The updated CodeableConcept with the new coding added @@ -648,15 +558,12 @@ def add_coding_to_codeable_concept( """ from healthchain.fhir.version import get_fhir_resource, get_resource_version - # Detect version from CodeableConcept if not provided - if version is None: - version = get_resource_version(codeable_concept) - - Coding = get_fhir_resource("Coding", version) + version = get_resource_version(codeable_concept) + CodingCls = get_fhir_resource("Coding", version) if not codeable_concept.coding: codeable_concept.coding = [] - codeable_concept.coding.append(Coding(system=system, code=code, display=display)) + codeable_concept.coding.append(CodingCls(system=system, code=code, display=display)) return codeable_concept diff --git a/healthchain/fhir/version.py b/healthchain/fhir/version.py index 19dd314e..1c399c4c 100644 --- a/healthchain/fhir/version.py +++ b/healthchain/fhir/version.py @@ -123,9 +123,9 @@ def get_default_version() -> FHIRVersion: """Get the current default FHIR version. Returns: - The current default FHIRVersion (R5 if not explicitly set) + The current default FHIRVersion (R4B if not explicitly set) """ - return _default_version or FHIRVersion.R5 + return _default_version or FHIRVersion.R4B def set_default_version(version: Union[FHIRVersion, str]) -> None: @@ -144,10 +144,10 @@ def set_default_version(version: Union[FHIRVersion, str]) -> None: def reset_default_version() -> None: - """Reset the default FHIR version to library default (R5).""" + """Reset the default FHIR version to R4B.""" global _default_version _default_version = None - logger.debug("Default FHIR version reset to R5") + logger.debug("Default FHIR version reset to R4B") @contextmanager diff --git a/healthchain/gateway/clients/fhir/aio/client.py b/healthchain/gateway/clients/fhir/aio/client.py index f190c370..a3c93da7 100644 --- a/healthchain/gateway/clients/fhir/aio/client.py +++ b/healthchain/gateway/clients/fhir/aio/client.py @@ -3,8 +3,8 @@ from typing import Any, Dict, Type, Union -from fhir.resources.bundle import Bundle -from fhir.resources.capabilitystatement import CapabilityStatement +from fhir.resources.R4B.bundle import Bundle +from fhir.resources.R4B.capabilitystatement import CapabilityStatement from fhir.resources.resource import Resource from healthchain.gateway.clients.auth import AsyncOAuth2TokenManager diff --git a/healthchain/gateway/clients/fhir/base.py b/healthchain/gateway/clients/fhir/base.py index b733476f..e4f641bc 100644 --- a/healthchain/gateway/clients/fhir/base.py +++ b/healthchain/gateway/clients/fhir/base.py @@ -7,8 +7,8 @@ from typing import Any, Dict, Optional, Type, Union from urllib.parse import urlencode, urljoin -from fhir.resources.bundle import Bundle -from fhir.resources.capabilitystatement import CapabilityStatement +from fhir.resources.R4B.bundle import Bundle +from fhir.resources.R4B.capabilitystatement import CapabilityStatement from fhir.resources.resource import Resource from healthchain.gateway.clients.auth import OAuth2Config @@ -302,7 +302,7 @@ def _resolve_resource_type( else: # It's a string, need to dynamically import type_name = str(resource_type) - module_name = f"fhir.resources.{type_name.lower()}" + module_name = f"fhir.resources.R4B.{type_name.lower()}" module = __import__(module_name, fromlist=[type_name]) resource_class = getattr(module, type_name) diff --git a/healthchain/gateway/clients/fhir/sync/client.py b/healthchain/gateway/clients/fhir/sync/client.py index 5d92d41c..99c5fd02 100644 --- a/healthchain/gateway/clients/fhir/sync/client.py +++ b/healthchain/gateway/clients/fhir/sync/client.py @@ -3,8 +3,8 @@ from typing import Any, Dict, Type, Union -from fhir.resources.bundle import Bundle -from fhir.resources.capabilitystatement import CapabilityStatement +from fhir.resources.R4B.bundle import Bundle +from fhir.resources.R4B.capabilitystatement import CapabilityStatement from fhir.resources.resource import Resource from healthchain.gateway.clients.auth import OAuth2TokenManager diff --git a/healthchain/gateway/fhir/aio.py b/healthchain/gateway/fhir/aio.py index 44c849bb..ba9e900a 100644 --- a/healthchain/gateway/fhir/aio.py +++ b/healthchain/gateway/fhir/aio.py @@ -3,8 +3,8 @@ from contextlib import asynccontextmanager from typing import Any, Dict, Optional, Type -from fhir.resources.bundle import Bundle -from fhir.resources.capabilitystatement import CapabilityStatement +from fhir.resources.R4B.bundle import Bundle +from fhir.resources.R4B.capabilitystatement import CapabilityStatement from fhir.resources.resource import Resource from healthchain.gateway.clients.fhir.base import FHIRServerInterface diff --git a/healthchain/gateway/fhir/base.py b/healthchain/gateway/fhir/base.py index 07705c7a..e3e9a2d0 100644 --- a/healthchain/gateway/fhir/base.py +++ b/healthchain/gateway/fhir/base.py @@ -4,10 +4,13 @@ from fastapi import Depends, HTTPException, Path, Query from datetime import datetime -from typing import Any, Callable, Dict, List, Type, TypeVar, Optional +from typing import TYPE_CHECKING, Any, Callable, Dict, List, Type, TypeVar, Optional from fastapi.responses import JSONResponse -from fhir.resources.capabilitystatement import CapabilityStatement +if TYPE_CHECKING: + from healthchain.config.appconfig import AppConfig + +from fhir.resources.R4B.capabilitystatement import CapabilityStatement from fhir.resources.resource import Resource from healthchain.gateway.clients.fhir.base import FHIRServerInterface @@ -440,6 +443,33 @@ async def handler( return handler + @classmethod + def from_config(cls, config: "AppConfig") -> "BaseFHIRGateway": + """ + Create a gateway with sources pre-registered from healthchain.yaml. + + Credentials stay in environment variables (via env_prefix); only + source names and prefixes are declared in config. + + Args: + config: AppConfig loaded from healthchain.yaml + + Returns: + A gateway instance with all configured sources added + + Example: + # healthchain.yaml: + # sources: + # medplum: + # env_prefix: MEDPLUM + gateway = FHIRGateway.from_config(AppConfig.load()) + """ + gateway = cls() + for name, source in config.sources.items(): + auth_config = source.to_fhir_auth_config() + gateway.add_source(name, auth_config.to_connection_string()) + return gateway + def add_source(self, name: str, connection_string: str) -> None: """ Add a FHIR data source using connection string with OAuth2.0 flow. diff --git a/healthchain/gateway/fhir/sync.py b/healthchain/gateway/fhir/sync.py index 6a2bf464..47beff9f 100644 --- a/healthchain/gateway/fhir/sync.py +++ b/healthchain/gateway/fhir/sync.py @@ -2,8 +2,8 @@ from typing import Any, Dict, Type, Optional -from fhir.resources.bundle import Bundle -from fhir.resources.capabilitystatement import CapabilityStatement +from fhir.resources.R4B.bundle import Bundle +from fhir.resources.R4B.capabilitystatement import CapabilityStatement from fhir.resources.resource import Resource from healthchain.gateway.clients.fhir.base import FHIRServerInterface diff --git a/healthchain/interop/engine.py b/healthchain/interop/engine.py index db7258d5..972778af 100644 --- a/healthchain/interop/engine.py +++ b/healthchain/interop/engine.py @@ -5,7 +5,7 @@ from pathlib import Path from fhir.resources.resource import Resource -from fhir.resources.bundle import Bundle +from fhir.resources.R4B.bundle import Bundle from pydantic import BaseModel from healthchain.config.base import ValidationLevel @@ -27,7 +27,11 @@ def normalize_resource_list( resources: Union[Resource, List[Resource], Bundle], ) -> List[Resource]: """Convert input resources to a normalized list format""" - if isinstance(resources, Bundle): + # Check for Bundle using duck typing to support both R4B and R5 bundles + if isinstance(resources, Bundle) or ( + hasattr(resources, "__resource_type__") + and resources.__resource_type__ == "Bundle" + ): return [entry.resource for entry in resources.entry if entry.resource] elif isinstance(resources, list): return resources diff --git a/healthchain/io/adapters/cdaadapter.py b/healthchain/io/adapters/cdaadapter.py index 91d5d456..10da6920 100644 --- a/healthchain/io/adapters/cdaadapter.py +++ b/healthchain/io/adapters/cdaadapter.py @@ -12,10 +12,6 @@ create_document_reference, read_content_attachment, ) -from fhir.resources.condition import Condition -from fhir.resources.medicationstatement import MedicationStatement -from fhir.resources.allergyintolerance import AllergyIntolerance -from fhir.resources.documentreference import DocumentReference log = logging.getLogger(__name__) @@ -109,14 +105,15 @@ def parse(self, cda_request: CdaRequest) -> Document: allergy_list = [] for resource in fhir_resources: - if isinstance(resource, Condition): + resource_type_name = resource.__class__.__name__ + if resource_type_name == "Condition": problem_list.append(resource) set_condition_category(resource, "problem-list-item") - elif isinstance(resource, MedicationStatement): + elif resource_type_name == "MedicationStatement": medication_list.append(resource) - elif isinstance(resource, AllergyIntolerance): + elif resource_type_name == "AllergyIntolerance": allergy_list.append(resource) - elif isinstance(resource, DocumentReference): + elif resource_type_name == "DocumentReference": if ( resource.content and resource.content[0].attachment diff --git a/healthchain/io/adapters/cdsfhiradapter.py b/healthchain/io/adapters/cdsfhiradapter.py index 42d36572..cf51ebfb 100644 --- a/healthchain/io/adapters/cdsfhiradapter.py +++ b/healthchain/io/adapters/cdsfhiradapter.py @@ -1,7 +1,7 @@ import logging from typing import Optional, Any -from fhir.resources.documentreference import DocumentReference +from fhir.resources.R4B.documentreference import DocumentReference from healthchain.io.containers import Document from healthchain.io.adapters.base import BaseAdapter diff --git a/healthchain/io/containers/dataset.py b/healthchain/io/containers/dataset.py index 20a4e36c..806023b9 100644 --- a/healthchain/io/containers/dataset.py +++ b/healthchain/io/containers/dataset.py @@ -5,8 +5,8 @@ from pathlib import Path from typing import Any, Dict, Iterator, List, Union, Optional -from fhir.resources.bundle import Bundle -from fhir.resources.riskassessment import RiskAssessment +from fhir.resources.R4B.bundle import Bundle +from fhir.resources.R4B.riskassessment import RiskAssessment from healthchain.io.containers.base import DataContainer from healthchain.io.containers.featureschema import FeatureSchema diff --git a/healthchain/io/containers/document.py b/healthchain/io/containers/document.py index 81f09d2b..ab398198 100644 --- a/healthchain/io/containers/document.py +++ b/healthchain/io/containers/document.py @@ -6,17 +6,17 @@ from spacy.tokens import Doc as SpacyDoc from spacy.tokens import Span -from fhir.resources.condition import Condition -from fhir.resources.medicationstatement import MedicationStatement -from fhir.resources.allergyintolerance import AllergyIntolerance -from fhir.resources.bundle import Bundle -from fhir.resources.documentreference import DocumentReference -from fhir.resources.resource import Resource -from fhir.resources.reference import Reference -from fhir.resources.documentreference import DocumentReferenceRelatesTo -from fhir.resources.operationoutcome import OperationOutcome -from fhir.resources.provenance import Provenance -from fhir.resources.patient import Patient +from fhir.resources.R4B.condition import Condition +from fhir.resources.R4B.medicationstatement import MedicationStatement +from fhir.resources.R4B.allergyintolerance import AllergyIntolerance +from fhir.resources.R4B.bundle import Bundle +from fhir.resources.R4B.documentreference import DocumentReference +from fhir_core.fhirabstractmodel import FHIRAbstractModel as Resource +from fhir.resources.R4B.reference import Reference +from fhir.resources.R4B.documentreference import DocumentReferenceRelatesTo +from fhir.resources.R4B.operationoutcome import OperationOutcome +from fhir.resources.R4B.provenance import Provenance +from fhir.resources.R4B.patient import Patient from healthchain.io.containers.base import BaseDocument from healthchain.models.responses import Action, Card @@ -26,7 +26,6 @@ get_resources, set_resources, extract_resources, - create_single_codeable_concept, read_content_attachment, create_condition, set_condition_category, @@ -421,14 +420,17 @@ def get_prefetch_resources(self, key: str) -> List[Any]: return [] return self._prefetch_resources.get(key, []) - def get_resources(self, resource_type: str) -> List[Any]: + def get_resources(self, resource_type: Union[str, type]) -> List[Any]: """Get resources of a specific type from the working bundle.""" if not self._bundle: return [] return get_resources(self._bundle, resource_type) def add_resources( - self, resources: List[Any], resource_type: str, replace: bool = False + self, + resources: List[Any], + resource_type: Union[str, type], + replace: bool = False, ): """Add resources to the working bundle.""" if not self._bundle: @@ -470,11 +472,7 @@ def add_document_reference( document.relatesTo.append( DocumentReferenceRelatesTo( target=Reference(reference=f"DocumentReference/{parent_id}"), - code=create_single_codeable_concept( - code=relationship_type, - display=relationship_type.capitalize(), - system="http://hl7.org/fhir/ValueSet/document-relationship-type", - ), + code=relationship_type, ) ) @@ -756,17 +754,20 @@ def __post_init__(self): """ super().__post_init__() - # Handle FHIR Bundle data - if isinstance(self.data, Bundle): + # Handle FHIR Bundle data (check both R4B and R5 via resource type) + if isinstance(self.data, Bundle) or ( + hasattr(self.data, "__resource_type__") + and self.data.__resource_type__ == "Bundle" + ): self._fhir._bundle = self.data # Extract OperationOutcome resources (operation results/errors) - outcomes = extract_resources(self._fhir._bundle, "OperationOutcome") + outcomes = extract_resources(self._fhir._bundle, OperationOutcome) if outcomes: self._fhir._operation_outcomes = outcomes # Extract Provenance resources (data lineage/origin) - provenances = extract_resources(self._fhir._bundle, "Provenance") + provenances = extract_resources(self._fhir._bundle, Provenance) if provenances: self._fhir._provenances = provenances diff --git a/healthchain/io/mappers/fhirfeaturemapper.py b/healthchain/io/mappers/fhirfeaturemapper.py index 24eba60f..509e9959 100644 --- a/healthchain/io/mappers/fhirfeaturemapper.py +++ b/healthchain/io/mappers/fhirfeaturemapper.py @@ -8,7 +8,7 @@ import pandas as pd import numpy as np -from fhir.resources.bundle import Bundle +from fhir.resources.R4B.bundle import Bundle from healthchain.io.containers.featureschema import FeatureSchema from healthchain.io.mappers.base import BaseMapper @@ -124,10 +124,10 @@ def _map_columns_to_schema(self, df: pd.DataFrame) -> pd.DataFrame: # Map observation columns obs_features = self.schema.get_features_by_resource("Observation") for feature_name, mapping in obs_features.items(): - # Generic converter creates columns like: "8867-4_Heart_rate" + # Generic converter creates columns like: "obs_8867-4_Heart_rate" # Find matching column in df for col in df.columns: - if col.startswith(mapping.code): + if col.startswith(f"obs_{mapping.code}"): rename_map[col] = feature_name break diff --git a/healthchain/sandbox/generators/basegenerators.py b/healthchain/sandbox/generators/basegenerators.py index e8a15ee5..556fed21 100644 --- a/healthchain/sandbox/generators/basegenerators.py +++ b/healthchain/sandbox/generators/basegenerators.py @@ -7,8 +7,8 @@ from faker import Faker -from fhir.resources.codeableconcept import CodeableConcept -from fhir.resources.coding import Coding +from fhir.resources.R4B.codeableconcept import CodeableConcept +from fhir.resources.R4B.coding import Coding faker = Faker() diff --git a/healthchain/sandbox/generators/conditiongenerators.py b/healthchain/sandbox/generators/conditiongenerators.py index 09dd6354..96c89b58 100644 --- a/healthchain/sandbox/generators/conditiongenerators.py +++ b/healthchain/sandbox/generators/conditiongenerators.py @@ -1,8 +1,8 @@ from typing import Optional from faker import Faker -from fhir.resources.reference import Reference -from fhir.resources.condition import ConditionStage, ConditionParticipant +from fhir.resources.R4B.reference import Reference +from fhir.resources.R4B.condition import ConditionStage from healthchain.fhir import create_single_codeable_concept, create_condition from healthchain.sandbox.generators.basegenerators import ( @@ -101,16 +101,6 @@ def generate(): ) -@register_generator -class ConditionParticipantGenerator(BaseGenerator): - @staticmethod - def generate(): - return ConditionParticipant( - type=generator_registry.get("CodeableConceptGenerator").generate(), - individual=generator_registry.get("ReferenceGenerator").generate(), - ) - - @register_generator class ConditionGenerator(BaseGenerator): @staticmethod diff --git a/healthchain/sandbox/generators/encountergenerators.py b/healthchain/sandbox/generators/encountergenerators.py index 133364f4..1d166a9f 100644 --- a/healthchain/sandbox/generators/encountergenerators.py +++ b/healthchain/sandbox/generators/encountergenerators.py @@ -1,12 +1,12 @@ from typing import Optional from faker import Faker -from fhir.resources.encounter import Encounter, EncounterLocation +from fhir.resources.R4B.encounter import Encounter, EncounterLocation -from fhir.resources.coding import Coding -from fhir.resources.codeableconcept import CodeableConcept -from fhir.resources.period import Period -from fhir.resources.reference import Reference +from fhir.resources.R4B.coding import Coding +from fhir.resources.R4B.codeableconcept import CodeableConcept +from fhir.resources.R4B.period import Period +from fhir.resources.R4B.reference import Reference from healthchain.sandbox.generators.basegenerators import ( BaseGenerator, generator_registry, @@ -157,10 +157,10 @@ def generate( "cancelled", ) ), - class_fhir=[generator_registry.get("ClassGenerator").generate()], + class_fhir=generator_registry.get("ClassGenerator").generate().coding[0], priority=generator_registry.get("EncounterPriorityGenerator").generate(), type=[generator_registry.get("EncounterTypeGenerator").generate()], subject={"reference": patient_reference, "display": patient_reference}, - actualPeriod=generator_registry.get("PeriodGenerator").generate(), + period=generator_registry.get("PeriodGenerator").generate(), location=[generator_registry.get("EncounterLocationGenerator").generate()], ) diff --git a/healthchain/sandbox/generators/medicationadministrationgenerators.py b/healthchain/sandbox/generators/medicationadministrationgenerators.py index ef42355a..056ac0eb 100644 --- a/healthchain/sandbox/generators/medicationadministrationgenerators.py +++ b/healthchain/sandbox/generators/medicationadministrationgenerators.py @@ -1,10 +1,9 @@ from typing import Optional from faker import Faker -from fhir.resources.medicationadministration import MedicationAdministration -from fhir.resources.medicationadministration import MedicationAdministrationDosage -from fhir.resources.reference import Reference -from fhir.resources.codeablereference import CodeableReference +from fhir.resources.R4B.medicationadministration import MedicationAdministration +from fhir.resources.R4B.medicationadministration import MedicationAdministrationDosage +from fhir.resources.R4B.reference import Reference from healthchain.sandbox.generators.basegenerators import ( BaseGenerator, generator_registry, @@ -40,14 +39,12 @@ def generate( return MedicationAdministration( id=generator_registry.get("IdGenerator").generate(), status=generator_registry.get("EventStatusGenerator").generate(), - occurenceDateTime=generator_registry.get("DateGenerator").generate(), - medication=CodeableReference( - concept=generator_registry.get( - "MedicationRequestContainedGenerator" - ).generate() - ), + effectiveDateTime=generator_registry.get("DateGenerator").generate(), + medicationCodeableConcept=generator_registry.get( + "MedicationRequestContainedGenerator" + ).generate(), subject=Reference(reference=subject_reference), - encounter=Reference(reference=encounter_reference), + context=Reference(reference=encounter_reference), dosage=generator_registry.get( "MedicationAdministrationDosageGenerator" ).generate(), diff --git a/healthchain/sandbox/generators/medicationrequestgenerators.py b/healthchain/sandbox/generators/medicationrequestgenerators.py index a1c48cfb..4df2bc37 100644 --- a/healthchain/sandbox/generators/medicationrequestgenerators.py +++ b/healthchain/sandbox/generators/medicationrequestgenerators.py @@ -10,10 +10,9 @@ from healthchain.sandbox.generators.value_sets.medicationcodes import ( MedicationRequestMedication, ) -from fhir.resources.medicationrequest import MedicationRequest -from fhir.resources.dosage import Dosage -from fhir.resources.reference import Reference -from fhir.resources.codeablereference import CodeableReference +from fhir.resources.R4B.medicationrequest import MedicationRequest +from fhir.resources.R4B.dosage import Dosage +from fhir.resources.R4B.reference import Reference faker = Faker() @@ -51,11 +50,9 @@ def generate( id=generator_registry.get("IdGenerator").generate(), status=generator_registry.get("EventStatusGenerator").generate(), intent=generator_registry.get("IntentGenerator").generate(), - medication=CodeableReference( - concept=generator_registry.get( - "MedicationRequestContainedGenerator" - ).generate() - ), + medicationCodeableConcept=generator_registry.get( + "MedicationRequestContainedGenerator" + ).generate(), subject=Reference(reference=subject_reference), encounter=Reference(reference=encounter_reference), authoredOn=generator_registry.get("DateTimeGenerator").generate(), diff --git a/healthchain/sandbox/generators/patientgenerators.py b/healthchain/sandbox/generators/patientgenerators.py index f14c9ca6..5a90091b 100644 --- a/healthchain/sandbox/generators/patientgenerators.py +++ b/healthchain/sandbox/generators/patientgenerators.py @@ -9,13 +9,13 @@ from datetime import datetime -from fhir.resources.humanname import HumanName -from fhir.resources.contactpoint import ContactPoint -from fhir.resources.address import Address -from fhir.resources.period import Period -from fhir.resources.codeableconcept import CodeableConcept -from fhir.resources.coding import Coding -from fhir.resources.patient import Patient +from fhir.resources.R4B.humanname import HumanName +from fhir.resources.R4B.contactpoint import ContactPoint +from fhir.resources.R4B.address import Address +from fhir.resources.R4B.period import Period +from fhir.resources.R4B.codeableconcept import CodeableConcept +from fhir.resources.R4B.coding import Coding +from fhir.resources.R4B.patient import Patient faker = Faker() diff --git a/healthchain/sandbox/generators/practitionergenerators.py b/healthchain/sandbox/generators/practitionergenerators.py index 284184a5..bba1712c 100644 --- a/healthchain/sandbox/generators/practitionergenerators.py +++ b/healthchain/sandbox/generators/practitionergenerators.py @@ -7,13 +7,12 @@ register_generator, ) -from fhir.resources.practitioner import ( +from fhir.resources.R4B.practitioner import ( Practitioner, - PractitionerCommunication, PractitionerQualification, ) -from fhir.resources.codeableconcept import CodeableConcept -from fhir.resources.coding import Coding +from fhir.resources.R4B.codeableconcept import CodeableConcept +from fhir.resources.R4B.coding import Coding faker = Faker() @@ -93,11 +92,8 @@ def generate(): class Practitioner_CommunicationGenerator(BaseGenerator): @staticmethod def generate(): - return PractitionerCommunication( - id=faker.uuid4(), - language=generator_registry.get("LanguageGenerator").generate(), - preferred=True, - ) + # R4B Practitioner.communication is List[CodeableConcept] directly + return generator_registry.get("LanguageGenerator").generate() @register_generator @@ -116,7 +112,5 @@ def generate(constraints: Optional[list] = None): qualification=[ generator_registry.get("Practitioner_QualificationGenerator").generate() ], - communication=[ - generator_registry.get("Practitioner_CommunicationGenerator").generate() - ], + communication=[generator_registry.get("LanguageGenerator").generate()], ) diff --git a/healthchain/sandbox/generators/proceduregenerators.py b/healthchain/sandbox/generators/proceduregenerators.py index a16f0ba6..2785e7d2 100644 --- a/healthchain/sandbox/generators/proceduregenerators.py +++ b/healthchain/sandbox/generators/proceduregenerators.py @@ -11,8 +11,8 @@ ProcedureCodeSimple, ProcedureCodeComplex, ) -from fhir.resources.procedure import Procedure -from fhir.resources.reference import Reference +from fhir.resources.R4B.procedure import Procedure +from fhir.resources.R4B.reference import Reference faker = Faker() @@ -56,5 +56,5 @@ def generate( code=code, subject=Reference(reference=subject_reference), encounter=Reference(reference=encounter_reference), - occurrencePeriod=generator_registry.get("PeriodGenerator").generate(), + performedPeriod=generator_registry.get("PeriodGenerator").generate(), ) diff --git a/tests/conftest.py b/tests/conftest.py index ee9e4b47..b1fcfb8d 100644 --- a/tests/conftest.py +++ b/tests/conftest.py @@ -21,7 +21,10 @@ create_single_reaction, ) -from fhir.resources.documentreference import DocumentReference, DocumentReferenceContent +from fhir.resources.R4B.documentreference import ( + DocumentReference, + DocumentReferenceContent, +) @pytest.fixture @@ -258,7 +261,7 @@ def doc_ref_without_content(): Returns: fhir.resources.documentreference.DocumentReference: An incomplete DocumentReference resource. """ - from fhir.resources.attachment import Attachment + from fhir.resources.R4B.attachment import Attachment return DocumentReference( status="current", diff --git a/tests/fhir/test_bundle_helpers.py b/tests/fhir/test_bundle_helpers.py index cfc89c4e..2585c6a3 100644 --- a/tests/fhir/test_bundle_helpers.py +++ b/tests/fhir/test_bundle_helpers.py @@ -1,11 +1,11 @@ """Tests for FHIR Bundle helper functions.""" import pytest -from fhir.resources.bundle import Bundle -from fhir.resources.condition import Condition -from fhir.resources.medicationstatement import MedicationStatement -from fhir.resources.allergyintolerance import AllergyIntolerance -from fhir.resources.documentreference import DocumentReference +from fhir.resources.R4B.bundle import Bundle +from fhir.resources.R4B.condition import Condition +from fhir.resources.R4B.medicationstatement import MedicationStatement +from fhir.resources.R4B.allergyintolerance import AllergyIntolerance +from fhir.resources.R4B.documentreference import DocumentReference from healthchain.fhir.bundlehelpers import ( create_bundle, diff --git a/tests/fhir/test_converters.py b/tests/fhir/test_converters.py index aa16c20a..7cfd8ac3 100644 --- a/tests/fhir/test_converters.py +++ b/tests/fhir/test_converters.py @@ -170,8 +170,8 @@ def test_bundle_to_dataframe_basic_conversion(): assert isinstance(df, pd.DataFrame) assert len(df) == 1 assert "age" in df.columns and "gender" in df.columns - assert "8867-4_Heart_rate" in df.columns - assert df["8867-4_Heart_rate"].iloc[0] == 85.0 + assert "obs_8867-4_Heart_rate" in df.columns + assert df["obs_8867-4_Heart_rate"].iloc[0] == 85.0 # Test with dict Bundle dict_bundle = { @@ -201,7 +201,7 @@ def test_bundle_to_dataframe_basic_conversion(): df = bundle_to_dataframe(dict_bundle) assert len(df) == 1 - assert "8310-5_Body_temperature" in df.columns + assert "obs_8310-5_Body_temperature" in df.columns @pytest.mark.parametrize( @@ -302,7 +302,7 @@ def test_bundle_to_dataframe_observation_aggregation_strategies( ) df = bundle_to_dataframe(bundle, config=config) - assert df["8867-4_Heart_rate"].iloc[0] == expected + assert df["obs_8867-4_Heart_rate"].iloc[0] == expected def test_bundle_to_dataframe_age_calculation_modes(): @@ -447,8 +447,12 @@ def test_bundle_to_dataframe_handles_multiple_patients(): assert len(df) == 2 assert set(df["patient_ref"]) == {"Patient/123", "Patient/456"} - assert df[df["patient_ref"] == "Patient/123"]["8867-4_Heart_rate"].iloc[0] == 85.0 - assert df[df["patient_ref"] == "Patient/456"]["8867-4_Heart_rate"].iloc[0] == 72.0 + assert ( + df[df["patient_ref"] == "Patient/123"]["obs_8867-4_Heart_rate"].iloc[0] == 85.0 + ) + assert ( + df[df["patient_ref"] == "Patient/456"]["obs_8867-4_Heart_rate"].iloc[0] == 72.0 + ) def test_bundle_converter_config_defaults(): diff --git a/tests/fhir/test_helpers.py b/tests/fhir/test_helpers.py index 35f7ce24..89ea008d 100644 --- a/tests/fhir/test_helpers.py +++ b/tests/fhir/test_helpers.py @@ -1,11 +1,10 @@ -from fhir.resources.condition import Condition -from fhir.resources.medicationstatement import MedicationStatement -from fhir.resources.allergyintolerance import AllergyIntolerance -from fhir.resources.codeableconcept import CodeableConcept -from fhir.resources.codeablereference import CodeableReference -from fhir.resources.documentreference import DocumentReference -from fhir.resources.attachment import Attachment -from fhir.resources.coding import Coding +from fhir.resources.R4B.condition import Condition +from fhir.resources.R4B.medicationstatement import MedicationStatement +from fhir.resources.R4B.allergyintolerance import AllergyIntolerance +from fhir.resources.R4B.codeableconcept import CodeableConcept +from fhir.resources.R4B.documentreference import DocumentReference +from fhir.resources.R4B.attachment import Attachment +from fhir.resources.R4B.coding import Coding from datetime import datetime @@ -56,12 +55,10 @@ def test_create_single_reaction(): assert len(reaction) == 1 assert reaction[0]["severity"] == "severe" assert len(reaction[0]["manifestation"]) == 1 - assert isinstance(reaction[0]["manifestation"][0], CodeableReference) - assert reaction[0]["manifestation"][0].concept.coding[0].code == "123" - assert reaction[0]["manifestation"][0].concept.coding[0].display == "Test Reaction" - assert ( - reaction[0]["manifestation"][0].concept.coding[0].system == "http://test.system" - ) + assert isinstance(reaction[0]["manifestation"][0], CodeableConcept) + assert reaction[0]["manifestation"][0].coding[0].code == "123" + assert reaction[0]["manifestation"][0].coding[0].display == "Test Reaction" + assert reaction[0]["manifestation"][0].coding[0].system == "http://test.system" def test_create_condition(): @@ -101,9 +98,9 @@ def test_create_medication_statement_minimal(): assert len(medication.id) > 3 # Ensure there's content after "hc-" assert medication.subject.reference == "Patient/123" assert medication.status == "recorded" - assert medication.medication.concept.coding[0].code == "123" - assert medication.medication.concept.coding[0].display == "Test Medication" - assert medication.medication.concept.coding[0].system == "http://test.system" + assert medication.medicationCodeableConcept.coding[0].code == "123" + assert medication.medicationCodeableConcept.coding[0].display == "Test Medication" + assert medication.medicationCodeableConcept.coding[0].system == "http://test.system" def test_create_allergy_intolerance_minimal(): diff --git a/tests/fhir/test_version.py b/tests/fhir/test_version.py index 352378e7..a44aaf47 100644 --- a/tests/fhir/test_version.py +++ b/tests/fhir/test_version.py @@ -32,7 +32,7 @@ def test_fhir_version_enum_from_string(): def test_resolve_version_with_none(): """Test _resolve_version returns default when None.""" reset_default_version() - assert _resolve_version(None) == FHIRVersion.R5 + assert _resolve_version(None) == FHIRVersion.R4B def test_resolve_version_with_enum(): @@ -53,9 +53,9 @@ def test_resolve_version_invalid_string(): def test_get_default_version_initial(): - """Test initial default version is R5.""" + """Test initial default version is R4B.""" reset_default_version() - assert get_default_version() == FHIRVersion.R5 + assert get_default_version() == FHIRVersion.R4B def test_set_default_version_with_enum(): @@ -75,17 +75,17 @@ def test_set_default_version_with_string(): def test_reset_default_version(): - """Test resetting default version to R5.""" - set_default_version(FHIRVersion.R4B) + """Test resetting default version to R4B.""" + set_default_version(FHIRVersion.R5) reset_default_version() - assert get_default_version() == FHIRVersion.R5 + assert get_default_version() == FHIRVersion.R4B -def test_get_fhir_resource_r5_default(): - """Test loading resource with default R5 version.""" +def test_get_fhir_resource_r4b_default(): + """Test loading resource with default R4B version.""" reset_default_version() Patient = get_fhir_resource("Patient") - assert Patient.__module__ == "fhir.resources.patient" + assert Patient.__module__ == "fhir.resources.R4B.patient" def test_get_fhir_resource_r4b(): @@ -137,43 +137,43 @@ def test_get_fhir_resource_respects_default_version(): def test_fhir_version_context_basic(): """Test fhir_version_context changes version temporarily.""" reset_default_version() - assert get_default_version() == FHIRVersion.R5 + assert get_default_version() == FHIRVersion.R4B - with fhir_version_context("R4B") as v: - assert v == FHIRVersion.R4B - assert get_default_version() == FHIRVersion.R4B + with fhir_version_context("R5") as v: + assert v == FHIRVersion.R5 + assert get_default_version() == FHIRVersion.R5 Patient = get_fhir_resource("Patient") - assert Patient.__module__ == "fhir.resources.R4B.patient" + assert Patient.__module__ == "fhir.resources.patient" - assert get_default_version() == FHIRVersion.R5 + assert get_default_version() == FHIRVersion.R4B def test_fhir_version_context_restores_on_exception(): """Test fhir_version_context restores version even on exception.""" reset_default_version() - assert get_default_version() == FHIRVersion.R5 + assert get_default_version() == FHIRVersion.R4B with pytest.raises(RuntimeError): - with fhir_version_context("R4B"): - assert get_default_version() == FHIRVersion.R4B + with fhir_version_context("R5"): + assert get_default_version() == FHIRVersion.R5 raise RuntimeError("Test exception") - assert get_default_version() == FHIRVersion.R5 + assert get_default_version() == FHIRVersion.R4B def test_fhir_version_context_nested(): """Test nested fhir_version_context restores correctly.""" reset_default_version() - with fhir_version_context("R4B"): - assert get_default_version() == FHIRVersion.R4B + with fhir_version_context("R5"): + assert get_default_version() == FHIRVersion.R5 with fhir_version_context("STU3"): assert get_default_version() == FHIRVersion.STU3 - assert get_default_version() == FHIRVersion.R4B + assert get_default_version() == FHIRVersion.R5 - assert get_default_version() == FHIRVersion.R5 + assert get_default_version() == FHIRVersion.R4B def test_convert_resource_r5_to_r4b(): @@ -245,63 +245,3 @@ class FakeResource: fake = FakeResource() version = get_resource_version(fake) assert version is None - - -# Integration tests for versioned resource helpers - - -def test_create_condition_with_version(): - """Test create_condition with version parameter.""" - from healthchain.fhir import create_condition - - cond_r5 = create_condition("Patient/1", code="123", display="Test") - cond_r4b = create_condition("Patient/1", code="123", display="Test", version="R4B") - - assert cond_r5.__class__.__module__ == "fhir.resources.condition" - assert cond_r4b.__class__.__module__ == "fhir.resources.R4B.condition" - - -def test_create_patient_with_version(): - """Test create_patient with version parameter.""" - from healthchain.fhir import create_patient - - patient_r5 = create_patient(gender="male") - patient_r4b = create_patient(gender="female", version="R4B") - - assert patient_r5.__class__.__module__ == "fhir.resources.patient" - assert patient_r4b.__class__.__module__ == "fhir.resources.R4B.patient" - - -def test_create_observation_with_version(): - """Test create_value_quantity_observation with version parameter.""" - from healthchain.fhir import create_value_quantity_observation - - obs_r5 = create_value_quantity_observation(code="12345", value=98.6, unit="F") - obs_r4b = create_value_quantity_observation( - code="12345", value=98.6, unit="F", version="R4B" - ) - - assert obs_r5.__class__.__module__ == "fhir.resources.observation" - assert obs_r4b.__class__.__module__ == "fhir.resources.R4B.observation" - - -def test_get_resource_type_with_version(): - """Test get_resource_type with version parameter.""" - from healthchain.fhir import get_resource_type - - Condition_R5 = get_resource_type("Condition") - Condition_R4B = get_resource_type("Condition", version="R4B") - - assert Condition_R5.__module__ == "fhir.resources.condition" - assert Condition_R4B.__module__ == "fhir.resources.R4B.condition" - - -def test_create_single_codeable_concept_with_version(): - """Test create_single_codeable_concept with version parameter.""" - from healthchain.fhir.elementhelpers import create_single_codeable_concept - - cc_r5 = create_single_codeable_concept("123", "Test") - cc_r4b = create_single_codeable_concept("123", "Test", version="R4B") - - assert cc_r5.__class__.__module__ == "fhir.resources.codeableconcept" - assert cc_r4b.__class__.__module__ == "fhir.resources.R4B.codeableconcept" diff --git a/tests/gateway/test_base_fhir_client.py b/tests/gateway/test_base_fhir_client.py index 16360200..f9591126 100644 --- a/tests/gateway/test_base_fhir_client.py +++ b/tests/gateway/test_base_fhir_client.py @@ -9,7 +9,7 @@ import json import httpx from unittest.mock import Mock, patch -from fhir.resources.patient import Patient +from fhir.resources.R4B.patient import Patient from healthchain.gateway.clients.fhir.sync import FHIRClient from healthchain.gateway.clients.fhir.aio import AsyncFHIRClient @@ -128,7 +128,7 @@ def test_fhir_client_resource_type_resolution(fhir_client): assert type_name == "Patient" assert resource_class == Patient mock_import.assert_called_once_with( - "fhir.resources.patient", fromlist=["Patient"] + "fhir.resources.R4B.patient", fromlist=["Patient"] ) # Test invalid resource type diff --git a/tests/gateway/test_fhir_client.py b/tests/gateway/test_fhir_client.py index c705765a..471ecaa7 100644 --- a/tests/gateway/test_fhir_client.py +++ b/tests/gateway/test_fhir_client.py @@ -7,9 +7,9 @@ import pytest import httpx from unittest.mock import Mock, patch -from fhir.resources.patient import Patient -from fhir.resources.bundle import Bundle -from fhir.resources.capabilitystatement import CapabilityStatement +from fhir.resources.R4B.patient import Patient +from fhir.resources.R4B.bundle import Bundle +from fhir.resources.R4B.capabilitystatement import CapabilityStatement from healthchain.gateway.clients.fhir.sync import FHIRClient from healthchain.gateway.clients.fhir.base import FHIRAuthConfig diff --git a/tests/gateway/test_fhir_client_async.py b/tests/gateway/test_fhir_client_async.py index 1edf41dc..93bc0527 100644 --- a/tests/gateway/test_fhir_client_async.py +++ b/tests/gateway/test_fhir_client_async.py @@ -7,9 +7,9 @@ import pytest import httpx from unittest.mock import Mock, AsyncMock, patch -from fhir.resources.patient import Patient -from fhir.resources.bundle import Bundle -from fhir.resources.capabilitystatement import CapabilityStatement +from fhir.resources.R4B.patient import Patient +from fhir.resources.R4B.bundle import Bundle +from fhir.resources.R4B.capabilitystatement import CapabilityStatement from healthchain.gateway.clients.fhir.aio import AsyncFHIRClient from healthchain.gateway.clients.fhir.base import FHIRAuthConfig diff --git a/tests/integration_tests/conftest.py b/tests/integration_tests/conftest.py index 311da567..b71791a3 100644 --- a/tests/integration_tests/conftest.py +++ b/tests/integration_tests/conftest.py @@ -12,9 +12,9 @@ from healthchain.pipeline.medicalcodingpipeline import MedicalCodingPipeline from healthchain.pipeline.summarizationpipeline import SummarizationPipeline from healthchain.fhir import create_document_reference -from fhir.resources.documentreference import DocumentReference -from fhir.resources.patient import Patient -from fhir.resources.meta import Meta +from fhir.resources.R4B.documentreference import DocumentReference +from fhir.resources.R4B.patient import Patient +from fhir.resources.R4B.meta import Meta @pytest.fixture diff --git a/tests/integration_tests/test_healthchain_api_e2e.py b/tests/integration_tests/test_healthchain_api_e2e.py index 80428a3d..26892625 100644 --- a/tests/integration_tests/test_healthchain_api_e2e.py +++ b/tests/integration_tests/test_healthchain_api_e2e.py @@ -34,8 +34,8 @@ def test_cds_service_processes_through_pipeline( def test_fhir_gateway_supports_multiple_resource_operations(fhir_gateway): """FHIR Gateway handles both transform and aggregate operations on different resource types.""" - from fhir.resources.documentreference import DocumentReference - from fhir.resources.patient import Patient + from fhir.resources.R4B.documentreference import DocumentReference + from fhir.resources.R4B.patient import Patient # Transform operation doc = fhir_gateway._resource_handlers[DocumentReference]["transform"]( diff --git a/tests/interop/test_engine.py b/tests/interop/test_engine.py index a2f97366..55c2187c 100644 --- a/tests/interop/test_engine.py +++ b/tests/interop/test_engine.py @@ -9,8 +9,8 @@ from healthchain.interop.types import FormatType, validate_format from healthchain.config.base import ValidationLevel -from fhir.resources.condition import Condition -from fhir.resources.medicationstatement import MedicationStatement +from fhir.resources.R4B.condition import Condition +from fhir.resources.R4B.medicationstatement import MedicationStatement @pytest.fixture diff --git a/tests/io/test_document.py b/tests/io/test_document.py index ab86a839..0a6472bf 100644 --- a/tests/io/test_document.py +++ b/tests/io/test_document.py @@ -2,7 +2,7 @@ from healthchain.io.containers.document import Document from unittest.mock import patch, MagicMock -from fhir.resources.bundle import Bundle +from fhir.resources.R4B.bundle import Bundle from healthchain.fhir import create_bundle, add_resource, create_condition @@ -108,7 +108,10 @@ def test_document_bundle_accessible_via_problem_list(): def test_document_operation_outcome_extraction( num_outcomes, expected_outcome_count, expected_remaining_entries ): - from fhir.resources.operationoutcome import OperationOutcome, OperationOutcomeIssue + from fhir.resources.R4B.operationoutcome import ( + OperationOutcome, + OperationOutcomeIssue, + ) bundle = create_bundle("collection") add_resource(bundle, create_condition(subject="Patient/123", code="E11.9")) @@ -149,8 +152,8 @@ def test_document_provenance_extraction( num_provenances, expected_provenance_count, expected_remaining_entries ): """Document automatically extracts Provenance resources during initialization.""" - from fhir.resources.provenance import Provenance, ProvenanceAgent - from fhir.resources.reference import Reference + from fhir.resources.R4B.provenance import Provenance, ProvenanceAgent + from fhir.resources.R4B.reference import Reference bundle = create_bundle("collection") add_resource(bundle, create_condition(subject="Patient/123", code="E11.9")) @@ -177,8 +180,8 @@ def test_document_provenance_extraction( @pytest.mark.parametrize("num_patients", [0, 1, 2]) def test_document_patient_convenience_properties_param(num_patients): """Patient convenience accessors behave for 0, 1, 2 patients without extraction.""" - from fhir.resources.patient import Patient - from fhir.resources.humanname import HumanName + from fhir.resources.R4B.patient import Patient + from fhir.resources.R4B.humanname import HumanName bundle = create_bundle("collection") diff --git a/tests/io/test_fhir_data.py b/tests/io/test_fhir_data.py index 0c28dc13..bfe774f4 100644 --- a/tests/io/test_fhir_data.py +++ b/tests/io/test_fhir_data.py @@ -158,15 +158,10 @@ def test_relationship_metadata(fhir_data, sample_document_reference): fhir_data.add_document_reference(child_doc, parent_id=doc_id) - # Verify relationship structure + # Verify relationship structure (R4B: code is a string, not CodeableConcept) child = fhir_data.get_resources("DocumentReference")[1] assert hasattr(child, "relatesTo") - assert child.relatesTo[0].code.coding[0].code == "transforms" - assert child.relatesTo[0].code.coding[0].display == "Transforms" - assert ( - child.relatesTo[0].code.coding[0].system - == "http://hl7.org/fhir/ValueSet/document-relationship-type" - ) + assert child.relatesTo[0].code == "transforms" assert child.relatesTo[0].target.reference == f"DocumentReference/{doc_id}" diff --git a/tests/pipeline/test_cdaadapter.py b/tests/pipeline/test_cdaadapter.py index 9f33b67d..157ff979 100644 --- a/tests/pipeline/test_cdaadapter.py +++ b/tests/pipeline/test_cdaadapter.py @@ -5,7 +5,7 @@ from healthchain.io.containers import Document from healthchain.io.adapters import CdaAdapter from healthchain.interop import FormatType -from fhir.resources.documentreference import DocumentReference +from fhir.resources.R4B.documentreference import DocumentReference @pytest.fixture diff --git a/tests/pipeline/test_cdsfhiradapter.py b/tests/pipeline/test_cdsfhiradapter.py index a8691aaf..a9eda957 100644 --- a/tests/pipeline/test_cdsfhiradapter.py +++ b/tests/pipeline/test_cdsfhiradapter.py @@ -3,8 +3,8 @@ from healthchain.io.containers import Document from healthchain.io.containers.document import CdsAnnotations from healthchain.models.responses.cdsresponse import Action, CDSResponse, Card -from fhir.resources.resource import Resource -from fhir.resources.documentreference import DocumentReference +from fhir.resources.R4B.resource import Resource +from fhir.resources.R4B.documentreference import DocumentReference def test_parse_with_no_document_reference(cds_fhir_adapter, test_cds_request): diff --git a/tests/sandbox/generators/test_cds_data_generator.py b/tests/sandbox/generators/test_cds_data_generator.py index 1597ff67..df5b94b6 100644 --- a/tests/sandbox/generators/test_cds_data_generator.py +++ b/tests/sandbox/generators/test_cds_data_generator.py @@ -1,9 +1,9 @@ import pytest -from fhir.resources.encounter import Encounter -from fhir.resources.condition import Condition -from fhir.resources.procedure import Procedure -from fhir.resources.patient import Patient +from fhir.resources.R4B.encounter import Encounter +from fhir.resources.R4B.condition import Condition +from fhir.resources.R4B.procedure import Procedure +from fhir.resources.R4B.patient import Patient from healthchain.sandbox.generators import CdsDataGenerator from healthchain.sandbox.workflows import Workflow diff --git a/tests/sandbox/generators/test_medication_administration_generators.py b/tests/sandbox/generators/test_medication_administration_generators.py index 24062c8d..49e40082 100644 --- a/tests/sandbox/generators/test_medication_administration_generators.py +++ b/tests/sandbox/generators/test_medication_administration_generators.py @@ -17,5 +17,5 @@ def test_MedicationAdministrationGenerator(): result = MedicationAdministrationGenerator.generate("Patient/123", "Encounter/123") assert result.id is not None assert result.status is not None - assert result.medication is not None - assert result.medication.concept.coding[0].code in value_set + assert result.medicationCodeableConcept is not None + assert result.medicationCodeableConcept.coding[0].code in value_set diff --git a/tests/sandbox/generators/test_medication_request_generators.py b/tests/sandbox/generators/test_medication_request_generators.py index 4d7437cf..aa325a5a 100644 --- a/tests/sandbox/generators/test_medication_request_generators.py +++ b/tests/sandbox/generators/test_medication_request_generators.py @@ -21,5 +21,5 @@ def test_MedicationRequestGenerator(): value_set = [x.code for x in MedicationRequestMedication().value_set] assert medication_request is not None assert medication_request.id is not None - assert medication_request.medication.concept.coding[0].code in value_set + assert medication_request.medicationCodeableConcept.coding[0].code in value_set assert medication_request.intent is not None diff --git a/tests/sandbox/generators/test_practitioner_generators.py b/tests/sandbox/generators/test_practitioner_generators.py index 75e8c9f1..b5354326 100644 --- a/tests/sandbox/generators/test_practitioner_generators.py +++ b/tests/sandbox/generators/test_practitioner_generators.py @@ -28,11 +28,10 @@ def test_practitioner_data_generator(): assert qualification_data.code is not None assert qualification_data.period is not None - # Assert that the communication data has the expected pydantic fields + # Assert that the communication data has the expected pydantic fields (R4B: List[CodeableConcept]) communication_data = practitioner_data.communication[0] - assert communication_data.id is not None - assert communication_data.language is not None - assert communication_data.preferred is not None + assert communication_data is not None + assert communication_data.coding is not None def test_practitioner_qualification_generator(): @@ -61,7 +60,6 @@ def test_practitioner_communication_generator(): # Assert that the communication is not empty assert communication is not None - # Assert that the communication has the expected pydantic fields - assert communication.id is not None - assert communication.language is not None - assert communication.preferred is not None + # Assert that the communication has the expected pydantic fields (R4B: CodeableConcept) + assert communication is not None + assert communication.coding is not None diff --git a/tests/sandbox/test_mimic_loader.py b/tests/sandbox/test_mimic_loader.py index bf4e10dd..83a88735 100644 --- a/tests/sandbox/test_mimic_loader.py +++ b/tests/sandbox/test_mimic_loader.py @@ -28,15 +28,13 @@ def mock_medication_resources(): "resourceType": "MedicationStatement", "id": "med-1", "status": "recorded", - "medication": { - "concept": { - "coding": [ - { - "system": "http://www.nlm.nih.gov/research/umls/rxnorm", - "code": "313782", - } - ] - } + "medicationCodeableConcept": { + "coding": [ + { + "system": "http://www.nlm.nih.gov/research/umls/rxnorm", + "code": "313782", + } + ] }, "subject": {"reference": "Patient/123"}, }, @@ -44,15 +42,13 @@ def mock_medication_resources(): "resourceType": "MedicationStatement", "id": "med-2", "status": "recorded", - "medication": { - "concept": { - "coding": [ - { - "system": "http://www.nlm.nih.gov/research/umls/rxnorm", - "code": "197361", - } - ] - } + "medicationCodeableConcept": { + "coding": [ + { + "system": "http://www.nlm.nih.gov/research/umls/rxnorm", + "code": "197361", + } + ] }, "subject": {"reference": "Patient/456"}, }, @@ -229,15 +225,13 @@ def test_mimic_loader_handles_malformed_json(temp_mimic_data_dir): "resourceType": "MedicationStatement", "id": "med-1", "status": "recorded", - "medication": { - "concept": { - "coding": [ - { - "system": "http://www.nlm.nih.gov/research/umls/rxnorm", - "code": "313782", - } - ] - } + "medicationCodeableConcept": { + "coding": [ + { + "system": "http://www.nlm.nih.gov/research/umls/rxnorm", + "code": "313782", + } + ] }, "subject": {"reference": "Patient/123"}, } @@ -292,15 +286,13 @@ def test_mimic_loader_skips_resources_without_resource_type(temp_mimic_data_dir) "resourceType": "MedicationStatement", "id": "med-2", "status": "recorded", - "medication": { - "concept": { - "coding": [ - { - "system": "http://www.nlm.nih.gov/research/umls/rxnorm", - "code": "313782", - } - ] - } + "medicationCodeableConcept": { + "coding": [ + { + "system": "http://www.nlm.nih.gov/research/umls/rxnorm", + "code": "313782", + } + ] }, "subject": {"reference": "Patient/123"}, }, diff --git a/tests/sandbox/test_synthea_loader.py b/tests/sandbox/test_synthea_loader.py index 0910b91b..0dd4fcb3 100644 --- a/tests/sandbox/test_synthea_loader.py +++ b/tests/sandbox/test_synthea_loader.py @@ -81,15 +81,13 @@ def mock_patient_bundle(): "resourceType": "MedicationStatement", "id": "med-1", "status": "recorded", - "medication": { - "concept": { - "coding": [ - { - "system": "http://www.nlm.nih.gov/research/umls/rxnorm", - "code": "313782", - } - ] - } + "medicationCodeableConcept": { + "coding": [ + { + "system": "http://www.nlm.nih.gov/research/umls/rxnorm", + "code": "313782", + } + ] }, "subject": { "reference": "Patient/a969c177-a995-7b89-7b6d-885214dfa253"