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

- 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 identifier
- `concept` - Human-readable template concept name
- `description` - Template description/purpose
- `language` - Primary language (e.g., "en")
- `archetype_id` - Root archetype ID
- `rm_type` - Reference Model type (usually "COMPOSITION")
- `root` - The root archetype node tree
- `all_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 value
- `DV_CODED_TEXT` - Text with terminology code
- `DV_BOOLEAN` - Boolean value
- `DV_IDENTIFIER` - Unique identifier
- `DV_URI` - URI reference
- `DV_EHR_URI` - EHR-specific URI

### Quantitative Types

- `DV_QUANTITY` - Quantity with units and magnitude
- `DV_COUNT` - Integer count
- `DV_PROPORTION` - Ratio or percentage
- `DV_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 time
- `DV_DATE` - Date only
- `DV_TIME` - Time only
- `DV_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.