python-midas 1.0.0__tar.gz → 1.0.1__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.
- {python_midas-1.0.0 → python_midas-1.0.1}/CHANGELOG.md +14 -0
- {python_midas-1.0.0 → python_midas-1.0.1}/PKG-INFO +2 -1
- {python_midas-1.0.0 → python_midas-1.0.1}/pyproject.toml +4 -1
- {python_midas-1.0.0 → python_midas-1.0.1}/src/midas/entities/models.py +10 -7
- {python_midas-1.0.0 → python_midas-1.0.1}/src/midas/enums.py +38 -0
- {python_midas-1.0.0 → python_midas-1.0.1}/tests/test_entities.py +59 -0
- {python_midas-1.0.0 → python_midas-1.0.1}/tests/test_integration.py +102 -2
- {python_midas-1.0.0 → python_midas-1.0.1}/.github/workflows/ci.yml +0 -0
- {python_midas-1.0.0 → python_midas-1.0.1}/.github/workflows/publish.yml +0 -0
- {python_midas-1.0.0 → python_midas-1.0.1}/.gitignore +0 -0
- {python_midas-1.0.0 → python_midas-1.0.1}/CONTRIBUTING.md +0 -0
- {python_midas-1.0.0 → python_midas-1.0.1}/LICENSE +0 -0
- {python_midas-1.0.0 → python_midas-1.0.1}/README.md +0 -0
- {python_midas-1.0.0 → python_midas-1.0.1}/doc/v2-migration.md +0 -0
- {python_midas-1.0.0 → python_midas-1.0.1}/src/midas/__init__.py +0 -0
- {python_midas-1.0.0 → python_midas-1.0.1}/src/midas/auth.py +0 -0
- {python_midas-1.0.0 → python_midas-1.0.1}/src/midas/client.py +0 -0
- {python_midas-1.0.0 → python_midas-1.0.1}/src/midas/entities/__init__.py +0 -0
- {python_midas-1.0.0 → python_midas-1.0.1}/src/midas/py.typed +0 -0
- {python_midas-1.0.0 → python_midas-1.0.1}/src/midas/time.py +0 -0
- {python_midas-1.0.0 → python_midas-1.0.1}/tests/test_auth.py +0 -0
- {python_midas-1.0.0 → python_midas-1.0.1}/tests/test_client.py +0 -0
|
@@ -4,6 +4,19 @@ All notable changes to this project will be documented in this file.
|
|
|
4
4
|
|
|
5
5
|
The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.1.0/), and this project follows [Semantic Versioning](https://semver.org/spec/v2.0.0.html). While the library was in early development (0.x), breaking changes appeared between minor versions when needed to fix correctness issues or align with the MIDAS API. The 1.0.0 release tracks the California Energy Commission's MIDAS v2.0 API.
|
|
6
6
|
|
|
7
|
+
## [1.0.1] - 2026-06-24
|
|
8
|
+
|
|
9
|
+
Patch release. Regenerating the spec examples against the live API surfaced two wire-shape details the lenient client silently tolerated; both are now handled and guarded by tests.
|
|
10
|
+
|
|
11
|
+
### Fixed
|
|
12
|
+
|
|
13
|
+
- **Integer day-types are no longer dropped.** Live v2.0 SGIP GHG (MOER) and Flex Alert (ALRT) responses encode `DayStart` / `DayEnd` as integers (1=Monday through 7=Sunday, 8=Holiday: the upload-format code), where v1.0 returned weekday strings. `_parse_day_type` previously tried `DayType(value)` and returned `None` on the resulting `ValueError`, so every MOER/ALRT interval lost its `day_start` / `day_end`. `DayType.from_wire` now accepts the integer code, a digit string, or a weekday string; the electricity-rate wire form (unconfirmed pending utility-data migration) is covered by accepting both.
|
|
14
|
+
|
|
15
|
+
### Added
|
|
16
|
+
|
|
17
|
+
- **`RateInfo.signal_type` and `RateInfo.description`.** v2.0 rate-values responses carry the per-RIN `SignalType` label and `Description` at the top level (as in the RIN list); these are now surfaced on the entity instead of being silently ignored.
|
|
18
|
+
- **Strict wire-contract validation in the integration suite.** New `TestWireContract` validates raw live responses against the `midas-api-specs` JSON Schemas (via `jsonschema`), and the coerced tests now assert day-type values, so future wire drift fails the suite rather than being absorbed by the lenient runtime models. Point `MIDAS_SPECS_DIR` at a `midas-api-specs` checkout to enable the schema checks (they skip cleanly if absent). Adds `jsonschema` as a dev dependency.
|
|
19
|
+
|
|
7
20
|
## [1.0.0] — 2026-06-22
|
|
8
21
|
|
|
9
22
|
The California Energy Commission released **MIDAS v2.0 on 2026-06-22**, a breaking change to the live API. v1.0 was removed from the live service that day, so python-midas 1.0.0 is a **v2-only release** — v1.0 compatibility is intentionally dropped. See [doc/v2-migration.md](doc/v2-migration.md) for the v1.0→v2.0 upgrade guide, and the upstream [`midas-api-specs`](https://github.com/grid-coordination/midas-api-specs) `v2` branch for the spec-level delta. Every change below was verified by a live smoke-test against the production v2.0 API on release day. python-midas is a **read-only consumer client**: v2.0 GET endpoints are unauthenticated, so `create_anonymous_client` needs no credentials; the authenticated constructors remain for the utility upload path only.
|
|
@@ -43,6 +56,7 @@ The California Energy Commission released **MIDAS v2.0 on 2026-06-22**, a breaki
|
|
|
43
56
|
|
|
44
57
|
Initial implementation. Two-layer raw/coerced data model: raw `httpx.Response` accessors plus coerced Pydantic entities (`RateInfo`, `ValueData`, `RinListEntry`, `Holiday`, `LookupEntry`) with `Decimal` prices and pendulum datetimes, each carrying its original wire dict on `_raw`. httpx-based `MIDASClient` with HTTP Basic → bearer-token auth and transparent auto-refresh (`AutoTokenAuth`); RIN list, rate values, lookup tables, holidays, and historical endpoints; signal-type helpers (`ghg`, `flex_alert`, `flex_alert_active`); domain enums (`SignalType`, `RateType`, `Unit`, `DayType`).
|
|
45
58
|
|
|
59
|
+
[1.0.1]: https://github.com/grid-coordination/python-midas/releases/tag/v1.0.1
|
|
46
60
|
[1.0.0]: https://github.com/grid-coordination/python-midas/releases/tag/v1.0.0
|
|
47
61
|
[0.1.1]: https://github.com/grid-coordination/python-midas/releases/tag/v0.1.1
|
|
48
62
|
[0.1.0]: https://github.com/grid-coordination/python-midas/releases/tag/v0.1.0
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: python-midas
|
|
3
|
-
Version: 1.0.
|
|
3
|
+
Version: 1.0.1
|
|
4
4
|
Summary: Python client library for the California Energy Commission MIDAS API
|
|
5
5
|
Project-URL: Homepage, https://grid-coordination.energy
|
|
6
6
|
Project-URL: Repository, https://github.com/grid-coordination/python-midas
|
|
@@ -23,6 +23,7 @@ Requires-Dist: httpx>=0.27
|
|
|
23
23
|
Requires-Dist: pendulum>=3.0
|
|
24
24
|
Requires-Dist: pydantic>=2.5
|
|
25
25
|
Provides-Extra: dev
|
|
26
|
+
Requires-Dist: jsonschema>=4.18; extra == 'dev'
|
|
26
27
|
Requires-Dist: pytest-httpx>=0.30; extra == 'dev'
|
|
27
28
|
Requires-Dist: pytest>=8.0; extra == 'dev'
|
|
28
29
|
Requires-Dist: ruff>=0.3; extra == 'dev'
|
|
@@ -4,7 +4,7 @@ build-backend = "hatchling.build"
|
|
|
4
4
|
|
|
5
5
|
[project]
|
|
6
6
|
name = "python-midas"
|
|
7
|
-
version = "1.0.
|
|
7
|
+
version = "1.0.1"
|
|
8
8
|
description = "Python client library for the California Energy Commission MIDAS API"
|
|
9
9
|
readme = "README.md"
|
|
10
10
|
license = "MIT"
|
|
@@ -35,6 +35,9 @@ dev = [
|
|
|
35
35
|
"pytest>=8.0",
|
|
36
36
|
"pytest-httpx>=0.30",
|
|
37
37
|
"ruff>=0.3",
|
|
38
|
+
# Strict wire-contract validation in the integration suite (validates raw
|
|
39
|
+
# responses against the midas-api-specs JSON Schemas).
|
|
40
|
+
"jsonschema>=4.18",
|
|
38
41
|
]
|
|
39
42
|
|
|
40
43
|
[project.urls]
|
|
@@ -38,13 +38,10 @@ def _parse_decimal(n: int | float | None) -> Decimal | None:
|
|
|
38
38
|
return Decimal(str(n))
|
|
39
39
|
|
|
40
40
|
|
|
41
|
-
def _parse_day_type(
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
return DayType(s)
|
|
46
|
-
except ValueError:
|
|
47
|
-
return None
|
|
41
|
+
def _parse_day_type(v: object) -> DayType | None:
|
|
42
|
+
# v2.0 MOER/ALRT send an integer code (1=Mon..8=Holiday); v1.0/electricity
|
|
43
|
+
# send a weekday string. DayType.from_wire accepts both.
|
|
44
|
+
return DayType.from_wire(v)
|
|
48
45
|
|
|
49
46
|
|
|
50
47
|
def _parse_unit(s: str | None) -> Unit | str | None:
|
|
@@ -114,6 +111,8 @@ class RateInfo(MIDASBase):
|
|
|
114
111
|
id: str | None = None
|
|
115
112
|
system_time: PendulumDateTime = None
|
|
116
113
|
name: str | None = None
|
|
114
|
+
signal_type: SignalType | None = None
|
|
115
|
+
description: str | None = None
|
|
117
116
|
type: RateType | str | None = None
|
|
118
117
|
sector: str | None = None
|
|
119
118
|
end_use: str | None = None
|
|
@@ -137,6 +136,10 @@ class RateInfo(MIDASBase):
|
|
|
137
136
|
id=raw.get("RateID"),
|
|
138
137
|
system_time=parse_instant(raw.get("SystemTime_UTC")),
|
|
139
138
|
name=raw.get("RateName"),
|
|
139
|
+
# v2.0 rate-values responses carry the per-RIN SignalType label and
|
|
140
|
+
# Description at the top level (as in the RIN list).
|
|
141
|
+
signal_type=_parse_signal_type(raw.get("SignalType")),
|
|
142
|
+
description=raw.get("Description"),
|
|
140
143
|
type=_parse_rate_type(raw.get("RateType")),
|
|
141
144
|
sector=raw.get("Sector"),
|
|
142
145
|
end_use=raw.get("EndUse"),
|
|
@@ -71,3 +71,41 @@ class DayType(str, Enum):
|
|
|
71
71
|
SATURDAY = "Saturday"
|
|
72
72
|
SUNDAY = "Sunday"
|
|
73
73
|
HOLIDAY = "Holiday"
|
|
74
|
+
|
|
75
|
+
@classmethod
|
|
76
|
+
def from_wire(cls, v: object) -> "DayType | None":
|
|
77
|
+
"""Coerce a wire day-type value to a DayType, or None if unmappable.
|
|
78
|
+
|
|
79
|
+
v2.0 SGIP GHG (MOER) and Flex Alert (ALRT) responses encode the day
|
|
80
|
+
type as an INTEGER upload code (1=Monday ... 7=Sunday, 8=Holiday); v1.0
|
|
81
|
+
and electricity rates use the weekday string ("Monday"). Both forms are
|
|
82
|
+
accepted (a digit string such as "1" is treated as the integer code).
|
|
83
|
+
"""
|
|
84
|
+
if isinstance(v, bool) or v is None:
|
|
85
|
+
return None
|
|
86
|
+
if isinstance(v, int):
|
|
87
|
+
return _DAY_TYPE_BY_CODE.get(v)
|
|
88
|
+
if isinstance(v, str):
|
|
89
|
+
s = v.strip()
|
|
90
|
+
if not s:
|
|
91
|
+
return None
|
|
92
|
+
if s.isdigit():
|
|
93
|
+
return _DAY_TYPE_BY_CODE.get(int(s))
|
|
94
|
+
try:
|
|
95
|
+
return cls(s)
|
|
96
|
+
except ValueError:
|
|
97
|
+
return None
|
|
98
|
+
return None
|
|
99
|
+
|
|
100
|
+
|
|
101
|
+
#: v2.0 integer day-type upload codes (1=Monday ... 7=Sunday, 8=Holiday).
|
|
102
|
+
_DAY_TYPE_BY_CODE: dict[int, DayType] = {
|
|
103
|
+
1: DayType.MONDAY,
|
|
104
|
+
2: DayType.TUESDAY,
|
|
105
|
+
3: DayType.WEDNESDAY,
|
|
106
|
+
4: DayType.THURSDAY,
|
|
107
|
+
5: DayType.FRIDAY,
|
|
108
|
+
6: DayType.SATURDAY,
|
|
109
|
+
7: DayType.SUNDAY,
|
|
110
|
+
8: DayType.HOLIDAY,
|
|
111
|
+
}
|
|
@@ -12,6 +12,7 @@ from midas.entities import (
|
|
|
12
12
|
coerce_rin_list,
|
|
13
13
|
)
|
|
14
14
|
from midas.entities.models import (
|
|
15
|
+
_parse_day_type,
|
|
15
16
|
_parse_rate_type,
|
|
16
17
|
_parse_signal_type,
|
|
17
18
|
_parse_unit,
|
|
@@ -149,6 +150,23 @@ def test_rate_type_moer_parses():
|
|
|
149
150
|
assert _parse_rate_type("Some Future Type") == "Some Future Type"
|
|
150
151
|
|
|
151
152
|
|
|
153
|
+
def test_day_type_integer_codes_parse():
|
|
154
|
+
# v2.0 MOER/ALRT send integer day-type codes (1=Mon..7=Sun, 8=Holiday).
|
|
155
|
+
assert _parse_day_type(1) == DayType.MONDAY
|
|
156
|
+
assert _parse_day_type(7) == DayType.SUNDAY
|
|
157
|
+
assert _parse_day_type(8) == DayType.HOLIDAY
|
|
158
|
+
# Digit strings are treated as the integer code.
|
|
159
|
+
assert _parse_day_type("3") == DayType.WEDNESDAY
|
|
160
|
+
# v1.0 / electricity weekday strings still parse.
|
|
161
|
+
assert _parse_day_type("Monday") == DayType.MONDAY
|
|
162
|
+
# Out-of-range, empty, None, and junk coerce to None (not an error).
|
|
163
|
+
assert _parse_day_type(0) is None
|
|
164
|
+
assert _parse_day_type(9) is None
|
|
165
|
+
assert _parse_day_type(None) is None
|
|
166
|
+
assert _parse_day_type("") is None
|
|
167
|
+
assert _parse_day_type("Funday") is None
|
|
168
|
+
|
|
169
|
+
|
|
152
170
|
# -- RIN List tests --
|
|
153
171
|
|
|
154
172
|
|
|
@@ -239,6 +257,47 @@ def test_rate_info_raw_preserved():
|
|
|
239
257
|
assert rate.values[0]._raw == RAW_RATE_INFO["ValueInformation"][0]
|
|
240
258
|
|
|
241
259
|
|
|
260
|
+
# v2.0 MOER/ALRT rate-values shape: top-level SignalType/Description, and
|
|
261
|
+
# integer day-type codes in ValueInformation.
|
|
262
|
+
RAW_MOER_RATE = {
|
|
263
|
+
"RateID": "USCA-SGIP-MOER-PGE",
|
|
264
|
+
"SystemTime_UTC": "2026-06-23T03:34:50.858999Z",
|
|
265
|
+
"RateName": "SGIP_CAISO_PGE Realtime GHG Emissions",
|
|
266
|
+
"RateType": "Greenhouse Gas emissions",
|
|
267
|
+
"SignalType": "Greenhouse Gas Emissions",
|
|
268
|
+
"Description": "SGIP_CAISO_PGE Realtime GHG Emissions - Greenhouse Gas emissions",
|
|
269
|
+
"ValueInformation": [
|
|
270
|
+
{
|
|
271
|
+
"ValueName": "Realtime SGIP GHG Emission",
|
|
272
|
+
"DateStart": "2026-06-22",
|
|
273
|
+
"DateEnd": "2026-06-22",
|
|
274
|
+
"DayStart": 1,
|
|
275
|
+
"DayEnd": 1,
|
|
276
|
+
"TimeStart": "07:00:00",
|
|
277
|
+
"TimeEnd": "07:04:59",
|
|
278
|
+
"Unit": "g/kWh CO2",
|
|
279
|
+
"Value": 643.19,
|
|
280
|
+
}
|
|
281
|
+
],
|
|
282
|
+
}
|
|
283
|
+
|
|
284
|
+
|
|
285
|
+
def test_rate_info_surfaces_signal_type_and_description():
|
|
286
|
+
# v2.0 carries the per-RIN SignalType label and Description at the top level.
|
|
287
|
+
rate = coerce_rate_info(RAW_MOER_RATE)
|
|
288
|
+
assert rate.signal_type == SignalType.GHG_EMISSIONS
|
|
289
|
+
assert rate.description == RAW_MOER_RATE["Description"]
|
|
290
|
+
|
|
291
|
+
|
|
292
|
+
def test_rate_info_integer_day_types_not_dropped():
|
|
293
|
+
# Integer DayStart/DayEnd must coerce, not silently become None
|
|
294
|
+
# (regression: python-midas-ib9).
|
|
295
|
+
rate = coerce_rate_info(RAW_MOER_RATE)
|
|
296
|
+
v = rate.values[0]
|
|
297
|
+
assert v.day_start == DayType.MONDAY
|
|
298
|
+
assert v.day_end == DayType.MONDAY
|
|
299
|
+
|
|
300
|
+
|
|
242
301
|
# -- Flex Alert tests --
|
|
243
302
|
|
|
244
303
|
|
|
@@ -16,14 +16,26 @@ paths; thin/absent data on a given RIN is expected this week, not a regression.
|
|
|
16
16
|
|
|
17
17
|
from __future__ import annotations
|
|
18
18
|
|
|
19
|
+
import json
|
|
20
|
+
import os
|
|
19
21
|
from decimal import Decimal
|
|
22
|
+
from pathlib import Path
|
|
20
23
|
|
|
21
24
|
import httpx
|
|
22
25
|
import pendulum
|
|
23
26
|
import pytest
|
|
24
27
|
|
|
28
|
+
try:
|
|
29
|
+
import jsonschema
|
|
30
|
+
from referencing import Registry, Resource
|
|
31
|
+
|
|
32
|
+
_HAVE_JSONSCHEMA = True
|
|
33
|
+
except ImportError: # pragma: no cover - dev extra not installed
|
|
34
|
+
_HAVE_JSONSCHEMA = False
|
|
35
|
+
|
|
25
36
|
from midas import (
|
|
26
37
|
MIDASClient,
|
|
38
|
+
DayType,
|
|
27
39
|
RateInfo,
|
|
28
40
|
RinListEntry,
|
|
29
41
|
LookupEntry,
|
|
@@ -138,6 +150,10 @@ class TestRateValues:
|
|
|
138
150
|
rin = _first_ghg_rin(client)
|
|
139
151
|
rate = client.rate_values(rin, query_type="realtime")
|
|
140
152
|
assert len(rate.values) > 0
|
|
153
|
+
# v2.0 surfaces the per-RIN SignalType label and Description at the top
|
|
154
|
+
# level of a rate-values response.
|
|
155
|
+
assert rate.signal_type == SignalType.GHG_EMISSIONS
|
|
156
|
+
assert rate.description is not None
|
|
141
157
|
|
|
142
158
|
v = rate.values[0]
|
|
143
159
|
assert isinstance(v, ValueData)
|
|
@@ -152,6 +168,10 @@ class TestRateValues:
|
|
|
152
168
|
assert start <= end
|
|
153
169
|
assert isinstance(v.value, Decimal)
|
|
154
170
|
assert v.unit is not None
|
|
171
|
+
# MOER sends integer day-type codes (1=Mon..8=Holiday); they must coerce
|
|
172
|
+
# to DayType, not silently drop to None (regression: python-midas-ib9).
|
|
173
|
+
assert v.day_start in DayType.__members__.values()
|
|
174
|
+
assert v.day_end in DayType.__members__.values()
|
|
155
175
|
|
|
156
176
|
def test_realtime_query(self, client: MIDASClient):
|
|
157
177
|
rate = client.rate_values(TOU_TEST_RIN, query_type="realtime")
|
|
@@ -170,6 +190,8 @@ class TestFlexAlert:
|
|
|
170
190
|
assert client.flex_alert(rate) is True
|
|
171
191
|
assert len(rate.values) > 0
|
|
172
192
|
assert rate.values[0].unit == Unit.EVENT
|
|
193
|
+
# ALRT also sends integer day-types; confirm they coerce.
|
|
194
|
+
assert rate.values[0].day_start in DayType.__members__.values()
|
|
173
195
|
|
|
174
196
|
def test_flex_alert_active_type(self, client: MIDASClient):
|
|
175
197
|
rate = client.rate_values(FLEX_RIN)
|
|
@@ -251,7 +273,85 @@ class TestGHG:
|
|
|
251
273
|
class TestRetiredRins:
|
|
252
274
|
def test_legacy_flex_rin_errors(self, client: MIDASClient):
|
|
253
275
|
# Legacy SGIP/Flex RINs are retired in v2.0. The live API returns
|
|
254
|
-
# HTTP 404 ("RIN not found")
|
|
255
|
-
#
|
|
276
|
+
# HTTP 404 ("RIN not found"), not 410 Gone as the migration notes
|
|
277
|
+
# state (filed as a midas-api-specs discrepancy).
|
|
256
278
|
resp = client.get_rate_values(LEGACY_FLEX_RIN)
|
|
257
279
|
assert resp.status_code == 404
|
|
280
|
+
|
|
281
|
+
|
|
282
|
+
# -- Strict wire-contract validation --
|
|
283
|
+
#
|
|
284
|
+
# The endpoint tests above assert on the lenient coerced model, which by design
|
|
285
|
+
# tolerates drift (extra='ignore', lenient day-type/rate-type passthrough). That
|
|
286
|
+
# leniency is correct for production but masks wire changes: it is how the
|
|
287
|
+
# integer-day-type bug (python-midas-ib9) shipped undetected. These tests
|
|
288
|
+
# validate the RAW response JSON against the authoritative midas-api-specs JSON
|
|
289
|
+
# Schemas, so any divergence from the documented wire contract (an unmodeled
|
|
290
|
+
# field, a wrong day-type encoding) fails the suite.
|
|
291
|
+
|
|
292
|
+
|
|
293
|
+
def _specs_schema_dir() -> Path | None:
|
|
294
|
+
"""Locate the midas-api-specs value-data schema directory.
|
|
295
|
+
|
|
296
|
+
Honours MIDAS_SPECS_DIR (the midas-api-specs checkout root); otherwise
|
|
297
|
+
falls back to a sibling checkout next to this repo. Returns None if not
|
|
298
|
+
found, so the strict tests skip rather than fail off a missing sibling.
|
|
299
|
+
"""
|
|
300
|
+
candidates = []
|
|
301
|
+
env = os.environ.get("MIDAS_SPECS_DIR")
|
|
302
|
+
if env:
|
|
303
|
+
candidates.append(Path(env) / "apis" / "value-data" / "schemas")
|
|
304
|
+
repo_root = Path(__file__).resolve().parents[1]
|
|
305
|
+
candidates.append(
|
|
306
|
+
repo_root.parent / "midas-api-specs" / "apis" / "value-data" / "schemas"
|
|
307
|
+
)
|
|
308
|
+
return next((c for c in candidates if c.is_dir()), None)
|
|
309
|
+
|
|
310
|
+
|
|
311
|
+
def _validator(schema_filename: str):
|
|
312
|
+
"""Build a Draft 2020-12 validator for one schema, with a registry that
|
|
313
|
+
resolves the sibling ``*.schema.json`` ``$ref`` files by their ``$id``."""
|
|
314
|
+
schema_dir = _specs_schema_dir()
|
|
315
|
+
if schema_dir is None:
|
|
316
|
+
return None
|
|
317
|
+
resources = []
|
|
318
|
+
for path in schema_dir.glob("*.schema.json"):
|
|
319
|
+
contents = json.loads(path.read_text())
|
|
320
|
+
resources.append(
|
|
321
|
+
(contents.get("$id", path.name), Resource.from_contents(contents))
|
|
322
|
+
)
|
|
323
|
+
registry = Registry().with_resources(resources)
|
|
324
|
+
root = json.loads((schema_dir / schema_filename).read_text())
|
|
325
|
+
return jsonschema.Draft202012Validator(root, registry=registry)
|
|
326
|
+
|
|
327
|
+
|
|
328
|
+
@pytest.mark.skipif(not _HAVE_JSONSCHEMA, reason="jsonschema dev extra not installed")
|
|
329
|
+
class TestWireContract:
|
|
330
|
+
def _assert_valid(self, schema_filename: str, payload) -> None:
|
|
331
|
+
validator = _validator(schema_filename)
|
|
332
|
+
if validator is None:
|
|
333
|
+
pytest.skip(
|
|
334
|
+
"midas-api-specs schemas not found; set MIDAS_SPECS_DIR to the "
|
|
335
|
+
"midas-api-specs checkout to enable strict wire-contract checks"
|
|
336
|
+
)
|
|
337
|
+
errors = sorted(validator.iter_errors(payload), key=lambda e: list(e.path))
|
|
338
|
+
assert not errors, "wire diverges from midas-api-specs:\n" + "\n".join(
|
|
339
|
+
f" {list(e.absolute_path)}: {e.message}" for e in errors
|
|
340
|
+
)
|
|
341
|
+
|
|
342
|
+
def test_moer_rate_values_matches_schema(self, client: MIDASClient):
|
|
343
|
+
rin = _first_ghg_rin(client)
|
|
344
|
+
raw = client.get_rate_values(rin, query_type="realtime").json()
|
|
345
|
+
self._assert_valid("midas-rate-info.schema.json", raw)
|
|
346
|
+
|
|
347
|
+
def test_flex_rate_values_matches_schema(self, client: MIDASClient):
|
|
348
|
+
raw = client.get_rate_values(FLEX_RIN).json()
|
|
349
|
+
self._assert_valid("midas-rate-info.schema.json", raw)
|
|
350
|
+
|
|
351
|
+
def test_rin_list_matches_schema(self, client: MIDASClient):
|
|
352
|
+
raw = client.get_rin_list(signal_type=2).json()
|
|
353
|
+
self._assert_valid("midas-rin-list-response.schema.json", raw)
|
|
354
|
+
|
|
355
|
+
def test_unit_lookup_matches_schema(self, client: MIDASClient):
|
|
356
|
+
raw = client.get_lookup_table("Unit").json()
|
|
357
|
+
self._assert_valid("midas-lookup-table-response.schema.json", raw)
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|