python-eia 0.1.0__tar.gz → 0.2.1__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,28 @@
1
+ name: Publish to PyPI
2
+
3
+ on:
4
+ push:
5
+ branches: [main]
6
+
7
+ jobs:
8
+ publish:
9
+ runs-on: ubuntu-latest
10
+ permissions:
11
+ id-token: write # Required for trusted publishing
12
+ steps:
13
+ - uses: actions/checkout@v4
14
+
15
+ - uses: actions/setup-python@v5
16
+ with:
17
+ python-version: "3.12"
18
+
19
+ - name: Install build tools
20
+ run: pip install build
21
+
22
+ - name: Build package
23
+ run: python -m build
24
+
25
+ - name: Publish to PyPI
26
+ uses: pypa/gh-action-pypi-publish@release/v1
27
+ with:
28
+ skip-existing: true
@@ -4,4 +4,5 @@ docs
4
4
  __pycache__
5
5
  .ipynb_checkpoints
6
6
  data
7
- sketch
7
+ sketch
8
+ dist/
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: python-eia
3
- Version: 0.1.0
3
+ Version: 0.2.1
4
4
  Summary: A Python client for the U.S. Energy Information Administration (EIA) API v2
5
5
  Project-URL: Homepage, https://github.com/datons/python-eia
6
6
  Project-URL: Repository, https://github.com/datons/python-eia.git
@@ -13,14 +13,16 @@ Classifier: Intended Audience :: Science/Research
13
13
  Classifier: License :: OSI Approved :: MIT License
14
14
  Classifier: Operating System :: OS Independent
15
15
  Classifier: Programming Language :: Python :: 3
16
- Classifier: Programming Language :: Python :: 3.7
17
- Classifier: Programming Language :: Python :: 3.8
18
- Classifier: Programming Language :: Python :: 3.9
19
16
  Classifier: Programming Language :: Python :: 3.10
20
17
  Classifier: Programming Language :: Python :: 3.11
18
+ Classifier: Programming Language :: Python :: 3.12
19
+ Classifier: Programming Language :: Python :: 3.13
21
20
  Classifier: Topic :: Scientific/Engineering
22
- Requires-Python: >=3.7
21
+ Requires-Python: >=3.10
22
+ Requires-Dist: pandas>=2.0
23
23
  Requires-Dist: requests>=2.31.0
24
+ Requires-Dist: rich>=13.0
25
+ Requires-Dist: typer>=0.9
24
26
  Requires-Dist: urllib3>=2.0.0
25
27
  Provides-Extra: dev
26
28
  Requires-Dist: black>=22.0; extra == 'dev'
