openadr3 0.1.0__tar.gz

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -0,0 +1,36 @@
1
+ name: Publish to PyPI
2
+
3
+ on:
4
+ push:
5
+ tags:
6
+ - "v*"
7
+
8
+ permissions:
9
+ id-token: write
10
+ contents: read
11
+
12
+ jobs:
13
+ test:
14
+ runs-on: ubuntu-latest
15
+ steps:
16
+ - uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4.2.2
17
+ - uses: actions/setup-python@a26af69be951a213d495a4c3e4e4022e16d87065 # v5.6.0
18
+ with:
19
+ python-version: "3.12"
20
+ - run: pip install -e ".[dev]"
21
+ - run: pytest tests/ -v
22
+
23
+ publish:
24
+ needs: test
25
+ runs-on: ubuntu-latest
26
+ environment: pypi
27
+ steps:
28
+ - uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4.2.2
29
+ - uses: actions/setup-python@a26af69be951a213d495a4c3e4e4022e16d87065 # v5.6.0
30
+ with:
31
+ python-version: "3.12"
32
+ - run: pip install build
33
+ - run: python -m build
34
+ - uses: pypa/gh-action-pypi-publish@ed0c53931b1dc9bd32cbe73a98c7f6766f8a527e # v1.13.0
35
+ with:
36
+ print-hash: true
@@ -0,0 +1,8 @@
1
+ __pycache__/
2
+ *.pyc
3
+ *.egg-info/
4
+ dist/
5
+ build/
6
+ .pytest_cache/
7
+ .ruff_cache/
8
+ .DS_Store
openadr3-0.1.0/LICENSE ADDED
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2026 Clark Communications Corporation
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in all
13
+ copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21
+ SOFTWARE.
@@ -0,0 +1,343 @@
1
+ Metadata-Version: 2.4
2
+ Name: openadr3
3
+ Version: 0.1.0
4
+ Summary: OpenADR 3 Entity API Library
5
+ Project-URL: Homepage, https://github.com/grid-coordination/python-oa3
6
+ Project-URL: Repository, https://github.com/grid-coordination/python-oa3
7
+ Project-URL: Issues, https://github.com/grid-coordination/python-oa3/issues
8
+ Author: Clark Communications Corporation
9
+ License-Expression: MIT
10
+ License-File: LICENSE
11
+ Keywords: demand-response,energy,grid,openadr,openadr3
12
+ Classifier: Development Status :: 3 - Alpha
13
+ Classifier: Intended Audience :: Developers
14
+ Classifier: License :: OSI Approved :: MIT License
15
+ Classifier: Programming Language :: Python :: 3
16
+ Classifier: Programming Language :: Python :: 3.10
17
+ Classifier: Programming Language :: Python :: 3.11
18
+ Classifier: Programming Language :: Python :: 3.12
19
+ Classifier: Programming Language :: Python :: 3.13
20
+ Classifier: Topic :: Software Development :: Libraries
21
+ Requires-Python: >=3.10
22
+ Requires-Dist: httpx>=0.27
23
+ Requires-Dist: openapi-core>=0.19
24
+ Requires-Dist: pendulum>=3.0
25
+ Requires-Dist: pydantic>=2.5
26
+ Requires-Dist: pyyaml>=6.0
27
+ Provides-Extra: dev
28
+ Requires-Dist: pytest-httpx>=0.30; extra == 'dev'
29
+ Requires-Dist: pytest>=8.0; extra == 'dev'
30
+ Requires-Dist: ruff>=0.3; extra == 'dev'
31
+ Description-Content-Type: text/markdown
32
+
33
+ # python-oa3
34
+
35
+ Python client library for the [OpenADR 3](https://www.openadr.org/) 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.
36
+
37
+ ## Installation
38
+
39
+ ```bash
40
+ pip install -e ".[dev]"
41
+ ```
42
+
43
+ ### Dependencies
44
+
45
+ | Package | Role |
46
+ |---------|------|
47
+ | [Pydantic](https://docs.pydantic.dev/) v2 | Schema validation, model coercion |
48
+ | [Pendulum](https://pendulum.eustace.io/) v3 | DateTime, Duration, timezone handling |
49
+ | [httpx](https://www.python-httpx.org/) | HTTP client with auth hooks |
50
+ | [openapi-core](https://openapi-core.readthedocs.io/) | Optional OpenAPI spec validation |
51
+ | [PyYAML](https://pyyaml.org/) | OpenAPI spec loading |
52
+
53
+ ## Architecture
54
+
55
+ ### Two-Layer Data Model
56
+
57
+ Every OpenADR 3 entity exists in two forms:
58
+
59
+ 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.
60
+
61
+ 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.
62
+
63
+ ```
64
+ JSON API response (camelCase, strings)
65
+
66
+
67
+ coerce(raw_dict) ──► Typed entity (snake_case, pendulum, Decimal)
68
+
69
+ └─► ._raw (original dict preserved)
70
+ ```
71
+
72
+ ### Raw Preservation
73
+
74
+ Every coerced entity carries its original raw dict as a Pydantic `PrivateAttr`:
75
+
76
+ ```python
77
+ program = openadr3.coerce(api_response)
78
+ program.program_name # "My DR Program"
79
+ program.created # DateTime(2024, 6, 15, 10, 0, 0, tzinfo=UTC)
80
+ program._raw["programName"] # "My DR Program" (original wire format)
81
+ ```
82
+
83
+ ### Entity Dispatch
84
+
85
+ The `coerce()` function dispatches on the `objectType` string in the raw dict:
86
+
87
+ ```python
88
+ from openadr3 import coerce
89
+
90
+ raw = {"objectType": "PROGRAM", "programName": "Test", ...}
91
+ program = coerce(raw) # Returns a Program instance
92
+
93
+ raw = {"objectType": "EVENT", "programID": "p1", ...}
94
+ event = coerce(raw) # Returns an Event instance
95
+ ```
96
+
97
+ Handles request variants too: `BL_VEN_REQUEST` and `VEN_VEN_REQUEST` coerce as `Ven`, `BL_RESOURCE_REQUEST` and `VEN_RESOURCE_REQUEST` coerce as `Resource`.
98
+
99
+ ### Notification Coercion
100
+
101
+ `coerce_notification()` handles both spec-compliant camelCase notifications and the snake_case format currently sent by the VTN Reference Implementation:
102
+
103
+ ```python
104
+ from openadr3 import coerce_notification, is_notification
105
+
106
+ # Spec-compliant (camelCase)
107
+ webhook_payload = {
108
+ "objectType": "EVENT",
109
+ "operation": "CREATE",
110
+ "object": {"programID": "prog-001", "eventName": "Peak Event", ...}
111
+ }
112
+
113
+ # VTN-RI (snake_case) — see oadr3-org/openadr3-vtn-reference-implementation#181
114
+ mqtt_payload = {
115
+ "object_type": "EVENT",
116
+ "operation": "CREATE",
117
+ "object": {"program_id": "prog-001", "event_name": "Peak Event", ...}
118
+ }
119
+
120
+ # Both formats work
121
+ for payload in [webhook_payload, mqtt_payload]:
122
+ if is_notification(payload):
123
+ notification = coerce_notification(payload)
124
+ notification.object # Coerced Event instance
125
+ ```
126
+
127
+ ## Entity Types
128
+
129
+ | Entity | Key Fields | Notes |
130
+ |--------|-----------|-------|
131
+ | `Program` | `program_name`, `interval_period`, `descriptions`, `payload_descriptors`, `attributes`, `targets` | Top-level DR program |
132
+ | `Event` | `program_id`, `event_name`, `duration`, `priority`, `intervals`, `targets` | DR event with signal intervals |
133
+ | `Ven` | `ven_name`, `client_id`, `attributes`, `targets` | Virtual End Node |
134
+ | `Resource` | `resource_name`, `ven_id`, `client_id`, `attributes`, `targets` | Device/load under a VEN |
135
+ | `Report` | `event_id`, `client_name`, `resources` | VEN telemetry report |
136
+ | `Subscription` | `client_name`, `object_operations`, `program_id`, `targets` | Webhook/MQTT subscription |
137
+ | `Notification` | `object_type`, `operation`, `object` | Push notification wrapper |
138
+
139
+ All top-level entities share common metadata: `id`, `created` (DateTime), `modified` (DateTime), `object_type`.
140
+
141
+ ### Supporting Types
142
+
143
+ | Type | Description |
144
+ |------|-------------|
145
+ | `IntervalPeriod` | Start datetime + duration + computed period tuple |
146
+ | `Interval` | Numbered interval with payloads |
147
+ | `Payload` | Type-tagged values (PRICE/USAGE get Decimal coercion) |
148
+ | `ObjectOperation` | Subscription callback definition |
149
+
150
+ ## API Client
151
+
152
+ ### Quick Start
153
+
154
+ ```python
155
+ import openadr3
156
+
157
+ # Create a VEN client
158
+ client = openadr3.create_ven_client(
159
+ base_url="https://vtn.example.com/openadr3/3.1.0",
160
+ token="your-bearer-token",
161
+ spec_path="resources/openadr3.yaml", # optional, for route introspection
162
+ )
163
+
164
+ # Coerced entity methods — returns typed models
165
+ programs = client.programs()
166
+ event = client.event("evt-001")
167
+ print(event.program_id) # "prog-001"
168
+ print(event.created) # DateTime(2024, 6, 15, ...)
169
+ print(event._raw) # Original API dict
170
+
171
+ # Raw HTTP methods — returns httpx.Response
172
+ resp = client.get_events(programID="prog-001")
173
+ if openadr3.success(resp):
174
+ data = openadr3.body(resp)
175
+ ```
176
+
177
+ ### Client Types
178
+
179
+ ```python
180
+ # VEN client — scopes: read_all, read_targets, read_ven_objects,
181
+ # write_reports, write_subscriptions, write_vens
182
+ ven = openadr3.create_ven_client(base_url, token)
183
+
184
+ # Business Logic client — scopes: read_all, read_bl,
185
+ # write_programs, write_events, write_subscriptions, write_vens
186
+ bl = openadr3.create_bl_client(base_url, token)
187
+
188
+ # Custom client
189
+ client = openadr3.OpenADRClient(
190
+ base_url="https://vtn.example.com/openadr3/3.1.0",
191
+ token="tok",
192
+ spec_path="resources/openadr3.yaml",
193
+ client_type="custom",
194
+ scopes=frozenset({"read_all", "write_events"}),
195
+ )
196
+ ```
197
+
198
+ ### Available Methods
199
+
200
+ **Coerced** (return entity models):
201
+
202
+ | Method | Returns |
203
+ |--------|---------|
204
+ | `client.programs()` | `list[Program]` |
205
+ | `client.program(id)` | `Program` |
206
+ | `client.events()` | `list[Event]` |
207
+ | `client.event(id)` | `Event` |
208
+ | `client.vens()` | `list[Ven]` |
209
+ | `client.ven(id)` | `Ven` |
210
+ | `client.resources()` | `list[Resource]` |
211
+ | `client.resource(id)` | `Resource` |
212
+ | `client.reports()` | `list[Report]` |
213
+ | `client.report(id)` | `Report` |
214
+ | `client.subscriptions()` | `list[Subscription]` |
215
+ | `client.subscription(id)` | `Subscription` |
216
+
217
+ **Raw** (return `httpx.Response`):
218
+
219
+ Each entity has: `get_<entities>()`, `get_<entity>_by_id(id)`, `create_<entity>(data)`, `update_<entity>(id, data)`, `delete_<entity>(id)`.
220
+
221
+ **Introspection** (requires `spec_path`):
222
+
223
+ ```python
224
+ client.all_routes() # ["/programs", "/programs/{programID}", ...]
225
+ client.endpoint_scopes("/programs", "get") # ["read_all"]
226
+ client.authorized("/events", "post") # True/False based on client scopes
227
+ ```
228
+
229
+ ### Context Manager
230
+
231
+ ```python
232
+ with openadr3.create_ven_client(base_url, token) as client:
233
+ programs = client.programs()
234
+ ```
235
+
236
+ ## Authentication
237
+
238
+ ```python
239
+ from openadr3 import BearerAuth, fetch_token
240
+
241
+ # Fetch an OAuth2 token
242
+ token = fetch_token(
243
+ base_url="https://vtn.example.com/openadr3/3.1.0",
244
+ client_id="my-client",
245
+ client_secret="secret",
246
+ scopes=["read_all", "write_reports"],
247
+ )
248
+
249
+ # Use BearerAuth directly with httpx
250
+ auth = BearerAuth(token)
251
+ ```
252
+
253
+ ## Time Utilities
254
+
255
+ ```python
256
+ from openadr3 import parse_datetime, parse_duration, to_zoned
257
+
258
+ # Parse datetimes (handles VTN-RI non-standard formats)
259
+ dt = parse_datetime("2024-06-15T14:00:00Z")
260
+ dt = parse_datetime("2024-06-15 14:00:00Z") # space instead of T
261
+
262
+ # Parse ISO 8601 durations
263
+ dur = parse_duration("PT2H30M")
264
+
265
+ # Timezone conversion
266
+ eastern = to_zoned(dt, "America/New_York")
267
+ ```
268
+
269
+ ### Pydantic Annotated Types
270
+
271
+ `PendulumDateTime` and `PendulumDuration` are `Annotated` types with `BeforeValidator` and `PlainSerializer`, ready for use in your own Pydantic models:
272
+
273
+ ```python
274
+ from pydantic import BaseModel
275
+ from openadr3 import PendulumDateTime, PendulumDuration
276
+
277
+ class MyModel(BaseModel):
278
+ start: PendulumDateTime = None
279
+ length: PendulumDuration = None
280
+ ```
281
+
282
+ ## Enums
283
+
284
+ ```python
285
+ from openadr3 import ObjectType, Operation, PayloadType
286
+
287
+ ObjectType.PROGRAM # "PROGRAM"
288
+ Operation.CREATE # "CREATE"
289
+ PayloadType.PRICE # "PRICE"
290
+ ```
291
+
292
+ ## Payload Coercion
293
+
294
+ Payload values are dispatched by type string:
295
+
296
+ | Payload Type | Coercion |
297
+ |-------------|----------|
298
+ | `PRICE` | Values become `Decimal`, type becomes `"price"` |
299
+ | `USAGE` | Values become `Decimal`, type becomes `"usage"` |
300
+ | All others | Values pass through, type lowercased |
301
+
302
+ The registry is extensible — add entries to `openadr3.entities.payloads._PAYLOAD_REGISTRY`.
303
+
304
+ ## Module Structure
305
+
306
+ ```
307
+ src/openadr3/
308
+ ├── __init__.py # Public API re-exports
309
+ ├── py.typed # PEP 561 type stub marker
310
+ ├── time.py # Pendulum parsing, annotated types
311
+ ├── enums.py # ObjectType, Operation, PayloadType
312
+ ├── auth.py # BearerAuth, OAuth2 token fetch
313
+ ├── api.py # OpenADRClient, create_ven_client, create_bl_client
314
+ └── entities/
315
+ ├── __init__.py # coerce(), coerce_notification(), is_notification()
316
+ ├── models.py # Coerced Pydantic models (snake_case, pendulum)
317
+ ├── raw.py # Raw Pydantic models (camelCase, strings)
318
+ └── payloads.py # Payload type dispatch (PRICE/USAGE → Decimal)
319
+ ```
320
+
321
+ ## Development
322
+
323
+ ```bash
324
+ # Install with dev dependencies
325
+ pip install -e ".[dev]"
326
+
327
+ # Run tests
328
+ pytest tests/ -v
329
+
330
+ # Lint
331
+ ruff check src/
332
+
333
+ # Type check (py.typed marker included)
334
+ mypy src/openadr3/
335
+ ```
336
+
337
+ ## OpenAPI Spec
338
+
339
+ The OpenADR 3.1.0 specification is embedded at `resources/openadr3.yaml`. See `resources/ORIGIN.md` for provenance and license.
340
+
341
+ ## License
342
+
343
+ [MIT License](LICENSE) — Copyright (c) 2026 Clark Communications Corporation