Skip to content

Commit b54f94a

Browse files
committed
feat: add archives sheets and exec CLI commands for I90 processing
Add two new CLI commands to close the gap between downloading and analyzing I90 files: - `esios archives sheets` — lists sheet TOC for an archive file - `esios archives exec` — parses sheets, evaluates pandas expressions, renders output (table/csv/json). Supports single date and date ranges. Updates the embedded agent skill with CLI examples and key sheet reference.
1 parent e8ffe78 commit b54f94a

2 files changed

Lines changed: 200 additions & 4 deletions

File tree

src/esios/.agents/skills/esios/SKILL.md

Lines changed: 49 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -39,8 +39,22 @@ esios indicators exec 600 -s 2025-01-01 -e 2025-01-31 --expr "df.resample('D').m
3939
### Archives
4040

4141
```bash
42+
# List all available archives
4243
esios archives list
43-
esios archives download 1 --start 2025-01-01 --end 2025-01-31 --output ./data
44+
45+
# Download archive files
46+
esios archives download 34 --start 2025-05-01 --end 2025-05-31 --output ./data
47+
esios archives download 34 --date 2025-06-01
48+
49+
# List sheets (table of contents) in an I90 file
50+
esios archives sheets 34 --date 2025-06-01
51+
52+
# Parse and query archive data (like indicators exec but for archives)
53+
esios archives exec 34 --sheet I90DIA03 --date 2025-06-01
54+
esios archives exec 34 --sheet I90DIA03 --date 2025-06-01 -x "df.describe()"
55+
esios archives exec 34 --sheet I90DIA03 -s 2025-05-05 -e 2025-06-08 \
56+
-x "df[df['Sentido']=='Bajar'].groupby('Unidad de Programación')['value'].sum().sort_values()"
57+
esios archives exec 34 --sheet I90DIA26 --date 2025-06-01 --format csv --output pbf.csv
4458
```
4559

4660
### Cache Management
@@ -132,11 +146,42 @@ df = client.indicators.compare([600, 10034, 10035], "2025-01-01", "2025-01-07")
132146

133147
## I90 Settlement Files
134148

149+
### CLI (quickest path)
150+
151+
```bash
152+
# Discover available sheets
153+
esios archives sheets 34 --date 2025-06-01
154+
155+
# Key I90DIA sheets:
156+
# I90DIA03 — Restricciones en el Mercado Diario (curtailment)
157+
# I90DIA08 — Restricciones en Tiempo Real
158+
# I90DIA26 — Programa Base de Funcionamiento (PBF, generation program)
159+
# I90DIA01 — Programa PVP
160+
# I90DIA07 — Regulación Terciaria (mFRR)
161+
162+
# Total curtailment by direction
163+
esios archives exec 34 --sheet I90DIA03 --date 2025-06-01 \
164+
-x "df.groupby('Sentido')['value'].sum()"
165+
166+
# Multi-day curtailment analysis
167+
esios archives exec 34 --sheet I90DIA03 -s 2025-05-05 -e 2025-06-08 \
168+
-x "df[df['Sentido']=='Bajar'].groupby('Unidad de Programación')['value'].sum().sort_values().head(20)"
169+
```
170+
171+
### Python library
172+
135173
```python
136-
from esios.processing import I90Book
174+
from esios import ESIOSClient
175+
from esios.processing.i90 import I90Book
176+
177+
# Download + parse in one step
178+
client = ESIOSClient()
179+
archive = client.archives.get(34)
180+
books = I90Book.from_archive(archive, start="2025-05-05", end="2025-06-08")
137181

138-
book = I90Book("path/to/I90DIA_20250101.xls")
139-
sheet = book["3.1"] # Access specific sheet
182+
# Access a sheet
183+
book = books[0]
184+
sheet = book["I90DIA03"]
140185
df = sheet.df # Preprocessed DataFrame with datetime index
141186
print(sheet.frequency) # "hourly" or "hourly-quarterly"
142187
```

src/esios/cli/archives.py

