python-midas 0.1.0__tar.gz → 1.0.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-1.0.0/.github/workflows/ci.yml +32 -0
- python_midas-1.0.0/CHANGELOG.md +48 -0
- python_midas-1.0.0/CONTRIBUTING.md +85 -0
- python_midas-1.0.0/PKG-INFO +417 -0
- python_midas-1.0.0/README.md +387 -0
- python_midas-1.0.0/doc/v2-migration.md +56 -0
- {python_midas-0.1.0 → python_midas-1.0.0}/pyproject.toml +3 -3
- {python_midas-0.1.0 → python_midas-1.0.0}/src/midas/__init__.py +19 -6
- {python_midas-0.1.0 → python_midas-1.0.0}/src/midas/auth.py +1 -3
- {python_midas-0.1.0 → python_midas-1.0.0}/src/midas/client.py +48 -58
- python_midas-1.0.0/src/midas/entities/__init__.py +52 -0
- {python_midas-0.1.0 → python_midas-1.0.0}/src/midas/entities/models.py +91 -64
- python_midas-1.0.0/src/midas/enums.py +73 -0
- python_midas-1.0.0/src/midas/time.py +101 -0
- {python_midas-0.1.0 → python_midas-1.0.0}/tests/test_client.py +69 -85
- python_midas-1.0.0/tests/test_entities.py +300 -0
- python_midas-1.0.0/tests/test_integration.py +257 -0
- python_midas-0.1.0/PKG-INFO +0 -404
- python_midas-0.1.0/README.md +0 -374
- python_midas-0.1.0/src/midas/entities/__init__.py +0 -59
- python_midas-0.1.0/src/midas/enums.py +0 -41
- python_midas-0.1.0/tests/test_entities.py +0 -262
- python_midas-0.1.0/tests/test_integration.py +0 -250
- {python_midas-0.1.0 → python_midas-1.0.0}/.github/workflows/publish.yml +0 -0
- {python_midas-0.1.0 → python_midas-1.0.0}/.gitignore +0 -0
- {python_midas-0.1.0 → python_midas-1.0.0}/LICENSE +0 -0
- {python_midas-0.1.0 → python_midas-1.0.0}/src/midas/py.typed +0 -0
- {python_midas-0.1.0 → python_midas-1.0.0}/tests/test_auth.py +0 -0
|
@@ -0,0 +1,32 @@
|
|
|
1
|
+
name: CI
|
|
2
|
+
|
|
3
|
+
on:
|
|
4
|
+
push:
|
|
5
|
+
branches: [main]
|
|
6
|
+
pull_request:
|
|
7
|
+
|
|
8
|
+
jobs:
|
|
9
|
+
lint:
|
|
10
|
+
runs-on: ubuntu-latest
|
|
11
|
+
steps:
|
|
12
|
+
- uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4.2.2
|
|
13
|
+
- uses: actions/setup-python@a26af69be951a213d495a4c3e4e4022e16d87065 # v5.6.0
|
|
14
|
+
with:
|
|
15
|
+
python-version: "3.12"
|
|
16
|
+
- run: pip install ruff
|
|
17
|
+
- run: ruff check .
|
|
18
|
+
- run: ruff format --check .
|
|
19
|
+
|
|
20
|
+
test:
|
|
21
|
+
runs-on: ubuntu-latest
|
|
22
|
+
strategy:
|
|
23
|
+
matrix:
|
|
24
|
+
python-version: ["3.10", "3.11", "3.12", "3.13"]
|
|
25
|
+
steps:
|
|
26
|
+
- uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4.2.2
|
|
27
|
+
- uses: actions/setup-python@a26af69be951a213d495a4c3e4e4022e16d87065 # v5.6.0
|
|
28
|
+
with:
|
|
29
|
+
python-version: ${{ matrix.python-version }}
|
|
30
|
+
- run: pip install -e ".[dev]"
|
|
31
|
+
# Integration tests hit the live MIDAS API; CI runs unit tests only.
|
|
32
|
+
- run: pytest tests/ -v -m "not integration"
|
|
@@ -0,0 +1,48 @@
|
|
|
1
|
+
# Changelog
|
|
2
|
+
|
|
3
|
+
All notable changes to this project will be documented in this file.
|
|
4
|
+
|
|
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
|
+
|
|
7
|
+
## [1.0.0] — 2026-06-22
|
|
8
|
+
|
|
9
|
+
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.
|
|
10
|
+
|
|
11
|
+
### Changed
|
|
12
|
+
|
|
13
|
+
- **Breaking: the per-interval value field is read from `Value` (capital V)** instead of v1.0's lowercase `value`. The CEC standardised the casing in v2.0; `ValueData.from_raw` reads `Value`.
|
|
14
|
+
- **Breaking: `ValueData` interval boundaries are a zone-aware `period` tuple.** The four zone-naive fields `date_start`, `date_end`, `time_start`, and `time_end` (`datetime.date` / `datetime.time`) are **removed**; each interval is exposed as `period: tuple[pendulum.DateTime, pendulum.DateTime] | None` — a `(start, end)` pair of zone-aware moments. A bare wall-clock time with no zone is ambiguous, and in v2.0 the wire delivers `DateStart`/`TimeStart`/`DateEnd`/`TimeEnd` in UTC for *every* signal type (v1.0 delivered SGIP GHG and Flex Alert in Pacific Time — an upstream-provider passthrough bug the CEC fixed in v2.0). The pair is composed from the UTC wire date+time and **preserved in UTC** — the library never normalizes to a display zone. Need a wall-clock date or time? Convert an endpoint yourself: `period[0].in_tz("America/Los_Angeles").date()`. The original wire strings remain on `_raw`. (Mirrors `python-oa3`'s `IntervalPeriod.period`.)
|
|
15
|
+
- **Breaking: datetime coercion is zone-aware `pendulum.DateTime`, preserving the wire zone.** Every coerced timestamp carries an honest timezone — `Z`-suffixed fields (`SystemTime_UTC`, `SignupCloseDate`) stay UTC, as does `LastUpdated`, which in v2.0 carries an explicit basic-format offset (`+0000`); `parse_instant` honours it (no Pacific-local shift). The library does not normalize to a single zone; consumers convert with `.in_tz(...)` as needed. This replaces the prior `_parse_datetime` that silently treated every naive field as UTC.
|
|
16
|
+
- **Breaking: `rin_list` / `coerce_rin_list` peel the v2.0 keyed-object response.** v2.0 wraps the entry array in a single-keyed object — on the live API the key is **always `Rates`**, regardless of the requested `SignalType` (the `GHGEmissions`/`FlexAlerts`/`All` keys implied by early design notes do not appear on the wire). `coerce_rin_list(raw, signal_type)` peels the single value without switching on the key name and returns a uniform `list[RinListEntry]`; the new `RinListResponse` model validates the shape.
|
|
17
|
+
- **Breaking: lookup tables return a keyed object.** v2.0 wraps lookup rows in `{table_name, data: [...]}` instead of v1.0's bare array. `lookup_table` / `coerce_lookup_table` peel `data`; the new `LookupTableResponse` model validates the shape. `LookupEntry` gains optional `payload_descriptor` and `unit_type` (extra columns the `Unit` table carries).
|
|
18
|
+
- **Breaking: `SignalType` enum carries the v2.0 long-form wire labels.** `SignalType.RATES` is now `"Electricity Rates"` (was `"Rates"`); new members `GHG_EMISSIONS = "Greenhouse Gas Emissions"` and `FLEX_ALERT = "California Independent System Operator Flex Alert"`. v2.0 always populates the per-entry `SignalType` field (v1.0 returned `null` for GHG/Flex Alert entries); the old labels no longer map.
|
|
19
|
+
- **Breaking: `RateType` enum tracks the v2.0 wire values, which differ by signal type.** Electricity rates return the short `Ratetype` UploadCode, so the enum now uses short codes (`TOU`, `CPP`, `RTP`, `VPP`, `DSR`, `V-D`, `C-D`, `R-D`, `T-D`) — was the long descriptions (`"Time of use"`, …). GHG returns the long Description `"Greenhouse Gas emissions"` (`RateType.GHG`) and Flex Alert returns `"Flex Alert"` (`RateType.FLEX_ALERT`); both are retained. Unknown values still pass through as plain strings.
|
|
20
|
+
- **Breaking: `Unit` reports v2.0 GHG emissions in grams.** New `Unit.G_CO2_PER_KWH = "g/kWh CO2"` — values are **1000× larger** than v1.0's `kg/kWh CO2` for the same physical reading. `Unit.KG_CO2_PER_KWH` is retained for pre-migration historical-archive reads; `MIDASClient.ghg()` recognises both.
|
|
21
|
+
- **Breaking: `get_historical_data` / `historical_data` use the path-param endpoint `/HistoricalData/{rate_id}`** (was the `?id=` query param), and reject a range longer than the v2.0 **6-month** max per call with `ValueError`. The Python signatures are unchanged.
|
|
22
|
+
|
|
23
|
+
### Added
|
|
24
|
+
|
|
25
|
+
- **`create_anonymous_client`** — an unauthenticated client for the v2.0 GET endpoints (no token acquired, no `Authorization` header sent). This is the default, supported access mode for this read-only consumer; `create_client` / `create_auto_client` remain for the utility upload (POST) path only.
|
|
26
|
+
- **`src/midas/time.py`** — pendulum-based time module shared in spirit with `python-oa3`: the `PendulumDateTime` annotated Pydantic type (parses on input, serialises to ISO 8601 preserving the wire offset) plus `parse_instant` (zone-tagged fields, incl. `LastUpdated`), `parse_local` (attach a documented zone to a bare wall-clock string), and `parse_value_moment` (compose a UTC `ValueInformation` boundary). `MIDAS_ZONE = "America/Los_Angeles"` documents MIDAS's native administrative zone.
|
|
27
|
+
- **`RinListResponse`** and **`LookupTableResponse`** entity models for the v2.0 keyed RIN-list and lookup-table responses, exported from `midas` and `midas.entities`.
|
|
28
|
+
- **`RateType.MOER`** for the v2.0 unified SGIP GHG signal, plus the short electricity `Ratetype` codes (`VPP`, `DSR`, `V-D`, `C-D`, `R-D`, `T-D`).
|
|
29
|
+
|
|
30
|
+
### Removed
|
|
31
|
+
|
|
32
|
+
- **`get_holidays` / `holidays` / `coerce_holidays` and the `Holiday` model** — v2.0 retired the standalone `/Holiday` endpoint (absent from the CEC's published OpenAPI). Remove any holiday calls; the `Holiday` *day-type* value in rate schedules (`DayType.HOLIDAY`) is unaffected.
|
|
33
|
+
- **`get_historical_list` / `historical_list` / `coerce_historical_list`** — v2.0 retires the `/HistoricalList` endpoint. Use `rin_list(signal_type=0)` for the full active RIN list.
|
|
34
|
+
- The `Holiday` and `TimeZone` **lookup tables** are retired in v2.0 (`?LookupTable=Holiday` / `TimeZone` no longer return data).
|
|
35
|
+
|
|
36
|
+
## [0.1.1] — 2026-03-20
|
|
37
|
+
|
|
38
|
+
### Fixed
|
|
39
|
+
|
|
40
|
+
- `RateInfo.id` accepts `null` for empty realtime responses (the live API returns an all-null `RateInfo` when a RIN has no current realtime datapoint).
|
|
41
|
+
|
|
42
|
+
## [0.1.0] — 2026-03-20
|
|
43
|
+
|
|
44
|
+
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
|
+
|
|
46
|
+
[1.0.0]: https://github.com/grid-coordination/python-midas/releases/tag/v1.0.0
|
|
47
|
+
[0.1.1]: https://github.com/grid-coordination/python-midas/releases/tag/v0.1.1
|
|
48
|
+
[0.1.0]: https://github.com/grid-coordination/python-midas/releases/tag/v0.1.0
|
|
@@ -0,0 +1,85 @@
|
|
|
1
|
+
# Contributing to python-midas
|
|
2
|
+
|
|
3
|
+
Thanks for your interest in contributing! This repo is a Python client library for the California Energy Commission's [MIDAS API](https://midasapi.energy.ca.gov/). It exposes a two-layer raw/coerced data model with Pydantic v2 entities, pendulum time types, and an httpx-based client. Unlike some sibling libraries it does **not** bundle an OpenAPI spec — endpoints are hand-written `httpx` calls, and the spec-level source of truth lives upstream in [`grid-coordination/midas-api-specs`](https://github.com/grid-coordination/midas-api-specs).
|
|
4
|
+
|
|
5
|
+
## How to contribute
|
|
6
|
+
|
|
7
|
+
### Discussions
|
|
8
|
+
|
|
9
|
+
Use [Discussions](https://github.com/grid-coordination/python-midas/discussions) for:
|
|
10
|
+
|
|
11
|
+
- Questions about how to use the library — clients, coercion, raw/coerced layering, time and timezone handling, signal-type helpers
|
|
12
|
+
- API and design judgment calls — "should python-midas model X?" / "is this the right shape for Y?"
|
|
13
|
+
- MIDAS API behavior gaps that affect python-midas — when the live API exposes something that doesn't fit the current entity shape and you want to scope what the library should do about it
|
|
14
|
+
- Coordination with the upstream [`midas-api-specs`](https://github.com/grid-coordination/midas-api-specs) spec repo (whose `doc/` notes — RIN structure, datetime/timezone semantics, the v2.0 migration map — this library follows)
|
|
15
|
+
- Sharing what you're building on top of python-midas
|
|
16
|
+
|
|
17
|
+
Discussions are open-ended — a good place to think out loud or scope something before it becomes a concrete change. Aligned outcomes from a Discussion often turn into one or more Issues.
|
|
18
|
+
|
|
19
|
+
### Issues
|
|
20
|
+
|
|
21
|
+
Use [Issues](https://github.com/grid-coordination/python-midas/issues) for actionable changes:
|
|
22
|
+
|
|
23
|
+
- Bugs in client construction, request building, response parsing, or coercion against the live MIDAS API
|
|
24
|
+
- Coercion or schema gaps surfaced by real API responses (a field the library doesn't handle, or a value that breaks the coerced shape)
|
|
25
|
+
- New endpoints or request parameters when MIDAS exposes them
|
|
26
|
+
- Test failures or unexpected behavior with concrete repro steps
|
|
27
|
+
- Documentation errors, unclear explanations, or stale prose in `README.md` or docstrings
|
|
28
|
+
- Discussion outcomes that have alignment and a clear scope
|
|
29
|
+
|
|
30
|
+
If you're not sure whether something is an Issue or a Discussion, start with a Discussion — we can convert it later.
|
|
31
|
+
|
|
32
|
+
### Pull requests
|
|
33
|
+
|
|
34
|
+
Pull requests are welcome.
|
|
35
|
+
|
|
36
|
+
- For small fixes (typos, broken links, single-test corrections, single-coercion bug fixes), open a PR directly.
|
|
37
|
+
- For substantive changes (new endpoints, new entity types, new coerced fields, time-handling changes), open a Discussion or Issue first so we can align on scope before you invest the effort.
|
|
38
|
+
- All changes pass `pytest tests/ -m "not integration"` and `ruff check src/ tests/` / `ruff format --check src/ tests/` cleanly.
|
|
39
|
+
- Match the existing tone and structure. The library composes HTTP client → raw response accessors → coerced Pydantic entities as roughly orthogonal layers; patches that fit cleanly into one layer without leaking concerns across them are the easiest to land.
|
|
40
|
+
- One commit per logical change is fine; we don't require squash or any particular branch naming.
|
|
41
|
+
|
|
42
|
+
## Development
|
|
43
|
+
|
|
44
|
+
```bash
|
|
45
|
+
pip install -e ".[dev]" # install with dev dependencies
|
|
46
|
+
pytest tests/ -v -m "not integration" # run the offline unit suite
|
|
47
|
+
ruff check src/ tests/ # lint
|
|
48
|
+
ruff format --check src/ tests/ # format check (drop --check to apply)
|
|
49
|
+
```
|
|
50
|
+
|
|
51
|
+
### Time and timezones
|
|
52
|
+
|
|
53
|
+
Every coerced datetime is a zone-aware `pendulum.DateTime`, and the library **preserves the honest wire zone** rather than normalizing to one display zone: `Z`-suffixed fields (`SystemTime_UTC`, `SignupCloseDate`) stay UTC; bare administrative fields (`LastUpdated`) are `America/Los_Angeles` local; `ValueData` interval boundaries are exposed as a `period` `(start, end)` tuple composed from the v2.0 UTC wire and kept in UTC. Convert to a zone of your choice yourself with `.in_tz(...)`. The parsing rules live in `src/midas/time.py`; the wire-level semantics they encode are documented in [`midas-api-specs/doc/datetime-and-timezone.md`](https://github.com/grid-coordination/midas-api-specs/blob/main/doc/datetime-and-timezone.md). When changing time handling, keep the "know the zone, convert it yourself" contract intact.
|
|
54
|
+
|
|
55
|
+
### Integration tests
|
|
56
|
+
|
|
57
|
+
Tests marked `integration` hit the live MIDAS API and require `MIDAS_USERNAME` / `MIDAS_PASSWORD`; they are excluded from CI and from the default offline run above. They target the **v2.0** API, which is live only after the CEC cutover on 2026-06-22 — running them before then (or without credentials) will fail or skip. Run them on release day to smoke-test against production:
|
|
58
|
+
|
|
59
|
+
```bash
|
|
60
|
+
MIDAS_USERNAME=... MIDAS_PASSWORD=... pytest tests/ -v -m integration
|
|
61
|
+
```
|
|
62
|
+
|
|
63
|
+
### Releases
|
|
64
|
+
|
|
65
|
+
`python-midas` is published to PyPI by a GitHub Actions workflow ([`.github/workflows/publish.yml`](.github/workflows/publish.yml)) using PyPI's [Trusted Publisher](https://docs.pypi.org/trusted-publishers/) flow — no API tokens or personal credentials are involved. Pushing a `v*` tag triggers the workflow, which runs the offline test suite, builds the sdist + wheel, and uploads via OpenID Connect.
|
|
66
|
+
|
|
67
|
+
To cut a release:
|
|
68
|
+
|
|
69
|
+
1. Bump `version` in `pyproject.toml`.
|
|
70
|
+
2. Move the `## [Unreleased]` entries in [`CHANGELOG.md`](CHANGELOG.md) under a `## [<version>] — <date>` heading (Added / Changed / Fixed / Removed). Update the reference links at the bottom.
|
|
71
|
+
3. Commit on `main` (e.g. `Bump version to X.Y.Z`).
|
|
72
|
+
4. Tag and push: `git tag vX.Y.Z && git push origin vX.Y.Z`.
|
|
73
|
+
5. The `Publish to PyPI` workflow runs the test job, then the publish job in the `pypi` environment. Watch it on the Actions tab.
|
|
74
|
+
|
|
75
|
+
The Trusted Publisher binding is `grid-coordination / python-midas / publish.yml / pypi`, configured at [pypi.org → Manage → Publishing](https://pypi.org/manage/account/publishing/). If a release fails with an OIDC error, verify the binding hasn't drifted (workflow filename, environment name, repo path).
|
|
76
|
+
|
|
77
|
+
Versioning follows [SemVer](https://semver.org/). The 1.0.0 release tracks MIDAS v2.0 and is a v2-only, breaking release (see [`CHANGELOG.md`](CHANGELOG.md)).
|
|
78
|
+
|
|
79
|
+
## Code of conduct
|
|
80
|
+
|
|
81
|
+
Be respectful and constructive. We're a small project and appreciate everyone who takes the time to file an issue or send a PR.
|
|
82
|
+
|
|
83
|
+
## Important notice
|
|
84
|
+
|
|
85
|
+
This library is provided on an "as-is" basis. Updates and maintenance, including responses to issues filed on GitHub, will take place on an "as time and resources permit" basis. Library output (raw API responses, coerced entities) is best-effort against the live MIDAS API and the upstream [`midas-api-specs`](https://github.com/grid-coordination/midas-api-specs) documentation. This library is not authoritative for billing, dispatch, or grid operations — independent verification against the MIDAS API's actual responses is recommended for any consumer relying on these results for operational correctness.
|
|
@@ -0,0 +1,417 @@
|
|
|
1
|
+
Metadata-Version: 2.4
|
|
2
|
+
Name: python-midas
|
|
3
|
+
Version: 1.0.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 :: 5 - Production/Stable
|
|
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
|
+
[](https://pypi.org/project/python-midas/)
|
|
34
|
+
[](https://pypi.org/project/python-midas/)
|
|
35
|
+
[](https://github.com/grid-coordination/python-midas/actions/workflows/ci.yml)
|
|
36
|
+
[](https://github.com/astral-sh/ruff)
|
|
37
|
+
[](https://opensource.org/licenses/MIT)
|
|
38
|
+
|
|
39
|
+
Python client library for the California Energy Commission [MIDAS](https://midasapi.energy.ca.gov/) (Market Informed Demand Automation Server) API.
|
|
40
|
+
|
|
41
|
+
MIDAS provides California energy rate data, greenhouse gas (GHG) emissions signals, and Flex Alert status. This library wraps the API with typed Pydantic models and a two-layer data model that preserves raw API responses alongside coerced Python-native types.
|
|
42
|
+
|
|
43
|
+
> **This is a read-only consumer client.** python-midas is built for *consuming* MIDAS data. In v2.0 all public GET endpoints are unauthenticated, so you need **no credentials** — just `create_anonymous_client()`. The authenticated constructors (`create_client` / `create_auto_client`) exist only for utilities that *upload* rate data to the CEC; that path requires CEC-issued utility credentials and is not exercised by this project.
|
|
44
|
+
|
|
45
|
+
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).
|
|
46
|
+
|
|
47
|
+
## Installation
|
|
48
|
+
|
|
49
|
+
```bash
|
|
50
|
+
pip install python-midas
|
|
51
|
+
```
|
|
52
|
+
|
|
53
|
+
The distribution is named `python-midas` (the bare `midas` name on PyPI belongs to an unrelated gas-detector driver), but it imports as `midas`:
|
|
54
|
+
|
|
55
|
+
```python
|
|
56
|
+
import midas
|
|
57
|
+
```
|
|
58
|
+
|
|
59
|
+
For development:
|
|
60
|
+
|
|
61
|
+
```bash
|
|
62
|
+
pip install -e ".[dev]"
|
|
63
|
+
```
|
|
64
|
+
|
|
65
|
+
Requires Python 3.10+.
|
|
66
|
+
|
|
67
|
+
## Quick Start
|
|
68
|
+
|
|
69
|
+
```python
|
|
70
|
+
from midas import create_anonymous_client
|
|
71
|
+
|
|
72
|
+
client = create_anonymous_client() # v2.0 GETs need no credentials
|
|
73
|
+
|
|
74
|
+
# List available Rate Identification Numbers (RINs)
|
|
75
|
+
rins = client.rin_list()
|
|
76
|
+
for rin in rins:
|
|
77
|
+
print(f"{rin.id} {rin.signal_type} {rin.description}")
|
|
78
|
+
|
|
79
|
+
# Get rate values for a specific RIN
|
|
80
|
+
rate = client.rate_values(rins[0].id)
|
|
81
|
+
print(f"{rate.name} ({rate.type})")
|
|
82
|
+
for v in rate.values:
|
|
83
|
+
start, end = v.period # zone-aware (start, end) — UTC on the wire
|
|
84
|
+
print(f" {start}–{end}: {v.value} {v.unit}")
|
|
85
|
+
```
|
|
86
|
+
|
|
87
|
+
## Authentication
|
|
88
|
+
|
|
89
|
+
In MIDAS **v2.0**, all public GET endpoints (rate values, RIN list, lookup tables, historical data) are **unauthenticated** — use `create_anonymous_client` for read-only access:
|
|
90
|
+
|
|
91
|
+
```python
|
|
92
|
+
from midas import create_anonymous_client
|
|
93
|
+
|
|
94
|
+
with create_anonymous_client() as client:
|
|
95
|
+
rins = client.rin_list()
|
|
96
|
+
rate = client.rate_values(rins[0].id)
|
|
97
|
+
```
|
|
98
|
+
|
|
99
|
+
Authentication exists only for **uploads** (LSE rate submission, POST) and requires CEC-issued utility credentials; it is **not exercised by this read-only consumer**. For completeness, the upload path is provided: MIDAS uses HTTP Basic authentication to acquire a short-lived bearer token (valid for 10 minutes), via `create_auto_client` (acquires a token and transparently refreshes it within a 30-second buffer), `create_client` (a single token), or the low-level `get_token` / `token_expired` helpers.
|
|
100
|
+
|
|
101
|
+
```python
|
|
102
|
+
from midas import create_auto_client, create_client, get_token, token_expired
|
|
103
|
+
|
|
104
|
+
client = create_auto_client("username", "password") # auto-refreshing (uploads)
|
|
105
|
+
client = create_client("username", "password") # single token (~10 min)
|
|
106
|
+
|
|
107
|
+
token_info = get_token("username", "password") # low-level
|
|
108
|
+
# token_info = {"token": "...", "acquired_at": DateTime, "expires_at": DateTime}
|
|
109
|
+
```
|
|
110
|
+
|
|
111
|
+
## API Coverage
|
|
112
|
+
|
|
113
|
+
The MIDAS API has a single multiplexed `/ValueData` endpoint that serves different response shapes depending on query parameters, plus a separate endpoint for historical data. All read operations are covered:
|
|
114
|
+
|
|
115
|
+
### RIN List
|
|
116
|
+
|
|
117
|
+
List available Rate Identification Numbers, optionally filtered by signal type:
|
|
118
|
+
|
|
119
|
+
```python
|
|
120
|
+
all_rins = client.rin_list() # All signal types
|
|
121
|
+
rate_rins = client.rin_list(signal_type=1) # Rates only
|
|
122
|
+
ghg_rins = client.rin_list(signal_type=2) # GHG only
|
|
123
|
+
flex_rins = client.rin_list(signal_type=3) # Flex Alert only
|
|
124
|
+
```
|
|
125
|
+
|
|
126
|
+
Each `RinListEntry` has:
|
|
127
|
+
|
|
128
|
+
- `id` — the RIN string (e.g. `"USCA-PGPG-ETOU-0000"`)
|
|
129
|
+
- `signal_type` — `SignalType.RATES`, `SignalType.GHG_EMISSIONS`, or `SignalType.FLEX_ALERT` (v2.0 long-form wire labels)
|
|
130
|
+
- `description` — human-readable description
|
|
131
|
+
- `last_updated` — `pendulum.DateTime` of last data update
|
|
132
|
+
|
|
133
|
+
### Rate Values
|
|
134
|
+
|
|
135
|
+
Fetch current rate/price data for a specific RIN:
|
|
136
|
+
|
|
137
|
+
```python
|
|
138
|
+
rate = client.rate_values("USCA-TSTS-TTOU-TEST")
|
|
139
|
+
rate = client.rate_values("USCA-TSTS-TTOU-TEST", query_type="realtime")
|
|
140
|
+
```
|
|
141
|
+
|
|
142
|
+
The `RateInfo` model contains:
|
|
143
|
+
|
|
144
|
+
- `id` — the RIN
|
|
145
|
+
- `name` — rate name (e.g. `"CEC TEST24HTOU"`)
|
|
146
|
+
- `type` — `RateType` enum or raw string. The wire value is inconsistent across signal types in v2.0: electricity rates return the short Ratetype code (`TOU`, `CPP`, `RTP`, …) while GHG returns `Greenhouse Gas emissions` and Flex Alert returns `Flex Alert`
|
|
147
|
+
- `system_time` — server timestamp as `pendulum.DateTime`
|
|
148
|
+
- `sector`, `end_use` — customer classification
|
|
149
|
+
- `rate_plan_url`, `api_url` — external links (the API's `"None"` string is coerced to `None`)
|
|
150
|
+
- `signup_close` — rate signup deadline as `pendulum.DateTime`
|
|
151
|
+
- `values` — list of `ValueData` intervals
|
|
152
|
+
|
|
153
|
+
Each `ValueData` interval has:
|
|
154
|
+
|
|
155
|
+
- `name` — interval description (e.g. `"winter off peak"`)
|
|
156
|
+
- `period` — `(start, end)` tuple of zone-aware `pendulum.DateTime` moments (or `None` when a boundary is absent). Composed from the v2.0 UTC wire date+time and kept in UTC; convert with `.in_tz(...)`. See [Time and timezones](#time-and-timezones).
|
|
157
|
+
- `day_start`, `day_end` — `DayType` enum (Monday through Sunday, plus Holiday)
|
|
158
|
+
- `value` — `Decimal` (preserves precision for financial data)
|
|
159
|
+
- `unit` — `Unit` enum (`$/kWh`, `$/kW`, `kg/kWh CO2`, `Event`, etc.)
|
|
160
|
+
|
|
161
|
+
### Lookup Tables
|
|
162
|
+
|
|
163
|
+
Fetch reference data tables:
|
|
164
|
+
|
|
165
|
+
```python
|
|
166
|
+
energies = client.lookup_table("Energy") # Energy providers
|
|
167
|
+
dists = client.lookup_table("Distribution") # Distribution companies
|
|
168
|
+
units = client.lookup_table("Unit") # Available units
|
|
169
|
+
sectors = client.lookup_table("Sector") # Customer sectors
|
|
170
|
+
```
|
|
171
|
+
|
|
172
|
+
Available tables: `Country`, `Daytype`, `Distribution`, `Enduse`, `Energy`, `Location`, `Ratetype`, `Sector`, `State`, `Unit`. (v2.0 retired the `Holiday` and `TimeZone` lookup tables.)
|
|
173
|
+
|
|
174
|
+
In v2.0 a lookup response is a keyed object `{table_name, data: [...]}`; the client peels `data` for you. Each `LookupEntry` has `code` and `description`, plus optional `payload_descriptor` and `unit_type` (the `Unit` table carries these extra columns).
|
|
175
|
+
|
|
176
|
+
### Historical Data
|
|
177
|
+
|
|
178
|
+
Query archived rate data for a RIN over a date range (v2.0 caps each call at a **6-month** range and takes the RIN as a path parameter):
|
|
179
|
+
|
|
180
|
+
```python
|
|
181
|
+
hist = client.historical_data("USCA-PGPG-ETOU-0000", "2023-01-01", "2023-06-30")
|
|
182
|
+
```
|
|
183
|
+
|
|
184
|
+
A range longer than six months raises `ValueError` — split it into multiple calls.
|
|
185
|
+
|
|
186
|
+
> The v1.0 `historical_list` / `get_historical_list` methods are **removed** — v2.0 retires the `/HistoricalList` endpoint. For the full active RIN list use `client.rin_list(signal_type=0)`.
|
|
187
|
+
|
|
188
|
+
## Signal Type Helpers
|
|
189
|
+
|
|
190
|
+
Convenience methods for identifying signal types, matching the [clj-midas](https://github.com/grid-coordination/clj-midas) API:
|
|
191
|
+
|
|
192
|
+
```python
|
|
193
|
+
rate = client.rate_values("USCA-GHGH-SGHT-0000")
|
|
194
|
+
|
|
195
|
+
client.ghg(rate) # True if GHG signal (by RateType or Unit)
|
|
196
|
+
client.flex_alert(rate) # True if Flex Alert signal
|
|
197
|
+
client.flex_alert_active(rate) # True if Flex Alert with any non-zero value
|
|
198
|
+
```
|
|
199
|
+
|
|
200
|
+
## Time and timezones
|
|
201
|
+
|
|
202
|
+
Every coerced datetime is a zone-aware `pendulum.DateTime` — Python's equivalent of Java's `ZonedDateTime` (it carries an IANA zone, not just a fixed offset, so it is DST-correct). The guiding principle, shared with [clj-midas](https://github.com/grid-coordination/clj-midas) and [python-oa3](https://github.com/grid-coordination/python-oa3): **you always know what zone a value is in, and you convert it yourself.** The library preserves the honest wire zone and never normalizes to a single display zone.
|
|
203
|
+
|
|
204
|
+
MIDAS mixes two wire conventions and does not tag the bare ones; python-midas encodes the documented zone for each field (see [midas-api-specs/doc/datetime-and-timezone.md](https://github.com/grid-coordination/midas-api-specs/blob/main/doc/datetime-and-timezone.md)):
|
|
205
|
+
|
|
206
|
+
| Field | Wire form (v2.0) | Coerced as |
|
|
207
|
+
|-------|------------------|------------|
|
|
208
|
+
| `system_time`, `signup_close` | `Z`-suffixed (UTC) | `pendulum.DateTime` in **UTC** — instant preserved |
|
|
209
|
+
| `ValueData.period` (start, end) | bare `DateStart`/`TimeStart`/…, **UTC** in v2.0 | pair of `pendulum.DateTime` in **UTC** |
|
|
210
|
+
| `last_updated` | UTC with basic-format offset (`+0000`) | `pendulum.DateTime` in **UTC** — instant preserved (absent on Flex Alert entries → `None`) |
|
|
211
|
+
|
|
212
|
+
```python
|
|
213
|
+
rate = client.rate_values("USCA-TSTS-TTOU-TEST")
|
|
214
|
+
start, end = rate.values[0].period
|
|
215
|
+
start # 2026-05-01 07:00:00+00:00 (UTC, as delivered)
|
|
216
|
+
start.in_tz("America/Los_Angeles") # 2026-05-01 00:00:00-07:00 (you convert)
|
|
217
|
+
start.in_tz("America/Los_Angeles").date() # datetime.date(2026, 5, 1) (wall-clock date)
|
|
218
|
+
```
|
|
219
|
+
|
|
220
|
+
In v2.0 (effective 2026-06-22) MIDAS delivers every `ValueInformation` date/time field in UTC for all signal types — fixing the v1.0 bug where SGIP GHG and Flex Alert timestamps arrived Pacific-local on the wire — and `LastUpdated` now carries an explicit `+0000` offset. The parsing rules live in `midas.time` (`parse_instant`, `parse_local`, `parse_value_moment`, and the `PendulumDateTime` Pydantic type).
|
|
221
|
+
|
|
222
|
+
> The v1.0 zone-naive fields `date_start`, `date_end`, `time_start`, `time_end` (`datetime.date` / `datetime.time`) are **removed** in favour of `period`: a bare wall-clock time with no zone is ambiguous, whereas the `(start, end)` moments are self-describing. The exact wire strings remain on `_raw`.
|
|
223
|
+
|
|
224
|
+
## Two-Layer Data Model
|
|
225
|
+
|
|
226
|
+
Following the [python-oa3](https://github.com/grid-coordination/python-oa3) pattern, every entity provides two layers:
|
|
227
|
+
|
|
228
|
+
**Raw layer** — the original API JSON dict (PascalCase keys, string values), accessible via `_raw`:
|
|
229
|
+
|
|
230
|
+
```python
|
|
231
|
+
rate = client.rate_values("USCA-TSTS-TTOU-TEST")
|
|
232
|
+
rate._raw["RateID"] # "USCA-TSTS-TTOU-TEST"
|
|
233
|
+
rate._raw["ValueInformation"][0]["Value"] # 0.1006 (v2.0 capitalises the key)
|
|
234
|
+
rate.values[0]._raw["Unit"] # "$/kWh"
|
|
235
|
+
```
|
|
236
|
+
|
|
237
|
+
**Coerced layer** — typed Pydantic models with snake_case fields and native Python types:
|
|
238
|
+
|
|
239
|
+
```python
|
|
240
|
+
rate.id # "USCA-TSTS-TTOU-TEST"
|
|
241
|
+
rate.type # RateType.TOU
|
|
242
|
+
rate.system_time # pendulum.DateTime in UTC (Z-suffixed wire field)
|
|
243
|
+
rate.values[0].value # Decimal("0.1006")
|
|
244
|
+
rate.values[0].unit # Unit.DOLLAR_PER_KWH
|
|
245
|
+
rate.values[0].day_start # DayType.MONDAY
|
|
246
|
+
rate.values[0].period # (DateTime, DateTime) — zone-aware (start, end)
|
|
247
|
+
```
|
|
248
|
+
|
|
249
|
+
This lets you work with clean, typed data while always being able to fall back to the exact API response when needed.
|
|
250
|
+
|
|
251
|
+
## Dual-Mode Client
|
|
252
|
+
|
|
253
|
+
Every endpoint is available in two forms:
|
|
254
|
+
|
|
255
|
+
**Raw methods** return `httpx.Response` for full HTTP control:
|
|
256
|
+
|
|
257
|
+
```python
|
|
258
|
+
resp = client.get_rin_list(signal_type=0)
|
|
259
|
+
resp.status_code # 200
|
|
260
|
+
resp.json() # raw JSON list
|
|
261
|
+
|
|
262
|
+
resp = client.get_rate_values("USCA-TSTS-TTOU-TEST", query_type="alldata")
|
|
263
|
+
resp = client.get_lookup_table("Energy")
|
|
264
|
+
resp = client.get_historical_data("USCA-PGPG-ETOU-0000", "2023-01-01", "2023-06-30")
|
|
265
|
+
```
|
|
266
|
+
|
|
267
|
+
**Coerced methods** return typed Pydantic models (call `raise_for_status()` internally):
|
|
268
|
+
|
|
269
|
+
```python
|
|
270
|
+
rins = client.rin_list(signal_type=0) # list[RinListEntry]
|
|
271
|
+
rate = client.rate_values("USCA-TSTS-TTOU-TEST") # RateInfo
|
|
272
|
+
entries = client.lookup_table("Energy") # list[LookupEntry]
|
|
273
|
+
rate = client.historical_data(rin, start, end) # RateInfo (≤ 6-month range)
|
|
274
|
+
```
|
|
275
|
+
|
|
276
|
+
## Coercion Functions
|
|
277
|
+
|
|
278
|
+
You can also coerce raw dicts directly, without going through the client:
|
|
279
|
+
|
|
280
|
+
```python
|
|
281
|
+
from midas import coerce_rate_info, coerce_rin_list, coerce_lookup_table
|
|
282
|
+
|
|
283
|
+
rate = coerce_rate_info({"RateID": "...", "ValueInformation": [...]})
|
|
284
|
+
# v2.0 wraps the RIN list under a single key (always "Rates"); coerce_rin_list peels it.
|
|
285
|
+
rins = coerce_rin_list({"Rates": [{"RateID": "...", "SignalType": "Electricity Rates", ...}]})
|
|
286
|
+
# v2.0 wraps lookup rows under {table_name, data: [...]}; coerce_lookup_table peels data.
|
|
287
|
+
units = coerce_lookup_table({"table_name": "Unit", "data": [{"UploadCode": "...", ...}]})
|
|
288
|
+
```
|
|
289
|
+
|
|
290
|
+
Available: `coerce_rate_info`, `coerce_rin_list`, `coerce_lookup_table`.
|
|
291
|
+
|
|
292
|
+
## Enums
|
|
293
|
+
|
|
294
|
+
Domain values are represented as `str` enums, so they compare equal to their string values:
|
|
295
|
+
|
|
296
|
+
```python
|
|
297
|
+
from midas import SignalType, RateType, Unit, DayType
|
|
298
|
+
|
|
299
|
+
SignalType.RATES # "Electricity Rates"
|
|
300
|
+
SignalType.GHG_EMISSIONS # "Greenhouse Gas Emissions"
|
|
301
|
+
SignalType.FLEX_ALERT # "California Independent System Operator Flex Alert"
|
|
302
|
+
|
|
303
|
+
# Electricity rates return the short Ratetype UploadCode in v2.0:
|
|
304
|
+
RateType.TOU # "TOU"
|
|
305
|
+
RateType.CPP # "CPP"
|
|
306
|
+
RateType.RTP # "RTP"
|
|
307
|
+
# (also VPP, DSR, V-D, C-D, R-D, T-D)
|
|
308
|
+
# GHG and Flex Alert return long Descriptions, not short codes:
|
|
309
|
+
RateType.GHG # "Greenhouse Gas emissions"
|
|
310
|
+
RateType.FLEX_ALERT # "Flex Alert"
|
|
311
|
+
RateType.MOER # "MOER" (v2.0 unified SGIP GHG signal)
|
|
312
|
+
|
|
313
|
+
Unit.DOLLAR_PER_KWH # "$/kWh"
|
|
314
|
+
Unit.DOLLAR_PER_KW # "$/kW"
|
|
315
|
+
Unit.EXPORT_DOLLAR_PER_KWH # "export $/kWh"
|
|
316
|
+
Unit.BACKUP_DOLLAR_PER_KWH # "backup $/kWh"
|
|
317
|
+
Unit.G_CO2_PER_KWH # "g/kWh CO2" (v2.0 GHG — grams, 1000× the v1.0 kg value)
|
|
318
|
+
Unit.KG_CO2_PER_KWH # "kg/kWh CO2" (historical-archive reads)
|
|
319
|
+
Unit.DOLLAR_PER_KVARH # "$/kvarh"
|
|
320
|
+
Unit.EVENT # "Event"
|
|
321
|
+
Unit.LEVEL # "Level"
|
|
322
|
+
|
|
323
|
+
DayType.MONDAY # "Monday"
|
|
324
|
+
# ... through SUNDAY, plus:
|
|
325
|
+
DayType.HOLIDAY # "Holiday"
|
|
326
|
+
```
|
|
327
|
+
|
|
328
|
+
## Type Coercion Details
|
|
329
|
+
|
|
330
|
+
The coercion layer applies the following transformations:
|
|
331
|
+
|
|
332
|
+
| API type | Python type | Notes |
|
|
333
|
+
|----------|-------------|-------|
|
|
334
|
+
| Zone-tagged datetime (`"…Z"`) | `pendulum.DateTime` | Instant preserved in UTC (`system_time`, `signup_close`) |
|
|
335
|
+
| `LastUpdated` (`"…+0000"`) | `pendulum.DateTime` | UTC instant; explicit offset honoured |
|
|
336
|
+
| `ValueInformation` date + time | `pendulum.DateTime` pair (`period`) | Composed as UTC in v2.0 — see [Time and timezones](#time-and-timezones) |
|
|
337
|
+
| Numeric values | `Decimal` | Preserves precision for financial data |
|
|
338
|
+
| Signal type strings | `SignalType` enum | `None` passes through as `None` |
|
|
339
|
+
| Rate type strings | `RateType` enum | Unknown values pass through as strings |
|
|
340
|
+
| Unit strings | `Unit` enum | Unknown values pass through as strings |
|
|
341
|
+
| Day type strings | `DayType` enum | `None` passes through (historical data) |
|
|
342
|
+
| `"None"` string (API_Url) | `None` | MIDAS API quirk |
|
|
343
|
+
|
|
344
|
+
## Context Manager
|
|
345
|
+
|
|
346
|
+
The client supports context manager protocol for clean resource management:
|
|
347
|
+
|
|
348
|
+
```python
|
|
349
|
+
from midas import create_anonymous_client
|
|
350
|
+
|
|
351
|
+
with create_anonymous_client() as client:
|
|
352
|
+
rins = client.rin_list()
|
|
353
|
+
rate = client.rate_values(rins[0].id)
|
|
354
|
+
# httpx client is closed automatically
|
|
355
|
+
```
|
|
356
|
+
|
|
357
|
+
## Project Structure
|
|
358
|
+
|
|
359
|
+
```
|
|
360
|
+
src/midas/
|
|
361
|
+
__init__.py # Public API re-exports
|
|
362
|
+
py.typed # PEP 561 type-checking marker
|
|
363
|
+
client.py # MIDASClient, create_anonymous_client, create_client, create_auto_client
|
|
364
|
+
auth.py # BearerAuth, BasicAuth, AutoTokenAuth, get_token (upload path)
|
|
365
|
+
enums.py # SignalType, RateType, Unit, DayType
|
|
366
|
+
time.py # pendulum parsing + PendulumDateTime Pydantic type
|
|
367
|
+
entities/
|
|
368
|
+
__init__.py # Coercion dispatch functions
|
|
369
|
+
models.py # Pydantic models: RateInfo, ValueData, RinListEntry, RinListResponse, LookupEntry, LookupTableResponse
|
|
370
|
+
tests/
|
|
371
|
+
test_entities.py # Entity coercion from raw fixture dicts
|
|
372
|
+
test_client.py # HTTP client tests with pytest-httpx
|
|
373
|
+
test_auth.py # Token parsing, expiry, auth headers
|
|
374
|
+
test_integration.py # Live API tests (anonymous, no credentials)
|
|
375
|
+
```
|
|
376
|
+
|
|
377
|
+
## Development
|
|
378
|
+
|
|
379
|
+
```bash
|
|
380
|
+
# Install with dev dependencies
|
|
381
|
+
pip install -e ".[dev]"
|
|
382
|
+
|
|
383
|
+
# Lint
|
|
384
|
+
ruff check src/ tests/
|
|
385
|
+
```
|
|
386
|
+
|
|
387
|
+
See [CONTRIBUTING.md](CONTRIBUTING.md) for the full contributor workflow and [CHANGELOG.md](CHANGELOG.md) for release history. python-midas `1.0.0` tracks the breaking MIDAS **v2.0** API (live 2026-06-22; v2-only — v1.0 support intentionally dropped) — see [doc/v2-migration.md](doc/v2-migration.md) for the v1.0→v2.0 upgrade guide.
|
|
388
|
+
|
|
389
|
+
### Tests
|
|
390
|
+
|
|
391
|
+
The test suite has two tiers:
|
|
392
|
+
|
|
393
|
+
**Unit tests** run entirely offline using fixture dicts and mocked HTTP (pytest-httpx):
|
|
394
|
+
|
|
395
|
+
```bash
|
|
396
|
+
pytest -m "not integration"
|
|
397
|
+
```
|
|
398
|
+
|
|
399
|
+
**Integration tests** run against the live MIDAS API at `midasapi.energy.ca.gov` using an anonymous client — **no credentials required** (v2.0 GETs are unauthenticated):
|
|
400
|
+
|
|
401
|
+
```bash
|
|
402
|
+
pytest -m integration
|
|
403
|
+
```
|
|
404
|
+
|
|
405
|
+
Integration tests exercise every read endpoint (RIN list, rate values, lookup tables, historical data), all entity coercion paths against real response shapes, the v2.0 wire-shape corrections (keyed `Rates` RIN list, keyed `{table_name, data}` lookup tables, UTC `LastUpdated`, signal-type-dependent `RateType`), and the signal type helpers (GHG, Flex Alert detection). Pre-migration historical data may be absent during the v2.0 cutover week, so the historical test skips gracefully on a 404.
|
|
406
|
+
|
|
407
|
+
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`.
|
|
408
|
+
|
|
409
|
+
## Related Projects
|
|
410
|
+
|
|
411
|
+
- **[midas-api-specs](https://github.com/grid-coordination/midas-api-specs)** — OpenAPI specifications for the MIDAS API, derived from documentation and live API validation
|
|
412
|
+
- **[clj-midas](https://github.com/grid-coordination/clj-midas)** — Clojure client for the MIDAS API (Martian-based, spec-driven)
|
|
413
|
+
- **[python-oa3](https://github.com/grid-coordination/python-oa3)** — Python client for OpenADR 3 (same entity API pattern)
|
|
414
|
+
|
|
415
|
+
## License
|
|
416
|
+
|
|
417
|
+
MIT
|