Skip to main content

OpenADR 3 Entity API Library

Project description

python-oa3

PyPI version Python versions CI Ruff License: MIT

Python client library for the OpenADR 3 API. Provides Pydantic v2 models with a two-layer coercion pattern (raw JSON shape + snake_case typed entities), an httpx-based API client, and pendulum-powered time types.

Installation

pip install -e ".[dev]"

Dependencies

Package Role
Pydantic v2 Schema validation, model coercion
Pendulum v3 DateTime, Duration, timezone handling
httpx HTTP client with auth hooks
openapi-core Optional OpenAPI spec validation
PyYAML OpenAPI spec loading

Architecture

Two-Layer Data Model

Every OpenADR 3 entity exists in two forms:

  1. Raw models (openadr3.entities.raw) — Mirror the JSON API shape exactly. CamelCase field aliases, string datetimes, string durations. Useful for serialization and wire-format validation.

  2. Coerced models (openadr3.entities.models) — Snake_case fields, pendulum.DateTime for timestamps, pendulum.Duration for durations, Decimal for PRICE/USAGE payloads. These are what you work with in application code.

JSON API response (camelCase, strings)
  │
  ▼
coerce(raw_dict)  ──►  Typed entity (snake_case, pendulum, Decimal)
                            │
                            └─► ._raw  (original dict preserved)

Raw Preservation

Every coerced entity carries its original raw dict as a Pydantic PrivateAttr:

program = openadr3.coerce(api_response)
program.program_name        # "My DR Program"
program.created             # DateTime(2024, 6, 15, 10, 0, 0, tzinfo=UTC)
program._raw["programName"] # "My DR Program" (original wire format)

Entity Dispatch

The coerce() function dispatches on the objectType string in the raw dict:

from openadr3 import coerce

raw = {"objectType": "PROGRAM", "programName": "Test", ...}
program = coerce(raw)  # Returns a Program instance

raw = {"objectType": "EVENT", "programID": "p1", ...}
event = coerce(raw)    # Returns an Event instance

Handles request variants too: BL_VEN_REQUEST and VEN_VEN_REQUEST coerce as Ven, BL_RESOURCE_REQUEST and VEN_RESOURCE_REQUEST coerce as Resource.

Notification Coercion

coerce_notification() handles both spec-compliant camelCase notifications and the snake_case format currently sent by the VTN Reference Implementation:

from openadr3 import coerce_notification, is_notification

# Spec-compliant (camelCase)
webhook_payload = {
    "objectType": "EVENT",
    "operation": "CREATE",
    "object": {"programID": "prog-001", "eventName": "Peak Event", ...}
}

# VTN-RI (snake_case) — see oadr3-org/openadr3-vtn-reference-implementation#181
mqtt_payload = {
    "object_type": "EVENT",
    "operation": "CREATE",
    "object": {"program_id": "prog-001", "event_name": "Peak Event", ...}
}

# Both formats work
for payload in [webhook_payload, mqtt_payload]:
    if is_notification(payload):
        notification = coerce_notification(payload)
        notification.object  # Coerced Event instance

Entity Types

Entity Key Fields Notes
Program program_name, interval_period, descriptions, payload_descriptors, attributes, targets Top-level DR program
Event program_id, event_name, duration, priority, intervals, targets DR event with signal intervals
Ven ven_name, client_id, attributes, targets Virtual End Node
Resource resource_name, ven_id, client_id, attributes, targets Device/load under a VEN
Report event_id, client_name, resources VEN telemetry report
Subscription client_name, object_operations, program_id, targets Webhook/MQTT subscription
Notification object_type, operation, object Push notification wrapper

All top-level entities share common metadata: id, created (DateTime), modified (DateTime), object_type.

Supporting Types

Type Description
IntervalPeriod Start datetime + duration + computed period tuple
Interval Numbered interval with payloads
Payload Type-tagged values (PRICE/USAGE get Decimal coercion)
EventPayloadDescriptor Event payload descriptor (payloadType, units, currency)
ReportPayloadDescriptor Report payload descriptor (payloadType, readingType, units, accuracy, confidence)
ObjectOperation Subscription callback definition