@@ -0,0 +1,176 @@
1
+ ---
2
+ name: eia
3
+ description: Query U.S. energy data (EIA API v2). Use when the user asks about U.S. electricity, petroleum, natural gas, or coal data from the EIA.
4
+ version: 1.0.0
5
+ ---
6
+
7
+ # EIA Data Assistant
8
+
9
+ You have access to the `python-eia` CLI and library for querying the U.S. Energy Information Administration (EIA) API v2.
10
+
11
+ The EIA API is a **route-based tree**. You navigate routes to find data endpoints, then query them with facet filters and frequency options.
12
+
13
+ ## CLI Reference
14
+
15
+ ### Explore the API tree
16
+
17
+ ```bash
18
+ # Top-level routes
19
+ eia routes
20
+
21
+ # Navigate deeper
22
+ eia routes electricity
23
+ eia routes electricity/rto
24
+ eia routes electricity/rto/fuel-type-data # Error if leaf — use 'eia meta' instead
25
+ ```
26
+
27
+ ### Inspect a data endpoint
28
+
29
+ ```bash
30
+ # Show facets, frequencies, and data columns
31
+ eia meta electricity/rto/fuel-type-data
32
+ eia meta petroleum/pri/spt
33
+ eia meta natural-gas/pri/sum
34
+ ```
35
+
36
+ ### List facet values
37
+
38
+ ```bash
39
+ # List all values for a facet
40
+ eia facets electricity/rto/fuel-type-data respondent
41
+ eia facets electricity/rto/fuel-type-data fueltype
42
+ eia facets petroleum/pri/spt series --format csv
43
+ ```
44
+
45
+ ### Fetch data
46
+
47
+ ```bash
48
+ # Basic query
49
+ eia get electricity/rto/fuel-type-data \
50
+ --start 2024-06-01 --end 2024-06-08 \
51
+ --frequency hourly \
52
+ --facet respondent=CISO \
53
+ --data value
54
+
55
+ # Multiple facet values (repeat --facet)
56
+ eia get electricity/retail-sales \
57
+ --start 2024-01-01 --end 2024-12-31 \
58
+ --facet stateid=CA --facet sectorid=RES --facet sectorid=COM \
59
+ --data revenue --data sales
60
+
61
+ # Export to CSV
62
+ eia get petroleum/pri/spt --start 2024-01-01 --end 2024-06-01 \
63
+ --format csv --output prices.csv
64
+
65
+ # Sort
66
+ eia get electricity/rto/fuel-type-data \
67
+ --start 2024-06-01 --end 2024-06-02 \
68
+ --frequency hourly --facet respondent=CISO --data value \
69
+ --sort period --sort-dir desc
70
+ ```
71
+
72
+ ### Fetch + eval pandas expression
73
+
74
+ ```bash
75
+ # Descriptive stats
76
+ eia exec electricity/rto/fuel-type-data \
77
+ --start 2024-06-01 --end 2024-06-08 \
78
+ --frequency hourly --facet respondent=CISO --data value \
79
+ -x "df.describe()"
80
+
81
+ # Group by fuel type
82
+ eia exec electricity/rto/fuel-type-data \
83
+ --start 2024-06-01 --end 2024-06-08 \
84
+ --frequency hourly --facet respondent=CISO --data value \
85
+ -x "df.groupby('fueltype')['value'].mean()"
86
+
87
+ # Daily aggregation
88
+ eia exec natural-gas/pri/sum \
89
+ --start 2024-01-01 --end 2024-06-01 \
90
+ -x "df.groupby('process')['value'].mean().sort_values(ascending=False)"
91
+ ```
92
+
93
+ ### Output options (all commands)
94
+
95
+ ```
96
+ --format table|csv|json (default: table)
97
+ --output file.csv (write to file instead of stdout)
98
+ ```
99
+
100
+ ## Common Routes
101
+
102
+ | Route | Description |
103
+ |-------|-------------|
104
+ | `electricity/rto/fuel-type-data` | Real-time grid generation by fuel type |
105
+ | `electricity/rto/region-data` | Real-time grid demand/generation by region |
106
+ | `electricity/rto/interchange-data` | Real-time interchange between regions |
107
+ | `electricity/retail-sales` | Retail electricity sales (monthly/annual) |
108
+ | `electricity/electric-power-operational-data` | Power plant operational data |
109
+ | `petroleum/pri/spt` | Spot petroleum prices (crude, gasoline, etc.) |
110
+ | `petroleum/sum/sndw` | Weekly petroleum supply/demand |
111
+ | `natural-gas/pri/sum` | Natural gas prices summary |
112
+ | `natural-gas/sum/lsum` | Natural gas supply/demand summary |
113
+ | `coal/shipments/receipts` | Coal shipments and receipts |
114
+ | `total-energy/data` | Total energy overview (monthly/annual) |
115
+
116
+ Use `eia routes` to discover more. Use `eia meta <route>` to see exact facets and frequencies.
117
+
118
+ ## Facet Conventions
119
+
120
+ - **Format**: `--facet key=value` (repeatable)
121
+ - **Multiple values**: repeat the flag — `--facet sectorid=RES --facet sectorid=COM`
122
+ - **Common facets**: `respondent` (grid operator), `fueltype`, `stateid`, `sectorid`, `series`
123
+ - Each endpoint has different facets — use `eia meta` and `eia facets` to discover them
124
+
125
+ ## Python Library
126
+
127
+ ```python
128
+ from eia import EIAClient
129
+
130
+ client = EIAClient() # reads config file, then EIA_API_KEY env var
131
+
132
+ # Navigate the route tree
133
+ route = client.route("electricity/rto/fuel-type-data")
134
+ route.routes # Dict of child routes (if branch node)
135
+ route.data # Data object (if leaf node)
136
+
137
+ # Inspect metadata
138
+ route.data.facets # FacetContainer with attribute access
139
+ route.data.frequencies # List[FrequencyInfo]
140
+ route.data.data_columns # Dict[str, DataColumnInfo]
141
+ route.data.facets.respondent.get_values() # List[FacetValue]
142
+
143
+ # Direct access to a data endpoint
144
+ data = client.get_data_endpoint("electricity/rto/fuel-type-data")
145
+
146
+ # Fetch data as DataFrame
147
+ df = data.get(
148
+ data_columns=["value"],
149
+ facets={"respondent": "CISO"},
150
+ frequency="hourly",
151
+ start="2024-01-01",
152
+ end="2024-01-31",
153
+ sort=[{"column": "period", "direction": "desc"}],
154
+ )
155
+ ```
156
+
157
+ ## Configuration
158
+
159
+ ```bash
160
+ # Store your API key persistently (recommended)
161
+ eia config set api-key YOUR_KEY
162
+
163
+ # Verify it's stored
164
+ eia config get api-key
165
+ ```
166
+
167
+ The config file is stored at `~/.config/eia/config.toml`.
168
+
169
+ ## Key Conventions
170
+
171
+ - The `period` column is auto-converted to datetime (UTC for non-local frequencies)
172
+ - The `value` column is auto-converted to numeric
173
+ - Pagination is automatic by default (fetches all pages)
174
+ - API key resolution: config file (`~/.config/eia/config.toml`) > `EIA_API_KEY` env var
175
+ - API page limit is 5000 rows per request
176
+ - Custom exception: `EIAError` (includes HTTP status code and API error code)
@@ -0,0 +1,10 @@
1
+ """EIA CLI — command-line interface for the EIA API."""
2
+
3
+ try:
4
+ from eia.cli.app import app
5
+ except ImportError:
6
+ raise ImportError(
7
+ "CLI dependencies not installed. Install with: pip install python-eia"
8
+ )
9
+
10
+ __all__ = ["app"]
@@ -0,0 +1,105 @@
1
+ """Shared output helpers for the EIA CLI."""
2
+
3
+ from __future__ import annotations
4
+
5
+ from pathlib import Path
6
+ from typing import Any
7
+
8
+ import pandas as pd
9
+ import typer
10
+ from rich.console import Console
11
+ from rich.table import Table
12
+
13
+ console = Console()
14
+
15
+
16
+ def render_dataframe(
17
+ df: pd.DataFrame,
18
+ format: str = "table",
19
+ output: str | None = None,
20
+ max_rows: int = 100,
21
+ ) -> None:
22
+ """Render a DataFrame in the requested format."""
23
+ if format == "csv":
24
+ text = df.to_csv(index=False)
25
+ elif format == "json":
26
+ text = df.to_json(orient="records", indent=2, date_format="iso")
27
+ else:
28
+ _print_rich_table(df, max_rows=max_rows)
29
+ return
30
+
31
+ if output:
32
+ Path(output).write_text(text)
33
+ typer.echo(f"Written to {output}")
34
+ else:
35
+ typer.echo(text)
36
+
37
+
38
+ def render_result(
39
+ result: Any,
40
+ format: str = "table",
41
+ output: str | None = None,
42
+ max_rows: int = 100,
43
+ ) -> None:
44
+ """Render an eval result (DataFrame, Series, or scalar)."""
45
+ if isinstance(result, pd.Series):
46
+ result = result.to_frame()
47
+
48
+ if isinstance(result, pd.DataFrame):
49
+ if format == "csv":
50
+ text = result.to_csv()
51
+ elif format == "json":
52
+ text = result.to_json(orient="records", indent=2, date_format="iso")
53
+ else:
54
+ _print_rich_table(result, max_rows=max_rows, show_index=True)
55
+ return
56
+
57
+ if output:
58
+ Path(output).write_text(text)
59
+ typer.echo(f"Written to {output}")
60
+ else:
61
+ typer.echo(text)
62
+ else:
63
+ text = str(result)
64
+ if output:
65
+ Path(output).write_text(text)
66
+ typer.echo(f"Written to {output}")
67
+ else:
68
+ typer.echo(text)
69
+
70
+
71
+ def _print_rich_table(
72
+ df: pd.DataFrame,
73
+ max_rows: int = 100,
74
+ show_index: bool = False,
75
+ ) -> None:
76
+ """Print a DataFrame as a Rich table."""
77
+ table = Table()
78
+
79
+ if show_index:
80
+ idx_name = str(df.index.name or "")
81
+ table.add_column(idx_name, style="cyan")
82
+
83
+ for col in df.columns:
84
+ table.add_column(str(col))
85
+
86
+ for idx, row in df.head(max_rows).iterrows():
87
+ values = []
88
+ if show_index:
89
+ values.append(str(idx))
90
+ values += [_fmt(row[c]) for c in df.columns]
91
+ table.add_row(*values)
92
+
93
+ if len(df) > max_rows:
94
+ table.caption = f"Showing {max_rows} of {len(df)} rows"
95
+
96
+ console.print(table)
97
+
98
+
99
+ def _fmt(val: Any) -> str:
100
+ """Format a value for table display."""
101
+ if isinstance(val, float):
102
+ return f"{val:.4f}"
103
+ if val is None:
104
+ return ""
105
+ return str(val)
@@ -0,0 +1,57 @@
1
+ """EIA CLI — main Typer application."""
2
+
3
+ from __future__ import annotations
4
+
5
+ import logging
6
+
7
+ import typer
8
+
9
+ from eia.cli.config import get_api_key
10
+
11
+ # Suppress the library's default INFO logging in CLI mode
12
+ logging.getLogger().setLevel(logging.WARNING)
13
+
14
+ app = typer.Typer(
15
+ name="eia",
16
+ help="CLI for the U.S. Energy Information Administration (EIA) API v2.",
17
+ no_args_is_help=True,
18
+ )
19
+
20
+
21
+ def get_client(api_key: str | None = None):
22
+ """Lazy import + construct client."""
23
+ from eia.client import EIAClient
24
+
25
+ resolved = api_key or get_api_key()
26
+ if not resolved:
27
+ typer.echo(
28
+ "Error: No API key. Set EIA_API_KEY or run: eia config set api-key <KEY>",
29
+ err=True,
30
+ )
31
+ raise typer.Exit(1)
32
+ return EIAClient(api_key=resolved)
33
+
34
+
35
+ # -- Register commands --------------------------------------------------------
36
+
37
+ from eia.cli.routes_cmd import routes_command # noqa: E402
38
+ from eia.cli.meta_cmd import meta_command # noqa: E402
39
+ from eia.cli.facets_cmd import facets_command # noqa: E402
40
+ from eia.cli.get_cmd import get_command # noqa: E402
41
+ from eia.cli.exec_cmd import exec_command # noqa: E402
42
+ from eia.cli.config_cmd import config_app # noqa: E402
43
+
44
+ app.command(name="routes")(routes_command)
45
+ app.command(name="meta")(meta_command)
46
+ app.command(name="facets")(facets_command)
47
+ app.command(name="get")(get_command)
48
+ app.command(name="exec")(exec_command)
49
+ app.add_typer(config_app, name="config", help="Configuration management")
50
+
51
+
52
+ def main() -> None:
53
+ app()
54
+
55
+
56
+ if __name__ == "__main__":
57
+ main()
@@ -0,0 +1,63 @@
1
+ """Configuration management — read/write config.toml and env vars."""
2
+
3
+ from __future__ import annotations
4
+
5
+ import os
6
+ from pathlib import Path
7
+
8
+ CONFIG_DIR = Path.home() / ".config" / "eia"
9
+ CONFIG_FILE = CONFIG_DIR / "config.toml"
10
+
11
+
12
+ def get_api_key() -> str | None:
13
+ """Resolve API key: config file > env var."""
14
+ # Try config file first
15
+ if CONFIG_FILE.exists():
16
+ try:
17
+ import tomllib
18
+ except ImportError:
19
+ import tomli as tomllib # type: ignore[no-redef]
20
+
21
+ with open(CONFIG_FILE, "rb") as f:
22
+ config = tomllib.load(f)
23
+ key = config.get("api-key")
24
+ if key:
25
+ return key
26
+
27
+ # Fall back to environment variable
28
+ return os.getenv("EIA_API_KEY")
29
+
30
+
31
+ def set_config(key: str, value: str) -> None:
32
+ """Write a key-value pair to config.toml."""
33
+ CONFIG_DIR.mkdir(parents=True, exist_ok=True)
34
+
35
+ config: dict = {}
36
+ if CONFIG_FILE.exists():
37
+ try:
38
+ import tomllib
39
+ except ImportError:
40
+ import tomli as tomllib # type: ignore[no-redef]
41
+
42
+ with open(CONFIG_FILE, "rb") as f:
43
+ config = tomllib.load(f)
44
+
45
+ config[key] = value
46
+
47
+ # Write as simple TOML
48
+ lines = [f'{k} = "{v}"' for k, v in config.items()]
49
+ CONFIG_FILE.write_text("\n".join(lines) + "\n")
50
+
51
+
52
+ def get_config(key: str) -> str | None:
53
+ """Read a value from config.toml."""
54
+ if not CONFIG_FILE.exists():
55
+ return None
56
+ try:
57
+ import tomllib
58
+ except ImportError:
59
+ import tomli as tomllib # type: ignore[no-redef]
60
+
61
+ with open(CONFIG_FILE, "rb") as f:
62
+ config = tomllib.load(f)
63
+ return config.get(key)
@@ -0,0 +1,34 @@
1
+ """CLI subcommands for configuration management."""
2
+
3
+ from __future__ import annotations
4
+
5
+ import typer
6
+
7
+ config_app = typer.Typer(no_args_is_help=True)
8
+
9
+
10
+ @config_app.command("set")
11
+ def config_set(
12
+ key: str = typer.Argument(..., help="Configuration key (e.g., 'api-key')"),
13
+ value: str = typer.Argument(..., help="Configuration value"),
14
+ ):
15
+ """Set a configuration value."""
16
+ from eia.cli.config import set_config
17
+
18
+ set_config(key, value)
19
+ typer.echo(f"Set {key} = {'***' if 'key' in key.lower() else value}")
20
+
21
+
22
+ @config_app.command("get")
23
+ def config_get(
24
+ key: str = typer.Argument(..., help="Configuration key to read"),
25
+ ):
26
+ """Get a configuration value."""
27
+ from eia.cli.config import get_config
28
+
29
+ val = get_config(key)
30
+ if val is None:
31
+ typer.echo(f"{key}: (not set)")
32
+ else:
33
+ display = "***" if "key" in key.lower() else val
34
+ typer.echo(f"{key} = {display}")
@@ -0,0 +1,78 @@
1
+ """CLI command: fetch data and evaluate a pandas expression."""
2
+
3
+ from __future__ import annotations
4
+
5
+ from typing import Optional
6
+
7
+ import typer
8
+
9
+ from eia.cli._output import render_result
10
+ from eia.cli.get_cmd import _parse_facets
11
+
12
+
13
+ def exec_command(
14
+ route: str = typer.Argument(..., help="Route path to a data endpoint"),
15
+ start: Optional[str] = typer.Option(None, "--start", "-s", help="Start date (e.g. 2024-01-01)"),
16
+ end: Optional[str] = typer.Option(None, "--end", "-e", help="End date (e.g. 2024-01-31)"),
17
+ frequency: Optional[str] = typer.Option(None, "--frequency", help="Data frequency (e.g. hourly, monthly)"),
18
+ facet: Optional[list[str]] = typer.Option(None, "--facet", help="Facet filter as key=value (repeatable)"),
19
+ data: Optional[list[str]] = typer.Option(None, "--data", "-d", help="Data column to include (repeatable)"),
20
+ expr: str = typer.Option("df", "--expr", "-x", help="Python expression to evaluate (df, pd, np available)"),
21
+ format: str = typer.Option("table", "--format", "-f", help="Output format: table, csv, json"),
22
+ output: Optional[str] = typer.Option(None, "--output", "-o", help="Output file path"),
23
+ api_key: Optional[str] = typer.Option(None, "--api-key", "-k", help="EIA API key"),
24
+ ):
25
+ """Fetch data and evaluate a Python expression on it.
26
+
27
+ The fetched data is available as `df` (pandas DataFrame).
28
+ `pd` (pandas) and `np` (numpy) are also available.
29
+
30
+ \b
31
+ Examples:
32
+ eia exec electricity/rto/fuel-type-data \\
33
+ --start 2024-06-01 --end 2024-06-08 \\
34
+ --frequency hourly \\
35
+ --facet respondent=CISO --data value \\
36
+ -x "df.groupby('fueltype')['value'].mean()"
37
+
38
+ eia exec electricity/rto/fuel-type-data \\
39
+ --start 2024-06-01 --end 2024-06-03 \\
40
+ --frequency hourly \\
41
+ --facet respondent=CISO --data value \\
42
+ -x "df.describe()"
43
+ """
44
+ import numpy as np
45
+ import pandas as pd
46
+
47
+ from eia.cli.app import get_client
48
+
49
+ client = get_client(api_key)
50
+
51
+ # Build the Data object
52
+ data_endpoint = client.get_data_endpoint(route)
53
+
54
+ # Parse facets
55
+ facets = _parse_facets(facet) if facet else None
56
+
57
+ # Fetch
58
+ df = data_endpoint.get(
59
+ data_columns=data or None,
60
+ facets=facets,
61
+ frequency=frequency,
62
+ start=start,
63
+ end=end,
64
+ )
65
+
66
+ if df.empty:
67
+ typer.echo("No data returned.")
68
+ raise typer.Exit(0)
69
+
70
+ # Evaluate expression
71
+ namespace = {"df": df, "pd": pd, "np": np}
72
+ try:
73
+ result = eval(expr, {"__builtins__": {}}, namespace) # noqa: S307
74
+ except Exception as exc:
75
+ typer.echo(f"Error evaluating expression: {exc}", err=True)
76
+ raise typer.Exit(1)
77
+
78
+ render_result(result, format=format, output=output)
@@ -0,0 +1,73 @@
1
+ """CLI command: list facet values for a data endpoint."""
2
+
3
+ from __future__ import annotations
4
+
5
+ from typing import Optional
6
+
7
+ import typer
8
+ from rich.console import Console
9
+ from rich.table import Table
10
+
11
+ console = Console()
12
+
13
+
14
+ def facets_command(
15
+ route: str = typer.Argument(..., help="Route path to a data endpoint"),
16
+ facet_id: str = typer.Argument(..., help="Facet ID to list values for (e.g. 'respondent', 'fueltype')"),
17
+ format: str = typer.Option("table", "--format", "-f", help="Output format: table, csv, json"),
18
+ output: Optional[str] = typer.Option(None, "--output", "-o", help="Output file path"),
19
+ api_key: Optional[str] = typer.Option(None, "--api-key", "-k", help="EIA API key"),
20
+ ):
21
+ """List available values for a facet on a data endpoint.
22
+
23
+ \b
24
+ Examples:
25
+ eia facets electricity/rto/fuel-type-data respondent
26
+ eia facets electricity/rto/fuel-type-data fueltype --format csv
27
+ """
28
+ from eia.cli.app import get_client
29
+
30
+ client = get_client(api_key)
31
+
32
+ response = client.get_facet_values(route, facet_id)
33
+ facet_values = response.get("facets", [])
34
+
35
+ if not facet_values:
36
+ typer.echo(f"No values found for facet '{facet_id}' at '{route}'.")
37
+ raise typer.Exit(0)
38
+
39
+ if format == "json":
40
+ import json
41
+
42
+ text = json.dumps(facet_values, indent=2)
43
+ if output:
44
+ from pathlib import Path
45
+ Path(output).write_text(text)
46
+ typer.echo(f"Written to {output}")
47
+ else:
48
+ typer.echo(text)
49
+ return
50
+
51
+ if format == "csv":
52
+ lines = ["id,name"]
53
+ for v in facet_values:
54
+ name = (v.get("name") or "").replace(",", ";")
55
+ lines.append(f"{v['id']},{name}")
56
+ text = "\n".join(lines)
57
+ if output:
58
+ from pathlib import Path
59
+ Path(output).write_text(text)
60
+ typer.echo(f"Written to {output}")
61
+ else:
62
+ typer.echo(text)
63
+ return
64
+
65
+ # Rich table
66
+ table = Table(title=f"Facet: {facet_id}")
67
+ table.add_column("ID", style="cyan")
68
+ table.add_column("Name")
69
+
70
+ for v in facet_values:
71
+ table.add_row(v.get("id", ""), v.get("name", ""))
72
+
73
+ console.print(table)
@@ -0,0 +1,89 @@
1
+ """CLI command: fetch data from a data endpoint."""
2
+
3
+ from __future__ import annotations
4
+
5
+ from typing import Optional
6
+
7
+ import typer
8
+
9
+ from eia.cli._output import render_dataframe
10
+
11
+
12
+ def _parse_facets(facet_args: list[str]) -> dict[str, str | list[str]]:
13
+ """Parse --facet key=value arguments into a dict.
14
+
15
+ Multiple values for the same key are collected into a list.
16
+ """
17
+ facets: dict[str, str | list[str]] = {}
18
+ for arg in facet_args:
19
+ if "=" not in arg:
20
+ typer.echo(f"Error: Invalid facet format '{arg}'. Use key=value.", err=True)
21
+ raise typer.Exit(1)
22
+ key, value = arg.split("=", 1)
23
+ if key in facets:
24
+ existing = facets[key]
25
+ if isinstance(existing, list):
26
+ existing.append(value)
27
+ else:
28
+ facets[key] = [existing, value]
29
+ else:
30
+ facets[key] = value
31
+ return facets
32
+
33
+
34
+ def get_command(
35
+ route: str = typer.Argument(..., help="Route path to a data endpoint"),
36
+ start: Optional[str] = typer.Option(None, "--start", "-s", help="Start date (e.g. 2024-01-01)"),
37
+ end: Optional[str] = typer.Option(None, "--end", "-e", help="End date (e.g. 2024-01-31)"),
38
+ frequency: Optional[str] = typer.Option(None, "--frequency", help="Data frequency (e.g. hourly, monthly)"),
39
+ facet: Optional[list[str]] = typer.Option(None, "--facet", help="Facet filter as key=value (repeatable)"),
40
+ data: Optional[list[str]] = typer.Option(None, "--data", "-d", help="Data column to include (repeatable)"),
41
+ sort_col: Optional[str] = typer.Option(None, "--sort", help="Sort column"),
42
+ sort_dir: str = typer.Option("asc", "--sort-dir", help="Sort direction: asc or desc"),
43
+ format: str = typer.Option("table", "--format", "-f", help="Output format: table, csv, json"),
44
+ output: Optional[str] = typer.Option(None, "--output", "-o", help="Output file path"),
45
+ api_key: Optional[str] = typer.Option(None, "--api-key", "-k", help="EIA API key"),
46
+ ):
47
+ """Fetch data from an EIA data endpoint.
48
+
49
+ \b
50
+ Examples:
51
+ eia get electricity/rto/fuel-type-data \\
52
+ --start 2024-06-01 --end 2024-06-08 \\
53
+ --frequency hourly \\
54
+ --facet respondent=CISO \\
55
+ --data value
56
+
57
+ eia get petroleum/pri/spt --start 2024-01-01 --end 2024-06-01 \\
58
+ --format csv --output prices.csv
59
+ """
60
+ from eia.cli.app import get_client
61
+
62
+ client = get_client(api_key)
63
+
64
+ # Build the Data object
65
+ data_endpoint = client.get_data_endpoint(route)
66
+
67
+ # Parse facets
68
+ facets = _parse_facets(facet) if facet else None
69
+
70
+ # Sort
71
+ sort = None
72
+ if sort_col:
73
+ sort = [{"column": sort_col, "direction": sort_dir}]
74
+
75
+ # Fetch
76
+ df = data_endpoint.get(
77
+ data_columns=data or None,
78
+ facets=facets,
79
+ frequency=frequency,
80
+ start=start,
81
+ end=end,
82
+ sort=sort,
83
+ )
84
+
85
+ if df.empty:
86
+ typer.echo("No data returned.")
87
+ raise typer.Exit(0)
88
+
89
+ render_dataframe(df, format=format, output=output)
@@ -0,0 +1,92 @@
1
+ """CLI command: inspect a data endpoint's metadata."""
2
+
3
+ from __future__ import annotations
4
+
5
+ from typing import Optional
6
+
7
+ import typer
8
+ from rich.console import Console
9
+ from rich.panel import Panel
10
+ from rich.table import Table
11
+
12
+ console = Console()
13
+
14
+
15
+ def meta_command(
16
+ route: str = typer.Argument(..., help="Route path to a data endpoint (e.g. 'electricity/rto/fuel-type-data')"),
17
+ format: str = typer.Option("table", "--format", "-f", help="Output format: table, json"),
18
+ api_key: Optional[str] = typer.Option(None, "--api-key", "-k", help="EIA API key"),
19
+ ):
20
+ """Show facets, frequencies, and data columns for a data endpoint.
21
+
22
+ \b
23
+ Examples:
24
+ eia meta electricity/rto/fuel-type-data
25
+ eia meta petroleum/pri/spt --format json
26
+ """
27
+ from eia.cli.app import get_client
28
+
29
+ client = get_client(api_key)
30
+
31
+ metadata = client.get_metadata(route)
32
+
33
+ if "routes" in metadata and "facets" not in metadata:
34
+ typer.echo(f"'{route}' is not a data endpoint — it has child routes.")
35
+ typer.echo("Use 'eia routes' to explore, or navigate deeper.")
36
+ raise typer.Exit(1)
37
+
38
+ if format == "json":
39
+ import json
40
+
41
+ typer.echo(json.dumps(metadata, indent=2))
42
+ return
43
+
44
+ # --- Rich formatted output ---
45
+
46
+ name = metadata.get("name", route)
47
+ description = metadata.get("description", "")
48
+ start_period = metadata.get("startPeriod", "?")
49
+ end_period = metadata.get("endPeriod", "?")
50
+ default_freq = metadata.get("defaultFrequency", "?")
51
+
52
+ header = f"[bold]{name}[/bold]\n{description}\n\nPeriod: {start_period} → {end_period} | Default frequency: {default_freq}"
53
+ console.print(Panel(header, title=f"[cyan]{route}[/cyan]"))
54
+
55
+ # Frequencies
56
+ freqs = metadata.get("frequency", [])
57
+ if freqs:
58
+ freq_table = Table(title="Frequencies")
59
+ freq_table.add_column("ID", style="cyan")
60
+ freq_table.add_column("Description")
61
+ freq_table.add_column("Format")
62
+ for f in freqs:
63
+ if isinstance(f, dict):
64
+ freq_table.add_row(f.get("id", ""), f.get("description", ""), f.get("format", ""))
65
+ console.print(freq_table)
66
+
67
+ # Facets
68
+ facets = metadata.get("facets", [])
69
+ if facets:
70
+ facet_table = Table(title="Facets")
71
+ facet_table.add_column("ID", style="cyan")
72
+ facet_table.add_column("Description")
73
+ for fct in facets:
74
+ if isinstance(fct, dict):
75
+ facet_table.add_row(fct.get("id", ""), fct.get("description", ""))
76
+ console.print(facet_table)
77
+
78
+ # Data columns
79
+ data_cols = metadata.get("data", {})
80
+ if data_cols:
81
+ col_table = Table(title="Data Columns")
82
+ col_table.add_column("ID", style="cyan")
83
+ col_table.add_column("Alias")
84
+ col_table.add_column("Units")
85
+ for col_id, col_data in data_cols.items():
86
+ if isinstance(col_data, dict):
87
+ col_table.add_row(
88
+ col_id,
89
+ col_data.get("alias", ""),
90
+ col_data.get("units", ""),
91
+ )
92
+ console.print(col_table)
@@ -0,0 +1,81 @@
1
+ """CLI command: explore API route tree."""
2
+
3
+ from __future__ import annotations
4
+
5
+ from typing import Optional
6
+
7
+ import typer
8
+ from rich.console import Console
9
+ from rich.table import Table
10
+
11
+ console = Console()
12
+
13
+
14
+ def routes_command(
15
+ route: Optional[str] = typer.Argument(None, help="Route path (e.g. 'electricity' or 'electricity/rto')"),
16
+ format: str = typer.Option("table", "--format", "-f", help="Output format: table, csv, json"),
17
+ output: Optional[str] = typer.Option(None, "--output", "-o", help="Output file path"),
18
+ api_key: Optional[str] = typer.Option(None, "--api-key", "-k", help="EIA API key"),
19
+ ):
20
+ """Explore the EIA API route tree.
21
+
22
+ \b
23
+ Examples:
24
+ eia routes # Top-level routes
25
+ eia routes electricity # Child routes of electricity
26
+ eia routes electricity/rto # Deeper navigation
27
+ """
28
+ from eia.cli.app import get_client
29
+
30
+ client = get_client(api_key)
31
+
32
+ slug = route or ""
33
+ metadata = client.get_metadata(slug)
34
+
35
+ routes_list = metadata.get("routes", [])
36
+ if not routes_list:
37
+ typer.echo(f"No child routes found at '{slug or '/'}'.")
38
+ if "data" in metadata or "facets" in metadata:
39
+ typer.echo("This is a data endpoint. Use 'eia meta' to inspect it.")
40
+ raise typer.Exit(0)
41
+
42
+ if format == "json":
43
+ import json
44
+
45
+ text = json.dumps(routes_list, indent=2)
46
+ if output:
47
+ from pathlib import Path
48
+ Path(output).write_text(text)
49
+ typer.echo(f"Written to {output}")
50
+ else:
51
+ typer.echo(text)
52
+ return
53
+
54
+ if format == "csv":
55
+ lines = ["id,name,description"]
56
+ for r in routes_list:
57
+ desc = (r.get("description") or "").replace(",", ";")
58
+ lines.append(f"{r['id']},{r.get('name', '')},{desc}")
59
+ text = "\n".join(lines)
60
+ if output:
61
+ from pathlib import Path
62
+ Path(output).write_text(text)
63
+ typer.echo(f"Written to {output}")
64
+ else:
65
+ typer.echo(text)
66
+ return
67
+
68
+ # Rich table
69
+ table = Table(title=f"Routes: {slug or '/'}")
70
+ table.add_column("ID", style="cyan")
71
+ table.add_column("Name")
72
+ table.add_column("Description")
73
+
74
+ for r in routes_list:
75
+ table.add_row(
76
+ r["id"],
77
+ r.get("name", ""),
78
+ r.get("description", ""),
79
+ )
80
+
81
+ console.print(table)
@@ -4,10 +4,10 @@ build-backend = "hatchling.build"
4
4
 
