Documentation
Welcome to the oehrpy documentation. This guide will help you get started with the Python openEHR SDK and explore its features.
Installation
Install oehrpy using pip:
pip install oehrpy
Or install from source for development:
git clone https://github.com/platzhersh/oehrpy.git
cd oehrpy
pip install -e ".[dev]"
Requirements: Python 3.10 or higher is required.
Quick Start
Here's a quick example to get you started with oehrpy:
from openehr_sdk.rm import DV_TEXT, DV_QUANTITY, CODE_PHRASE, TERMINOLOGY_ID
# Create a simple text value
text = DV_TEXT(value="Patient temperature recorded")
# Create a quantity with units
temperature = DV_QUANTITY(
magnitude=37.2,
units="°C",
property=CODE_PHRASE(
terminology_id=TERMINOLOGY_ID(value="openehr"),
code_string="127"
)
)
print(f"Temperature: {temperature.magnitude} {temperature.units}")
Overview
oehrpy is a comprehensive Python SDK for openEHR that provides:
- Type-safe RM Classes: 134 Pydantic models for openEHR Reference Model 1.1.0 (includes BASE types)
- OPT Parser & Code Generation: Parse OPT 1.4 XML files and auto-generate type-safe Python builder classes
- Template Builders: Pre-built composition builders for common clinical templates
- FLAT Format Support: Full support for EHRBase FLAT format serialization
- Canonical JSON: Convert between RM objects and openEHR canonical JSON
- EHRBase Client: Async REST client for EHRBase CDR operations
- AQL Builder: Fluent API for building type-safe AQL queries
- Full IDE Support: Complete autocomplete and type checking
RM Classes
The Reference Model (RM) classes form the foundation of oehrpy. These are type-safe Pydantic models that represent openEHR data structures.
New in RM 1.1.0: Support for DV_SCALE (decimal scale values), preferred_term field in CODE_PHRASE, and enhanced Folder support. All 134 types include both RM and BASE components.
Data Types
oehrpy includes all major openEHR data types:
Text and Coded Values
from openehr_sdk.rm import DV_TEXT, DV_CODED_TEXT, CODE_PHRASE, TERMINOLOGY_ID
# Simple text
text = DV_TEXT(value="Blood pressure measurement")
# Coded text with terminology
status = DV_CODED_TEXT(
value="Normal",
defining_code=CODE_PHRASE(
terminology_id=TERMINOLOGY_ID(value="local"),
code_string="at0001"
)
)
Quantities and Measurements
from openehr_sdk.rm import DV_QUANTITY, DV_COUNT
# Blood pressure (quantity with units)
systolic = DV_QUANTITY(
magnitude=120.0,
units="mm[Hg]",
property=CODE_PHRASE(
terminology_id=TERMINOLOGY_ID(value="openehr"),
code_string="382" # Pressure
)
)
# Heart rate (count)
heart_rate = DV_COUNT(magnitude=72)
Date and Time
from openehr_sdk.rm import DV_DATE_TIME, DV_DATE, DV_TIME, DV_DURATION
# Date and time
timestamp = DV_DATE_TIME(value="2024-01-15T14:30:00Z")
date = DV_DATE(value="2024-01-15")
time = DV_TIME(value="14:30:00")
# Duration
duration = DV_DURATION(value="PT2H30M") # 2 hours 30 minutes
Structures
Build complex clinical structures using openEHR structural types:
from openehr_sdk.rm import ELEMENT, CLUSTER, ITEM_TREE
# Create an element
bp_element = ELEMENT(
name=DV_TEXT(value="Systolic"),
value=systolic
)
# Group elements in a cluster
bp_cluster = CLUSTER(
name=DV_TEXT(value="Blood Pressure"),
items=[bp_element, ...]
)
OPT Parser & Builder Generator
oehrpy provides powerful tools for working with OPT (Operational Template) files. OPT files are XML documents that define constraints on openEHR archetypes for specific clinical use cases. With oehrpy, you can parse OPT files and automatically generate type-safe Python builder classes.
Key Feature: Generate template-specific builders with full IDE autocomplete from your OPT files - no manual FLAT path construction required!
Parsing OPT Files
Use the parse_opt() function to parse an OPT file and extract template metadata:
from openehr_sdk.templates import parse_opt
# Parse an OPT file
template = parse_opt("path/to/vital_signs.opt")
# Access template metadata
print(f"Template ID: {template.template_id}")
print(f"Concept: {template.concept}")
print(f"Language: {template.language}")
# List all observations in the template
for obs in template.list_observations():
print(f" - {obs.name} ({obs.archetype_id})")
# List all entry types (OBSERVATION, EVALUATION, etc.)
entries = template.list_entries()
print(f"Found {len(entries)} entries")
Template Definition
The parsed TemplateDefinition provides access to:
template_id- The unique template identifierconcept- Human-readable template concept namedescription- Template description/purposelanguage- Primary language (e.g., "en")archetype_id- Root archetype IDrm_type- Reference Model type (usually "COMPOSITION")root- The root archetype node treeall_nodes- Dictionary of all nodes by path
Generating Builder Classes
The most powerful feature is automatic builder generation. This creates type-safe Python classes with methods for each observation type:
from openehr_sdk.templates import generate_builder_from_opt
# Generate builder code from OPT file
code = generate_builder_from_opt("vital_signs.opt")
# Save to file for use in your project
generate_builder_from_opt(
"vital_signs.opt",
output_path="my_project/builders/vital_signs_builder.py"
)
# Use a custom class name
generate_builder_from_opt(
"vital_signs.opt",
output_path="vital_signs_builder.py",
class_name="MyVitalSignsBuilder"
)
Using the BuilderGenerator Class
For more control, use the BuilderGenerator class directly:
from openehr_sdk.templates import parse_opt, BuilderGenerator
# Parse the template
template = parse_opt("vital_signs.opt")
# Create generator and generate code
generator = BuilderGenerator()
code = generator.generate(template)
# Or generate directly to a file
generator.generate_to_file(
template,
output_path="generated_builder.py",
class_name="VitalSignsBuilder"
)
Complete Workflow Example
Here's a complete example of the OPT-to-composition workflow:
# Step 1: Generate the builder (do this once)
from openehr_sdk.templates import generate_builder_from_opt
generate_builder_from_opt(
"vital_signs.opt",
output_path="vital_signs_builder.py"
)
# Step 2: Use the generated builder in your application
from vital_signs_builder import VitalSignsBuilder
# Create the builder
builder = VitalSignsBuilder(composer_name="Dr. Smith")
# Add clinical observations (type-safe with IDE autocomplete!)
builder.add_blood_pressure(systolic=120, diastolic=80)
builder.add_pulse(rate=72)
builder.add_temperature(magnitude=37.2)
builder.add_respiration(rate=16)
builder.add_oxygen_saturation(spo2=98)
# Build FLAT format data
flat_data = builder.build()
# Submit to EHRBase
async with EHRBaseClient(...) as client:
result = await client.create_composition(
ehr_id=ehr_id,
template_id=builder.template_id,
composition=flat_data,
format="FLAT"
)
Note: The generated builder classes extend TemplateBuilder and automatically handle FLAT path construction, event indexing, and time formatting.
Template Builders
Template builders provide a high-level API for creating compositions without knowing FLAT paths.
Vital Signs Builder
from openehr_sdk.templates import VitalSignsBuilder
# Create a vital signs composition
builder = VitalSignsBuilder(composer_name="Dr. Smith")
# Add measurements
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)
# Build FLAT format for EHRBase
flat_data = builder.build()
# {
# "ctx/language": "en",
# "ctx/territory": "US",
# "vital_signs/blood_pressure:0/any_event:0/systolic|magnitude": 120,
# ...
# }
Creating Custom Builders
You can create your own template builders for custom archetypes:
from openehr_sdk.serialization import FlatBuilder
class CustomTemplateBuilder:
def __init__(self, composer_name: str):
self.builder = FlatBuilder()
self.builder.context(
language="en",
territory="US",
composer_name=composer_name
)
def add_observation(self, value: float, unit: str):
self.builder.set_quantity(
"template/observation/value",
value,
unit
)
return self
def build(self):
return self.builder.build()
Serialization
oehrpy supports two main serialization formats: Canonical JSON and FLAT format.
Canonical JSON
Canonical JSON is the standard openEHR format with _type fields:
from openehr_sdk.serialization import to_canonical, from_canonical
from openehr_sdk.rm import DV_QUANTITY
# Serialize to canonical JSON
quantity = DV_QUANTITY(magnitude=120.0, units="mm[Hg]", ...)
canonical = to_canonical(quantity)
# {"_type": "DV_QUANTITY", "magnitude": 120.0, "units": "mm[Hg]", ...}
# Deserialize from canonical JSON
restored = from_canonical(canonical, expected_type=DV_QUANTITY)
FLAT Format
FLAT format is EHRBase's simplified format using paths:
from openehr_sdk.serialization import FlatBuilder
builder = FlatBuilder()
builder.context(language="en", territory="US", composer_name="Dr. Smith")
builder.set_quantity("vital_signs/bp/systolic", 120.0, "mm[Hg]")
builder.set_text("vital_signs/notes", "Patient stable")
flat_data = builder.build()
EHRBase Client
The EHRBase client provides an async REST API for interacting with EHRBase CDR.
Basic Operations
from openehr_sdk.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"
)
# Retrieve a composition
composition = await client.get_composition(
ehr_id=ehr.ehr_id,
composition_uid=result.uid,
format="FLAT"
)
Querying with AQL
# Execute AQL query
query_result = await client.query(
"""SELECT c/uid/value, c/context/start_time/value
FROM EHR e CONTAINS COMPOSITION c
WHERE e/ehr_id/value = :ehr_id""",
query_parameters={"ehr_id": ehr.ehr_id}
)
for row in query_result.rows:
print(f"Composition: {row[0]} at {row[1]}")
AQL Query Builder
Build complex AQL queries with a fluent, type-safe API.
Basic Queries
from openehr_sdk.aql import AQLBuilder
# Simple query
query = (
AQLBuilder()
.select("c/uid/value", alias="composition_id")
.select("c/name/value", alias="name")
.from_ehr()
.contains_composition()
.where_ehr_id()
.build()
)
print(query.to_string())
# SELECT c/uid/value AS composition_id, c/name/value AS name
# FROM EHR e CONTAINS COMPOSITION c
# WHERE e/ehr_id/value = :ehr_id
Complex Queries
# Query with observations and ordering
query = (
AQLBuilder()
.select("c/context/start_time/value", alias="time")
.select("o/data[at0001]/events[at0006]/data[at0003]/items[at0004]/value/magnitude",
alias="systolic")
.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()
)
Data Types Reference
Complete list of available RM data types:
Basic Types
DV_TEXT- Plain text valueDV_CODED_TEXT- Text with terminology codeDV_BOOLEAN- Boolean valueDV_IDENTIFIER- Unique identifierDV_URI- URI referenceDV_EHR_URI- EHR-specific URI
Quantitative Types
DV_QUANTITY- Quantity with units and magnitudeDV_COUNT- Integer countDV_PROPORTION- Ratio or percentageDV_ORDINAL- Ordered value (e.g., severity scale)DV_SCALE- Scale/score with decimal values (new in RM 1.1.0)
Using DV_SCALE (RM 1.1.0)
from openehr_sdk.rm import DV_SCALE, DV_CODED_TEXT, CODE_PHRASE, TERMINOLOGY_ID
# Pain scale with decimal value
pain_scale = DV_SCALE(
value=7.5, # Decimal scale value
symbol=DV_CODED_TEXT(
value="Severe pain",
defining_code=CODE_PHRASE(
terminology_id=TERMINOLOGY_ID(value="local"),
code_string="at0075",
preferred_term="Severe" # New in RM 1.1.0
)
)
)
Temporal Types
DV_DATE_TIME- Date and timeDV_DATE- Date onlyDV_TIME- Time onlyDV_DURATION- Time duration (ISO 8601)
Complex Types
DV_MULTIMEDIA- Media content (images, video, etc.)DV_PARSABLE- Parsable text in specific format
Validation
oehrpy uses Pydantic v2 for comprehensive validation.
Automatic Validation
from openehr_sdk.rm import DV_QUANTITY
try:
# This will raise a validation error
invalid = DV_QUANTITY(
magnitude="not a number", # Should be float
units="mm[Hg]"
)
except ValidationError as e:
print(e)
Custom Validation
All RM classes support Pydantic's validation features:
from pydantic import ValidationError
# Validate required fields
try:
text = DV_TEXT() # Missing required 'value' field
except ValidationError as e:
print(e.errors())
Type Safety
Full type hints enable IDE autocomplete and static type checking.
Using mypy
# Type checking with mypy
from openehr_sdk.rm import DV_TEXT, DV_QUANTITY
text: DV_TEXT = DV_TEXT(value="example") # OK
quantity: DV_QUANTITY = DV_TEXT(value="wrong") # mypy error
IDE Support
Modern IDEs like VS Code, PyCharm, and others provide full autocomplete:
- Field names and types
- Method signatures
- Constructor parameters
- Return types
Development
Information for contributors and developers.
Setup Development Environment
# Clone repository
git clone https://github.com/platzhersh/oehrpy.git
cd oehrpy
# Install with development dependencies
pip install -e ".[dev,generator]"
# Run tests
pytest tests/ -v
# Type checking
mypy src/openehr_sdk
Regenerating RM Classes
The RM classes are generated from openEHR JSON Schema specifications (RM 1.1.0):
python -m generator.generate_rm_1_1_0
Project Structure
oehrpy/
├── src/openehr_sdk/ # Main package
│ ├── rm/ # Generated RM + BASE classes (134 types)
│ ├── serialization/ # JSON serialization
│ ├── client/ # EHRBase REST client
│ ├── templates/ # Template builders
│ └── aql/ # AQL query builder
├── generator/ # Code generation tools
├── tests/ # Test suite
└── docs/ # Documentation
Contributing: We welcome contributions! Check out the GitHub repository for guidelines.
Running Tests
# Run all tests
pytest
# Run with coverage
pytest --cov=openehr_sdk --cov-report=html
# Run specific test file
pytest tests/test_rm.py -v
Note: Some tests require a running EHRBase instance. Use the provided Docker Compose setup for integration tests.