Skip to content

EHRbase Client

EHRbase client for OpenEHR operations.

get_auth_header

get_auth_header()

Get Basic Auth header for EHRbase.

Source code in app/ehrbase_client.py
17
18
19
20
21
22
def get_auth_header() -> dict[str, str]:
    """Get Basic Auth header for EHRbase."""
    _require_clinical_services()
    credentials = f"{settings.EHRBASE_API_USER}:{settings.EHRBASE_API_PASSWORD.get_secret_value()}"
    encoded = base64.b64encode(credentials.encode()).decode()
    return {"Authorization": f"Basic {encoded}"}

create_ehr

create_ehr(subject_id, subject_namespace='fhir')

Create a new EHR in EHRbase.

Parameters:

Name Type Description Default
subject_id str

The FHIR Patient ID

required
subject_namespace str

The namespace (default: 'fhir')

'fhir'

Returns:

Type Description
dict[str, Any]

EHR response containing ehr_id

Raises:

Type Description
ValueError

If subject_id is empty or invalid

Source code in app/ehrbase_client.py
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
def create_ehr(
    subject_id: str, subject_namespace: str = "fhir"
) -> dict[str, Any]:
    """
    Create a new EHR in EHRbase.

    Args:
        subject_id: The FHIR Patient ID
        subject_namespace: The namespace (default: 'fhir')

    Returns:
        EHR response containing ehr_id

    Raises:
        ValueError: If subject_id is empty or invalid
    """
    # Defensive programming: validate inputs
    if not subject_id or not subject_id.strip():
        raise ValueError("subject_id cannot be empty")
    if not subject_namespace or not subject_namespace.strip():
        raise ValueError("subject_namespace cannot be empty")
    url = f"{settings.EHRBASE_URL}/rest/openehr/v1/ehr"
    headers = {
        **get_auth_header(),
        "Content-Type": "application/json",
    }

    payload = {
        "_type": "EHR_STATUS",
        "subject": {
            "external_ref": {
                "id": {
                    "_type": "GENERIC_ID",
                    "value": subject_id,
                    "scheme": subject_namespace,
                },
                "namespace": subject_namespace,
                "type": "PERSON",
            }
        },
        "is_modifiable": True,
        "is_queryable": True,
    }

    response = requests.post(url, json=payload, headers=headers)
    response.raise_for_status()
    return response.json()  # type: ignore[no-any-return]

get_ehr_by_subject

get_ehr_by_subject(subject_id, subject_namespace='fhir')

Get an EHR by subject ID.

Parameters:

Name Type Description Default
subject_id str

The FHIR Patient ID

required
subject_namespace str

The namespace (default: 'fhir')

'fhir'

Returns:

Type Description
dict[str, Any] | None

EHR response or None if not found

Raises:

Type Description
ValueError

If subject_id is empty

Source code in app/ehrbase_client.py
 74
 75
 76
 77
 78
 79
 80
 81
 82
 83
 84
 85
 86
 87
 88
 89
 90
 91
 92
 93
 94
 95
 96
 97
 98
 99
100
101
def get_ehr_by_subject(
    subject_id: str, subject_namespace: str = "fhir"
) -> dict[str, Any] | None:
    """
    Get an EHR by subject ID.

    Args:
        subject_id: The FHIR Patient ID
        subject_namespace: The namespace (default: 'fhir')

    Returns:
        EHR response or None if not found

    Raises:
        ValueError: If subject_id is empty
    """
    # Defensive programming: validate inputs
    if not subject_id or not subject_id.strip():
        raise ValueError("subject_id cannot be empty")
    url = f"{settings.EHRBASE_URL}/rest/openehr/v1/ehr"
    headers = get_auth_header()
    params = {"subject_id": subject_id, "subject_namespace": subject_namespace}

    response = requests.get(url, params=params, headers=headers)
    if response.status_code == 404:
        return None
    response.raise_for_status()
    return response.json()  # type: ignore[no-any-return]

