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.
@@ -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,8 @@
1
+ __pycache__/
2
+ *.pyc
3
+ *.egg-info/
4
+ dist/
5
+ build/
6
+ .pytest_cache/
7
+ .ruff_cache/
8
+ .DS_Store
@@ -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