OpenADR 3 Entity API Library
Project description
python-oa3
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:
-
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. -
Coerced models (
openadr3.entities.models) — Snake_case fields,pendulum.DateTimefor timestamps,pendulum.Durationfor durations,Decimalfor 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
Release history Release notifications | RSS feed
Download files
Download the file for your platform. If you're not sure which to choose, learn more about installing packages.
Source Distribution
Built Distribution
Filter files by name, interpreter, ABI, and platform.
If you're not sure about the file name format, learn more about wheel file names.
Copy a direct link to the current filters
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
| Algorithm | Hash digest | |
|---|---|---|
| SHA256 |
d04b84963cbf40ab4d3bfc5d554ae8feb2cd9b7281cc435abffeb7bdd81dbd16
|
|
| MD5 |
4d4194c6d9c734f19f4b9043c42dd094
|
|
| BLAKE2b-256 |
8054386d2cc468473b0fbff12682e1bcfd4d48d9da9c6accb58045335c55aad6
|
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
| Algorithm | Hash digest | |
|---|---|---|
| SHA256 |
23308e6835066fcbbd21cdcd641dd55ba88a2560e87f252022f29d2fb4a2118f
|
|
| MD5 |
6d2153808e56249920cbba3f3fc6f294
|
|
| BLAKE2b-256 |
361ae646b3d8c359fc0f1bca0f61b0f9721610dbd411a7908df920d577475ddb
|