get_or_create_ehr

get_or_create_ehr(subject_id, subject_namespace='fhir')

Get existing EHR or create new one for a subject.

Parameters:

Name Type Description Default
subject_id str

The FHIR Patient ID

required
subject_namespace str

The namespace (default: 'fhir')

'fhir'

Returns:

Type Description
str

ehr_id (UUID string)

Source code in app/ehrbase_client.py
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
def get_or_create_ehr(subject_id: str, subject_namespace: str = "fhir") -> str:
    """
    Get existing EHR or create new one for a subject.

    Args:
        subject_id: The FHIR Patient ID
        subject_namespace: The namespace (default: 'fhir')

    Returns:
        ehr_id (UUID string)
    """
    ehr = get_ehr_by_subject(subject_id, subject_namespace)
    if ehr:
        return ehr["ehr_id"]["value"]  # type: ignore[no-any-return]

    new_ehr = create_ehr(subject_id, subject_namespace)
    return new_ehr["ehr_id"]["value"]  # type: ignore[no-any-return]

upload_template

upload_template(template_xml)

Upload an OpenEHR template (OPT) to EHRbase.

Parameters:

Name Type Description Default
template_xml str

The template in XML format

required

Returns:

Type Description
dict[str, Any]

Template upload response

Source code in app/ehrbase_client.py
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
def upload_template(template_xml: str) -> dict[str, Any]:
    """
    Upload an OpenEHR template (OPT) to EHRbase.

    Args:
        template_xml: The template in XML format

    Returns:
        Template upload response
    """
    url = f"{settings.EHRBASE_URL}/rest/openehr/v1/definition/template/adl1.4"
    headers = {
        **get_auth_header(),
        "Content-Type": "application/xml",
    }

    response = requests.post(url, data=template_xml, headers=headers)
    response.raise_for_status()
    return response.json()  # type: ignore[no-any-return]

list_templates

list_templates()

List all templates available in EHRbase.

Returns:

Type Description
list[str]

List of template IDs

Source code in app/ehrbase_client.py
144
145
146
147
148
149
150
151
152
153
154
155
156
def list_templates() -> list[str]:
    """
    List all templates available in EHRbase.

    Returns:
        List of template IDs
    """
    url = f"{settings.EHRBASE_URL}/rest/openehr/v1/definition/template/adl1.4"
    headers = get_auth_header()

    response = requests.get(url, headers=headers)
    response.raise_for_status()
    return response.json()  # type: ignore[no-any-return]

create_composition

create_composition(ehr_id, template_id, composition_data)

Create a composition (clinical document) in EHRbase.

Parameters:

Name Type Description Default
ehr_id str

The EHR UUID

required
template_id str

The template ID

required
composition_data dict[str, Any]

The composition content in JSON format

required

Returns:

Type Description
dict[str, Any]

Created composition response

Source code in app/ehrbase_client.py
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
def create_composition(
    ehr_id: str, template_id: str, composition_data: dict[str, Any]
) -> dict[str, Any]:
    """
    Create a composition (clinical document) in EHRbase.

    Args:
        ehr_id: The EHR UUID
        template_id: The template ID
        composition_data: The composition content in JSON format

    Returns:
        Created composition response
    """
    url = f"{settings.EHRBASE_URL}/rest/openehr/v1/ehr/{ehr_id}/composition"
    headers = {
        **get_auth_header(),
        "Content-Type": "application/json",
        "Prefer": "return=representation",
    }

    response = requests.post(url, json=composition_data, headers=headers)
    response.raise_for_status()
    return response.json()  # type: ignore[no-any-return]

get_composition

get_composition(ehr_id, composition_uid)

Retrieve a composition by UID.

Parameters:

Name Type Description Default
ehr_id str

The EHR UUID

required
composition_uid str

The composition UID

required

Returns:

