Skip to content
platzhershPublic

About

openEHR SDK for Python

Topics

Resources

Contributing

Stars

13 stars

Watchers

1 watching

Forks

Latest commit

 

History

99 Commits

Folders and files

NameName
Last commit message
Last commit date
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 

Repository files navigation

oehrpy — openEHR, the Pythonic way

PyPI version Python versions Downloads License CI Changelog Documentation

Pronunciation: /oʊ.ɛər.paɪ/ ("o-air-pie") — short for "openehrpy", where "ehr" is pronounced like "air" (as in openEHR).

A comprehensive Python SDK for openEHR that provides type-safe Reference Model classes, template-specific composition builders, REST clients for EHRBase and FerroEHR, and AQL query builder.

Overview

This project addresses the gap in the openEHR ecosystem where no comprehensive, actively maintained Python SDK exists. It eliminates the need for developers to manually construct complex nested JSON structures when working with openEHR compositions.

New to openEHR? Start with the workflow overview to see where oehrpy fits in the openEHR data lifecycle.

Installation

pip install oehrpy

Or install from source:

git clone https://github.com/platzhersh/oehrpy.git
cd oehrpy
pip install -e .

Compatibility

  • Python: 3.10+
  • openEHR RM: 1.1.0
  • EHRBase: 2.26.0+ (uses new FLAT format with composition tree IDs)
  • FerroEHR: 4.3+ (see Supported CDRs)

Note: EHRBase 2.0+ introduced breaking changes to the FLAT format. This SDK implements the new format used by EHRBase 2.26.0. For details, see FLAT Format Versions.

Features

  • Type-safe RM Classes: 134 Pydantic models for openEHR Reference Model 1.1.0 types (includes BASE types)
  • Template Builders: Pre-built composition builders for common templates (Vital Signs)
  • OPT Parser & Generator: Parse OPT files and auto-generate type-safe builder classes
  • FLAT Format: Full support for EHRBase 2.26.0+ FLAT format serialization
  • Canonical JSON: Convert RM objects to/from openEHR canonical JSON format
  • CDR Clients: Async ITS-REST client with adapters for EHRBase and FerroEHR, Basic and OIDC bearer auth
  • Contributions & Audit: Commit multiple changes atomically with audit metadata via a fluent builder
  • AQL Builder: Fluent API for building type-safe AQL queries
  • OPT Validator: Validate OPT 1.4 XML files before CDR upload (well-formedness, semantics, FLAT path impact)
  • IDE Support: Full autocomplete and type checking support
  • Validation: Pydantic v2 validation for all fields

Quick Start

Creating RM Objects

from oehrpy.rm import (
    DV_QUANTITY, DV_TEXT, DV_CODED_TEXT,
    CODE_PHRASE, TERMINOLOGY_ID
)

# Create a simple text value
text = DV_TEXT(value="Patient vital signs recorded")

# Create a quantity (e.g., blood pressure)
bp_systolic = DV_QUANTITY(
    magnitude=120.0,
    units="mm[Hg]",
    property=CODE_PHRASE(
        terminology_id=TERMINOLOGY_ID(value="openehr"),
        code_string="382"
    )
)
print(f"Blood pressure: {bp_systolic.magnitude} {bp_systolic.units}")

End-to-End: RM Values → FLAT → CDR

You do not need to write your own FLAT serializer or subclass RM types. FlatBuilder maps RM values onto FLAT |attribute keys (e.g. DV_QUANTITY → |magnitude + |unit, DV_CODED_TEXT → |value + |code + |terminology), and the paths themselves come from the CDR's Web Template (ADR-0005). The same FLAT payload works on EHRBase and FerroEHR, since both implement the openEHR Simplified Formats spec.

from oehrpy.client import EHRBaseClient
from oehrpy.rm import DV_QUANTITY
from oehrpy.serialization import FlatBuilder

BP = "vital_signs_observations/vital_signs/blood_pressure"  # from the Web Template

systolic = DV_QUANTITY(magnitude=120.0, units="mm[Hg]")

builder = FlatBuilder(composition_prefix="vital_signs_observations")
builder.context(language="en", territory="US", composer_name="Dr. Smith")
builder.set(f"{BP}/history_origin", "2024-01-15T10:30:00Z")
builder.set_quantity(f"{BP}/systolic", systolic.magnitude, systolic.units)
flat = builder.build()

async with EHRBaseClient(base_url=..., username=..., password=...) as client:
    await client.upload_template(opt_xml)
    web_template = await client.get_web_template("IDCR - Vital Signs Encounter.v1")  # valid paths
    ehr = await client.create_ehr()
    result = await client.create_composition(
        ehr_id=ehr.ehr_id, composition=flat,
        template_id="IDCR - Vital Signs Encounter.v1", format="FLAT",
    )

