python-midas 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.
- python_midas-0.1.0/.github/workflows/publish.yml +36 -0
- python_midas-0.1.0/.gitignore +8 -0
- python_midas-0.1.0/LICENSE +21 -0
- python_midas-0.1.0/PKG-INFO +404 -0
- python_midas-0.1.0/README.md +374 -0
- python_midas-0.1.0/pyproject.toml +55 -0
- python_midas-0.1.0/src/midas/__init__.py +61 -0
- python_midas-0.1.0/src/midas/auth.py +109 -0
- python_midas-0.1.0/src/midas/client.py +205 -0
- python_midas-0.1.0/src/midas/entities/__init__.py +59 -0
- python_midas-0.1.0/src/midas/entities/models.py +231 -0
- python_midas-0.1.0/src/midas/enums.py +41 -0
- python_midas-0.1.0/src/midas/py.typed +0 -0
- python_midas-0.1.0/tests/test_auth.py +52 -0
- python_midas-0.1.0/tests/test_client.py +302 -0
- python_midas-0.1.0/tests/test_entities.py +262 -0
- python_midas-0.1.0/tests/test_integration.py +250 -0
|
@@ -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 -m "not integration"
|
|
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,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,404 @@
|
|
|
1
|
+
Metadata-Version: 2.4
|
|
2
|
+
Name: python-midas
|
|
3
|
+
Version: 0.1.0
|
|
4
|
+
Summary: Python client library for the California Energy Commission MIDAS API
|
|
5
|
+
Project-URL: Homepage, https://grid-coordination.energy
|
|
6
|
+
Project-URL: Repository, https://github.com/grid-coordination/python-midas
|
|
7
|
+
Project-URL: Issues, https://github.com/grid-coordination/python-midas/issues
|
|
8
|
+
Author: Clark Communications Corporation
|
|
9
|
+
License-Expression: MIT
|
|
10
|
+
License-File: LICENSE
|
|
11
|
+
Keywords: california,energy,ghg,grid,midas,rates
|
|
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: pendulum>=3.0
|
|
24
|
+
Requires-Dist: pydantic>=2.5
|
|
25
|
+
Provides-Extra: dev
|
|
26
|
+
Requires-Dist: pytest-httpx>=0.30; extra == 'dev'
|
|
27
|
+
Requires-Dist: pytest>=8.0; extra == 'dev'
|
|
28
|
+
Requires-Dist: ruff>=0.3; extra == 'dev'
|
|
29
|
+
Description-Content-Type: text/markdown
|
|
30
|
+
|
|
31
|
+
# python-midas
|
|
32
|
+
|
|
33
|
+
Python client library for the California Energy Commission [MIDAS](https://midasapi.energy.ca.gov/) (Market Informed Demand Automation Server) API.
|
|
34
|
+
|
|
35
|
+
MIDAS provides California energy rate data, greenhouse gas (GHG) emissions signals, and Flex Alert status. This library wraps the API with typed Pydantic models, automatic token management, and a two-layer data model that preserves raw API responses alongside coerced Python-native types.
|
|
36
|
+
|
|
37
|
+
Part of the [grid-coordination](https://github.com/grid-coordination) project family, alongside [clj-midas](https://github.com/grid-coordination/clj-midas) (Clojure client) and [midas-api-specs](https://github.com/grid-coordination/midas-api-specs) (OpenAPI specifications).
|
|
38
|
+
|
|
39
|
+
## Installation
|
|
40
|
+
|
|
41
|
+
```bash
|
|
42
|
+
pip install midas
|
|
43
|
+
```
|
|
44
|
+
|
|
45
|
+
For development:
|
|
46
|
+
|
|
47
|
+
```bash
|
|
48
|
+
pip install -e ".[dev]"
|
|
49
|
+
```
|
|
50
|
+
|
|
51
|
+
Requires Python 3.10+.
|
|
52
|
+
|
|
53
|
+
## Quick Start
|
|
54
|
+
|
|
55
|
+
```python
|
|
56
|
+
from midas import create_auto_client
|
|
57
|
+
|
|
58
|
+
client = create_auto_client("username", "password")
|
|
59
|
+
|
|
60
|
+
# List available Rate Identification Numbers (RINs)
|
|
61
|
+
rins = client.rin_list()
|
|
62
|
+
for rin in rins:
|
|
63
|
+
print(f"{rin.id} {rin.signal_type} {rin.description}")
|
|
64
|
+
|
|
65
|
+
# Get rate values for a specific RIN
|
|
66
|
+
rate = client.rate_values(rins[0].id)
|
|
67
|
+
print(f"{rate.name} ({rate.type})")
|
|
68
|
+
for v in rate.values:
|
|
69
|
+
print(f" {v.date_start} {v.time_start}-{v.time_end}: {v.value} {v.unit}")
|
|
70
|
+
```
|
|
71
|
+
|
|
72
|
+
## Authentication
|
|
73
|
+
|
|
74
|
+
MIDAS uses HTTP Basic authentication to acquire a short-lived bearer token (valid for 10 minutes). The library provides two client creation modes:
|
|
75
|
+
|
|
76
|
+
### Auto-refreshing client (recommended)
|
|
77
|
+
|
|
78
|
+
`create_auto_client` acquires a token on creation and transparently refreshes it before any request where the token is expired or about to expire (within a 30-second buffer):
|
|
79
|
+
|
|
80
|
+
```python
|
|
81
|
+
from midas import create_auto_client
|
|
82
|
+
|
|
83
|
+
client = create_auto_client("username", "password")
|
|
84
|
+
# Token refreshes automatically — use the client for as long as you need
|
|
85
|
+
```
|
|
86
|
+
|
|
87
|
+
### Manual token client
|
|
88
|
+
|
|
89
|
+
`create_client` acquires a single token. You are responsible for creating a new client when it expires:
|
|
90
|
+
|
|
91
|
+
```python
|
|
92
|
+
from midas import create_client
|
|
93
|
+
|
|
94
|
+
client = create_client("username", "password")
|
|
95
|
+
# Token is valid for ~10 minutes
|
|
96
|
+
```
|
|
97
|
+
|
|
98
|
+
### Low-level token management
|
|
99
|
+
|
|
100
|
+
For advanced use cases, you can manage tokens directly:
|
|
101
|
+
|
|
102
|
+
```python
|
|
103
|
+
from midas import get_token, token_expired, MIDASClient
|
|
104
|
+
|
|
105
|
+
token_info = get_token("username", "password")
|
|
106
|
+
# token_info = {"token": "...", "acquired_at": DateTime, "expires_at": DateTime}
|
|
107
|
+
|
|
108
|
+
if token_expired(token_info):
|
|
109
|
+
token_info = get_token("username", "password")
|
|
110
|
+
|
|
111
|
+
client = MIDASClient(token=token_info["token"])
|
|
112
|
+
```
|
|
113
|
+
|
|
114
|
+
## API Coverage
|
|
115
|
+
|
|
116
|
+
The MIDAS API has a single multiplexed `/ValueData` endpoint that serves different response shapes depending on query parameters, plus separate endpoints for holidays and historical data. All six operations are covered:
|
|
117
|
+
|
|
118
|
+
### RIN List
|
|
119
|
+
|
|
120
|
+
List available Rate Identification Numbers, optionally filtered by signal type:
|
|
121
|
+
|
|
122
|
+
```python
|
|
123
|
+
all_rins = client.rin_list() # All signal types
|
|
124
|
+
rate_rins = client.rin_list(signal_type=1) # Rates only
|
|
125
|
+
ghg_rins = client.rin_list(signal_type=2) # GHG only
|
|
126
|
+
flex_rins = client.rin_list(signal_type=3) # Flex Alert only
|
|
127
|
+
```
|
|
128
|
+
|
|
129
|
+
Each `RinListEntry` has:
|
|
130
|
+
- `id` — the RIN string (e.g. `"USCA-PGPG-ETOU-0000"`)
|
|
131
|
+
- `signal_type` — `SignalType.RATES`, `SignalType.GHG`, or `SignalType.FLEX_ALERT`
|
|
132
|
+
- `description` — human-readable description
|
|
133
|
+
- `last_updated` — `pendulum.DateTime` of last data update
|
|
134
|
+
|
|
135
|
+
### Rate Values
|
|
136
|
+
|
|
137
|
+
Fetch current rate/price data for a specific RIN:
|
|
138
|
+
|
|
139
|
+
```python
|
|
140
|
+
rate = client.rate_values("USCA-TSTS-TTOU-TEST")
|
|
141
|
+
rate = client.rate_values("USCA-TSTS-TTOU-TEST", query_type="realtime")
|
|
142
|
+
```
|
|
143
|
+
|
|
144
|
+
The `RateInfo` model contains:
|
|
145
|
+
- `id` — the RIN
|
|
146
|
+
- `name` — rate name (e.g. `"CEC TEST24HTOU"`)
|
|
147
|
+
- `type` — `RateType` enum (`TOU`, `CPP`, `RTP`, `GHG`, `FLEX_ALERT`) or raw string
|
|
148
|
+
- `system_time` — server timestamp as `pendulum.DateTime`
|
|
149
|
+
- `sector`, `end_use` — customer classification
|
|
150
|
+
- `rate_plan_url`, `api_url` — external links (the API's `"None"` string is coerced to `None`)
|
|
151
|
+
- `signup_close` — rate signup deadline as `pendulum.DateTime`
|
|
152
|
+
- `values` — list of `ValueData` intervals
|
|
153
|
+
|
|
154
|
+
Each `ValueData` interval has:
|
|
155
|
+
- `name` — period description (e.g. `"winter off peak"`)
|
|
156
|
+
- `date_start`, `date_end` — `datetime.date`
|
|
157
|
+
- `day_start`, `day_end` — `DayType` enum (Monday through Sunday, plus Holiday)
|
|
158
|
+
- `time_start`, `time_end` — `datetime.time` (handles both `HH:MM:SS` and `HH:MM` formats)
|
|
159
|
+
- `value` — `Decimal` (preserves precision for financial data)
|
|
160
|
+
- `unit` — `Unit` enum (`$/kWh`, `$/kW`, `kg/kWh CO2`, `Event`, etc.)
|
|
161
|
+
|
|
162
|
+
### Lookup Tables
|
|
163
|
+
|
|
164
|
+
Fetch reference data tables:
|
|
165
|
+
|
|
166
|
+
```python
|
|
167
|
+
energies = client.lookup_table("Energy") # Energy providers
|
|
168
|
+
dists = client.lookup_table("Distribution") # Distribution companies
|
|
169
|
+
units = client.lookup_table("Unit") # Available units
|
|
170
|
+
sectors = client.lookup_table("Sector") # Customer sectors
|
|
171
|
+
```
|
|
172
|
+
|
|
173
|
+
Available tables: `Country`, `Daytype`, `Distribution`, `Enduse`, `Energy`, `Location`, `Ratetype`, `Sector`, `State`, `Unit`.
|
|
174
|
+
|
|
175
|
+
Each `LookupEntry` has `code` and `description`.
|
|
176
|
+
|
|
177
|
+
### Holidays
|
|
178
|
+
|
|
179
|
+
Fetch utility-observed holidays:
|
|
180
|
+
|
|
181
|
+
```python
|
|
182
|
+
holidays = client.holidays()
|
|
183
|
+
for h in holidays:
|
|
184
|
+
print(f"{h.energy_name}: {h.date} — {h.description}")
|
|
185
|
+
```
|
|
186
|
+
|
|
187
|
+
Each `Holiday` has `energy_code`, `energy_name`, `date` (`datetime.date`), and `description`.
|
|
188
|
+
|
|
189
|
+
### Historical Data
|
|
190
|
+
|
|
191
|
+
Query archived rate data by provider and date range:
|
|
192
|
+
|
|
193
|
+
```python
|
|
194
|
+
# List RINs with historical data for a provider pair
|
|
195
|
+
hist_rins = client.historical_list("PG", "PG") # PG&E distribution + energy
|
|
196
|
+
|
|
197
|
+
# Fetch archived data for a date range
|
|
198
|
+
hist = client.historical_data("USCA-PGPG-ETOU-0000", "2023-01-01", "2023-12-31")
|
|
199
|
+
```
|
|
200
|
+
|
|
201
|
+
The historical list is automatically deduplicated (the live API returns duplicate entries).
|
|
202
|
+
|
|
203
|
+
## Signal Type Helpers
|
|
204
|
+
|
|
205
|
+
Convenience methods for identifying signal types, matching the [clj-midas](https://github.com/grid-coordination/clj-midas) API:
|
|
206
|
+
|
|
207
|
+
```python
|
|
208
|
+
rate = client.rate_values("USCA-GHGH-SGHT-0000")
|
|
209
|
+
|
|
210
|
+
client.ghg(rate) # True if GHG signal (by RateType or Unit)
|
|
211
|
+
client.flex_alert(rate) # True if Flex Alert signal
|
|
212
|
+
client.flex_alert_active(rate) # True if Flex Alert with any non-zero value
|
|
213
|
+
```
|
|
214
|
+
|
|
215
|
+
## Two-Layer Data Model
|
|
216
|
+
|
|
217
|
+
Following the [python-oa3](https://github.com/grid-coordination/python-oa3) pattern, every entity provides two layers:
|
|
218
|
+
|
|
219
|
+
**Raw layer** — the original API JSON dict (PascalCase keys, string values), accessible via `_raw`:
|
|
220
|
+
|
|
221
|
+
```python
|
|
222
|
+
rate = client.rate_values("USCA-TSTS-TTOU-TEST")
|
|
223
|
+
rate._raw["RateID"] # "USCA-TSTS-TTOU-TEST"
|
|
224
|
+
rate._raw["ValueInformation"][0]["value"] # 0.1006
|
|
225
|
+
rate.values[0]._raw["Unit"] # "$/kWh"
|
|
226
|
+
```
|
|
227
|
+
|
|
228
|
+
**Coerced layer** — typed Pydantic models with snake_case fields and native Python types:
|
|
229
|
+
|
|
230
|
+
```python
|
|
231
|
+
rate.id # "USCA-TSTS-TTOU-TEST"
|
|
232
|
+
rate.type # RateType.TOU
|
|
233
|
+
rate.system_time # pendulum.DateTime (UTC)
|
|
234
|
+
rate.values[0].value # Decimal("0.1006")
|
|
235
|
+
rate.values[0].unit # Unit.DOLLAR_PER_KWH
|
|
236
|
+
rate.values[0].day_start # DayType.MONDAY
|
|
237
|
+
rate.values[0].date_start # datetime.date(2023, 5, 1)
|
|
238
|
+
rate.values[0].time_start # datetime.time(7, 0, 0)
|
|
239
|
+
```
|
|
240
|
+
|
|
241
|
+
This lets you work with clean, typed data while always being able to fall back to the exact API response when needed.
|
|
242
|
+
|
|
243
|
+
## Dual-Mode Client
|
|
244
|
+
|
|
245
|
+
Every endpoint is available in two forms:
|
|
246
|
+
|
|
247
|
+
**Raw methods** return `httpx.Response` for full HTTP control:
|
|
248
|
+
|
|
249
|
+
```python
|
|
250
|
+
resp = client.get_rin_list(signal_type=0)
|
|
251
|
+
resp.status_code # 200
|
|
252
|
+
resp.json() # raw JSON list
|
|
253
|
+
|
|
254
|
+
resp = client.get_rate_values("USCA-TSTS-TTOU-TEST", query_type="alldata")
|
|
255
|
+
resp = client.get_lookup_table("Energy")
|
|
256
|
+
resp = client.get_holidays()
|
|
257
|
+
resp = client.get_historical_list("PG", "PG")
|
|
258
|
+
resp = client.get_historical_data("USCA-PGPG-ETOU-0000", "2023-01-01", "2023-12-31")
|
|
259
|
+
```
|
|
260
|
+
|
|
261
|
+
**Coerced methods** return typed Pydantic models (call `raise_for_status()` internally):
|
|
262
|
+
|
|
263
|
+
```python
|
|
264
|
+
rins = client.rin_list(signal_type=0) # list[RinListEntry]
|
|
265
|
+
rate = client.rate_values("USCA-TSTS-TTOU-TEST") # RateInfo
|
|
266
|
+
entries = client.lookup_table("Energy") # list[LookupEntry]
|
|
267
|
+
holidays = client.holidays() # list[Holiday]
|
|
268
|
+
rins = client.historical_list("PG", "PG") # list[RinListEntry]
|
|
269
|
+
rate = client.historical_data(rin, start, end) # RateInfo
|
|
270
|
+
```
|
|
271
|
+
|
|
272
|
+
## Coercion Functions
|
|
273
|
+
|
|
274
|
+
You can also coerce raw dicts directly, without going through the client:
|
|
275
|
+
|
|
276
|
+
```python
|
|
277
|
+
from midas import coerce_rate_info, coerce_rin_list, coerce_holidays
|
|
278
|
+
|
|
279
|
+
rate = coerce_rate_info({"RateID": "...", "ValueInformation": [...]})
|
|
280
|
+
rins = coerce_rin_list([{"RateID": "...", "SignalType": "Rates", ...}])
|
|
281
|
+
```
|
|
282
|
+
|
|
283
|
+
Available: `coerce_rate_info`, `coerce_rin_list`, `coerce_holidays`, `coerce_lookup_table`, `coerce_historical_list`.
|
|
284
|
+
|
|
285
|
+
## Enums
|
|
286
|
+
|
|
287
|
+
Domain values are represented as `str` enums, so they compare equal to their string values:
|
|
288
|
+
|
|
289
|
+
```python
|
|
290
|
+
from midas import SignalType, RateType, Unit, DayType
|
|
291
|
+
|
|
292
|
+
SignalType.RATES # "Rates"
|
|
293
|
+
SignalType.GHG # "GHG"
|
|
294
|
+
SignalType.FLEX_ALERT # "Flex Alert"
|
|
295
|
+
|
|
296
|
+
RateType.TOU # "Time of use"
|
|
297
|
+
RateType.CPP # "Critical Peak Pricing"
|
|
298
|
+
RateType.RTP # "Real Time Pricing"
|
|
299
|
+
RateType.GHG # "Greenhouse Gas emissions"
|
|
300
|
+
RateType.FLEX_ALERT # "Flex Alert"
|
|
301
|
+
|
|
302
|
+
Unit.DOLLAR_PER_KWH # "$/kWh"
|
|
303
|
+
Unit.DOLLAR_PER_KW # "$/kW"
|
|
304
|
+
Unit.EXPORT_DOLLAR_PER_KWH # "export $/kWh"
|
|
305
|
+
Unit.BACKUP_DOLLAR_PER_KWH # "backup $/kWh"
|
|
306
|
+
Unit.KG_CO2_PER_KWH # "kg/kWh CO2"
|
|
307
|
+
Unit.DOLLAR_PER_KVARH # "$/kvarh"
|
|
308
|
+
Unit.EVENT # "Event"
|
|
309
|
+
Unit.LEVEL # "Level"
|
|
310
|
+
|
|
311
|
+
DayType.MONDAY # "Monday"
|
|
312
|
+
# ... through SUNDAY, plus:
|
|
313
|
+
DayType.HOLIDAY # "Holiday"
|
|
314
|
+
```
|
|
315
|
+
|
|
316
|
+
## Type Coercion Details
|
|
317
|
+
|
|
318
|
+
The coercion layer applies the following transformations:
|
|
319
|
+
|
|
320
|
+
| API type | Python type | Notes |
|
|
321
|
+
|----------|-------------|-------|
|
|
322
|
+
| Date strings (`"2023-05-01"`) | `datetime.date` | Extracts date from datetime strings too |
|
|
323
|
+
| Datetime strings | `pendulum.DateTime` | Naive datetimes treated as UTC |
|
|
324
|
+
| Time strings (`"07:00:00"`, `"03:11"`) | `datetime.time` | Handles both `HH:MM:SS` and `HH:MM` |
|
|
325
|
+
| Numeric values | `Decimal` | Preserves precision for financial data |
|
|
326
|
+
| Signal type strings | `SignalType` enum | `None` passes through as `None` |
|
|
327
|
+
| Rate type strings | `RateType` enum | Unknown values pass through as strings |
|
|
328
|
+
| Unit strings | `Unit` enum | Unknown values pass through as strings |
|
|
329
|
+
| Day type strings | `DayType` enum | `None` passes through (historical data) |
|
|
330
|
+
| `"None"` string (API_Url) | `None` | MIDAS API quirk |
|
|
331
|
+
|
|
332
|
+
## Context Manager
|
|
333
|
+
|
|
334
|
+
The client supports context manager protocol for clean resource management:
|
|
335
|
+
|
|
336
|
+
```python
|
|
337
|
+
from midas import create_auto_client
|
|
338
|
+
|
|
339
|
+
with create_auto_client("user", "pass") as client:
|
|
340
|
+
rins = client.rin_list()
|
|
341
|
+
rate = client.rate_values(rins[0].id)
|
|
342
|
+
# httpx client is closed automatically
|
|
343
|
+
```
|
|
344
|
+
|
|
345
|
+
## Project Structure
|
|
346
|
+
|
|
347
|
+
```
|
|
348
|
+
src/midas/
|
|
349
|
+
__init__.py # Public API re-exports
|
|
350
|
+
py.typed # PEP 561 type-checking marker
|
|
351
|
+
client.py # MIDASClient, create_client, create_auto_client
|
|
352
|
+
auth.py # BearerAuth, BasicAuth, AutoTokenAuth, get_token
|
|
353
|
+
enums.py # SignalType, RateType, Unit, DayType
|
|
354
|
+
entities/
|
|
355
|
+
__init__.py # Coercion dispatch functions
|
|
356
|
+
models.py # Pydantic models: RateInfo, ValueData, RinListEntry, Holiday, LookupEntry
|
|
357
|
+
tests/
|
|
358
|
+
test_entities.py # Entity coercion from raw fixture dicts
|
|
359
|
+
test_client.py # HTTP client tests with pytest-httpx
|
|
360
|
+
test_auth.py # Token parsing, expiry, auth headers
|
|
361
|
+
test_integration.py # Live API tests (requires MIDAS credentials)
|
|
362
|
+
```
|
|
363
|
+
|
|
364
|
+
## Development
|
|
365
|
+
|
|
366
|
+
```bash
|
|
367
|
+
# Install with dev dependencies
|
|
368
|
+
pip install -e ".[dev]"
|
|
369
|
+
|
|
370
|
+
# Lint
|
|
371
|
+
ruff check src/ tests/
|
|
372
|
+
```
|
|
373
|
+
|
|
374
|
+
### Tests
|
|
375
|
+
|
|
376
|
+
The test suite has two tiers:
|
|
377
|
+
|
|
378
|
+
**Unit tests** run entirely offline using fixture dicts and mocked HTTP (pytest-httpx):
|
|
379
|
+
|
|
380
|
+
```bash
|
|
381
|
+
pytest -m "not integration"
|
|
382
|
+
```
|
|
383
|
+
|
|
384
|
+
**Integration tests** run against the live MIDAS API at `midasapi.energy.ca.gov`. They require credentials in environment variables and are skipped automatically when the variables are not set:
|
|
385
|
+
|
|
386
|
+
```bash
|
|
387
|
+
export MIDAS_USERNAME="you@example.com"
|
|
388
|
+
export MIDAS_PASSWORD="your-password"
|
|
389
|
+
pytest -m integration
|
|
390
|
+
```
|
|
391
|
+
|
|
392
|
+
Integration tests exercise the full auth flow (token acquisition, expiry checks), every endpoint (RIN list, rate values, lookup tables, holidays, historical list/data), all entity coercion paths against real response shapes, and the signal type helpers (GHG, Flex Alert detection).
|
|
393
|
+
|
|
394
|
+
Note that the MIDAS API server can be slow (5-20+ seconds per request is normal), so the integration suite takes a few minutes to complete. Run everything together with just `pytest`.
|
|
395
|
+
|
|
396
|
+
## Related Projects
|
|
397
|
+
|
|
398
|
+
- **[midas-api-specs](https://github.com/grid-coordination/midas-api-specs)** — OpenAPI specifications for the MIDAS API, derived from documentation and live API validation
|
|
399
|
+
- **[clj-midas](https://github.com/grid-coordination/clj-midas)** — Clojure client for the MIDAS API (Martian-based, spec-driven)
|
|
400
|
+
- **[python-oa3](https://github.com/grid-coordination/python-oa3)** — Python client for OpenADR 3 (same entity API pattern)
|
|
401
|
+
|
|
402
|
+
## License
|
|
403
|
+
|
|
404
|
+
MIT
|