Type Description
dict[str, Any]

Composition data

Source code in app/ehrbase_client.py
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
def get_composition(ehr_id: str, composition_uid: str) -> dict[str, Any]:
    """
    Retrieve a composition by UID.

    Args:
        ehr_id: The EHR UUID
        composition_uid: The composition UID

    Returns:
        Composition data
    """
    url = f"{settings.EHRBASE_URL}/rest/openehr/v1/ehr/{ehr_id}/composition/{composition_uid}"
    headers = get_auth_header()

    response = requests.get(url, headers=headers)
    response.raise_for_status()
    return response.json()  # type: ignore[no-any-return]

query_aql

query_aql(aql_query)

Execute an AQL query against EHRbase.

Parameters:

Name Type Description Default
aql_query str

The AQL query string

required

Returns:

Type Description
dict[str, Any]

Query results

Source code in app/ehrbase_client.py
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
def query_aql(aql_query: str) -> dict[str, Any]:
    """
    Execute an AQL query against EHRbase.

    Args:
        aql_query: The AQL query string

    Returns:
        Query results
    """
    url = f"{settings.EHRBASE_URL}/rest/openehr/v1/query/aql"
    headers = {
        **get_auth_header(),
        "Content-Type": "application/json",
    }

    payload = {"q": aql_query}

    response = requests.post(url, json=payload, headers=headers)
    response.raise_for_status()
    return response.json()  # type: ignore[no-any-return]

list_compositions_for_ehr

list_compositions_for_ehr(ehr_id)

List all compositions for an EHR using AQL.

Parameters:

Name Type Description Default
ehr_id str

The EHR UUID

required

Returns:

Type Description
list[dict[str, Any]]

List of compositions

Source code in app/ehrbase_client.py
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
def list_compositions_for_ehr(ehr_id: str) -> list[dict[str, Any]]:
    """
    List all compositions for an EHR using AQL.

    Args:
        ehr_id: The EHR UUID

    Returns:
        List of compositions
    """
    aql = f"""
    SELECT c
    FROM EHR e[ehr_id/value='{ehr_id}']
    CONTAINS COMPOSITION c
    """

    result = query_aql(aql)
    return result.get("rows", [])  # type: ignore[no-any-return]

create_letter_composition

create_letter_composition(patient_id, title, body, author_name=None)

Create a letter/correspondence composition in OpenEHR.

Parameters:

Name Type Description Default
patient_id str

FHIR Patient ID

required
title str

Letter title

required
body str

Letter content (markdown)

required
author_name str | None

Optional author name

None

Returns:

Type Description
dict[str, Any]

Created composition response with composition_uid