API Client

Quick Start

import openadr3

# Create a VEN client
client = openadr3.create_ven_client(
    base_url="https://vtn.example.com/openadr3/3.1.0",
    token="your-bearer-token",
    spec_path="resources/openadr3.yaml",  # optional, for route introspection
)

# Coerced entity methods — returns typed models
programs = client.programs()
event = client.event("evt-001")
print(event.program_id)   # "prog-001"
print(event.created)      # DateTime(2024, 6, 15, ...)
print(event._raw)         # Original API dict

# Raw HTTP methods — returns httpx.Response
resp = client.get_events(programID="prog-001")
if openadr3.success(resp):
    data = openadr3.body(resp)

Client Types

# VEN client — scopes: read_all, read_targets, read_ven_objects,
#              write_reports, write_subscriptions, write_vens
ven = openadr3.create_ven_client(base_url, token)

# Business Logic client — scopes: read_all, read_bl,
#              write_programs, write_events, write_subscriptions, write_vens
bl = openadr3.create_bl_client(base_url, token)

# Custom client
client = openadr3.OpenADRClient(
    base_url="https://vtn.example.com/openadr3/3.1.0",
    token="tok",
    spec_path="resources/openadr3.yaml",
    client_type="custom",
    scopes=frozenset({"read_all", "write_events"}),
)

Available Methods

Coerced (return entity models):

Method Returns
client.programs() list[Program]
client.program(id) Program
client.events() list[Event]
client.event(id) Event
client.vens() list[Ven]
client.ven(id) Ven
client.resources() list[Resource]
client.resource(id) Resource
client.reports() list[Report]
client.report(id) Report
client.subscriptions() list[Subscription]
client.subscription(id) Subscription
client.find_program_by_name(name) Program | None
client.find_ven_by_name(name) Ven | None

Raw (return httpx.Response):

Each entity has: get_<entities>(), get_<entity>_by_id(id), create_<entity>(data), update_<entity>(id, data), delete_<entity>(id).

Introspection (requires spec_path):

client.all_routes()                        # ["/programs", "/programs/{programID}", ...]
client.endpoint_scopes("/programs", "get") # ["read_all"]
client.authorized("/events", "post")       # True/False based on client scopes

User-Agent

Every client sends a User-Agent header for server-side log identification. The default is openadr3/<version> (node=<hex>) where the node ID comes from uuid.getnode() (MAC-derived, stable across restarts).

Override it to identify your application:

client = openadr3.create_ven_client(
    base_url=base_url,
    token=token,
    user_agent="my-ven-app/1.0",
)

# Or compose a layered string with the default
from openadr3.api import DEFAULT_USER_AGENT
client = openadr3.OpenADRClient(
    base_url=base_url,
    user_agent=f"my-app/1.0 {DEFAULT_USER_AGENT}",
)

The User-Agent is preserved across fetch_token() client recreation.

Context Manager

with openadr3.create_ven_client(base_url, token) as client:
    programs = client.programs()

Authentication

from openadr3 import BearerAuth, fetch_token

# Fetch an OAuth2 token
token = fetch_token(
    base_url="https://vtn.example.com/openadr3/3.1.0",
    client_id="my-client",
    client_secret="secret",
    scopes=["read_all", "write_reports"],
)

# Use BearerAuth directly with httpx
auth = BearerAuth(token)

Time Utilities

from openadr3 import parse_datetime, parse_duration, to_zoned

# Parse datetimes (handles VTN-RI non-standard formats)
dt = parse_datetime("2024-06-15T14:00:00Z")
dt = parse_datetime("2024-06-15 14:00:00Z")  # space instead of T

# Parse ISO 8601 durations
dur = parse_duration("PT2H30M")

# Timezone conversion
eastern = to_zoned(dt, "America/New_York")

Pydantic Annotated Types

PendulumDateTime and PendulumDuration are Annotated types with BeforeValidator and PlainSerializer, ready for use in your own Pydantic models:

from pydantic import BaseModel
from openadr3 import PendulumDateTime, PendulumDuration

class MyModel(BaseModel):
    start: PendulumDateTime = None
    length: PendulumDuration = None

