power-openapi-models 0.1.0__tar.gz → 0.2.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/src/power_openapi_models.egg-info → power_openapi_models-0.2.0}/PKG-INFO +46 -4
- {power_openapi_models-0.1.0 → power_openapi_models-0.2.0}/README.md +42 -3
- {power_openapi_models-0.1.0 → power_openapi_models-0.2.0}/pyproject.toml +4 -2
- {power_openapi_models-0.1.0 → power_openapi_models-0.2.0}/src/power_openapi_models/__init__.py +3 -0
- power_openapi_models-0.2.0/src/power_openapi_models/_schema_version.txt +1 -0
- power_openapi_models-0.2.0/src/power_openapi_models/_versioning.py +195 -0
- power_openapi_models-0.2.0/src/power_openapi_models/bundles/0.2.0/PortfolioDocument.json +3752 -0
- power_openapi_models-0.2.0/src/power_openapi_models/bundles/0.2.0/SystemDocument.json +8795 -0
- {power_openapi_models-0.1.0 → power_openapi_models-0.2.0}/src/power_openapi_models/core/models.py +13 -6
- {power_openapi_models-0.1.0 → power_openapi_models-0.2.0}/src/power_openapi_models/document.py +224 -44
- power_openapi_models-0.2.0/src/power_openapi_models/dynamics/models.py +1623 -0
- {power_openapi_models-0.1.0 → power_openapi_models-0.2.0}/src/power_openapi_models/infrastructure_core/models.py +17 -10
- {power_openapi_models-0.1.0 → power_openapi_models-0.2.0}/src/power_openapi_models/investments/models.py +0 -7
- {power_openapi_models-0.1.0 → power_openapi_models-0.2.0}/src/power_openapi_models/operations/models.py +383 -343
- {power_openapi_models-0.1.0 → power_openapi_models-0.2.0}/src/power_openapi_models/timeseries/models.py +7 -6
- power_openapi_models-0.2.0/src/power_openapi_models/timestamps.py +39 -0
- {power_openapi_models-0.1.0 → power_openapi_models-0.2.0/src/power_openapi_models.egg-info}/PKG-INFO +46 -4
- {power_openapi_models-0.1.0 → power_openapi_models-0.2.0}/src/power_openapi_models.egg-info/SOURCES.txt +7 -1
- {power_openapi_models-0.1.0 → power_openapi_models-0.2.0}/src/power_openapi_models.egg-info/requires.txt +4 -0
- {power_openapi_models-0.1.0 → power_openapi_models-0.2.0}/tests/test_document.py +2 -2
- power_openapi_models-0.2.0/tests/test_document_versioning.py +410 -0
- {power_openapi_models-0.1.0 → power_openapi_models-0.2.0}/tests/test_public_api.py +13 -2
- {power_openapi_models-0.1.0 → power_openapi_models-0.2.0}/tests/test_serde_fixture.py +1 -0
- power_openapi_models-0.2.0/tests/test_timestamps.py +75 -0
- power_openapi_models-0.1.0/src/power_openapi_models/_schema_version.txt +0 -1
- power_openapi_models-0.1.0/src/power_openapi_models/dynamics/models.py +0 -257
- {power_openapi_models-0.1.0 → power_openapi_models-0.2.0}/LICENSE +0 -0
- {power_openapi_models-0.1.0 → power_openapi_models-0.2.0}/setup.cfg +0 -0
- {power_openapi_models-0.1.0 → power_openapi_models-0.2.0}/src/power_openapi_models/core/__init__.py +0 -0
- {power_openapi_models-0.1.0 → power_openapi_models-0.2.0}/src/power_openapi_models/dynamics/__init__.py +0 -0
- {power_openapi_models-0.1.0 → power_openapi_models-0.2.0}/src/power_openapi_models/infrastructure_core/__init__.py +0 -0
- {power_openapi_models-0.1.0 → power_openapi_models-0.2.0}/src/power_openapi_models/investments/__init__.py +0 -0
- {power_openapi_models-0.1.0 → power_openapi_models-0.2.0}/src/power_openapi_models/operations/__init__.py +0 -0
- {power_openapi_models-0.1.0 → power_openapi_models-0.2.0}/src/power_openapi_models/py.typed +0 -0
- {power_openapi_models-0.1.0 → power_openapi_models-0.2.0}/src/power_openapi_models/timeseries/__init__.py +0 -0
- {power_openapi_models-0.1.0 → power_openapi_models-0.2.0}/src/power_openapi_models.egg-info/dependency_links.txt +0 -0
- {power_openapi_models-0.1.0 → power_openapi_models-0.2.0}/src/power_openapi_models.egg-info/top_level.txt +0 -0
- {power_openapi_models-0.1.0 → power_openapi_models-0.2.0}/tests/test_import.py +0 -0
- {power_openapi_models-0.1.0 → power_openapi_models-0.2.0}/tests/test_readme.py +0 -0
{power_openapi_models-0.1.0/src/power_openapi_models.egg-info → power_openapi_models-0.2.0}/PKG-INFO
RENAMED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: power-openapi-models
|
|
3
|
-
Version: 0.
|
|
3
|
+
Version: 0.2.0
|
|
4
4
|
Summary: Typed Python models for the Sienna power system data format
|
|
5
5
|
Author: Sienna Platform
|
|
6
6
|
License-Expression: BSD-3-Clause
|
|
@@ -22,7 +22,10 @@ Requires-Python: >=3.10
|
|
|
22
22
|
Description-Content-Type: text/markdown
|
|
23
23
|
License-File: LICENSE
|
|
24
24
|
Requires-Dist: pydantic>=2.0
|
|
25
|
+
Provides-Extra: source-version
|
|
26
|
+
Requires-Dist: jsonschema>=4; extra == "source-version"
|
|
25
27
|
Provides-Extra: dev
|
|
28
|
+
Requires-Dist: jsonschema>=4; extra == "dev"
|
|
26
29
|
Requires-Dist: ruff>=0.6; extra == "dev"
|
|
27
30
|
Requires-Dist: pytest>=8; extra == "dev"
|
|
28
31
|
Requires-Dist: pyright>=1.1.380; extra == "dev"
|
|
@@ -111,6 +114,20 @@ print(UnitSystem.NATURAL_UNITS.value)
|
|
|
111
114
|
`base_power`. Getting this wrong is the most common source of
|
|
112
115
|
wrong-by-a-factor-of-100 bugs across the ecosystem.
|
|
113
116
|
|
|
117
|
+
### Timestamps may omit the offset
|
|
118
|
+
|
|
119
|
+
The schema says RFC 3339, which requires an offset, but producers write
|
|
120
|
+
`2024-01-01T00:00:00`, so all three packages accept it and read it as UTC. An offset is
|
|
121
|
+
kept; a bare date or text that is not a timestamp is rejected. Fields are typed
|
|
122
|
+
`power_openapi_models.timestamps.UtcDatetime`, so they always hold an aware `datetime`.
|
|
123
|
+
|
|
124
|
+
```python
|
|
125
|
+
from power_openapi_models.infrastructure_core.models import DataSource
|
|
126
|
+
|
|
127
|
+
source = DataSource(id=1, fields=[], retrieved_at="2024-01-01T00:00:00")
|
|
128
|
+
assert source.retrieved_at.utcoffset().total_seconds() == 0
|
|
129
|
+
```
|
|
130
|
+
|
|
114
131
|
### Discriminated unions carry a `.root`
|
|
115
132
|
|
|
116
133
|
Several types are one of several shapes, chosen by a discriminator field.
|
|
@@ -167,9 +184,34 @@ consumer's job.
|
|
|
167
184
|
## Round-tripping
|
|
168
185
|
|
|
169
186
|
`read_document` followed by `write_document` reproduces the input byte for
|
|
170
|
-
byte
|
|
171
|
-
|
|
172
|
-
|
|
187
|
+
byte, for a document in canonical form. Optional properties that are absent,
|
|
188
|
+
null or equal to their schema default (an empty `trading_hub_associations`,
|
|
189
|
+
say) are omitted rather than written back, top-level keys are sorted after the
|
|
190
|
+
`schema_version` stamp, and the file ends with a newline.
|
|
191
|
+
|
|
192
|
+
## Schema versions
|
|
193
|
+
|
|
194
|
+
Both documents carry a required `schema_version`: the schema release that wrote
|
|
195
|
+
them. `read_document` and `read_portfolio_document` check it on the raw JSON,
|
|
196
|
+
before decoding, and raise `SchemaVersionError` (with `outcome`, `reader` and
|
|
197
|
+
`document` attributes) unless the document is from this package's schema version
|
|
198
|
+
or an older one on the same compatibility line. `check_schema_version(raw)` runs
|
|
199
|
+
the same rule on a parsed dict and returns the outcome without raising (a
|
|
200
|
+
non-object root raises pydantic's `ValidationError`).
|
|
201
|
+
|
|
202
|
+
```python
|
|
203
|
+
from power_openapi_models.document import get_source_schema_version, read_document
|
|
204
|
+
|
|
205
|
+
doc = read_document("../fixtures/case14_operations.NATURAL_UNITS.json")
|
|
206
|
+
print(get_source_schema_version(doc))
|
|
207
|
+
```
|
|
208
|
+
|
|
209
|
+
`write_document(doc, path)` stamps this package's version, so reading then writing
|
|
210
|
+
upgrades a document; `upgrade_document(src, dst)` does exactly that. To keep the
|
|
211
|
+
version a document was read at, pass `schema_version="source"`. When that version
|
|
212
|
+
is older, the encoded document is validated against that version's strict schema
|
|
213
|
+
first, and the write fails listing every path that version does not know; nothing
|
|
214
|
+
is dropped. That check needs `pip install "power-openapi-models[source-version]"`.
|
|
173
215
|
|
|
174
216
|
## Worked example
|
|
175
217
|
|
|
@@ -78,6 +78,20 @@ print(UnitSystem.NATURAL_UNITS.value)
|
|
|
78
78
|
`base_power`. Getting this wrong is the most common source of
|
|
79
79
|
wrong-by-a-factor-of-100 bugs across the ecosystem.
|
|
80
80
|
|
|
81
|
+
### Timestamps may omit the offset
|
|
82
|
+
|
|
83
|
+
The schema says RFC 3339, which requires an offset, but producers write
|
|
84
|
+
`2024-01-01T00:00:00`, so all three packages accept it and read it as UTC. An offset is
|
|
85
|
+
kept; a bare date or text that is not a timestamp is rejected. Fields are typed
|
|
86
|
+
`power_openapi_models.timestamps.UtcDatetime`, so they always hold an aware `datetime`.
|
|
87
|
+
|
|
88
|
+
```python
|
|
89
|
+
from power_openapi_models.infrastructure_core.models import DataSource
|
|
90
|
+
|
|
91
|
+
source = DataSource(id=1, fields=[], retrieved_at="2024-01-01T00:00:00")
|
|
92
|
+
assert source.retrieved_at.utcoffset().total_seconds() == 0
|
|
93
|
+
```
|
|
94
|
+
|
|
81
95
|
### Discriminated unions carry a `.root`
|
|
82
96
|
|
|
83
97
|
Several types are one of several shapes, chosen by a discriminator field.
|
|
@@ -134,9 +148,34 @@ consumer's job.
|
|
|
134
148
|
## Round-tripping
|
|
135
149
|
|
|
136
150
|
`read_document` followed by `write_document` reproduces the input byte for
|
|
137
|
-
byte
|
|
138
|
-
|
|
139
|
-
|
|
151
|
+
byte, for a document in canonical form. Optional properties that are absent,
|
|
152
|
+
null or equal to their schema default (an empty `trading_hub_associations`,
|
|
153
|
+
say) are omitted rather than written back, top-level keys are sorted after the
|
|
154
|
+
`schema_version` stamp, and the file ends with a newline.
|
|
155
|
+
|
|
156
|
+
## Schema versions
|
|
157
|
+
|
|
158
|
+
Both documents carry a required `schema_version`: the schema release that wrote
|
|
159
|
+
them. `read_document` and `read_portfolio_document` check it on the raw JSON,
|
|
160
|
+
before decoding, and raise `SchemaVersionError` (with `outcome`, `reader` and
|
|
161
|
+
`document` attributes) unless the document is from this package's schema version
|
|
162
|
+
or an older one on the same compatibility line. `check_schema_version(raw)` runs
|
|
163
|
+
the same rule on a parsed dict and returns the outcome without raising (a
|
|
164
|
+
non-object root raises pydantic's `ValidationError`).
|
|
165
|
+
|
|
166
|
+
```python
|
|
167
|
+
from power_openapi_models.document import get_source_schema_version, read_document
|
|
168
|
+
|
|
169
|
+
doc = read_document("../fixtures/case14_operations.NATURAL_UNITS.json")
|
|
170
|
+
print(get_source_schema_version(doc))
|
|
171
|
+
```
|
|
172
|
+
|
|
173
|
+
`write_document(doc, path)` stamps this package's version, so reading then writing
|
|
174
|
+
upgrades a document; `upgrade_document(src, dst)` does exactly that. To keep the
|
|
175
|
+
version a document was read at, pass `schema_version="source"`. When that version
|
|
176
|
+
is older, the encoded document is validated against that version's strict schema
|
|
177
|
+
first, and the write fails listing every path that version does not know; nothing
|
|
178
|
+
is dropped. That check needs `pip install "power-openapi-models[source-version]"`.
|
|
140
179
|
|
|
141
180
|
## Worked example
|
|
142
181
|
|
|
@@ -4,7 +4,7 @@ build-backend = "setuptools.build_meta"
|
|
|
4
4
|
|
|
5
5
|
[project]
|
|
6
6
|
name = "power-openapi-models"
|
|
7
|
-
version = "0.
|
|
7
|
+
version = "0.2.0"
|
|
8
8
|
description = "Typed Python models for the Sienna power system data format"
|
|
9
9
|
readme = "README.md"
|
|
10
10
|
license = "BSD-3-Clause"
|
|
@@ -41,7 +41,9 @@ Issues = "https://github.com/Sienna-Platform/power-openapi-models/issues"
|
|
|
41
41
|
Changelog = "https://github.com/Sienna-Platform/power-openapi-models/blob/main/CHANGELOG.md"
|
|
42
42
|
|
|
43
43
|
[project.optional-dependencies]
|
|
44
|
+
source-version = ["jsonschema>=4"]
|
|
44
45
|
dev = [
|
|
46
|
+
"jsonschema>=4",
|
|
45
47
|
"ruff>=0.6",
|
|
46
48
|
"pytest>=8",
|
|
47
49
|
"pyright>=1.1.380",
|
|
@@ -54,7 +56,7 @@ dev = [
|
|
|
54
56
|
where = ["src"]
|
|
55
57
|
|
|
56
58
|
[tool.setuptools.package-data]
|
|
57
|
-
power_openapi_models = ["py.typed", "_schema_version.txt"]
|
|
59
|
+
power_openapi_models = ["py.typed", "_schema_version.txt", "bundles/*/*.json"]
|
|
58
60
|
|
|
59
61
|
[tool.ruff]
|
|
60
62
|
line-length = 100
|
{power_openapi_models-0.1.0 → power_openapi_models-0.2.0}/src/power_openapi_models/__init__.py
RENAMED
|
@@ -17,6 +17,7 @@ from power_openapi_models import (
|
|
|
17
17
|
operations,
|
|
18
18
|
timeseries,
|
|
19
19
|
)
|
|
20
|
+
from power_openapi_models._versioning import SchemaVersionError, check_schema_version
|
|
20
21
|
|
|
21
22
|
try:
|
|
22
23
|
__version__ = _version("power-openapi-models")
|
|
@@ -47,6 +48,8 @@ __all__ = [
|
|
|
47
48
|
"investments",
|
|
48
49
|
"operations",
|
|
49
50
|
"timeseries",
|
|
51
|
+
"SchemaVersionError",
|
|
52
|
+
"check_schema_version",
|
|
50
53
|
"__version__",
|
|
51
54
|
"__schema_version__",
|
|
52
55
|
]
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
v0.2.0
|
|
@@ -0,0 +1,195 @@
|
|
|
1
|
+
"""Hand-written (NOT generated): the schema-version reader rule and the strict-bundle
|
|
2
|
+
check behind `write_document(..., schema_version="source")`.
|
|
3
|
+
|
|
4
|
+
The rule, its outcomes and the canonical message texts are defined once, in
|
|
5
|
+
SiennaSchemas' `docs/VERSIONING.md`; `scripts/schema_version.py` there is the reference
|
|
6
|
+
implementation. `check_schema_version` must agree with `tests/fixtures/versioning/cases.json`.
|
|
7
|
+
"""
|
|
8
|
+
|
|
9
|
+
from __future__ import annotations
|
|
10
|
+
|
|
11
|
+
import json
|
|
12
|
+
import re
|
|
13
|
+
from pathlib import Path
|
|
14
|
+
from typing import Literal
|
|
15
|
+
|
|
16
|
+
from pydantic import ValidationError
|
|
17
|
+
|
|
18
|
+
Outcome = Literal["missing", "malformed", "incompatible", "newer", "upgradable", "current"]
|
|
19
|
+
|
|
20
|
+
READER_VERSION = (
|
|
21
|
+
(Path(__file__).parent / "_schema_version.txt").read_text().strip().removeprefix("v")
|
|
22
|
+
)
|
|
23
|
+
BUNDLE_DIR = Path(__file__).parent / "bundles"
|
|
24
|
+
|
|
25
|
+
_PATTERN = re.compile(r"(0|[1-9][0-9]*)\.(0|[1-9][0-9]*)\.(0|[1-9][0-9]*)(-[0-9A-Za-z.-]+)?")
|
|
26
|
+
|
|
27
|
+
|
|
28
|
+
class SchemaVersionError(ValueError):
|
|
29
|
+
"""A document whose `schema_version` this reader cannot read."""
|
|
30
|
+
|
|
31
|
+
def __init__(self, outcome: Outcome, reader: str, document: object, message: str):
|
|
32
|
+
super().__init__(message)
|
|
33
|
+
self.outcome = outcome
|
|
34
|
+
self.reader = reader
|
|
35
|
+
self.document = document
|
|
36
|
+
|
|
37
|
+
|
|
38
|
+
def _parse(version: object) -> tuple[tuple[int, str], ...] | None:
|
|
39
|
+
"""Components as (digit count, digits): the pattern forbids leading zeros, so this
|
|
40
|
+
orders like the integers without a size limit."""
|
|
41
|
+
if not isinstance(version, str):
|
|
42
|
+
return None
|
|
43
|
+
m = _PATTERN.fullmatch(version)
|
|
44
|
+
if m is None:
|
|
45
|
+
return None
|
|
46
|
+
pre = (0, m[4][1:]) if m[4] else (0, "")
|
|
47
|
+
return (*((len(m[i]), m[i]) for i in (1, 2, 3)), pre)
|
|
48
|
+
|
|
49
|
+
|
|
50
|
+
def _prerelease(version) -> bool:
|
|
51
|
+
return bool(version[3][1])
|
|
52
|
+
|
|
53
|
+
|
|
54
|
+
def _line(version) -> tuple[tuple[int, str], ...]:
|
|
55
|
+
major, minor = version[:2]
|
|
56
|
+
return (major, minor) if major[1] == "0" else (major,)
|
|
57
|
+
|
|
58
|
+
|
|
59
|
+
def _line_name(version) -> str:
|
|
60
|
+
return ".".join(digits for _, digits in _line(version))
|
|
61
|
+
|
|
62
|
+
|
|
63
|
+
def _classify(raw: object, reader: str) -> Outcome:
|
|
64
|
+
if not isinstance(raw, dict):
|
|
65
|
+
raise ValidationError.from_exception_data(
|
|
66
|
+
"document", [{"type": "dict_type", "loc": (), "input": raw}]
|
|
67
|
+
)
|
|
68
|
+
if "schema_version" not in raw:
|
|
69
|
+
return "missing"
|
|
70
|
+
d = _parse(raw["schema_version"])
|
|
71
|
+
if d is None:
|
|
72
|
+
return "malformed"
|
|
73
|
+
r = _parse(reader)
|
|
74
|
+
if r is None:
|
|
75
|
+
raise ValueError(f"reader schema version {reader!r} is not a valid version")
|
|
76
|
+
if (_prerelease(d) or _prerelease(r)) and d != r:
|
|
77
|
+
return "incompatible"
|
|
78
|
+
if _line(d) != _line(r):
|
|
79
|
+
return "incompatible"
|
|
80
|
+
if d[:3] > r[:3]:
|
|
81
|
+
return "newer"
|
|
82
|
+
if d[:3] < r[:3]:
|
|
83
|
+
return "upgradable"
|
|
84
|
+
return "current"
|
|
85
|
+
|
|
86
|
+
|
|
87
|
+
def _message(outcome: Outcome, reader: str, document: object) -> str:
|
|
88
|
+
if outcome == "missing":
|
|
89
|
+
return (
|
|
90
|
+
"document has no schema_version: it predates versioning; re-export it with a "
|
|
91
|
+
"current producer (psy5 bundles: PowerSystemsUpdater)"
|
|
92
|
+
)
|
|
93
|
+
if outcome == "malformed":
|
|
94
|
+
encoded = json.dumps(document, separators=(",", ":"), ensure_ascii=False)
|
|
95
|
+
return f"document schema_version {encoded} is not a valid version"
|
|
96
|
+
d = _parse(document)
|
|
97
|
+
r = _parse(reader)
|
|
98
|
+
assert d is not None and r is not None
|
|
99
|
+
if outcome == "incompatible":
|
|
100
|
+
if _prerelease(d) or _prerelease(r):
|
|
101
|
+
return (
|
|
102
|
+
f"document written by schema {document} cannot be read by schema {reader}: "
|
|
103
|
+
"dev builds read only their own output"
|
|
104
|
+
)
|
|
105
|
+
return (
|
|
106
|
+
f"document written by schema {document} (line {_line_name(d)}) cannot be read by "
|
|
107
|
+
f"schema {reader} (line {_line_name(r)}): documents do not cross compatibility "
|
|
108
|
+
"lines; migrating between lines is a separate upgrade tool's job "
|
|
109
|
+
"(psy5 bundles: PowerSystemsUpdater)"
|
|
110
|
+
)
|
|
111
|
+
return (
|
|
112
|
+
f"document written by schema {document}; this reader understands up to {reader}; "
|
|
113
|
+
f"update the model package to one built from >= {document}"
|
|
114
|
+
)
|
|
115
|
+
|
|
116
|
+
|
|
117
|
+
def _check(raw: object, reader: str) -> Outcome:
|
|
118
|
+
outcome = _classify(raw, reader)
|
|
119
|
+
if outcome in ("upgradable", "current"):
|
|
120
|
+
return outcome
|
|
121
|
+
document = raw.get("schema_version")
|
|
122
|
+
raise SchemaVersionError(outcome, reader, document, _message(outcome, reader, document))
|
|
123
|
+
|
|
124
|
+
|
|
125
|
+
def check_schema_version(raw: object) -> Outcome:
|
|
126
|
+
"""Classify the raw parsed JSON of a document against this package's schema version.
|
|
127
|
+
|
|
128
|
+
Pure: returns the outcome and never raises `SchemaVersionError` (the read path does).
|
|
129
|
+
A non-object root is a format error and raises pydantic's `ValidationError`, as
|
|
130
|
+
reading it does. Run before decoding, so a newer document reports its version
|
|
131
|
+
rather than an unknown key.
|
|
132
|
+
"""
|
|
133
|
+
return _classify(raw, READER_VERSION)
|
|
134
|
+
|
|
135
|
+
|
|
136
|
+
def _covered(error, branch: int, root: dict) -> int:
|
|
137
|
+
"""How many instance keys the `branch` of an anyOf/oneOf `error` declares."""
|
|
138
|
+
sub = error.schema[error.validator][branch]
|
|
139
|
+
while "$ref" in sub:
|
|
140
|
+
node = root
|
|
141
|
+
for part in sub["$ref"].removeprefix("#/").split("/"):
|
|
142
|
+
node = node[part]
|
|
143
|
+
sub = node
|
|
144
|
+
if not isinstance(error.instance, dict):
|
|
145
|
+
return 0
|
|
146
|
+
return sum(1 for key in error.instance if key in sub.get("properties", {}))
|
|
147
|
+
|
|
148
|
+
|
|
149
|
+
def _leaves(error, root: dict):
|
|
150
|
+
"""The offending (path, message) pairs under one validation error."""
|
|
151
|
+
if error.validator in ("anyOf", "oneOf") and error.context:
|
|
152
|
+
branches: dict[int, list] = {}
|
|
153
|
+
for e in error.context:
|
|
154
|
+
branches.setdefault(e.schema_path[0], []).extend(_leaves(e, root))
|
|
155
|
+
pick = min(
|
|
156
|
+
branches,
|
|
157
|
+
key=lambda i: (len(branches[i]), -_covered(error, i, root)),
|
|
158
|
+
)
|
|
159
|
+
yield from branches[pick]
|
|
160
|
+
return
|
|
161
|
+
path = "/" + "/".join(str(p) for p in error.absolute_path)
|
|
162
|
+
if error.validator == "additionalProperties" and isinstance(error.instance, dict):
|
|
163
|
+
known = error.schema.get("properties", {})
|
|
164
|
+
for key in error.instance:
|
|
165
|
+
if key not in known:
|
|
166
|
+
yield f"{path.rstrip('/')}/{key}", "is not a property of this schema version"
|
|
167
|
+
return
|
|
168
|
+
yield path, error.message
|
|
169
|
+
|
|
170
|
+
|
|
171
|
+
def validate_source(tree: dict, document: str, bundle_name: str, bundle_dir: Path) -> None:
|
|
172
|
+
"""Validate the encoded `tree` against the strict bundle of schema `document`.
|
|
173
|
+
|
|
174
|
+
Raises one `ValueError` listing every offending path.
|
|
175
|
+
"""
|
|
176
|
+
try:
|
|
177
|
+
from jsonschema import Draft7Validator
|
|
178
|
+
except ImportError as e:
|
|
179
|
+
raise ImportError(
|
|
180
|
+
f"writing at the source schema version {document} needs a validator; "
|
|
181
|
+
"install power-openapi-models[source-version]"
|
|
182
|
+
) from e
|
|
183
|
+
bundle = Path(bundle_dir) / document / f"{bundle_name}.json"
|
|
184
|
+
if not bundle.is_file():
|
|
185
|
+
raise FileNotFoundError(f"strict bundle for schema {document} not found at {bundle}")
|
|
186
|
+
root = json.loads(bundle.read_text())
|
|
187
|
+
found = sorted(
|
|
188
|
+
{leaf for e in Draft7Validator(root).iter_errors(tree) for leaf in _leaves(e, root)}
|
|
189
|
+
)
|
|
190
|
+
if found:
|
|
191
|
+
lines = "\n".join(f" {path}: {message}" for path, message in found)
|
|
192
|
+
raise ValueError(
|
|
193
|
+
f'document does not fit schema {document}; save with schema_version="current" '
|
|
194
|
+
f"to write the current version:\n{lines}"
|
|
195
|
+
)
|