|
| 1 | +# ADR-0003: Direct httpx Integration for openEHR API |
| 2 | + |
| 3 | +## Status |
| 4 | +Accepted |
| 5 | + |
| 6 | +## Context |
| 7 | + |
| 8 | +Open CIS needs to interact with EHRBase (an openEHR Clinical Data Repository) to create, retrieve, and query clinical compositions. We must decide how to implement this integration: use an existing SDK/library or build a custom client using a low-level HTTP library. |
| 9 | + |
| 10 | +### Problem |
| 11 | + |
| 12 | +Clinical applications need to: |
| 13 | +1. Create and manage EHRs (Electronic Health Records) |
| 14 | +2. Store clinical data as compositions using templates |
| 15 | +3. Query clinical data using AQL (Archetype Query Language) |
| 16 | +4. Retrieve compositions in different formats (FLAT, STRUCTURED, CANONICAL) |
| 17 | +5. Manage templates and archetypes |
| 18 | + |
| 19 | +### Available SDK Options |
| 20 | + |
| 21 | +#### 1. EHRbase openEHR SDK (Java) |
| 22 | +**Repository**: https://github.com/ehrbase/openEHR_SDK |
| 23 | +**Language**: Java |
| 24 | +**Status**: Active (v2.27.0, October 2025) |
| 25 | + |
| 26 | +**Features**: |
| 27 | +- Entity class generation from openEHR templates with JPA-like annotations |
| 28 | +- Bidirectional mapping between entity objects and Archie RM (Reference Model) |
| 29 | +- REST client implementation for openEHR API |
| 30 | +- AQL query builder and parser |
| 31 | +- JSON/XML composition serialization/deserialization |
| 32 | +- Template validation engine |
| 33 | +- ~20 specialized modules, 2,008 commits |
| 34 | + |
| 35 | +**Limitations**: |
| 36 | +- Java-only (incompatible with Python backend) |
| 37 | +- Most modules in beta status |
| 38 | +- AQL module missing: XOR operations, aggregate functions, pattern matching, path comparisons |
| 39 | +- Complex architecture with many dependencies |
| 40 | +- Requires Java 11+ runtime |
| 41 | + |
| 42 | +#### 2. pyEHR (Python) |
| 43 | +**Repository**: https://github.com/crs4/pyEHR |
| 44 | +**Language**: Python |
| 45 | +**Status**: Dormant (last active ~2017-2018) |
| 46 | + |
| 47 | +**Features**: |
| 48 | +- Multi-backend support (MongoDB, Elasticsearch 1.5) |
| 49 | +- Dataset creation and querying |
| 50 | +- REST service API |
| 51 | +- Archetype model support (JSON format) |
| 52 | + |
| 53 | +**Limitations**: |
| 54 | +- **Inactive maintenance**: No recent commits, outdated dependencies |
| 55 | +- **Outdated stack**: References Python 2 syntax, Elasticsearch 1.5 |
| 56 | +- **Different focus**: Designed for secondary data analysis, not primary EHR operations |
| 57 | +- **Heavy dependencies**: Requires BaseX XML database, Java 8 |
| 58 | +- **Not EHRbase-specific**: Generic openEHR support, not optimized for EHRbase REST API |
| 59 | + |
| 60 | +#### 3. Other Python Options |
| 61 | +**Status**: None found |
| 62 | + |
| 63 | +Our research found no other actively maintained Python SDKs for openEHR integration. The community discussion in ehrbase/openEHR_SDK#24 (2020) shows Python client libraries were requested but never developed. |
| 64 | + |
| 65 | +## Decision |
| 66 | + |
| 67 | +We will use **httpx directly** to interact with the EHRbase REST API, implementing a lightweight custom `EHRBaseClient` wrapper class. |
| 68 | + |
| 69 | +### Implementation Approach |
| 70 | + |
| 71 | +```python |
| 72 | +# api/src/ehrbase/client.py |
| 73 | +class EHRBaseClient: |
| 74 | + """Async client for EHRBase REST API.""" |
| 75 | + |
| 76 | + def __init__(self): |
| 77 | + self.base_url = settings.ehrbase_url |
| 78 | + self._client: httpx.AsyncClient | None = None |
| 79 | + |
| 80 | + async def _get_client(self) -> httpx.AsyncClient: |
| 81 | + if self._client is None: |
| 82 | + self._client = httpx.AsyncClient( |
| 83 | + base_url=self.base_url, |
| 84 | + auth=httpx.BasicAuth(...), |
| 85 | + headers={"Content-Type": "application/json"} |
| 86 | + ) |
| 87 | + return self._client |
| 88 | + |
| 89 | + async def create_composition( |
| 90 | + self, ehr_id: str, template_id: str, |
| 91 | + composition: dict[str, Any], format: str = "FLAT" |
| 92 | + ) -> dict[str, Any]: |
| 93 | + client = await self._get_client() |
| 94 | + response = await client.post( |
| 95 | + f"/openehr/v1/ehr/{ehr_id}/composition", |
| 96 | + json=composition, |
| 97 | + params={"templateId": template_id, "format": format} |
| 98 | + ) |
| 99 | + response.raise_for_status() |
| 100 | + return response.json() |
| 101 | +``` |
| 102 | + |
| 103 | +### Architecture |
| 104 | + |
| 105 | +``` |
| 106 | +api/src/ehrbase/ |
| 107 | +├── client.py # EHRBaseClient - thin wrapper around httpx |
| 108 | +├── compositions.py # Composition building helpers |
| 109 | +├── queries.py # AQL query templates and builders |
| 110 | +└── templates.py # Template management utilities |
| 111 | +``` |
| 112 | + |
| 113 | +## Rationale |
| 114 | + |
| 115 | +### Why Not Java SDK? |
| 116 | + |
| 117 | +1. **Language Mismatch**: Our backend is Python (FastAPI), not Java |
| 118 | +2. **Unnecessary Complexity**: We'd need to run a Java service or use JNI/Py4J |
| 119 | +3. **Overkill**: We don't need entity generation or complex ORM-like features |
| 120 | +4. **Learning Curve**: Understanding 20+ modules vs simple HTTP requests |
| 121 | + |
| 122 | +### Why Not pyEHR? |
| 123 | + |
| 124 | +1. **Inactive Maintenance**: No commits since ~2018, stale dependencies |
| 125 | +2. **Outdated Stack**: Python 2 syntax, Elasticsearch 1.5 (current: 8.x) |
| 126 | +3. **Wrong Abstraction**: Built for secondary analysis, not primary EHR operations |
| 127 | +4. **Heavy Dependencies**: Requires BaseX XML database, Java runtime |
| 128 | +5. **Risk**: Unmaintained library could break with Python/dependency updates |
| 129 | + |
| 130 | +### Why Direct httpx? |
| 131 | + |
| 132 | +1. **Simplicity**: EHRbase REST API is well-documented and straightforward |
| 133 | +2. **Control**: Full transparency over requests/responses, easy debugging |
| 134 | +3. **Async-First**: httpx natively supports async/await (FastAPI best practice) |
| 135 | +4. **Maintained**: httpx is actively maintained, widely used, stable |
| 136 | +5. **Lightweight**: Single dependency vs complex SDK |
| 137 | +6. **Type Safety**: Works seamlessly with Python type hints and mypy |
| 138 | +7. **Learning**: For an educational project, understanding the raw API is valuable |
| 139 | + |
| 140 | +### EHRbase REST API Coverage |
| 141 | + |
| 142 | +Our custom client covers all needed endpoints: |
| 143 | + |
| 144 | +| Operation | Endpoint | Method | |
| 145 | +|-----------|----------|--------| |
| 146 | +| Create EHR | `/openehr/v1/ehr` | POST/PUT | |
| 147 | +| Get EHR | `/openehr/v1/ehr/{ehr_id}` | GET | |
| 148 | +| Create Composition | `/openehr/v1/ehr/{ehr_id}/composition` | POST | |
| 149 | +| Get Composition | `/openehr/v1/ehr/{ehr_id}/composition/{uid}` | GET | |
| 150 | +| Delete Composition | `/openehr/v1/ehr/{ehr_id}/composition/{uid}` | DELETE | |
| 151 | +| AQL Query | `/openehr/v1/query/aql` | POST | |
| 152 | +| List Templates | `/openehr/v1/definition/template/adl1.4` | GET | |
| 153 | +| Upload Template | `/openehr/v1/definition/template/adl1.4` | POST | |
| 154 | + |
| 155 | +All endpoints are simple HTTP requests with JSON payloads. |
| 156 | + |
| 157 | +## Consequences |
| 158 | + |
| 159 | +### Positive |
| 160 | + |
| 161 | +- **Minimal dependencies**: Only httpx (already used for HTTP requests) |
| 162 | +- **Full control**: Direct access to request/response cycle |
| 163 | +- **Easy debugging**: Clear request/response logs, no hidden abstractions |
| 164 | +- **Type-safe**: Python type hints throughout, mypy validation |
| 165 | +- **Future-proof**: Not dependent on unmaintained libraries |
| 166 | +- **Educational value**: Learn openEHR API directly vs hidden in SDK |
| 167 | +- **Performance**: No SDK overhead, direct HTTP requests |
| 168 | +- **Maintainable**: ~150 lines of client code vs thousands in SDK |
| 169 | + |
| 170 | +### Negative |
| 171 | + |
| 172 | +- **Manual work**: Must implement each endpoint method ourselves |
| 173 | +- **No validation helpers**: Must manually construct FLAT compositions |
| 174 | +- **AQL string-based**: No type-safe query builder (write raw AQL strings) |
| 175 | +- **Template handling**: Manual path mapping vs auto-generated classes |
| 176 | +- **Maintenance burden**: Must keep up with EHRbase API changes ourselves |
| 177 | + |
| 178 | +### Neutral |
| 179 | + |
| 180 | +- **Testing**: Need to mock HTTP responses (would need mocks for SDK too) |
| 181 | +- **Documentation**: Must refer to EHRbase API docs (vs SDK docs) |
| 182 | +- **Learning curve**: Need to understand openEHR concepts either way |
| 183 | + |
| 184 | +## Mitigation Strategies |
| 185 | + |
| 186 | +To address the negative consequences: |
| 187 | + |
| 188 | +1. **Composition Builders**: Create helper functions in `compositions.py` |
| 189 | + ```python |
| 190 | + def build_vital_signs_composition( |
| 191 | + systolic: int, diastolic: int, pulse_rate: int, |
| 192 | + recorded_at: datetime |
| 193 | + ) -> dict[str, Any]: |
| 194 | + # Encapsulate FLAT path knowledge |
| 195 | + ``` |
| 196 | + |
| 197 | +2. **AQL Templates**: Store common queries in `queries.py` with placeholders |
| 198 | + ```python |
| 199 | + VITAL_SIGNS_QUERY = """ |
| 200 | + SELECT ... FROM EHR e CONTAINS COMPOSITION c |
| 201 | + WHERE c/archetype_details/template_id/value = '{template_id}' |
| 202 | + """ |
| 203 | + ``` |
| 204 | + |
| 205 | +3. **Error Handling**: Centralized HTTP error handling with helpful messages |
| 206 | + ```python |
| 207 | + except httpx.HTTPStatusError as e: |
| 208 | + error_body = e.response.text |
| 209 | + logging.error(f"EHRBase API error: {e.status_code} - {error_body}") |
| 210 | + ``` |
| 211 | + |
| 212 | +4. **Type Definitions**: Define Pydantic schemas for all request/response shapes |
| 213 | + ```python |
| 214 | + class VitalSignsCreate(BaseModel): |
| 215 | + patient_id: str |
| 216 | + systolic: int | None |
| 217 | + # ... |
| 218 | + ``` |
| 219 | + |
| 220 | +## Alternatives Considered |
| 221 | + |
| 222 | +### 1. Wait for Official Python SDK |
| 223 | +**Rejected**: No indication one is being developed. Issue from 2020 still open. |
| 224 | + |
| 225 | +### 2. Fork pyEHR and Update It |
| 226 | +**Rejected**: Would need to: |
| 227 | +- Port from Python 2 to Python 3 |
| 228 | +- Update all dependencies (Elasticsearch 1.5 → 8.x) |
| 229 | +- Refactor for EHRbase-specific use |
| 230 | +- Ongoing maintenance burden equivalent to maintaining SDK |
| 231 | + |
| 232 | +More work than building a focused client. |
| 233 | + |
| 234 | +### 3. Use Java SDK via Py4J or JNI |
| 235 | +**Rejected**: |
| 236 | +- Adds Java runtime dependency |
| 237 | +- Complex inter-process communication |
| 238 | +- Performance overhead |
| 239 | +- Deployment complexity (Java + Python containers) |
| 240 | + |
| 241 | +### 4. Build Full-Featured Python SDK |
| 242 | +**Rejected**: Out of scope for learning project. Would need: |
| 243 | +- Template → Python class generation |
| 244 | +- Full RM (Reference Model) implementation |
| 245 | +- AQL parser and builder |
| 246 | +- Months of development |
| 247 | + |
| 248 | +Our focused client is sufficient. |
| 249 | + |
| 250 | +## Migration Path |
| 251 | + |
| 252 | +If Python SDK emerges in the future: |
| 253 | + |
| 254 | +1. **Easy migration**: Our service layer already abstracts EHRbase calls |
| 255 | +2. **Incremental adoption**: Can replace methods in `EHRBaseClient` one at a time |
| 256 | +3. **No API changes**: Service and router layers remain unchanged |
| 257 | + |
| 258 | +Example: |
| 259 | +```python |
| 260 | +# Before |
| 261 | +result = await ehrbase_client.create_composition(ehr_id, template_id, composition) |
| 262 | + |
| 263 | +# After (hypothetical SDK) |
| 264 | +result = await openehr_sdk.composition.create(ehr_id, template_id, composition) |
| 265 | +``` |
| 266 | + |
| 267 | +Service layer insulates us from client implementation details. |
| 268 | + |
| 269 | +## Related |
| 270 | + |
| 271 | +- ADR-0001: Use openEHR for Clinical Data (chose EHRbase as CDR) |
| 272 | +- ADR-0002: FastAPI Backend (async Python framework) |
| 273 | +- EHRbase REST API: https://ehrbase.readthedocs.io/en/latest/03_development/04_rest_api/ |
| 274 | +- httpx documentation: https://www.python-httpx.org/ |
| 275 | + |
| 276 | +## References |
| 277 | + |
| 278 | +- EHRbase openEHR_SDK: https://github.com/ehrbase/openEHR_SDK |
| 279 | +- pyEHR: https://github.com/crs4/pyEHR |
| 280 | +- httpx: https://github.com/encode/httpx |
| 281 | +- openEHR REST API specification: https://specifications.openehr.org/releases/ITS-REST/latest/ |
0 commit comments