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_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
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
- 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
References
- 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
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
Metadata
Release files for oehrpy 0.17.0
For a detailed explanation of source distributions (sdists) and built distributions (wheels), please see the package formats documentation.
Source distribution (sdist)
| File | Size | Uploaded | |
|---|---|---|---|
| oehrpy-0.17.0.tar.gz | 1.4 MB | Details |
Built distribution (wheel)
| File | Interpreter | ABI | Platform | Reset |
|---|---|---|---|---|
| oehrpy-0.17.0-py3-none-any.whl | Python 3 | none | any | Details |
Total release size: 1.5 MB
Release files / oehrpy-0.17.0.tar.gz
| Download URL | oehrpy-0.17.0.tar.gz |
|---|---|
| Size | 1.4 MB |
| Tags | Source |
|
SHA-256 checksum How to use checksums |
e526c115e95587589ec57b1779f681041c6a7371f7326c249472a1a507ff38f6
|
|
BLAKE2b-256 checksum How to use checksums |
d2c25ecfdd64c894a0c031ee24f1f288d9a9cd84cb1a081d676c1f0198e802cb
|
| Upload date | |
|
Uploaded using Trusted Publishing? What is trusted publishing? |
Yes |
| Uploaded via |
twine/7.0.0 CPython/3.13.14
|
Provenance
Provenance describes where a file came from. On PyPI, provenance is shared via attestations, which provide a verifiable record of the build or publishing details. View details, limitations and caveats.
PyPI Publish Attestation
PyPI verified that this artifact, at this checksum, originated from the publisher listed below.
Signed by GitHub Actions, verified by PyPI on Oct 9, 2026.
Transparency logRelease files / oehrpy-0.17.0-py3-none-any.whl
| Download URL | oehrpy-0.17.0-py3-none-any.whl |
|---|---|
| Size | 99.9 kB |
| Tags | Python 3 |
|
SHA-256 checksum How to use checksums |
239e4da7f5e56b5111ab7d7032c51dc9bb7c0df6846816f0bb1ca11a2027625f
|
|
BLAKE2b-256 checksum How to use checksums |
7ac04117c68e9316a08cada0135be1e9e258cf2dbc4488784d846212a6b248a1
|
| Upload date | |
|
Uploaded using Trusted Publishing? What is trusted publishing? |
Yes |
| Uploaded via |
twine/7.0.0 CPython/3.13.14
|
Provenance
Provenance describes where a file came from. On PyPI, provenance is shared via attestations, which provide a verifiable record of the build or publishing details. View details, limitations and caveats.
PyPI Publish Attestation
PyPI verified that this artifact, at this checksum, originated from the publisher listed below.
Signed by GitHub Actions, verified by PyPI on Oct 9, 2026.
Transparency log