A complete, runnable version (upload, Web Template, POST, read back) is in examples/flat_end_to_end.py.

Template Builders

Build compositions using type-safe builders without knowing FLAT paths:

from oehrpy.templates import VitalSignsBuilder

# Create a vital signs composition
builder = VitalSignsBuilder(composer_name="Dr. Smith")
builder.add_blood_pressure(systolic=120, diastolic=80)
builder.add_pulse(rate=72)
builder.add_temperature(37.2)
builder.add_respiration(rate=16)
builder.add_oxygen_saturation(spo2=98)

# Get FLAT format for EHRBase submission
flat_data = builder.build()
# {
#   "vital_signs_observations/language|code": "en",
#   "vital_signs_observations/territory|code": "US",
#   "vital_signs_observations/composer|name": "Dr. Smith",
#   "vital_signs_observations/category|code": "433",
#   "vital_signs_observations/vital_signs/blood_pressure/systolic|magnitude": 120,
#   "vital_signs_observations/vital_signs/blood_pressure/systolic|unit": "mm[Hg]",
#   "vital_signs_observations/vital_signs/body_temperature/temperature|unit": "°C",
#   ...
# }

Generate Builder Skeletons from OPT Files

Generate template metadata skeletons from OPT (Operational Template) files. The generated code includes the template ID, concept, and discovered archetypes, but not FLAT path strings — FLAT paths must come from the Web Template JSON provided by the CDR:

from oehrpy.templates import generate_builder_from_opt, parse_opt

# Parse an OPT file (metadata extraction)
template = parse_opt("path/to/your-template.opt")
print(f"Template: {template.template_id}")
print(f"Observations: {len(template.list_observations())}")

# Generate a Python builder skeleton (metadata only, no FLAT paths)
code = generate_builder_from_opt("path/to/your-template.opt")
print(code)  # Class skeleton with template_id and archetype list

# Or save directly to a file
from oehrpy.templates import BuilderGenerator

generator = BuilderGenerator()
generator.generate_to_file(template, "my_template_builder.py")

Command-line tool:

python examples/generate_builder_from_opt.py path/to/template.opt

The generated skeleton must be supplemented with FLAT paths from the Web Template. Fetch it via EHRBaseClient.get_web_template(template_id) after uploading the OPT to a CDR. See ADR-0005 for the rationale.

OPT Validation

Validate OPT 1.4 XML files before uploading to a CDR. The validator checks for XML well-formedness, semantic integrity, structural issues, and FLAT path impact:

from oehrpy.validation import OPTValidator

validator = OPTValidator()
result = validator.validate_file("path/to/template.opt")

if result.is_valid:
    print(f"Template '{result.template_id}' is valid!")
    print(f"  Archetypes: {result.archetype_count}, Nodes: {result.node_count}")
else:
    for issue in result.errors:
        print(f"[{issue.code}] {issue.message}")
        if issue.suggestion:
            print(f"  -> {issue.suggestion}")

# Warnings are always available even when valid
for w in result.warnings:
    print(f"Warning: [{w.code}] {w.message}")

Validation categories:

Category Severity Examples
Well-formedness Error Invalid XML, wrong namespace, missing template_id, unknown RM types
Semantic integrity Error Missing term definitions, orphan terminology bindings
Structural Warning Draft lifecycle, v0 archetypes, prohibited nodes, unconstrained slots
FLAT path impact Info Renamed nodes, path collisions, special characters in concept

Command-line tool:

# Validate one or more OPT files
oehrpy-validate-opt path/to/template.opt

# JSON output for CI/CD pipelines
oehrpy-validate-opt template.opt --output json

# Treat warnings as errors (strict mode)
oehrpy-validate-opt template.opt --strict

# Show FLAT path impact details
oehrpy-validate-opt template.opt --show-flat-paths

Integrate with OPT parsing and builder generation:

from oehrpy.templates import parse_opt, generate_builder_from_opt

# Validate during parsing (raises OPTValidationError on errors)
template = parse_opt("template.opt", validate=True)

# Validate before generating builder code
code = generate_builder_from_opt("template.opt", validate=True)

Canonical JSON Serialization

from oehrpy.rm import DV_QUANTITY, CODE_PHRASE, TERMINOLOGY_ID
from oehrpy.serialization import to_canonical, from_canonical

# Serialize to canonical JSON (with _type fields)
quantity = DV_QUANTITY(magnitude=120.0, units="mm[Hg]", ...)
canonical = to_canonical(quantity)
# {"_type": "DV_QUANTITY", "magnitude": 120.0, "units": "mm[Hg]", ...}

# Deserialize back to Python object
restored = from_canonical(canonical, expected_type=DV_QUANTITY)

FLAT Format Builder

from oehrpy.serialization import FlatBuilder

