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
FLAT Format Validator
The FlatValidator validates FLAT format compositions against Web Template definitions before submission to a CDR. It catches invalid paths, wrong suffixes, missing required fields, and provides "did you mean?" suggestions for renamed nodes.
Try it in the browser: The FLAT Validator web tool runs entirely client-side — paste your Web Template and FLAT composition and validate instantly.
Python API
from openehr_sdk.validation import FlatValidator
# Initialize with a Web Template JSON dict
validator = FlatValidator.from_web_template(web_template, platform="ehrbase")
# Validate a FLAT composition
result = validator.validate(flat_composition)
if not result.is_valid:
for error in result.errors:
print(f" {error.path}: {error.message}")
if error.suggestion:
print(f" Did you mean: {error.suggestion}")
Fetching from EHRBase
# Or fetch the Web Template directly from EHRBase
validator = await FlatValidator.from_ehrbase(
client=ehrbase_client,
template_id="IDCR - Adverse Reaction List.v1"
)
result = validator.validate(flat_data)
What It Catches
- Unknown paths — paths not in the Web Template, with fuzzy "did you mean?" suggestions
- Renamed nodes — detects when a template renames a node (e.g.,
substance→causative_agent) and suggests the correct path - Wrong suffixes — e.g.,
|valueon aDV_QUANTITYinstead of|magnitude - Index notation mismatch — EHRBase 2.x doesn't use
:0for single-occurrence items - Missing required fields —
category,language,territory,composer,context/start_time,context/setting
Platform Support
Pass platform="ehrbase" or platform="better" to match your CDR's FLAT format dialect:
- EHRBase 2.x — no
:0indexing on single-occurrence items, no/any_event/nodes,tree.idprefix - Better — may include
:0indexing on tree-based nodes (the validator and tests verify that some leaf paths carry:0);/any_event:0/andtemplate_idprefix support are defined in the dialect configuration but not yet implemented in the path enumerator
Validation Result
# The result contains all details
result.is_valid # bool
result.errors # list[ValidationError] - invalid paths
result.warnings # list[ValidationError] - missing required fields
result.platform # "ehrbase" or "better"
result.template_id # template ID from the Web Template
result.valid_path_count # total valid paths in the template
result.checked_path_count # paths checked in the composition
# Each error has:
error.path # the invalid path
error.error_type # "unknown_path", "wrong_suffix", "missing_required", "index_mismatch"
error.message # human-readable explanation
error.suggestion # suggested fix (if available)
error.valid_alternatives # list of alternative valid paths
Exploring Valid Paths
# List all valid FLAT paths for a template
validator = FlatValidator.from_web_template(wt, platform="ehrbase")
for path in validator.valid_paths:
print(path)
OPT Validator
The OPTValidator validates OPT 1.4 (Operational Template) XML files before parsing or code generation. It checks well-formedness, semantic integrity, structural quality, and FLAT path impact — catching issues that would otherwise surface as cryptic errors downstream.
Basic Usage
from openehr_sdk.validation.opt import OPTValidator
validator = OPTValidator()
# Validate from a file
result = validator.validate_file("vital_signs.opt")
# Or validate an XML string
result = validator.validate_string(xml_content)
if result.is_valid:
print(f"Valid: {result.template_id} ({result.archetype_count} archetypes)")
else:
for issue in result.errors:
print(f" [{issue.code}] {issue.message}")
if issue.suggestion:
print(f" Fix: {issue.suggestion}")
Integrated Validation
Both parse_opt() and generate_builder_from_opt() accept a validate=True flag for fail-fast validation:
from openehr_sdk.templates import parse_opt, generate_builder_from_opt
from openehr_sdk.validation.opt import OPTValidationError
# Validate before parsing
try:
template = parse_opt("template.opt", validate=True)
except OPTValidationError as e:
print(f"Invalid: {e.result.error_count} errors")
for issue in e.result.errors:
print(f" {issue.message}")
# Validate before generating a builder
try:
code = generate_builder_from_opt("template.opt", validate=True)
except OPTValidationError as e:
print(f"Cannot generate: {e.result.error_count} errors")
CLI
Validate OPT files from the command line:
# Text output (default)
python -m openehr_sdk.validate_opt_cli template.opt
# JSON output (for CI/CD pipelines)
python -m openehr_sdk.validate_opt_cli template.opt --output json
# Treat warnings as errors
python -m openehr_sdk.validate_opt_cli template.opt --strict
# Include FLAT path impact analysis
python -m openehr_sdk.validate_opt_cli template.opt --show-flat-paths
Exit code 0 means valid, 1 means errors were found (or warnings in --strict mode).
Validation Categories
The validator runs four categories of checks in order:
- Well-formedness (errors) — XML validity, correct namespace, required fields (
template_id,concept,language), valid archetype IDs, RM type names against the 1.1.0 registry, occurrence constraints, and duplicate node IDs - Semantic integrity (errors) — missing term definitions for referenced
node_ids, orphaned terminology bindings, mandatory nodes without resolvable names - Structural warnings — draft lifecycle state, unstable archetype versions (
v0), prohibited nodes still in the tree, unconstrained archetype slots, special characters in concept names or term names, overuse of the same archetype - FLAT path impact (info) — detects renamed nodes whose FLAT paths differ from the archetype defaults, and sibling nodes that produce colliding FLAT path segments
Validation Result
# OPTValidationResult fields
result.is_valid # bool - True only if zero errors
result.template_id # str | None - extracted template ID
result.concept # str | None - extracted concept name
result.node_count # int - total nodes parsed
result.archetype_count # int - distinct archetypes found
result.error_count # int - number of errors
result.warning_count # int - number of warnings
result.errors # list[OPTValidationIssue] - error-severity issues
result.warnings # list[OPTValidationIssue] - warning-severity issues
# Each issue has:
issue.severity # "error", "warning", or "info"
issue.category # "wellformedness", "semantic", "structural", "flat_impact"
issue.code # e.g., "MISSING_TERM_DEF", "INVALID_RM_TYPE"
issue.message # human-readable description
issue.xpath # XPath to the offending element (if applicable)
issue.node_id # at-code (if applicable)
issue.archetype_id # archetype ID (if applicable)
issue.suggestion # recommended fix (if available)
# Serialize for reporting
result.to_dict() # dict
result.to_json(indent=2) # JSON string
Filtering Issues
# Filter by category
semantic = [i for i in result.issues if i.category == "semantic"]
# Filter by specific code
missing_terms = [i for i in result.issues if i.code == "MISSING_TERM_DEF"]
# Get FLAT path impact analysis
flat_issues = [i for i in result.issues if i.category == "flat_impact"]
RM 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
│ ├── validation/ # FLAT & OPT validators
│ └── 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.