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.
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.
pip install oehrpyOr install from source:
git clone https://github.com/platzhersh/oehrpy.git
cd oehrpy
pip install -e .- 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.
- 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
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}")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.
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 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.optThe 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.
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-pathsIntegrate 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)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)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/settingfrom 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},
)| 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 | ✅ | ✅ | 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.
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)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 100The SDK includes all major openEHR RM 1.1.0 types:
Data Types:
DV_TEXT,DV_CODED_TEXT,CODE_PHRASEDV_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_DURATIONDV_BOOLEAN,DV_IDENTIFIER,DV_URI,DV_EHR_URIDV_MULTIMEDIA,DV_PARSABLE
Structures:
COMPOSITION,SECTION,ENTRYOBSERVATION,EVALUATION,INSTRUCTION,ACTIONITEM_TREE,ITEM_LIST,CLUSTER,ELEMENTHISTORY,EVENT,POINT_EVENT,INTERVAL_EVENT
Support:
PARTY_IDENTIFIED,PARTY_SELF,PARTICIPATIONOBJECT_REF,OBJECT_ID,HIER_OBJECT_IDARCHETYPED,LOCATABLE,PATHABLE
- 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.
- Python 3.10+
- pip
# Clone the repository
git clone https://github.com/platzhersh/oehrpy.git
cd oehrpy
# Install development dependencies
pip install -e ".[dev]"pytest tests/ -vmypy src/oehrpyThe RM classes are generated from openEHR BMM specifications:
python -m generator.pydantic_generatoroehrpy/
├── 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
Contributions are welcome! Please see CONTRIBUTING.md for guidelines on how to get started.
Apache-2.0
- FLAT Format Versions - Understanding EHRBase 2.0+ FLAT format changes
- FLAT Format Learnings - Comprehensive FLAT format guide
- ADR-0001: RM 1.1.0 Support
- ADR-0002: Integration Testing
- ADR-0007: Dual-Backend FLAT Validation
- ADR-0008: OPT Validation in VS Code via the Python CLI
- ADR-0009: Astro for the GitHub Pages Site
- PRD-0000: Python openEHR SDK
- PRD-0008: OPT Validator
- openEHR BMM Specifications
- openEHR RM Specification
- EHRBase
- EHRBase Documentation (Note: FLAT format docs may be outdated, see our FLAT Format Versions guide)
- Building Open CIS Part 4: The openEHR SDK Landscape
- Building Open CIS Part 5: oehrpy — A Python SDK for openEHR
- 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