# For EHRBase 2.26.0+, use composition tree ID as prefix
builder = FlatBuilder(composition_prefix="vital_signs_observations")
builder.context(language="en", territory="US", composer_name="Dr. Smith")
builder.set_quantity("vital_signs_observations/vital_signs/blood_pressure/systolic", 120.0, "mm[Hg]")
builder.set_coded_text("vital_signs_observations/vital_signs/blood_pressure/position", "Sitting", "at0001")

flat_data = builder.build()
# Automatically includes required fields: category, context/start_time, context/setting

EHRBase REST Client

from oehrpy.client import EHRBaseClient

async with EHRBaseClient(
    base_url="http://localhost:8080/ehrbase",
    username="admin",
    password="admin",
) as client:
    # Create an EHR
    ehr = await client.create_ehr()
    print(f"Created EHR: {ehr.ehr_id}")

    # Create a composition
    result = await client.create_composition(
        ehr_id=ehr.ehr_id,
        template_id="IDCR - Vital Signs Encounter.v1",
        composition=flat_data,
        format="FLAT",
    )
    print(f"Created composition: {result.uid}")

    # Query compositions
    query_result = await client.query(
        "SELECT c FROM EHR e CONTAINS COMPOSITION c WHERE e/ehr_id/value = :ehr_id",
        query_parameters={"ehr_id": ehr.ehr_id},
    )

Supported CDRs

CDR Status Client Notes
EHRBase 2.26+ ✅ Supported (default) EHRBaseClient Admin API at /rest/admin
FerroEHR 4.3+ ✅ Supported FerroEHRClient Admin API at /rest/openehr/v1/admin (ADMIN role); known server issue: FLAT GET drops nested HISTORY content (OEH-50)
Other ITS-REST 1.1.0 servers ⚙️ Generic OpenEHRClient No vendor admin API
Better Platform 🛠️ Tooling only — FLAT validation, conversion and template explorer support the Better dialect; no dedicated client yet (OEH-87)
EHRServer 🗓️ Planned —
Feature EHRBase FerroEHR Better Other ITS-REST
REST client ✅ EHRBaseClient ✅ FerroEHRClient ❌ not yet ⚙️ OpenEHRClient
Admin API ✅ ✅ ❌ not yet ❌
Template upload/list ✅ ✅ ❓ untested ✅
Template delete ✅ ✅ ⚠️ only via EHRBaseClient(cdr_type=CDRType.BETTER) ✅
FLAT dialect (validator, converter, explorer, CLI) ✅ platform="ehrbase" no separate dialect ✅ platform="better" (/any_event:0/ and template_id prefix not yet enumerated) —
Integration tests in CI ✅ ✅ (non-blocking) ❌ ❌

Where the CDRs deviate (FLAT media type, template header, status endpoint, EHR_STATUS.archetype_details, template versions, admin paths), the adapters handle it for you; see ADR-0011.

from oehrpy.client import BearerAuth, FerroEHRClient, create_client

# FerroEHR with Basic auth (separate admin user for the admin API)
async with FerroEHRClient(
    base_url="http://localhost:8080/ferroehr",
    username="ferroehr",
    password="ferroehr",
    admin_username="ferroehr-admin",
    admin_password="ferroehr",
) as client:
    info = await client.get_server_info()  # unauthenticated, works with bad creds
    ehr = await client.create_ehr()
    await client.delete_ehr(ehr.ehr_id)     # admin API

# FerroEHR with an OIDC access token; the provider is called per request,
# so cache the token and refresh it when it is about to expire
async with FerroEHRClient(
    base_url="https://cdr.example.org/ferroehr",
    auth_method=BearerAuth(token_provider=get_access_token),
) as client:
    ...

# Pick the CDR from configuration ("ehrbase", "ferroehr" or "generic")
client = create_client(os.environ["CDR_TYPE"], base_url=os.environ["CDR_URL"],
                       username=os.environ["CDR_USER"], password=os.environ["CDR_PASSWORD"])

EHRBaseError remains importable as an alias of the new base exception OpenEHRError; a 403 (e.g. a READONLY user writing) raises AuthorizationError.

Contributions & Audit

Group one or more versioned-object changes into a single atomic changeset with shared audit metadata. ContributionBuilder assembles the CANONICAL request body so you don't hand-write ORIGINAL_VERSION wrappers. It supports all four openEHR change types: creation, amendment, modification, and deleted.

from oehrpy.client import ContributionBuilder, EHRBaseClient