Enums

from openadr3 import ObjectType, Operation, PayloadType

ObjectType.PROGRAM   # "PROGRAM"
Operation.CREATE     # "CREATE"
PayloadType.PRICE    # "PRICE"

Payload Coercion

Payload values are dispatched by type string:

Payload Type Coercion
PRICE Values become Decimal, type becomes "price"
USAGE Values become Decimal, type becomes "usage"
All others Values pass through, type lowercased

The registry is extensible — add entries to openadr3.entities.payloads._PAYLOAD_REGISTRY.

Module Structure

src/openadr3/
├── __init__.py          # Public API re-exports
├── py.typed             # PEP 561 type stub marker
├── time.py              # Pendulum parsing, annotated types
├── enums.py             # ObjectType, Operation, PayloadType
├── auth.py              # BearerAuth, OAuth2 token fetch
├── api.py               # OpenADRClient, create_ven_client, create_bl_client
└── entities/
    ├── __init__.py      # coerce(), coerce_notification(), is_notification()
    ├── models.py        # Coerced Pydantic models (snake_case, pendulum)
    ├── raw.py           # Raw Pydantic models (camelCase, strings)
    └── payloads.py      # Payload type dispatch (PRICE/USAGE → Decimal)

Development

# Install with dev dependencies
pip install -e ".[dev]"

# Run tests
pytest tests/ -v

# Lint and format
ruff check src/
ruff format --check src/

# Type check (py.typed marker included)
mypy src/openadr3/

Pre-commit Hooks

This project uses pre-commit to run Ruff lint and format checks automatically:

pip install pre-commit
pre-commit install

Ruff lint + format are also enforced in CI via .github/workflows/ci.yml.

OpenAPI Spec

The OpenADR 3.1.0 specification is embedded at resources/openadr3.yaml. See resources/ORIGIN.md for provenance and license.

License

MIT License — Copyright (c) 2026 Clark Communications Corporation

Project details


Download files

Download the file for your platform. If you're not sure which to choose, learn more about installing packages.

Source Distribution

openadr3-0.3.0.tar.gz (335.8 kB view details)

Uploaded Source

Built Distribution

If you're not sure about the file name format, learn more about wheel file names.

openadr3-0.3.0-py3-none-any.whl (18.8 kB view details)

Uploaded Python 3

File details

Details for the file openadr3-0.3.0.tar.gz.

File metadata

  • Download URL: openadr3-0.3.0.tar.gz
  • Upload date:
  • Size: 335.8 kB
  • Tags: Source
  • Uploaded using Trusted Publishing? No
  • Uploaded via: twine/6.2.0 CPython/3.10.20

File hashes

Hashes for openadr3-0.3.0.tar.gz
Algorithm Hash digest
SHA256 d04b84963cbf40ab4d3bfc5d554ae8feb2cd9b7281cc435abffeb7bdd81dbd16
MD5 4d4194c6d9c734f19f4b9043c42dd094
BLAKE2b-256 8054386d2cc468473b0fbff12682e1bcfd4d48d9da9c6accb58045335c55aad6

See more details on using hashes here.

File details

Details for the file openadr3-0.3.0-py3-none-any.whl.

File metadata

  • Download URL: openadr3-0.3.0-py3-none-any.whl
  • Upload date:
  • Size: 18.8 kB
  • Tags: Python 3
  • Uploaded using Trusted Publishing? No
  • Uploaded via: twine/6.2.0 CPython/3.10.20

File hashes

Hashes for openadr3-0.3.0-py3-none-any.whl
Algorithm Hash digest
SHA256 23308e6835066fcbbd21cdcd641dd55ba88a2560e87f252022f29d2fb4a2118f
MD5 6d2153808e56249920cbba3f3fc6f294
BLAKE2b-256 361ae646b3d8c359fc0f1bca0f61b0f9721610dbd411a7908df920d577475ddb

See more details on using hashes here.

Supported by

AWS Cloud computing and Security Sponsor Datadog Monitoring Depot Continuous Integration Fastly CDN Google Download Analytics Pingdom Monitoring Sentry Error logging StatusPage Status page