Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
10 changes: 10 additions & 0 deletions .gitattributes
Original file line number Diff line number Diff line change
@@ -0,0 +1,10 @@
# Reproducible output and captured responses are expandable in GitHub reviews.
tinyercot/_generated.py linguist-generated=true
api_response_fields.json linguist-generated=true
tools/inputs/operations.json linguist-generated=true
tools/inputs/esr-operations.json linguist-generated=true
tools/inputs/products.json linguist-generated=true
tools/inputs/*-sample.json linguist-generated=true
tools/inputs/samples/** linguist-generated=true
tools/inputs/dashboards/** linguist-generated=true
# Hand-maintained mappings, overrides and evidence remain visible.
133 changes: 21 additions & 112 deletions CLAUDE.md
Original file line number Diff line number Diff line change
@@ -1,112 +1,21 @@
# CLAUDE.md

This file provides guidance to Claude Code (claude.ai/code) when working with code in this repository.

## Project Overview

**tinyercot** is a fully-typed Python client for the ERCOT Public API. The entire hand-written codebase is ~230 lines—everything else is auto-generated from ERCOT's OpenAPI spec and response field definitions.

## Commands

### Development Environment

Start an IPython session with dev dependencies and environment variables:
```bash
uv run --env-file .env --group dev -- ipython
```

### Code Generation

Regenerate the client from the ERCOT OpenAPI spec:
```bash
uv run python tools/generate_client.py
```

Regenerate with fresh response field data (requires ERCOT credentials):
```bash
uv run --env-file .env python tools/generate_client.py --refresh
```

Generate a pandas-based client (returns DataFrames instead of TypedDicts):
```bash
uv run python tools/generate_client.py --pandas
```

## Architecture

### Core Components

1. **`tinyercot/_client.py`** (~58 lines)
- OAuth 2.0 authentication with ERCOT's B2C endpoint
- Token caching (1-hour TTL) via `cachetools`
- HTTP client with automatic retries via `httpx-retries`
- `_get()` function for authenticated API calls
- `_to_df()` function for pandas DataFrame conversion with proper type coercion

2. **`tools/generate_client.py`** (~238 lines)
- Fetches ERCOT's OpenAPI spec to extract all endpoints and parameters
- Fetches response field schemas from ERCOT's products API (cached in `api_response_fields.json`)
- Generates `tinyercot/_generated.py` with fully-typed classes and methods
- Maps OpenAPI types → Python types for query parameters
- Maps ERCOT dataTypes → Python types for response fields

3. **`tinyercot/_generated.py`** (AUTO-GENERATED)
- One class per EMIL ID (e.g., `np4_190_cd`)
- Each class contains methods for endpoint suffixes
- Each method has:
- `TypedDict` for params (query parameters)
- `TypedDict` for response rows (individual data records)
- `TypedDict` for full response (meta, links, data)
- **Never edit this file manually**—always regenerate via `generate_client.py`

4. **`api_response_fields.json`**
- Cache of response field schemas for all ERCOT endpoints
- Maps `{emil_id}/{suffix}` → `{field_name: dataType}`
- Regenerate with `--refresh` flag (rate-limited to 1 req/sec)

### Code Generation Flow

```
ERCOT OpenAPI Spec → parse_openapi() → endpoints + tags
↓
ERCOT Products API → fetch_response_fields() → api_response_fields.json
↓
generate() → tinyercot/_generated.py
```

### Type Mapping

**Query Parameters** (from OpenAPI schema):
- `string` → `str`
- `integer` → `int`
- `number` → `Decimal`
- `boolean` → `bool`
- `format: "yyyy-MM-dd"` → `datetime.date`
- `format: "yyyy-MM-ddTH24:mm:ss"` → `datetime.datetime`

**Response Fields** (from ERCOT dataType):
- `VARCHAR` → `str`
- `INTEGER` → `int`
- `DOUBLE` → `Decimal`
- `BOOLEAN` → `bool`
- `DATE` → `datetime.date`
- `DATETIME` → `datetime.datetime`
- `TIME` → `datetime.time`

## Environment Variables

Required in `.env`:
```
ERCOT_USERNAME=your-username
ERCOT_PASSWORD=your-password
ERCOT_SUBSCRIPTION_KEY=your-subscription-key
```

Pass to commands via `uv run --env-file .env`.

## Key Design Principles

1. **Minimize hand-written code**: Use code generation to reduce maintenance burden
2. **Full typing**: All responses and parameters are fully typed via `TypedDict`
3. **Smart caching**: Response fields are cached to avoid slow API calls during generation
4. **Flexible output**: Support both raw JSON (`TypedDict`) and pandas `DataFrame` outputs
# tinyercot

Keep the SDK small and fully typed. Product methods and row models are generated;
handwritten code handles shared transport, typed metadata, and public dashboards.

- `tinyercot/_client.py`: authentication, sync/async retrieval, pagination, metadata, ZIP downloads.
- `tinyercot/_generated.py`: generated typed product namespaces; never edit manually.
- `tinyercot/_dashboards.py`: typed public dashboard payloads and retrieval.
- `tools/generate_client.py`: offline generation from `tools/inputs/operations.json`
and `api_response_fields.json`.
- `tools/inputs/*overrides.json`, `query-defaults.json`, `row-fields.json`: narrowly
documented corrections to ERCOT's inconsistent contracts.
- `tests`: captured public responses, mocked transport, and static typing checks.

Run `uv run python tools/generate_client.py`, `uv run pytest`,
`uv run mypy tinyercot tests/typing_client.py --strict --follow-untyped-imports`,
`uv run ruff check .`, and `uv build`.

No credentials are needed for imports, generation, or normal tests. Live API use
accepts constructor credentials or `ERCOT_USERNAME`, `ERCOT_PASSWORD`, and
`ERCOT_SUBSCRIPTION_KEY`. Never commit credentials. MIS is outside the current scope.
3 changes: 3 additions & 0 deletions MANIFEST.in
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
include api_response_fields.json
recursive-include tools *.py *.json
recursive-include tests *.py
140 changes: 56 additions & 84 deletions README.md
Original file line number Diff line number Diff line change
@@ -1,107 +1,79 @@
# tinyercot

Fully-typed Python client for the ERCOT Public API.
A small, fully typed client for ERCOT public data.

**Why tiny?** The entire hand-written codebase is ~420 lines. Everything else is auto-generated from ERCOT's OpenAPI spec.

## Install

```bash
uv add tinyercot
```

## Setup

```bash
export ERCOT_USERNAME="your-username"
export ERCOT_PASSWORD="your-password"
export ERCOT_SUBSCRIPTION_KEY="your-subscription-key"
```

## Usage

### Single Page
**Development status:** all 242 Public Reports endpoints have typed methods and captured-response tests. The separate ESR API is awaiting an enabled subscription key. MIS is not included.

```python
from datetime import date
import tinyercot

# Returns typed response with .data, .meta, .links
response = tinyercot.np4_190_cd.dam_stlmnt_pnt_prices(
deliveryDateFrom=date(2025, 12, 29),
settlementPoint="HB_HOUSTON",
)

# Convert to pandas DataFrame
df = response.to_df()
```

### Pagination (Sync)
from tinyercot import Client

```python
# Iterator - yields typed rows from all pages
for row in tinyercot.np4_190_cd.dam_stlmnt_pnt_prices_iter(
deliveryDateFrom=date(2025, 12, 29),
):
print(row.settlementPoint, row.settlementPointPrice)

# DataFrame - fetches all pages, returns single DataFrame
df = tinyercot.np4_190_cd.dam_stlmnt_pnt_prices_df(
deliveryDateFrom=date(2025, 12, 29),
)
with Client() as ercot:
prices = ercot.np4_190_cd.dam_stlmnt_pnt_prices_iter(
deliveryDateFrom=date(2026, 1, 1),
deliveryDateTo=date(2026, 1, 2),
settlementPoint="HB_HOUSTON",
)
for price in prices:
print(price.deliveryDate, price.settlementPointPrice)
```

### Pagination (Async + Rate Limited)
Pass `username`, `password`, and `subscription_key` to `Client`, or set
`ERCOT_USERNAME`, `ERCOT_PASSWORD`, and `ERCOT_SUBSCRIPTION_KEY`.
Authentication is lazy; the client refreshes tokens and retries transient failures.

Async methods automatically rate-limit to 20 req/min with retry on 429:
Each product exposes generated methods with named, typed filters and Pydantic row
models. A method returns one `Page[Row]`; its `_iter` variant streams all pages.
`_async` and `_iter_async` provide asynchronous access. Date and decimal values
retain their Python types. Nullable source cells are represented by `None`.

```python
import asyncio
import tinyercot

async def main():
# Async iterator - rate-limited, non-blocking
async for row in tinyercot.np4_190_cd.dam_stlmnt_pnt_prices_iter_async(
deliveryDateFrom=date(2025, 12, 29),
):
print(row.settlementPoint, row.settlementPointPrice)

# Async DataFrame - rate-limited, non-blocking
df = await tinyercot.np4_190_cd.dam_stlmnt_pnt_prices_df_async(
deliveryDateFrom=date(2025, 12, 29),
from pathlib import Path
from tinyercot import Client

with Client() as ercot:
products = ercot.products()
history = ercot.archives("np4-190-cd")
document = history.archives[0]
Path("prices.zip").write_bytes(
ercot.download("np4-190-cd", [document.docId])
)

asyncio.run(main())
bundles = ercot.bundles("np4-190-cd")
Path("monthly.zip").write_bytes(
ercot.download("np4-190-cd", [bundles.bundles[0].docId], kind="bundle")
)
```

## API Pattern

Every endpoint generates 5 methods:

| Method | Returns | Use Case |
|--------|---------|----------|
| `endpoint()` | `Response` | Single page |
| `endpoint_iter()` | `Iterator[Row]` | Stream all pages (sync) |
| `endpoint_df()` | `DataFrame` | All pages as DataFrame (sync) |
| `endpoint_iter_async()` | `AsyncIterator[Row]` | Stream all pages (async, rate-limited) |
| `endpoint_df_async()` | `DataFrame` | All pages as DataFrame (async, rate-limited) |
Archive and bundle listings expose typed metadata. Downloads return the original
ZIP bytes. Listing metadata includes the server's total and page counts; `iter_documents()` streams all listing pages.

## Development

Create a `.env` file with your credentials, then run IPython with dev deps:

```bash
uv run --env-file .env --group dev -- ipython
```

## Regenerate
Runtime dependencies are `httpx`, `httpx-retries`, and `pydantic`. The handwritten
core handles authentication, transport, pagination, and metadata. Product methods
and row models are generated from saved ERCOT operation and field definitions.

```bash
```sh
uv sync --group dev
uv run python tools/generate_client.py
uv run mypy tinyercot tests/typing_client.py --strict --follow-untyped-imports
uv run pytest
uv build
```

With fresh response field data (requires credentials):

```bash
uv run --env-file .env python tools/generate_client.py --refresh
```
Generation fails if an endpoint has no field schema. During discovery only,
`--allow-incomplete` emits available endpoints and reports the missing count.
Public dashboards are available through `ercot.dashboards`: fuel mix, grid
conditions, energy storage, generation outages, DC-tie flows, system prices,
supply/demand, combined wind/solar, system demand, ancillary services, weather,
and the current locational price map.

Source-specific corrections live in `tools/inputs/type-overrides.json`,
`row-fields.json`, and `query-defaults.json`. ERCOT declares hour-ending labels
and offer identifiers as numeric on two reports. The ESR-west report includes
two metadata-only columns absent from its rows and has an invalid default sort;
its generated method uses `SCEDTimestamp` by default. Captured responses support
these small corrections. Large forecasts may need a date filter or a longer
client timeout. Specify `sort` when supplying `dir`. Tests use saved public responses and mocked
HTTP; credentials are not part of the test suite.
Loading