async with EHRBaseClient(base_url="http://localhost:8080/ehrbase") as client:
    contribution = (
        # `system_id` is optional — EHRBase fills it (and time_committed)
        # server-side when omitted; pass it for an RM-complete audit.
        ContributionBuilder(system_id="oehrpy.example.org")
        # `composition` is a CANONICAL composition dict
        # (e.g. from oehrpy.serialization.to_canonical)
        .add_creation(composition=vitals_canonical)
        .add_amendment(
            preceding_version_uid="abc::ehrbase::1",
            composition=updated_canonical,
            description="Corrected systolic value",
        )
        .set_audit(committer="Dr. Smith", description="Routine vitals and correction")
        .build()
    )

    result = await client.create_contribution(ehr.ehr_id, contribution)
    print(result.contribution_uid, result.versions)

    # Retrieve a contribution (audit metadata + referenced version UIDs)
    fetched = await client.get_contribution(ehr.ehr_id, result.contribution_uid)

AQL Query Builder

from oehrpy.aql import AQLBuilder

# Build complex queries with a fluent API
query = (
    AQLBuilder()
    .select("c/uid/value", alias="composition_id")
    .select("c/context/start_time/value", alias="time")
    .from_ehr()
    .contains_composition()
    .contains_observation(archetype_id="openEHR-EHR-OBSERVATION.blood_pressure.v1")
    .where_ehr_id()
    .order_by_time(descending=True)
    .limit(100)
    .build()
)

print(query.to_string())
# SELECT c/uid/value AS composition_id, c/context/start_time/value AS time
# FROM EHR e CONTAINS COMPOSITION c CONTAINS OBSERVATION o[...]
# WHERE e/ehr_id/value = :ehr_id
# ORDER BY c/context/start_time/value DESC
# LIMIT 100

Available RM Types

The SDK includes all major openEHR RM 1.1.0 types:

Data Types:

  • DV_TEXT, DV_CODED_TEXT, CODE_PHRASE
  • DV_QUANTITY, DV_COUNT, DV_PROPORTION, DV_SCALE (new in 1.1.0)
  • DV_ORDINAL (integer values only - use DV_SCALE for decimal scale values)
  • DV_DATE_TIME, DV_DATE, DV_TIME, DV_DURATION
  • DV_BOOLEAN, DV_IDENTIFIER, DV_URI, DV_EHR_URI
  • DV_MULTIMEDIA, DV_PARSABLE

Structures:

  • COMPOSITION, SECTION, ENTRY
  • OBSERVATION, EVALUATION, INSTRUCTION, ACTION
  • ITEM_TREE, ITEM_LIST, CLUSTER, ELEMENT
  • HISTORY, EVENT, POINT_EVENT, INTERVAL_EVENT

Support:

  • PARTY_IDENTIFIED, PARTY_SELF, PARTICIPATION
  • OBJECT_REF, OBJECT_ID, HIER_OBJECT_ID
  • ARCHETYPED, LOCATABLE, PATHABLE

New in RM 1.1.0

  • DV_SCALE: Data type for scales/scores with decimal values (extends DV_ORDINAL for non-integer scales)
  • preferred_term: New optional field in DV_CODED_TEXT for terminology mapping
  • Enhanced Folder support: Archetypeable meta-data in EHR folders

For details, see ADR-0001: Support RM 1.1.0.

Development

Prerequisites

  • Python 3.10+
  • pip

Setup

# Clone the repository
git clone https://github.com/platzhersh/oehrpy.git
cd oehrpy

# Install development dependencies
pip install -e ".[dev]"

Running Tests

pytest tests/ -v

Type Checking

mypy src/oehrpy

Regenerating RM Classes

The RM classes are generated from openEHR BMM specifications:

python -m generator.pydantic_generator

Project Structure

oehrpy/
├── src/oehrpy/       # Main package
│   ├── rm/                # Generated RM + BASE classes (134 types)
│   ├── serialization/     # JSON serialization (canonical + FLAT)
│   ├── client/            # openEHR REST clients (generic, EHRBase, FerroEHR)
│   ├── templates/         # Template builders (Vital Signs, etc.)
│   ├── validation/        # FLAT composition & OPT template validation
│   │   └── opt/           # OPT 1.4 XML validator (4 check categories)
│   └── aql/               # AQL query builder
├── generator/             # Code generation tools
│   ├── bmm_parser.py      # BMM JSON parser
│   ├── pydantic_generator.py  # Pydantic code generator
│   └── bmm/               # BMM specification files
├── tests/                 # Test suite
├── website/               # GitHub Pages site (Astro)
└── docs/                  # Documentation

Contributing

Contributions are welcome! Please see CONTRIBUTING.md for guidelines on how to get started.

License

Apache-2.0

Documentation

References

Related Projects

  • openEHR Explorer — cross-platform desktop app for browsing, querying, and inspecting openEHR CDRs (openehr-explorer.dev)
  • Open CIS — open-source clinical information system built on openEHR, and the project oehrpy grew out of

Star History

Star History Chart

About

openEHR SDK for Python

Topics

Resources

Contributing

Stars

13 stars

Watchers

1 watching

Forks

Releases

Packages

Used by

Contributors

Languages