5
5
  [project]
6
6
  name = "python-eia"
7
- version = "0.1.0"
7
+ version = "0.2.1"
8
8
  description = "A Python client for the U.S. Energy Information Administration (EIA) API v2"
9
9
  readme = "README.md"
10
- requires-python = ">=3.7"
10
+ requires-python = ">=3.10"
11
11
  license = "MIT"
12
12
  authors = [{ name = "Jesus Lopez", email = "jesus.lopez@datons.ai" }]
13
13
  classifiers = [
@@ -17,14 +17,16 @@ classifiers = [
17
17
  "License :: OSI Approved :: MIT License",
18
18
  "Operating System :: OS Independent",
19
19
  "Programming Language :: Python :: 3",
20
- "Programming Language :: Python :: 3.7",
21
- "Programming Language :: Python :: 3.8",
22
- "Programming Language :: Python :: 3.9",
23
20
  "Programming Language :: Python :: 3.10",
24
21
  "Programming Language :: Python :: 3.11",
22
+ "Programming Language :: Python :: 3.12",
23
+ "Programming Language :: Python :: 3.13",
25
24
  "Topic :: Scientific/Engineering",
26
25
  ]
27
- dependencies = ["requests>=2.31.0", "urllib3>=2.0.0"]
26
+ dependencies = ["requests>=2.31.0", "urllib3>=2.0.0", "pandas>=2.0", "typer>=0.9", "rich>=13.0"]
27
+
28
+ [project.scripts]
29
+ eia = "eia.cli:app"
28
30
 
29
31
  [project.optional-dependencies]
30
32
  dev = [
@@ -0,0 +1,176 @@
1
+ ---
2
+ name: eia
3
+ description: Query U.S. energy data (EIA API v2). Use when the user asks about U.S. electricity, petroleum, natural gas, or coal data from the EIA.
4
+ version: 1.0.0
5
+ ---
6
+
7
+ # EIA Data Assistant
8
+
9
+ You have access to the `python-eia` CLI and library for querying the U.S. Energy Information Administration (EIA) API v2.
10
+
11
+ The EIA API is a **route-based tree**. You navigate routes to find data endpoints, then query them with facet filters and frequency options.
12
+
13
+ ## CLI Reference
14
+
15
+ ### Explore the API tree
16
+
17
+ ```bash
18
+ # Top-level routes
19
+ eia routes
20
+
21
+ # Navigate deeper
22
+ eia routes electricity
23
+ eia routes electricity/rto
24
+ eia routes electricity/rto/fuel-type-data # Error if leaf — use 'eia meta' instead
25
+ ```
26
+
27
+ ### Inspect a data endpoint
28
+
29
+ ```bash
30
+ # Show facets, frequencies, and data columns
31
+ eia meta electricity/rto/fuel-type-data
32
+ eia meta petroleum/pri/spt
33
+ eia meta natural-gas/pri/sum
34
+ ```
35
+
36
+ ### List facet values
37
+
38
+ ```bash
39
+ # List all values for a facet
40
+ eia facets electricity/rto/fuel-type-data respondent
41
+ eia facets electricity/rto/fuel-type-data fueltype
42
+ eia facets petroleum/pri/spt series --format csv
43
+ ```
44
+
45
+ ### Fetch data
46
+
47
+ ```bash
48
+ # Basic query
49
+ eia get electricity/rto/fuel-type-data \
50
+ --start 2024-06-01 --end 2024-06-08 \
51
+ --frequency hourly \
52
+ --facet respondent=CISO \
53
+ --data value
54
+
55
+ # Multiple facet values (repeat --facet)
56
+ eia get electricity/retail-sales \
57
+ --start 2024-01-01 --end 2024-12-31 \
58
+ --facet stateid=CA --facet sectorid=RES --facet sectorid=COM \
59
+ --data revenue --data sales
60
+
61
+ # Export to CSV
62
+ eia get petroleum/pri/spt --start 2024-01-01 --end 2024-06-01 \
63
+ --format csv --output prices.csv
64
+
65
+ # Sort
66
+ eia get electricity/rto/fuel-type-data \
67
+ --start 2024-06-01 --end 2024-06-02 \
68
+ --frequency hourly --facet respondent=CISO --data value \
69
+ --sort period --sort-dir desc
70
+ ```
71
+
72
+ ### Fetch + eval pandas expression
73
+
74
+ ```bash
75
+ # Descriptive stats
76
+ eia exec electricity/rto/fuel-type-data \
77
+ --start 2024-06-01 --end 2024-06-08 \
78
+ --frequency hourly --facet respondent=CISO --data value \
79
+ -x "df.describe()"
80
+
81
+ # Group by fuel type
82
+ eia exec electricity/rto/fuel-type-data \
83
+ --start 2024-06-01 --end 2024-06-08 \
84
+ --frequency hourly --facet respondent=CISO --data value \
85
+ -x "df.groupby('fueltype')['value'].mean()"
86
+
87
+ # Daily aggregation
88
+ eia exec natural-gas/pri/sum \
89
+ --start 2024-01-01 --end 2024-06-01 \
90
+ -x "df.groupby('process')['value'].mean().sort_values(ascending=False)"
91
+ ```
92
+
93
+ ### Output options (all commands)
94
+
95
+ ```
96
+ --format table|csv|json (default: table)
97
+ --output file.csv (write to file instead of stdout)
98
+ ```
99
+
100
+ ## Common Routes
101
+
102
+ | Route | Description |
103
+ |-------|-------------|
104
+ | `electricity/rto/fuel-type-data` | Real-time grid generation by fuel type |
105
+ | `electricity/rto/region-data` | Real-time grid demand/generation by region |
106
+ | `electricity/rto/interchange-data` | Real-time interchange between regions |
107
+ | `electricity/retail-sales` | Retail electricity sales (monthly/annual) |
108
+ | `electricity/electric-power-operational-data` | Power plant operational data |
109
+ | `petroleum/pri/spt` | Spot petroleum prices (crude, gasoline, etc.) |
110
+ | `petroleum/sum/sndw` | Weekly petroleum supply/demand |
111
+ | `natural-gas/pri/sum` | Natural gas prices summary |
112
+ | `natural-gas/sum/lsum` | Natural gas supply/demand summary |
113
+ | `coal/shipments/receipts` | Coal shipments and receipts |
114
+ | `total-energy/data` | Total energy overview (monthly/annual) |
115
+
116
+ Use `eia routes` to discover more. Use `eia meta <route>` to see exact facets and frequencies.
117
+
118
+ ## Facet Conventions
119
+
120
+ - **Format**: `--facet key=value` (repeatable)
121
+ - **Multiple values**: repeat the flag — `--facet sectorid=RES --facet sectorid=COM`
122
+ - **Common facets**: `respondent` (grid operator), `fueltype`, `stateid`, `sectorid`, `series`
123
+ - Each endpoint has different facets — use `eia meta` and `eia facets` to discover them
124
+
125
+ ## Python Library
126
+
127
+ ```python
128
+ from eia import EIAClient
129
+
130
+ client = EIAClient() # reads config file, then EIA_API_KEY env var
131
+
132
+ # Navigate the route tree
133
+ route = client.route("electricity/rto/fuel-type-data")
134
+ route.routes # Dict of child routes (if branch node)
135
+ route.data # Data object (if leaf node)
136
+
137
+ # Inspect metadata
138
+ route.data.facets # FacetContainer with attribute access
139
+ route.data.frequencies # List[FrequencyInfo]
140
+ route.data.data_columns # Dict[str, DataColumnInfo]
141
+ route.data.facets.respondent.get_values() # List[FacetValue]
142
+
143
+ # Direct access to a data endpoint
144
+ data = client.get_data_endpoint("electricity/rto/fuel-type-data")
145
+
146
+ # Fetch data as DataFrame
147
+ df = data.get(
148
+ data_columns=["value"],
149
+ facets={"respondent": "CISO"},
150
+ frequency="hourly",
151
+ start="2024-01-01",
152
+ end="2024-01-31",
153
+ sort=[{"column": "period", "direction": "desc"}],
154
+ )
155
+ ```
156
+
157
+ ## Configuration
158
+
159
+ ```bash
160
+ # Store your API key persistently (recommended)
161
+ eia config set api-key YOUR_KEY
162
+
163
+ # Verify it's stored
164
+ eia config get api-key
165
+ ```
166
+
167
+ The config file is stored at `~/.config/eia/config.toml`.
168
+
169
+ ## Key Conventions
170
+
171
+ - The `period` column is auto-converted to datetime (UTC for non-local frequencies)
172
+ - The `value` column is auto-converted to numeric
173
+ - Pagination is automatic by default (fetches all pages)
174
+ - API key resolution: config file (`~/.config/eia/config.toml`) > `EIA_API_KEY` env var
175
+ - API page limit is 5000 rows per request
176
+ - Custom exception: `EIAError` (includes HTTP status code and API error code)
File without changes
File without changes
File without changes