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

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.2.0.tar.gz (32.0 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.2.0-py3-none-any.whl (18.4 kB view details)

Uploaded Python 3

File details

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

File metadata

  • Download URL: openadr3-0.2.0.tar.gz
  • Upload date:
  • Size: 32.0 kB
  • Tags: Source
  • Uploaded using Trusted Publishing? Yes
  • Uploaded via: twine/6.1.0 CPython/3.13.7

File hashes

Hashes for openadr3-0.2.0.tar.gz
Algorithm Hash digest
SHA256 e349f2199123d6f3e4521dc6a2f59c60f06255301a5bfec97920ffffbd170860
MD5 e9637452ff7cb90e5a4d5166d660f4a4
BLAKE2b-256 fa11f20707f68a4405c913ccb79da2104739972492593860d6c53d11d24d4ccd

See more details on using hashes here.

Provenance

The following attestation bundles were made for openadr3-0.2.0.tar.gz:

Publisher: publish.yml on grid-coordination/python-oa3

Attestations: Values shown here reflect the state when the release was signed and may no longer be current.

File details

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

File metadata

  • Download URL: openadr3-0.2.0-py3-none-any.whl
  • Upload date:
  • Size: 18.4 kB
  • Tags: Python 3
  • Uploaded using Trusted Publishing? Yes
  • Uploaded via: twine/6.1.0 CPython/3.13.7

File hashes

Hashes for openadr3-0.2.0-py3-none-any.whl
Algorithm Hash digest
SHA256 d6cd134b885817005b0d2b063c2d5a291a31c48c3f4a21865f7a8e2aef32d0dc
MD5 750c457b68d7c8cbf85a837dd83c6039
BLAKE2b-256 36977738a932a0248dad2c0e25c59fe1178c53b3c347c6b68f64277692994b63

See more details on using hashes here.

Provenance

The following attestation bundles were made for openadr3-0.2.0-py3-none-any.whl:

Publisher: publish.yml on grid-coordination/python-oa3

Attestations: Values shown here reflect the state when the release was signed and may no longer be current.

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