python-eia 0.1.0__py3-none-any.whl → 0.2.0__py3-none-any.whl
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.
- eia/cli/__init__.py +10 -0
- eia/cli/_output.py +105 -0
- eia/cli/app.py +57 -0
- eia/cli/config.py +63 -0
- eia/cli/config_cmd.py +34 -0
- eia/cli/exec_cmd.py +78 -0
- eia/cli/facets_cmd.py +73 -0
- eia/cli/get_cmd.py +89 -0
- eia/cli/meta_cmd.py +92 -0
- eia/cli/routes_cmd.py +81 -0
- {python_eia-0.1.0.dist-info → python_eia-0.2.0.dist-info}/METADATA +7 -5
- python_eia-0.2.0.dist-info/RECORD +16 -0
- {python_eia-0.1.0.dist-info → python_eia-0.2.0.dist-info}/WHEEL +1 -1
- python_eia-0.2.0.dist-info/entry_points.txt +2 -0
- python_eia-0.1.0.dist-info/RECORD +0 -5
eia/cli/__init__.py
ADDED
eia/cli/_output.py
ADDED
|
@@ -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)
|
eia/cli/app.py
ADDED
|
@@ -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()
|
eia/cli/config.py
ADDED
|
@@ -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)
|
eia/cli/config_cmd.py
ADDED
|
@@ -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}")
|
eia/cli/exec_cmd.py
ADDED
|
@@ -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)
|
eia/cli/facets_cmd.py
ADDED
|
@@ -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)
|
eia/cli/get_cmd.py
ADDED
|
@@ -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)
|
eia/cli/meta_cmd.py
ADDED
|
@@ -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)
|
eia/cli/routes_cmd.py
ADDED
|
@@ -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)
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: python-eia
|
|
3
|
-
Version: 0.
|
|
3
|
+
Version: 0.2.0
|
|
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.
|
|
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,16 @@
|
|
|
1
|
+
eia/__init__.py,sha256=twAaCUfzQE4vBKQ8HIH1G_VKNiWau_yYI9ou054babM,223
|
|
2
|
+
eia/client.py,sha256=O-EEYVVNSr2yGWE94-KnNjc3tD1TAgtlhwwq6gnkkOM,33514
|
|
3
|
+
eia/cli/__init__.py,sha256=KCvyQU45hrJdWk3cra4mSHhW-u_5qFeQDfeE5KkDdN4,243
|
|
4
|
+
eia/cli/_output.py,sha256=mhD1j5TyuHtw5BcjwUvHZ8-a87xpBPcEKtoPqMk0-pA,2618
|
|
5
|
+
eia/cli/app.py,sha256=UxFrnxzwopZLeWXQ_U2S--BChIoBEu-5g58E20vGZ3w,1560
|
|
6
|
+
eia/cli/config.py,sha256=V_1wiXixPPIgVivIUDuAIS9aL9JcPu0qTshlVw9XDxQ,1657
|
|
7
|
+
eia/cli/config_cmd.py,sha256=rUdnM-G3UHSoiBqfqxHXrRDdf7TFjAD6t8QLkuTJ0Ao,917
|
|
8
|
+
eia/cli/exec_cmd.py,sha256=DkP8Bcm9RNjdC-D1E6k0skRJmGH1uRy8ALpyXFdr488,2776
|
|
9
|
+
eia/cli/facets_cmd.py,sha256=urpbAtqsZKnNn-Hiqf8hR8tAyZCd9Hm7xJmSH3YONuo,2208
|
|
10
|
+
eia/cli/get_cmd.py,sha256=MtHQ4yM_mHqAf7YdeYZUqL7lfd6w3-tSVMQPSHIVM94,3036
|
|
11
|
+
eia/cli/meta_cmd.py,sha256=p8JObyCMG-VnPnMjKVYpVGTJh38SU_eQ9zp2x7ET6LA,3112
|
|
12
|
+
eia/cli/routes_cmd.py,sha256=FuK90wjFklUb5KtUWDoufEYjc2b5KhE-KpsPWXc4iFM,2427
|
|
13
|
+
python_eia-0.2.0.dist-info/METADATA,sha256=yY4cIa5TxOSqrBwAqpCEiNGHk5_nYAbySwAyl4A6VpA,2083
|
|
14
|
+
python_eia-0.2.0.dist-info/WHEEL,sha256=QccIxa26bgl1E6uMy58deGWi-0aeIkkangHcxk2kWfw,87
|
|
15
|
+
python_eia-0.2.0.dist-info/entry_points.txt,sha256=oi4FTIzWeuoYDkpi_yywwRlEMBMLceEVJQKKD7zrRO0,36
|
|
16
|
+
python_eia-0.2.0.dist-info/RECORD,,
|
|
@@ -1,5 +0,0 @@
|
|
|
1
|
-
eia/__init__.py,sha256=twAaCUfzQE4vBKQ8HIH1G_VKNiWau_yYI9ou054babM,223
|
|
2
|
-
eia/client.py,sha256=O-EEYVVNSr2yGWE94-KnNjc3tD1TAgtlhwwq6gnkkOM,33514
|
|
3
|
-
python_eia-0.1.0.dist-info/METADATA,sha256=PEA93c9o1e0A_Yxz0AD-b7phE5JZ2E4R93ulqOOloZc,2051
|
|
4
|
-
python_eia-0.1.0.dist-info/WHEEL,sha256=qtCwoSJWgHk21S1Kb4ihdzI2rlJ1ZKaIurTj_ngOhyQ,87
|
|
5
|
-
python_eia-0.1.0.dist-info/RECORD,,
|