oehrpy

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:

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 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 to extract template metadata (template ID, concept, archetypes) and generate builder class skeletons.

ADR-0005: FLAT paths cannot be reliably derived from OPT XML. The BuilderGenerator produces metadata-only class skeletons — no FLAT path strings. Use the Web Template JSON from the CDR for accurate FLAT paths. See ADR-0005 for details.

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:

Generating Builder Skeletons

Generate metadata-only class skeletons from OPT files. The generated code includes template ID, concept, and discovered archetypes, but not FLAT path strings (see ADR-0005):

from openehr_sdk.templates import generate_builder_from_opt

# Generate a builder skeleton (metadata only, no FLAT paths)
code = generate_builder_from_opt("vital_signs.opt")

# Save to file
generate_builder_from_opt(
    "vital_signs.opt",
    output_path="my_project/builders/vital_signs_skeleton.py"
)

# Use a custom class name
generate_builder_from_opt(
    "vital_signs.opt",
    output_path="vital_signs_skeleton.py",
    class_name="MyVitalSignsBuilder"
)

The generated skeleton must be supplemented with FLAT paths from the Web Template JSON. Fetch it after uploading the OPT to a CDR:

async with EHRBaseClient(...) as client:
    # Fetch Web Template (cached automatically)
    wt = await client.get_web_template("IDCR - Vital Signs Encounter.v1")
    # wt["tree"] contains the authoritative FLAT path segments

Web Template as FLAT Path Source

The Web Template JSON is the sole authoritative source for FLAT path derivation (ADR-0005). The EHRBaseClient provides cached fetching:

async with EHRBaseClient(...) as client:
    # Fetch with Accept: application/openehr.wt+json (cached in memory)
    wt = await client.get_web_template("IDCR - Vital Signs Encounter.v1")

    # Force refresh from CDR
    wt = await client.get_web_template("IDCR - Vital Signs Encounter.v1", use_cache=False)

    # Clear cache
    client.clear_web_template_cache()

Complete Workflow Example

Here's a complete example using the pre-built VitalSignsBuilder (which has FLAT paths sourced from the Web Template):

from openehr_sdk.templates import VitalSignsBuilder

# Create the builder (FLAT paths sourced from Web Template)
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: Pre-built builders like VitalSignsBuilder have FLAT paths verified against the Web Template from EHRBase. The BuilderGenerator produces metadata-only skeletons that need FLAT paths added manually from the Web Template.

Template Builders

Template builders provide a high-level API for creating compositions. FLAT paths in pre-built builders are sourced from the Web Template JSON (ADR-0005).

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

Quantitative Types

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

Complex Types

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

Platform Support

Pass platform="ehrbase" or platform="better" to match your CDR's FLAT format dialect:

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, and structural quality — catching issues that would otherwise surface as cryptic errors downstream. It also provides informational hints about potential FLAT path implications (though authoritative FLAT paths come from the Web Template, per ADR-0005).

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 skeleton
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 hints (informational, see ADR-0005)
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:

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:

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.