Python client library for the California Energy Commission MIDAS (Market Informed Demand Automation Server) API.
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.
Part of the grid-coordination project family, alongside clj-midas (Clojure client) and midas-api-specs (OpenAPI specifications).
pip install midasFor development:
pip install -e ".[dev]"Requires Python 3.10+.
from midas import create_auto_client
client = create_auto_client("username", "password")
# List available Rate Identification Numbers (RINs)
rins = client.rin_list()
for rin in rins:
print(f"{rin.id} {rin.signal_type} {rin.description}")
# Get rate values for a specific RIN
rate = client.rate_values(rins[0].id)
print(f"{rate.name} ({rate.type})")
for v in rate.values:
print(f" {v.date_start} {v.time_start}-{v.time_end}: {v.value} {v.unit}")MIDAS uses HTTP Basic authentication to acquire a short-lived bearer token (valid for 10 minutes). The library provides two client creation modes:
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):
from midas import create_auto_client
client = create_auto_client("username", "password")
# Token refreshes automatically — use the client for as long as you needcreate_client acquires a single token. You are responsible for creating a new client when it expires:
from midas import create_client
client = create_client("username", "password")
# Token is valid for ~10 minutesFor advanced use cases, you can manage tokens directly:
from midas import get_token, token_expired, MIDASClient
token_info = get_token("username", "password")
# token_info = {"token": "...", "acquired_at": DateTime, "expires_at": DateTime}
if token_expired(token_info):
token_info = get_token("username", "password")
client = MIDASClient(token=token_info["token"])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:
List available Rate Identification Numbers, optionally filtered by signal type:
all_rins = client.rin_list() # All signal types
rate_rins = client.rin_list(signal_type=1) # Rates only
ghg_rins = client.rin_list(signal_type=2) # GHG only
flex_rins = client.rin_list(signal_type=3) # Flex Alert onlyEach RinListEntry has:
id— the RIN string (e.g."USCA-PGPG-ETOU-0000")signal_type—SignalType.RATES,SignalType.GHG, orSignalType.FLEX_ALERTdescription— human-readable descriptionlast_updated—pendulum.DateTimeof last data update
Fetch current rate/price data for a specific RIN:
rate = client.rate_values("USCA-TSTS-TTOU-TEST")
rate = client.rate_values("USCA-TSTS-TTOU-TEST", query_type="realtime")The RateInfo model contains:
id— the RINname— rate name (e.g."CEC TEST24HTOU")type—RateTypeenum (TOU,CPP,RTP,GHG,FLEX_ALERT) or raw stringsystem_time— server timestamp aspendulum.DateTimesector,end_use— customer classificationrate_plan_url,api_url— external links (the API's"None"string is coerced toNone)signup_close— rate signup deadline aspendulum.DateTimevalues— list ofValueDataintervals
Each ValueData interval has:
name— period description (e.g."winter off peak")date_start,date_end—datetime.dateday_start,day_end—DayTypeenum (Monday through Sunday, plus Holiday)time_start,time_end—datetime.time(handles bothHH:MM:SSandHH:MMformats)value—Decimal(preserves precision for financial data)unit—Unitenum ($/kWh,$/kW,kg/kWh CO2,Event, etc.)
Fetch reference data tables:
energies = client.lookup_table("Energy") # Energy providers
dists = client.lookup_table("Distribution") # Distribution companies
units = client.lookup_table("Unit") # Available units
sectors = client.lookup_table("Sector") # Customer sectorsAvailable tables: Country, Daytype, Distribution, Enduse, Energy, Location, Ratetype, Sector, State, Unit.
Each LookupEntry has code and description.
Fetch utility-observed holidays:
holidays = client.holidays()
for h in holidays:
print(f"{h.energy_name}: {h.date} — {h.description}")Each Holiday has energy_code, energy_name, date (datetime.date), and description.
Query archived rate data by provider and date range:
# List RINs with historical data for a provider pair
hist_rins = client.historical_list("PG", "PG") # PG&E distribution + energy
# Fetch archived data for a date range
hist = client.historical_data("USCA-PGPG-ETOU-0000", "2023-01-01", "2023-12-31")The historical list is automatically deduplicated (the live API returns duplicate entries).
Convenience methods for identifying signal types, matching the clj-midas API:
rate = client.rate_values("USCA-GHGH-SGHT-0000")
client.ghg(rate) # True if GHG signal (by RateType or Unit)
client.flex_alert(rate) # True if Flex Alert signal
client.flex_alert_active(rate) # True if Flex Alert with any non-zero valueFollowing the python-oa3 pattern, every entity provides two layers:
Raw layer — the original API JSON dict (PascalCase keys, string values), accessible via _raw:
rate = client.rate_values("USCA-TSTS-TTOU-TEST")
rate._raw["RateID"] # "USCA-TSTS-TTOU-TEST"
rate._raw["ValueInformation"][0]["value"] # 0.1006
rate.values[0]._raw["Unit"] # "$/kWh"Coerced layer — typed Pydantic models with snake_case fields and native Python types:
rate.id # "USCA-TSTS-TTOU-TEST"
rate.type # RateType.TOU
rate.system_time # pendulum.DateTime (UTC)
rate.values[0].value # Decimal("0.1006")
rate.values[0].unit # Unit.DOLLAR_PER_KWH
rate.values[0].day_start # DayType.MONDAY
rate.values[0].date_start # datetime.date(2023, 5, 1)
rate.values[0].time_start # datetime.time(7, 0, 0)This lets you work with clean, typed data while always being able to fall back to the exact API response when needed.
Every endpoint is available in two forms:
Raw methods return httpx.Response for full HTTP control:
resp = client.get_rin_list(signal_type=0)
resp.status_code # 200
resp.json() # raw JSON list
resp = client.get_rate_values("USCA-TSTS-TTOU-TEST", query_type="alldata")
resp = client.get_lookup_table("Energy")
resp = client.get_holidays()
resp = client.get_historical_list("PG", "PG")
resp = client.get_historical_data("USCA-PGPG-ETOU-0000", "2023-01-01", "2023-12-31")Coerced methods return typed Pydantic models (call raise_for_status() internally):
rins = client.rin_list(signal_type=0) # list[RinListEntry]
rate = client.rate_values("USCA-TSTS-TTOU-TEST") # RateInfo
entries = client.lookup_table("Energy") # list[LookupEntry]
holidays = client.holidays() # list[Holiday]
rins = client.historical_list("PG", "PG") # list[RinListEntry]
rate = client.historical_data(rin, start, end) # RateInfoYou can also coerce raw dicts directly, without going through the client:
from midas import coerce_rate_info, coerce_rin_list, coerce_holidays
rate = coerce_rate_info({"RateID": "...", "ValueInformation": [...]})
rins = coerce_rin_list([{"RateID": "...", "SignalType": "Rates", ...}])Available: coerce_rate_info, coerce_rin_list, coerce_holidays, coerce_lookup_table, coerce_historical_list.
Domain values are represented as str enums, so they compare equal to their string values:
from midas import SignalType, RateType, Unit, DayType
SignalType.RATES # "Rates"
SignalType.GHG # "GHG"
SignalType.FLEX_ALERT # "Flex Alert"
RateType.TOU # "Time of use"
RateType.CPP # "Critical Peak Pricing"
RateType.RTP # "Real Time Pricing"
RateType.GHG # "Greenhouse Gas emissions"
RateType.FLEX_ALERT # "Flex Alert"
Unit.DOLLAR_PER_KWH # "$/kWh"
Unit.DOLLAR_PER_KW # "$/kW"
Unit.EXPORT_DOLLAR_PER_KWH # "export $/kWh"
Unit.BACKUP_DOLLAR_PER_KWH # "backup $/kWh"
Unit.KG_CO2_PER_KWH # "kg/kWh CO2"
Unit.DOLLAR_PER_KVARH # "$/kvarh"
Unit.EVENT # "Event"
Unit.LEVEL # "Level"
DayType.MONDAY # "Monday"
# ... through SUNDAY, plus:
DayType.HOLIDAY # "Holiday"The coercion layer applies the following transformations:
| API type | Python type | Notes |
|---|---|---|
Date strings ("2023-05-01") |
datetime.date |
Extracts date from datetime strings too |
| Datetime strings | pendulum.DateTime |
Naive datetimes treated as UTC |
Time strings ("07:00:00", "03:11") |
datetime.time |
Handles both HH:MM:SS and HH:MM |
| Numeric values | Decimal |
Preserves precision for financial data |
| Signal type strings | SignalType enum |
None passes through as None |
| Rate type strings | RateType enum |
Unknown values pass through as strings |
| Unit strings | Unit enum |
Unknown values pass through as strings |
| Day type strings | DayType enum |
None passes through (historical data) |
"None" string (API_Url) |
None |
MIDAS API quirk |
The client supports context manager protocol for clean resource management:
from midas import create_auto_client
with create_auto_client("user", "pass") as client:
rins = client.rin_list()
rate = client.rate_values(rins[0].id)
# httpx client is closed automaticallysrc/midas/
__init__.py # Public API re-exports
py.typed # PEP 561 type-checking marker
client.py # MIDASClient, create_client, create_auto_client
auth.py # BearerAuth, BasicAuth, AutoTokenAuth, get_token
enums.py # SignalType, RateType, Unit, DayType
entities/
__init__.py # Coercion dispatch functions
models.py # Pydantic models: RateInfo, ValueData, RinListEntry, Holiday, LookupEntry
tests/
test_entities.py # Entity coercion from raw fixture dicts
test_client.py # HTTP client tests with pytest-httpx
test_auth.py # Token parsing, expiry, auth headers
test_integration.py # Live API tests (requires MIDAS credentials)
# Install with dev dependencies
pip install -e ".[dev]"
# Lint
ruff check src/ tests/The test suite has two tiers:
Unit tests run entirely offline using fixture dicts and mocked HTTP (pytest-httpx):
pytest -m "not integration"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:
export MIDAS_USERNAME="you@example.com"
export MIDAS_PASSWORD="your-password"
pytest -m integrationIntegration 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).
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.
- midas-api-specs — OpenAPI specifications for the MIDAS API, derived from documentation and live API validation
- clj-midas — Clojure client for the MIDAS API (Martian-based, spec-driven)
- python-oa3 — Python client for OpenADR 3 (same entity API pattern)
MIT