power-openapi-models 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.
- power_openapi_models-0.1.0/LICENSE +29 -0
- power_openapi_models-0.1.0/PKG-INFO +237 -0
- power_openapi_models-0.1.0/README.md +204 -0
- power_openapi_models-0.1.0/pyproject.toml +80 -0
- power_openapi_models-0.1.0/setup.cfg +4 -0
- power_openapi_models-0.1.0/src/power_openapi_models/__init__.py +52 -0
- power_openapi_models-0.1.0/src/power_openapi_models/_schema_version.txt +1 -0
- power_openapi_models-0.1.0/src/power_openapi_models/core/__init__.py +5 -0
- power_openapi_models-0.1.0/src/power_openapi_models/core/models.py +818 -0
- power_openapi_models-0.1.0/src/power_openapi_models/document.py +290 -0
- power_openapi_models-0.1.0/src/power_openapi_models/dynamics/__init__.py +5 -0
- power_openapi_models-0.1.0/src/power_openapi_models/dynamics/models.py +257 -0
- power_openapi_models-0.1.0/src/power_openapi_models/infrastructure_core/__init__.py +5 -0
- power_openapi_models-0.1.0/src/power_openapi_models/infrastructure_core/models.py +175 -0
- power_openapi_models-0.1.0/src/power_openapi_models/investments/__init__.py +5 -0
- power_openapi_models-0.1.0/src/power_openapi_models/investments/models.py +564 -0
- power_openapi_models-0.1.0/src/power_openapi_models/operations/__init__.py +5 -0
- power_openapi_models-0.1.0/src/power_openapi_models/operations/models.py +2958 -0
- power_openapi_models-0.1.0/src/power_openapi_models/py.typed +0 -0
- power_openapi_models-0.1.0/src/power_openapi_models/timeseries/__init__.py +5 -0
- power_openapi_models-0.1.0/src/power_openapi_models/timeseries/models.py +626 -0
- power_openapi_models-0.1.0/src/power_openapi_models.egg-info/PKG-INFO +237 -0
- power_openapi_models-0.1.0/src/power_openapi_models.egg-info/SOURCES.txt +29 -0
- power_openapi_models-0.1.0/src/power_openapi_models.egg-info/dependency_links.txt +1 -0
- power_openapi_models-0.1.0/src/power_openapi_models.egg-info/requires.txt +9 -0
- power_openapi_models-0.1.0/src/power_openapi_models.egg-info/top_level.txt +1 -0
- power_openapi_models-0.1.0/tests/test_document.py +219 -0
- power_openapi_models-0.1.0/tests/test_import.py +107 -0
- power_openapi_models-0.1.0/tests/test_public_api.py +129 -0
- power_openapi_models-0.1.0/tests/test_readme.py +39 -0
- power_openapi_models-0.1.0/tests/test_serde_fixture.py +165 -0
|
@@ -0,0 +1,29 @@
|
|
|
1
|
+
BSD 3-Clause License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2026 Alliance for Energy Innovation, LLC and QXT Energy
|
|
4
|
+
All rights reserved.
|
|
5
|
+
|
|
6
|
+
Redistribution and use in source and binary forms, with or without
|
|
7
|
+
modification, are permitted provided that the following conditions are met:
|
|
8
|
+
|
|
9
|
+
* Redistributions of source code must retain the above copyright notice, this
|
|
10
|
+
list of conditions and the following disclaimer.
|
|
11
|
+
|
|
12
|
+
* Redistributions in binary form must reproduce the above copyright notice,
|
|
13
|
+
this list of conditions and the following disclaimer in the documentation
|
|
14
|
+
and/or other materials provided with the distribution.
|
|
15
|
+
|
|
16
|
+
* Neither the name of the copyright holder nor the names of its
|
|
17
|
+
contributors may be used to endorse or promote products derived from
|
|
18
|
+
this software without specific prior written permission.
|
|
19
|
+
|
|
20
|
+
THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS"
|
|
21
|
+
AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
|
|
22
|
+
IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE
|
|
23
|
+
DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE
|
|
24
|
+
FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL
|
|
25
|
+
DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR
|
|
26
|
+
SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER
|
|
27
|
+
CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY,
|
|
28
|
+
OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
|
|
29
|
+
OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
|
|
@@ -0,0 +1,237 @@
|
|
|
1
|
+
Metadata-Version: 2.4
|
|
2
|
+
Name: power-openapi-models
|
|
3
|
+
Version: 0.1.0
|
|
4
|
+
Summary: Typed Python models for the Sienna power system data format
|
|
5
|
+
Author: Sienna Platform
|
|
6
|
+
License-Expression: BSD-3-Clause
|
|
7
|
+
Project-URL: Homepage, https://github.com/Sienna-Platform/power-openapi-models
|
|
8
|
+
Project-URL: Repository, https://github.com/Sienna-Platform/power-openapi-models
|
|
9
|
+
Project-URL: Issues, https://github.com/Sienna-Platform/power-openapi-models/issues
|
|
10
|
+
Project-URL: Changelog, https://github.com/Sienna-Platform/power-openapi-models/blob/main/CHANGELOG.md
|
|
11
|
+
Keywords: power-systems,energy,openapi,pydantic,sienna,time-series
|
|
12
|
+
Classifier: Development Status :: 3 - Alpha
|
|
13
|
+
Classifier: Intended Audience :: Science/Research
|
|
14
|
+
Classifier: Programming Language :: Python :: 3
|
|
15
|
+
Classifier: Programming Language :: Python :: 3.10
|
|
16
|
+
Classifier: Programming Language :: Python :: 3.11
|
|
17
|
+
Classifier: Programming Language :: Python :: 3.12
|
|
18
|
+
Classifier: Programming Language :: Python :: 3.13
|
|
19
|
+
Classifier: Programming Language :: Python :: 3.14
|
|
20
|
+
Classifier: Typing :: Typed
|
|
21
|
+
Requires-Python: >=3.10
|
|
22
|
+
Description-Content-Type: text/markdown
|
|
23
|
+
License-File: LICENSE
|
|
24
|
+
Requires-Dist: pydantic>=2.0
|
|
25
|
+
Provides-Extra: dev
|
|
26
|
+
Requires-Dist: ruff>=0.6; extra == "dev"
|
|
27
|
+
Requires-Dist: pytest>=8; extra == "dev"
|
|
28
|
+
Requires-Dist: pyright>=1.1.380; extra == "dev"
|
|
29
|
+
Requires-Dist: datamodel-code-generator; extra == "dev"
|
|
30
|
+
Requires-Dist: build; extra == "dev"
|
|
31
|
+
Requires-Dist: twine; extra == "dev"
|
|
32
|
+
Dynamic: license-file
|
|
33
|
+
|
|
34
|
+
# power-openapi-models (Python)
|
|
35
|
+
|
|
36
|
+
[](https://pypi.org/project/power-openapi-models/)
|
|
37
|
+
[](https://pypi.org/project/power-openapi-models/)
|
|
38
|
+
[](https://github.com/Sienna-Platform/power-openapi-models/actions/workflows/test.yml)
|
|
39
|
+
[](../LICENSE)
|
|
40
|
+
|
|
41
|
+
Typed Python models for the Sienna power system data format — every
|
|
42
|
+
component, association, and time series row as a validated pydantic v2
|
|
43
|
+
model.
|
|
44
|
+
|
|
45
|
+
> [!WARNING]
|
|
46
|
+
> **Pre-release.** Every version below `1.0` can change incompatibly in any
|
|
47
|
+
> release; no stability is promised until `1.0`. Under PEP 440, `0.1.0` is
|
|
48
|
+
> not itself a "pre-release" version, so `pip install power-openapi-models`
|
|
49
|
+
> installs it with no `--pre` flag and no warning. Pin an exact version.
|
|
50
|
+
|
|
51
|
+
## Install
|
|
52
|
+
|
|
53
|
+
```bash
|
|
54
|
+
pip install power-openapi-models
|
|
55
|
+
```
|
|
56
|
+
|
|
57
|
+
```bash
|
|
58
|
+
uv add power-openapi-models
|
|
59
|
+
```
|
|
60
|
+
|
|
61
|
+
## Quickstart
|
|
62
|
+
|
|
63
|
+
```python
|
|
64
|
+
from power_openapi_models.core.models import ACBus, ACBusType
|
|
65
|
+
|
|
66
|
+
bus = ACBus(
|
|
67
|
+
id=1,
|
|
68
|
+
name="bus-1",
|
|
69
|
+
available=True,
|
|
70
|
+
number=1,
|
|
71
|
+
bustype=ACBusType.PV,
|
|
72
|
+
base_voltage=230.0,
|
|
73
|
+
)
|
|
74
|
+
print(bus.name, bus.bustype, bus.base_voltage)
|
|
75
|
+
print(bus.model_dump_json(exclude_none=True))
|
|
76
|
+
```
|
|
77
|
+
|
|
78
|
+
## The modules
|
|
79
|
+
|
|
80
|
+
| Module | Holds | Reach for it when |
|
|
81
|
+
|---|---|---|
|
|
82
|
+
| `infrastructure_core` | Units (`UnitSystem`), function data, shared value shapes (`MinMax`, `UpDown`, ...) | You need a domain-neutral building block used across every other module |
|
|
83
|
+
| `core` | Power enums, curves, costs, buses | You are working with shared power types or need `ACBus`/`DCBus` |
|
|
84
|
+
| `operations` | Topology, branches, injections, services, market | You are working with the grid itself |
|
|
85
|
+
| `investments` | Technologies, financials, requirements, regions | You are doing capacity expansion |
|
|
86
|
+
| `dynamics` | Dynamic generator and inverter components | You are doing transient stability |
|
|
87
|
+
| `timeseries` | The six time series association types | You are handling forecasts or profiles |
|
|
88
|
+
| `document` | `SystemDocument`, the hand-written envelope, plus `read_document`/`write_document` | You are loading or saving a whole serialized system |
|
|
89
|
+
|
|
90
|
+
Import a class from its module's `.models` submodule:
|
|
91
|
+
`from power_openapi_models.core.models import ACBus`.
|
|
92
|
+
|
|
93
|
+
## Concepts worth knowing before you start
|
|
94
|
+
|
|
95
|
+
### Unit systems
|
|
96
|
+
|
|
97
|
+
Many component fields carry a sibling `*_units` field (for example
|
|
98
|
+
`ThermalStandard.power_units`) that says how to read every other power-family
|
|
99
|
+
field on that same component. There is no document-level unit system: each
|
|
100
|
+
component blob is self-describing.
|
|
101
|
+
|
|
102
|
+
```python
|
|
103
|
+
from power_openapi_models.infrastructure_core.models import UnitSystem
|
|
104
|
+
|
|
105
|
+
print(list(UnitSystem))
|
|
106
|
+
print(UnitSystem.NATURAL_UNITS.value)
|
|
107
|
+
```
|
|
108
|
+
|
|
109
|
+
`NATURAL_UNITS` means values are in physical units — MW, MVAr, kV, ohm.
|
|
110
|
+
`COMPONENT_BASE` means per-unit against the component's own recorded
|
|
111
|
+
`base_power`. Getting this wrong is the most common source of
|
|
112
|
+
wrong-by-a-factor-of-100 bugs across the ecosystem.
|
|
113
|
+
|
|
114
|
+
### Discriminated unions carry a `.root`
|
|
115
|
+
|
|
116
|
+
Several types are one of several shapes, chosen by a discriminator field.
|
|
117
|
+
They are pydantic `RootModel`s, and the concrete variant is reached through
|
|
118
|
+
`.root`.
|
|
119
|
+
|
|
120
|
+
```python
|
|
121
|
+
from power_openapi_models.infrastructure_core.models import (
|
|
122
|
+
FunctionData,
|
|
123
|
+
LinearFunctionData,
|
|
124
|
+
)
|
|
125
|
+
|
|
126
|
+
parsed = FunctionData.model_validate(
|
|
127
|
+
{"function_type": "LINEAR", "proportional_term": 2.0, "constant_term": 5.0}
|
|
128
|
+
)
|
|
129
|
+
assert isinstance(parsed.root, LinearFunctionData)
|
|
130
|
+
print(parsed.root.proportional_term)
|
|
131
|
+
```
|
|
132
|
+
|
|
133
|
+
> [!IMPORTANT]
|
|
134
|
+
> **This differs from the TypeScript package**, where the same types are zod
|
|
135
|
+
> unions and `parse` returns the variant directly, with no `.root`. Porting
|
|
136
|
+
> code between the two languages, this is the first thing that breaks.
|
|
137
|
+
|
|
138
|
+
### Components reference each other by integer id
|
|
139
|
+
|
|
140
|
+
There are no object references on the wire. `ThermalStandard.bus`, for
|
|
141
|
+
example, names its bus by id, and resolving it is the reader's job.
|
|
142
|
+
|
|
143
|
+
```python
|
|
144
|
+
from power_openapi_models.core.models import ACBus
|
|
145
|
+
|
|
146
|
+
buses = [
|
|
147
|
+
ACBus(id=1, name="a", available=True, number=1, base_voltage=230.0),
|
|
148
|
+
ACBus(id=2, name="b", available=True, number=2, base_voltage=230.0),
|
|
149
|
+
]
|
|
150
|
+
by_id = {bus.id: bus for bus in buses}
|
|
151
|
+
print(by_id[2].name)
|
|
152
|
+
```
|
|
153
|
+
|
|
154
|
+
### The document envelope
|
|
155
|
+
|
|
156
|
+
A serialized system is one object: `components` keyed by type name, a flat
|
|
157
|
+
`supplemental_attributes` array, several association arrays, and
|
|
158
|
+
`time_series_storage_file` naming a sidecar. There is no document-level
|
|
159
|
+
`unit_system` or `base_power` — `SystemDocument` forbids both fields
|
|
160
|
+
outright, since every component already carries its own basis.
|
|
161
|
+
|
|
162
|
+
**Time series values never appear in the document.**
|
|
163
|
+
`time_series_associations` carries only the metadata rows; the values live
|
|
164
|
+
in the sidecar named by `time_series_storage_file`, and reading it is the
|
|
165
|
+
consumer's job.
|
|
166
|
+
|
|
167
|
+
## Round-tripping
|
|
168
|
+
|
|
169
|
+
`read_document` followed by `write_document` reproduces the input byte for
|
|
170
|
+
byte. Fields the input never carried stay omitted rather than being written
|
|
171
|
+
back as explicit nulls, top-level keys are sorted, and the file ends with a
|
|
172
|
+
newline.
|
|
173
|
+
|
|
174
|
+
## Worked example
|
|
175
|
+
|
|
176
|
+
```python
|
|
177
|
+
from power_openapi_models.document import read_document
|
|
178
|
+
|
|
179
|
+
doc = read_document("../fixtures/case14_operations.NATURAL_UNITS.json")
|
|
180
|
+
|
|
181
|
+
buses = doc.components["ACBus"]
|
|
182
|
+
generators = doc.components["ThermalStandard"]
|
|
183
|
+
print(f"{len(buses)} buses, {len(generators)} thermal generators")
|
|
184
|
+
|
|
185
|
+
gen = generators[0]
|
|
186
|
+
limits = gen["active_power_limits"]
|
|
187
|
+
print(gen["name"], "active power limits (MW):", limits["min"], "-", limits["max"])
|
|
188
|
+
```
|
|
189
|
+
|
|
190
|
+
## Validation
|
|
191
|
+
|
|
192
|
+
Bad data raises `pydantic.ValidationError`, which names the field and the
|
|
193
|
+
rule:
|
|
194
|
+
|
|
195
|
+
```python
|
|
196
|
+
from pydantic import ValidationError
|
|
197
|
+
|
|
198
|
+
from power_openapi_models.infrastructure_core.models import LinearFunctionData
|
|
199
|
+
|
|
200
|
+
try:
|
|
201
|
+
LinearFunctionData(function_type="NOT_A_TYPE", proportional_term=1.0, constant_term=0.0)
|
|
202
|
+
except ValidationError as exc:
|
|
203
|
+
print(exc.error_count(), "error(s)")
|
|
204
|
+
print(exc.errors()[0]["loc"], exc.errors()[0]["type"])
|
|
205
|
+
```
|
|
206
|
+
|
|
207
|
+
## Type checking
|
|
208
|
+
|
|
209
|
+
The package ships a PEP 561 `py.typed` marker, so mypy and pyright pick the
|
|
210
|
+
annotations up with no configuration.
|
|
211
|
+
|
|
212
|
+
## Versioning and provenance
|
|
213
|
+
|
|
214
|
+
These models are generated. The schema release they came from is recorded
|
|
215
|
+
in the package:
|
|
216
|
+
|
|
217
|
+
```python
|
|
218
|
+
import power_openapi_models
|
|
219
|
+
|
|
220
|
+
print(power_openapi_models.__version__)
|
|
221
|
+
print(power_openapi_models.__schema_version__)
|
|
222
|
+
```
|
|
223
|
+
|
|
224
|
+
Quote `__schema_version__` in bug reports — it identifies the exact schema
|
|
225
|
+
release the models were built from.
|
|
226
|
+
|
|
227
|
+
## Links
|
|
228
|
+
|
|
229
|
+
- [SiennaSchemas](https://github.com/Sienna-Platform/SiennaSchemas) — the schemas these models are generated from, and where schema bugs belong
|
|
230
|
+
- [@sienna-platform/power-openapi-models](../typescript/README.md) — the TypeScript package generated from the same schemas, kept in lockstep by a CI equivalence gate
|
|
231
|
+
- [PowerOpenAPIModels.jl](https://github.com/Sienna-Platform/PowerOpenAPIModels) — the Julia packages generated from the same schemas
|
|
232
|
+
- [CONTRIBUTING.md](../CONTRIBUTING.md) — regenerating, and why you must not edit the models by hand
|
|
233
|
+
- [CHANGELOG.md](../CHANGELOG.md)
|
|
234
|
+
|
|
235
|
+
## License
|
|
236
|
+
|
|
237
|
+
BSD 3-Clause. See [LICENSE](../LICENSE).
|
|
@@ -0,0 +1,204 @@
|
|
|
1
|
+
# power-openapi-models (Python)
|
|
2
|
+
|
|
3
|
+
[](https://pypi.org/project/power-openapi-models/)
|
|
4
|
+
[](https://pypi.org/project/power-openapi-models/)
|
|
5
|
+
[](https://github.com/Sienna-Platform/power-openapi-models/actions/workflows/test.yml)
|
|
6
|
+
[](../LICENSE)
|
|
7
|
+
|
|
8
|
+
Typed Python models for the Sienna power system data format — every
|
|
9
|
+
component, association, and time series row as a validated pydantic v2
|
|
10
|
+
model.
|
|
11
|
+
|
|
12
|
+
> [!WARNING]
|
|
13
|
+
> **Pre-release.** Every version below `1.0` can change incompatibly in any
|
|
14
|
+
> release; no stability is promised until `1.0`. Under PEP 440, `0.1.0` is
|
|
15
|
+
> not itself a "pre-release" version, so `pip install power-openapi-models`
|
|
16
|
+
> installs it with no `--pre` flag and no warning. Pin an exact version.
|
|
17
|
+
|
|
18
|
+
## Install
|
|
19
|
+
|
|
20
|
+
```bash
|
|
21
|
+
pip install power-openapi-models
|
|
22
|
+
```
|
|
23
|
+
|
|
24
|
+
```bash
|
|
25
|
+
uv add power-openapi-models
|
|
26
|
+
```
|
|
27
|
+
|
|
28
|
+
## Quickstart
|
|
29
|
+
|
|
30
|
+
```python
|
|
31
|
+
from power_openapi_models.core.models import ACBus, ACBusType
|
|
32
|
+
|
|
33
|
+
bus = ACBus(
|
|
34
|
+
id=1,
|
|
35
|
+
name="bus-1",
|
|
36
|
+
available=True,
|
|
37
|
+
number=1,
|
|
38
|
+
bustype=ACBusType.PV,
|
|
39
|
+
base_voltage=230.0,
|
|
40
|
+
)
|
|
41
|
+
print(bus.name, bus.bustype, bus.base_voltage)
|
|
42
|
+
print(bus.model_dump_json(exclude_none=True))
|
|
43
|
+
```
|
|
44
|
+
|
|
45
|
+
## The modules
|
|
46
|
+
|
|
47
|
+
| Module | Holds | Reach for it when |
|
|
48
|
+
|---|---|---|
|
|
49
|
+
| `infrastructure_core` | Units (`UnitSystem`), function data, shared value shapes (`MinMax`, `UpDown`, ...) | You need a domain-neutral building block used across every other module |
|
|
50
|
+
| `core` | Power enums, curves, costs, buses | You are working with shared power types or need `ACBus`/`DCBus` |
|
|
51
|
+
| `operations` | Topology, branches, injections, services, market | You are working with the grid itself |
|
|
52
|
+
| `investments` | Technologies, financials, requirements, regions | You are doing capacity expansion |
|
|
53
|
+
| `dynamics` | Dynamic generator and inverter components | You are doing transient stability |
|
|
54
|
+
| `timeseries` | The six time series association types | You are handling forecasts or profiles |
|
|
55
|
+
| `document` | `SystemDocument`, the hand-written envelope, plus `read_document`/`write_document` | You are loading or saving a whole serialized system |
|
|
56
|
+
|
|
57
|
+
Import a class from its module's `.models` submodule:
|
|
58
|
+
`from power_openapi_models.core.models import ACBus`.
|
|
59
|
+
|
|
60
|
+
## Concepts worth knowing before you start
|
|
61
|
+
|
|
62
|
+
### Unit systems
|
|
63
|
+
|
|
64
|
+
Many component fields carry a sibling `*_units` field (for example
|
|
65
|
+
`ThermalStandard.power_units`) that says how to read every other power-family
|
|
66
|
+
field on that same component. There is no document-level unit system: each
|
|
67
|
+
component blob is self-describing.
|
|
68
|
+
|
|
69
|
+
```python
|
|
70
|
+
from power_openapi_models.infrastructure_core.models import UnitSystem
|
|
71
|
+
|
|
72
|
+
print(list(UnitSystem))
|
|
73
|
+
print(UnitSystem.NATURAL_UNITS.value)
|
|
74
|
+
```
|
|
75
|
+
|
|
76
|
+
`NATURAL_UNITS` means values are in physical units — MW, MVAr, kV, ohm.
|
|
77
|
+
`COMPONENT_BASE` means per-unit against the component's own recorded
|
|
78
|
+
`base_power`. Getting this wrong is the most common source of
|
|
79
|
+
wrong-by-a-factor-of-100 bugs across the ecosystem.
|
|
80
|
+
|
|
81
|
+
### Discriminated unions carry a `.root`
|
|
82
|
+
|
|
83
|
+
Several types are one of several shapes, chosen by a discriminator field.
|
|
84
|
+
They are pydantic `RootModel`s, and the concrete variant is reached through
|
|
85
|
+
`.root`.
|
|
86
|
+
|
|
87
|
+
```python
|
|
88
|
+
from power_openapi_models.infrastructure_core.models import (
|
|
89
|
+
FunctionData,
|
|
90
|
+
LinearFunctionData,
|
|
91
|
+
)
|
|
92
|
+
|
|
93
|
+
parsed = FunctionData.model_validate(
|
|
94
|
+
{"function_type": "LINEAR", "proportional_term": 2.0, "constant_term": 5.0}
|
|
95
|
+
)
|
|
96
|
+
assert isinstance(parsed.root, LinearFunctionData)
|
|
97
|
+
print(parsed.root.proportional_term)
|
|
98
|
+
```
|
|
99
|
+
|
|
100
|
+
> [!IMPORTANT]
|
|
101
|
+
> **This differs from the TypeScript package**, where the same types are zod
|
|
102
|
+
> unions and `parse` returns the variant directly, with no `.root`. Porting
|
|
103
|
+
> code between the two languages, this is the first thing that breaks.
|
|
104
|
+
|
|
105
|
+
### Components reference each other by integer id
|
|
106
|
+
|
|
107
|
+
There are no object references on the wire. `ThermalStandard.bus`, for
|
|
108
|
+
example, names its bus by id, and resolving it is the reader's job.
|
|
109
|
+
|
|
110
|
+
```python
|
|
111
|
+
from power_openapi_models.core.models import ACBus
|
|
112
|
+
|
|
113
|
+
buses = [
|
|
114
|
+
ACBus(id=1, name="a", available=True, number=1, base_voltage=230.0),
|
|
115
|
+
ACBus(id=2, name="b", available=True, number=2, base_voltage=230.0),
|
|
116
|
+
]
|
|
117
|
+
by_id = {bus.id: bus for bus in buses}
|
|
118
|
+
print(by_id[2].name)
|
|
119
|
+
```
|
|
120
|
+
|
|
121
|
+
### The document envelope
|
|
122
|
+
|
|
123
|
+
A serialized system is one object: `components` keyed by type name, a flat
|
|
124
|
+
`supplemental_attributes` array, several association arrays, and
|
|
125
|
+
`time_series_storage_file` naming a sidecar. There is no document-level
|
|
126
|
+
`unit_system` or `base_power` — `SystemDocument` forbids both fields
|
|
127
|
+
outright, since every component already carries its own basis.
|
|
128
|
+
|
|
129
|
+
**Time series values never appear in the document.**
|
|
130
|
+
`time_series_associations` carries only the metadata rows; the values live
|
|
131
|
+
in the sidecar named by `time_series_storage_file`, and reading it is the
|
|
132
|
+
consumer's job.
|
|
133
|
+
|
|
134
|
+
## Round-tripping
|
|
135
|
+
|
|
136
|
+
`read_document` followed by `write_document` reproduces the input byte for
|
|
137
|
+
byte. Fields the input never carried stay omitted rather than being written
|
|
138
|
+
back as explicit nulls, top-level keys are sorted, and the file ends with a
|
|
139
|
+
newline.
|
|
140
|
+
|
|
141
|
+
## Worked example
|
|
142
|
+
|
|
143
|
+
```python
|
|
144
|
+
from power_openapi_models.document import read_document
|
|
145
|
+
|
|
146
|
+
doc = read_document("../fixtures/case14_operations.NATURAL_UNITS.json")
|
|
147
|
+
|
|
148
|
+
buses = doc.components["ACBus"]
|
|
149
|
+
generators = doc.components["ThermalStandard"]
|
|
150
|
+
print(f"{len(buses)} buses, {len(generators)} thermal generators")
|
|
151
|
+
|
|
152
|
+
gen = generators[0]
|
|
153
|
+
limits = gen["active_power_limits"]
|
|
154
|
+
print(gen["name"], "active power limits (MW):", limits["min"], "-", limits["max"])
|
|
155
|
+
```
|
|
156
|
+
|
|
157
|
+
## Validation
|
|
158
|
+
|
|
159
|
+
Bad data raises `pydantic.ValidationError`, which names the field and the
|
|
160
|
+
rule:
|
|
161
|
+
|
|
162
|
+
```python
|
|
163
|
+
from pydantic import ValidationError
|
|
164
|
+
|
|
165
|
+
from power_openapi_models.infrastructure_core.models import LinearFunctionData
|
|
166
|
+
|
|
167
|
+
try:
|
|
168
|
+
LinearFunctionData(function_type="NOT_A_TYPE", proportional_term=1.0, constant_term=0.0)
|
|
169
|
+
except ValidationError as exc:
|
|
170
|
+
print(exc.error_count(), "error(s)")
|
|
171
|
+
print(exc.errors()[0]["loc"], exc.errors()[0]["type"])
|
|
172
|
+
```
|
|
173
|
+
|
|
174
|
+
## Type checking
|
|
175
|
+
|
|
176
|
+
The package ships a PEP 561 `py.typed` marker, so mypy and pyright pick the
|
|
177
|
+
annotations up with no configuration.
|
|
178
|
+
|
|
179
|
+
## Versioning and provenance
|
|
180
|
+
|
|
181
|
+
These models are generated. The schema release they came from is recorded
|
|
182
|
+
in the package:
|
|
183
|
+
|
|
184
|
+
```python
|
|
185
|
+
import power_openapi_models
|
|
186
|
+
|
|
187
|
+
print(power_openapi_models.__version__)
|
|
188
|
+
print(power_openapi_models.__schema_version__)
|
|
189
|
+
```
|
|
190
|
+
|
|
191
|
+
Quote `__schema_version__` in bug reports — it identifies the exact schema
|
|
192
|
+
release the models were built from.
|
|
193
|
+
|
|
194
|
+
## Links
|
|
195
|
+
|
|
196
|
+
- [SiennaSchemas](https://github.com/Sienna-Platform/SiennaSchemas) — the schemas these models are generated from, and where schema bugs belong
|
|
197
|
+
- [@sienna-platform/power-openapi-models](../typescript/README.md) — the TypeScript package generated from the same schemas, kept in lockstep by a CI equivalence gate
|
|
198
|
+
- [PowerOpenAPIModels.jl](https://github.com/Sienna-Platform/PowerOpenAPIModels) — the Julia packages generated from the same schemas
|
|
199
|
+
- [CONTRIBUTING.md](../CONTRIBUTING.md) — regenerating, and why you must not edit the models by hand
|
|
200
|
+
- [CHANGELOG.md](../CHANGELOG.md)
|
|
201
|
+
|
|
202
|
+
## License
|
|
203
|
+
|
|
204
|
+
BSD 3-Clause. See [LICENSE](../LICENSE).
|
|
@@ -0,0 +1,80 @@
|
|
|
1
|
+
[build-system]
|
|
2
|
+
requires = ["setuptools>=77.0"]
|
|
3
|
+
build-backend = "setuptools.build_meta"
|
|
4
|
+
|
|
5
|
+
[project]
|
|
6
|
+
name = "power-openapi-models"
|
|
7
|
+
version = "0.1.0"
|
|
8
|
+
description = "Typed Python models for the Sienna power system data format"
|
|
9
|
+
readme = "README.md"
|
|
10
|
+
license = "BSD-3-Clause"
|
|
11
|
+
license-files = ["LICENSE"]
|
|
12
|
+
requires-python = ">=3.10"
|
|
13
|
+
authors = [{ name = "Sienna Platform" }]
|
|
14
|
+
keywords = [
|
|
15
|
+
"power-systems",
|
|
16
|
+
"energy",
|
|
17
|
+
"openapi",
|
|
18
|
+
"pydantic",
|
|
19
|
+
"sienna",
|
|
20
|
+
"time-series",
|
|
21
|
+
]
|
|
22
|
+
classifiers = [
|
|
23
|
+
"Development Status :: 3 - Alpha",
|
|
24
|
+
"Intended Audience :: Science/Research",
|
|
25
|
+
"Programming Language :: Python :: 3",
|
|
26
|
+
"Programming Language :: Python :: 3.10",
|
|
27
|
+
"Programming Language :: Python :: 3.11",
|
|
28
|
+
"Programming Language :: Python :: 3.12",
|
|
29
|
+
"Programming Language :: Python :: 3.13",
|
|
30
|
+
"Programming Language :: Python :: 3.14",
|
|
31
|
+
"Typing :: Typed",
|
|
32
|
+
]
|
|
33
|
+
dependencies = [
|
|
34
|
+
"pydantic>=2.0",
|
|
35
|
+
]
|
|
36
|
+
|
|
37
|
+
[project.urls]
|
|
38
|
+
Homepage = "https://github.com/Sienna-Platform/power-openapi-models"
|
|
39
|
+
Repository = "https://github.com/Sienna-Platform/power-openapi-models"
|
|
40
|
+
Issues = "https://github.com/Sienna-Platform/power-openapi-models/issues"
|
|
41
|
+
Changelog = "https://github.com/Sienna-Platform/power-openapi-models/blob/main/CHANGELOG.md"
|
|
42
|
+
|
|
43
|
+
[project.optional-dependencies]
|
|
44
|
+
dev = [
|
|
45
|
+
"ruff>=0.6",
|
|
46
|
+
"pytest>=8",
|
|
47
|
+
"pyright>=1.1.380",
|
|
48
|
+
"datamodel-code-generator",
|
|
49
|
+
"build",
|
|
50
|
+
"twine",
|
|
51
|
+
]
|
|
52
|
+
|
|
53
|
+
[tool.setuptools.packages.find]
|
|
54
|
+
where = ["src"]
|
|
55
|
+
|
|
56
|
+
[tool.setuptools.package-data]
|
|
57
|
+
power_openapi_models = ["py.typed", "_schema_version.txt"]
|
|
58
|
+
|
|
59
|
+
[tool.ruff]
|
|
60
|
+
line-length = 100
|
|
61
|
+
# models.py files are generated; their formatting is the generator's business.
|
|
62
|
+
# SiennaSchemas is a sibling checkout (local dev) or a CI-only checkout for the
|
|
63
|
+
# document tests (see .github/workflows/test.yml); its own scripts are out of scope.
|
|
64
|
+
extend-exclude = ["src/power_openapi_models/*/models.py", "SiennaSchemas"]
|
|
65
|
+
|
|
66
|
+
[tool.ruff.lint]
|
|
67
|
+
select = ["E", "F", "I", "UP", "B", "W"]
|
|
68
|
+
# E741 (ambiguous variable name): the generated models are field-for-field with
|
|
69
|
+
# SiennaSchemas' power-system quantities, which use standard electrical-engineering
|
|
70
|
+
# single-letter names (e.g. `l` for inductance) -- not something to rename away from.
|
|
71
|
+
ignore = ["E741"]
|
|
72
|
+
|
|
73
|
+
[tool.pytest.ini_options]
|
|
74
|
+
testpaths = ["tests"]
|
|
75
|
+
addopts = "-ra --strict-markers"
|
|
76
|
+
|
|
77
|
+
[tool.pyright]
|
|
78
|
+
include = ["src"]
|
|
79
|
+
extraPaths = ["src"]
|
|
80
|
+
pythonVersion = "3.10"
|
|
@@ -0,0 +1,52 @@
|
|
|
1
|
+
"""Typed Python models for the Sienna power system data format.
|
|
2
|
+
|
|
3
|
+
Generated from SiennaSchemas. See `__schema_version__` for the schema release
|
|
4
|
+
these models were built from.
|
|
5
|
+
"""
|
|
6
|
+
|
|
7
|
+
from importlib.metadata import PackageNotFoundError
|
|
8
|
+
from importlib.metadata import version as _version
|
|
9
|
+
from pathlib import Path
|
|
10
|
+
|
|
11
|
+
from power_openapi_models import (
|
|
12
|
+
core,
|
|
13
|
+
document,
|
|
14
|
+
dynamics,
|
|
15
|
+
infrastructure_core,
|
|
16
|
+
investments,
|
|
17
|
+
operations,
|
|
18
|
+
timeseries,
|
|
19
|
+
)
|
|
20
|
+
|
|
21
|
+
try:
|
|
22
|
+
__version__ = _version("power-openapi-models")
|
|
23
|
+
except PackageNotFoundError: # running from a source tree, not installed
|
|
24
|
+
__version__ = "0.0.0.dev0"
|
|
25
|
+
|
|
26
|
+
|
|
27
|
+
def _read_schema_version() -> str:
|
|
28
|
+
"""The SiennaSchemas release these models were generated from.
|
|
29
|
+
|
|
30
|
+
Shipped as package data so it is answerable at runtime -- the first
|
|
31
|
+
question worth asking about a generated model is which schema produced it.
|
|
32
|
+
"""
|
|
33
|
+
marker = Path(__file__).parent / "_schema_version.txt"
|
|
34
|
+
try:
|
|
35
|
+
return marker.read_text().strip()
|
|
36
|
+
except OSError:
|
|
37
|
+
return "unknown"
|
|
38
|
+
|
|
39
|
+
|
|
40
|
+
__schema_version__ = _read_schema_version()
|
|
41
|
+
|
|
42
|
+
__all__ = [
|
|
43
|
+
"core",
|
|
44
|
+
"document",
|
|
45
|
+
"dynamics",
|
|
46
|
+
"infrastructure_core",
|
|
47
|
+
"investments",
|
|
48
|
+
"operations",
|
|
49
|
+
"timeseries",
|
|
50
|
+
"__version__",
|
|
51
|
+
"__schema_version__",
|
|
52
|
+
]
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
v0.1.0
|