Lines changed: 151 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -64,3 +64,154 @@ def download_archive(
6464
output_dir=output,
6565
)
6666
typer.echo(f"Download complete → {output}")
67+
68+
69+
def _download_files(
70+
archive_id: int,
71+
token: str | None,
72+
date: str | None,
73+
start: str | None,
74+
end: str | None,
75+
) -> list:
76+
"""Download archive files (shared by sheets and exec commands)."""
77+
from esios.cli.app import get_client
78+
79+
if not date and not (start and end):
80+
typer.echo("Provide --date or both --start and --end.", err=True)
81+
raise typer.Exit(1)
82+
83+
client = get_client(token)
84+
85+
if date:
86+
files = client.archives.download(archive_id, date=date)
87+
else:
88+
files = client.archives.download(archive_id, start=start, end=end)
89+
90+
if not files:
91+
typer.echo("No files found for the given date range.", err=True)
92+
raise typer.Exit(1)
93+
94+
return files
95+
96+
97+
@archives_app.command("sheets")
98+
def sheets(
99+
archive_id: int = typer.Argument(..., help="Archive ID (e.g. 34 for I90DIA)"),
100+
date: Optional[str] = typer.Option(None, "--date", "-d", help="Single date (YYYY-MM-DD)"),
101+
start: Optional[str] = typer.Option(None, "--start", "-s", help="Start date (YYYY-MM-DD)"),
102+
end: Optional[str] = typer.Option(None, "--end", "-e", help="End date (YYYY-MM-DD)"),
103+
token: Optional[str] = typer.Option(None, "--token", "-t"),
104+
):
105+
"""List sheets (table of contents) in an archive file.
106+
107+
Downloads and parses the first file to show available sheets.
108+
109+
\b
110+
Examples:
111+
esios archives sheets 34 --date 2025-06-01
112+
esios archives sheets 34 --start 2025-06-01 --end 2025-06-01
113+
"""
114+
from esios.processing.i90 import I90Book
115+
116+
files = _download_files(archive_id, token, date, start, end)
117+
118+
try:
119+
book = I90Book(files[0])
120+
except Exception as exc:
121+
typer.echo(f"Error parsing {files[0].name}: {exc}", err=True)
122+
raise typer.Exit(1)
123+
124+
table = Table(title=f"Sheets in {files[0].name}")
125+
table.add_column("Sheet", style="cyan")
126+
table.add_column("Description")
127+
128+
for sheet_name, description in book.table_of_contents.items():
129+
if not isinstance(sheet_name, str) or not sheet_name.strip():
130+
continue
131+
table.add_row(str(sheet_name), str(description))
132+
133+
console.print(table)
134+
135+
136+
@archives_app.command("exec")
137+
def exec_archive(
138+
archive_id: int = typer.Argument(..., help="Archive ID (e.g. 34 for I90DIA)"),
139+
sheet: str = typer.Option(..., "--sheet", help="Sheet name (e.g. I90DIA03, I90DIA26)"),
140+
date: Optional[str] = typer.Option(None, "--date", "-d", help="Single date (YYYY-MM-DD)"),
141+
start: Optional[str] = typer.Option(None, "--start", "-s", help="Start date (YYYY-MM-DD)"),
142+
end: Optional[str] = typer.Option(None, "--end", "-e", help="End date (YYYY-MM-DD)"),
143+
expr: str = typer.Option("df", "--expr", "-x", help="Python expression to evaluate (df, pd, np available)"),
144+
format: str = typer.Option("table", "--format", "-f", help="Output format: table, csv, json"),
145+
output: Optional[str] = typer.Option(None, "--output", "-o", help="Output file path"),
146+
token: Optional[str] = typer.Option(None, "--token", "-t"),
147+
):
148+
"""Parse archive files and evaluate a Python expression on the data.
149+
150+
Downloads archive files (cached), parses the specified sheet from each
151+
file using I90Book, concatenates the DataFrames, and evaluates the
152+
expression. The parsed data is available as `df` (pandas DataFrame).
153+
`pd` (pandas) and `np` (numpy) are also available.
154+
155+
\b
156+
Examples:
157+
# Show curtailment data for a single day
158+
esios archives exec 34 --sheet I90DIA03 --date 2025-06-01
159+
160+
# Curtailment by technology over a month
161+
esios archives exec 34 --sheet I90DIA03 -s 2025-05-05 -e 2025-06-08 \\
162+
-x "df[df['Sentido']=='Bajar'].groupby('Tecnología')['value'].sum().sort_values()"
163+
164+
# Export PBF generation program to CSV
165+
esios archives exec 34 --sheet I90DIA26 --date 2025-06-01 -f csv -o pbf.csv
166+
167+
# Descriptive statistics
168+
esios archives exec 34 --sheet I90DIA03 --date 2025-06-01 -x "df.describe()"
169+
"""
170+
import numpy as np
171+
import pandas as pd
172+
173+
from esios.processing.i90 import I90Book
174+
175+
files = _download_files(archive_id, token, date, start, end)
176+
177+
# Parse all files and extract the requested sheet
178+
all_dfs = []
179+
for f in files:
180+
try:
181+
book = I90Book(f)
182+
s = book[sheet]
183+
if s.df is not None and not s.df.empty:
184+
all_dfs.append(s.df.reset_index())
185+
except KeyError:
186+
# Sheet not found — show available sheets
187+
try:
188+
book = I90Book(f)
189+
available = list(book.table_of_contents.keys())
190+
except Exception:
191+
available = []
192+
typer.echo(f"Sheet '{sheet}' not found in {f.name}.", err=True)
193+
if available:
194+
typer.echo(f"Available sheets: {', '.join(str(s) for s in available)}", err=True)
195+
raise typer.Exit(1)
196+
except Exception as exc:
197+
typer.echo(f"Warning: skipping {f.name}: {exc}", err=True)
198+
continue
199+
200+
if not all_dfs:
201+
typer.echo("No data extracted from the specified sheet.", err=True)
202+
raise typer.Exit(1)
203+
204+
df = pd.concat(all_dfs, ignore_index=True)
205+
206+
# Evaluate expression
207+
namespace = {"df": df, "pd": pd, "np": np}
208+
try:
209+
result = eval(expr, {"__builtins__": {}}, namespace) # noqa: S307
210+
except Exception as exc:
211+
typer.echo(f"Error evaluating expression: {exc}", err=True)
212+
raise typer.Exit(1)
213+
214+
# Render output — reuse _render from exec_cmd
215+
from esios.cli.exec_cmd import _render
216+
217+
_render(result, format, output)

0 commit comments

Comments
 (0)