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 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:

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

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, 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:

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.