Skip to content

Commit 612dd36

Browse files
committed
docs: add ADR-0003 detailing the decision to use direct httpx integration for the openEHR API.
1 parent 864b948 commit 612dd36

File tree

1 file changed

+281
-0
lines changed

1 file changed

+281
-0
lines changed
Lines changed: 281 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,281 @@
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

Comments
 (0)