Source code in app/ehrbase_client.py
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
def create_letter_composition(
    patient_id: str, title: str, body: str, author_name: str | None = None
) -> dict[str, Any]:
    """
    Create a letter/correspondence composition in OpenEHR.

    Args:
        patient_id: FHIR Patient ID
        title: Letter title
        body: Letter content (markdown)
        author_name: Optional author name

    Returns:
        Created composition response with composition_uid
    """
    from datetime import UTC, datetime

    # Get or create EHR for this patient
    ehr_id = get_or_create_ehr(patient_id)

    # Create a simple letter composition
    # Using a generic composition structure for letters
    composition_data = {
        "_type": "COMPOSITION",
        "name": {"_type": "DV_TEXT", "value": title},
        "archetype_node_id": "openEHR-EHR-COMPOSITION.report.v1",
        "language": {
            "_type": "CODE_PHRASE",
            "terminology_id": {
                "_type": "TERMINOLOGY_ID",
                "value": "ISO_639-1",
            },
            "code_string": "en",
        },
        "territory": {
            "_type": "CODE_PHRASE",
            "terminology_id": {
                "_type": "TERMINOLOGY_ID",
                "value": "ISO_3166-1",
            },
            "code_string": "US",
        },
        "category": {
            "_type": "DV_CODED_TEXT",
            "value": "event",
            "defining_code": {
                "_type": "CODE_PHRASE",
                "terminology_id": {
                    "_type": "TERMINOLOGY_ID",
                    "value": "openehr",
                },
                "code_string": "433",
            },
        },
        "composer": {
            "_type": "PARTY_IDENTIFIED",
            "name": author_name or "System",
        },
        "context": {
            "_type": "EVENT_CONTEXT",
            "start_time": {
                "_type": "DV_DATE_TIME",
                "value": datetime.now(UTC).isoformat(),
            },
            "setting": {
                "_type": "DV_CODED_TEXT",
                "value": "other care",
                "defining_code": {
                    "_type": "CODE_PHRASE",
                    "terminology_id": {
                        "_type": "TERMINOLOGY_ID",
                        "value": "openehr",
                    },
                    "code_string": "238",
                },
            },
        },
        "content": [
            {
                "_type": "EVALUATION",
                "name": {"_type": "DV_TEXT", "value": "Letter"},
                "archetype_node_id": "openEHR-EHR-EVALUATION.clinical_synopsis.v1",
                "data": {
                    "_type": "ITEM_TREE",
                    "name": {"_type": "DV_TEXT", "value": "Tree"},
                    "archetype_node_id": "at0001",
                    "items": [
                        {
                            "_type": "ELEMENT",
                            "name": {"_type": "DV_TEXT", "value": "Synopsis"},
                            "archetype_node_id": "at0002",
                            "value": {"_type": "DV_TEXT", "value": body},
                        }
                    ],
                },
            }
        ],
    }

    return create_composition(
        ehr_id, "openEHR-EHR-COMPOSITION.report.v1", composition_data
    )

get_letter_composition

get_letter_composition(patient_id, composition_uid)

Retrieve a letter composition from OpenEHR.

Parameters:

Name Type Description Default
patient_id str

FHIR Patient ID

required
composition_uid str

The composition UID

required

Returns:

Type Description
dict[str, Any] | None

Composition data or None if not found

Source code in app/ehrbase_client.py
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
def get_letter_composition(
    patient_id: str, composition_uid: str
) -> dict[str, Any] | None:
    """
    Retrieve a letter composition from OpenEHR.

    Args:
        patient_id: FHIR Patient ID
        composition_uid: The composition UID

    Returns:
        Composition data or None if not found
    """
    try:
        ehr = get_ehr_by_subject(patient_id)
        if not ehr:
            return None

        ehr_id = ehr["ehr_id"]["value"]
        return get_composition(ehr_id, composition_uid)
    except Exception:
        return None

list_letters_for_patient

list_letters_for_patient(patient_id)

List all letter compositions for a patient.

Parameters:

Name Type Description Default
patient_id str

FHIR Patient ID

required

Returns:

Type Description
list[dict[str, Any]]

List of letter compositions with metadata

Source code in app/ehrbase_client.py
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
def list_letters_for_patient(patient_id: str) -> list[dict[str, Any]]:
    """
    List all letter compositions for a patient.

    Args:
        patient_id: FHIR Patient ID

    Returns:
        List of letter compositions with metadata
    """
    try:
        ehr = get_ehr_by_subject(patient_id)
        if not ehr:
            return []

        ehr_id = ehr["ehr_id"]["value"]

        # Query for all report compositions (letters)
        aql = f"""
        SELECT
            c/uid/value as composition_uid,
            c/name/value as title,
            c/context/start_time/value as created_at,
            c/composer/name as author
        FROM EHR e[ehr_id/value='{ehr_id}']
        CONTAINS COMPOSITION c[openEHR-EHR-COMPOSITION.report.v1]
        ORDER BY c/context/start_time/value DESC
        """

        result = query_aql(aql)
        return result.get("rows", [])  # type: ignore[no-any-return]
    except Exception:
        return []