python-esios 2.0.1__py3-none-any.whl → 2.0.2__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.
- esios/.agents/skills/esios/SKILL.md +49 -4
- esios/cli/archives.py +151 -0
- esios/managers/indicators.py +5 -0
- esios/processing/i90.py +25 -4
- {python_esios-2.0.1.dist-info → python_esios-2.0.2.dist-info}/METADATA +1 -1
- {python_esios-2.0.1.dist-info → python_esios-2.0.2.dist-info}/RECORD +9 -9
- {python_esios-2.0.1.dist-info → python_esios-2.0.2.dist-info}/WHEEL +0 -0
- {python_esios-2.0.1.dist-info → python_esios-2.0.2.dist-info}/entry_points.txt +0 -0
- {python_esios-2.0.1.dist-info → python_esios-2.0.2.dist-info}/licenses/LICENSE +0 -0
|
@@ -39,8 +39,22 @@ esios indicators exec 600 -s 2025-01-01 -e 2025-01-31 --expr "df.resample('D').m
|
|
|
39
39
|
### Archives
|
|
40
40
|
|
|
41
41
|
```bash
|
|
42
|
+
# List all available archives
|
|
42
43
|
esios archives list
|
|
43
|
-
|
|
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
|
|
44
58
|
```
|
|
45
59
|
|
|
46
60
|
### Cache Management
|
|
@@ -132,11 +146,42 @@ df = client.indicators.compare([600, 10034, 10035], "2025-01-01", "2025-01-07")
|
|
|
132
146
|
|
|
133
147
|
## I90 Settlement Files
|
|
134
148
|
|
|
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
|
+
|
|
135
173
|
```python
|
|
136
|
-
from esios
|
|
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")
|
|
137
181
|
|
|
138
|
-
|
|
139
|
-
|
|
182
|
+
# Access a sheet
|
|
183
|
+
book = books[0]
|
|
184
|
+
sheet = book["I90DIA03"]
|
|
140
185
|
df = sheet.df # Preprocessed DataFrame with datetime index
|
|
141
186
|
print(sheet.frequency) # "hourly" or "hourly-quarterly"
|
|
142
187
|
```
|
esios/cli/archives.py
CHANGED
|
@@ -64,3 +64,154 @@ def download_archive(
|
|
|
64
64
|
output_dir=output,
|
|
65
65
|
)
|
|
66
66
|
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)
|
esios/managers/indicators.py
CHANGED
|
@@ -385,6 +385,11 @@ class IndicatorsManager(BaseManager):
|
|
|
385
385
|
indicator = Indicator.from_api(raw)
|
|
386
386
|
handle = IndicatorHandle(self, indicator)
|
|
387
387
|
|
|
388
|
+
# Enrich geos from values returned in metadata response.
|
|
389
|
+
# Some indicators (e.g. province-level breakdown) have an empty
|
|
390
|
+
# "geos" field in the metadata but geo_id/geo_name in the values.
|
|
391
|
+
handle._enrich_geo_map(raw.get("values", []))
|
|
392
|
+
|
|
388
393
|
# Persist metadata and geos
|
|
389
394
|
if cache.config.enabled:
|
|
390
395
|
handle._persist_meta()
|
esios/processing/i90.py
CHANGED
|
@@ -30,7 +30,7 @@ def _get_idx_column_start(columns: np.ndarray) -> int:
|
|
|
30
30
|
|
|
31
31
|
def _any_value_greater_than_30(series: np.ndarray) -> bool:
|
|
32
32
|
"""Check if any numeric value exceeds 30 (quarter-hourly indicator)."""
|
|
33
|
-
return any(v > 30 for v in series if isinstance(v, (int, float)) and not np.isnan(v))
|
|
33
|
+
return any(v > 30 for v in series if isinstance(v, (int, float, np.integer, np.floating)) and not np.isnan(v))
|
|
34
34
|
|
|
35
35
|
|
|
36
36
|
class I90Book:
|
|
@@ -164,15 +164,36 @@ class I90Sheet:
|
|
|
164
164
|
return arr
|
|
165
165
|
|
|
166
166
|
def _normalize_datetime_columns(self, columns: np.ndarray) -> np.ndarray:
|
|
167
|
-
"""Normalize time column headers to integer period indices.
|
|
167
|
+
"""Normalize time column headers to integer period indices.
|
|
168
|
+
|
|
169
|
+
Handles three column formats found in I90 files:
|
|
170
|
+
- Sequential integers 1–24 (hourly) or 1–96 (quarterly)
|
|
171
|
+
- H-Q format with dash notation: "1-1", "1-2", "1-3", "1-4", "2-1", …
|
|
172
|
+
- NaN-filler format: [1, NaN, NaN, NaN, 2, …] (one label per hour,
|
|
173
|
+
three trailing NaNs for quarters 2–4)
|
|
174
|
+
"""
|
|
168
175
|
if any(pd.isna(columns)):
|
|
169
176
|
self._n_columns_totals = 3
|
|
170
177
|
else:
|
|
171
178
|
self._n_columns_totals = 2
|
|
172
179
|
|
|
173
180
|
series = pd.Series(columns, dtype=str).ffill()
|
|
174
|
-
|
|
175
|
-
|
|
181
|
+
parts = series.str.split("-")
|
|
182
|
+
hours = parts.str[0].astype(float).astype(int)
|
|
183
|
+
|
|
184
|
+
# H-Q format: any column carries an explicit quarter suffix (e.g. "1-2")
|
|
185
|
+
if (parts.str.len() > 1).any():
|
|
186
|
+
quarters = parts.apply(
|
|
187
|
+
lambda x: int(x[1]) if len(x) > 1 and str(x[1]).isdigit() else 1
|
|
188
|
+
)
|
|
189
|
+
return ((hours - 1) * 4 + quarters).values
|
|
190
|
+
|
|
191
|
+
# NaN-filler quarterly format: after ffill the same hour number repeats
|
|
192
|
+
# four times (quarters share the hour label). Assign sequential indices.
|
|
193
|
+
if hours.duplicated().any():
|
|
194
|
+
return np.arange(1, len(hours) + 1)
|
|
195
|
+
|
|
196
|
+
return hours.values
|
|
176
197
|
|
|
177
198
|
def _preprocess_double_index(
|
|
178
199
|
self, idx: int, columns: np.ndarray
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: python-esios
|
|
3
|
-
Version: 2.0.
|
|
3
|
+
Version: 2.0.2
|
|
4
4
|
Summary: A Python wrapper for the ESIOS API (Spanish electricity market)
|
|
5
5
|
Project-URL: Homepage, https://github.com/datons/python-esios
|
|
6
6
|
Project-URL: Repository, https://github.com/datons/python-esios
|
|
@@ -4,10 +4,10 @@ esios/cache.py,sha256=GgbrL9Rc9aLrEWHvXtQOCGQRgq2T4m6VBJDvBJfWMTk,18920
|
|
|
4
4
|
esios/client.py,sha256=rE06YD70oTFA7Tc9Okf5Rcsntnx9W_3U4A6rbZlfsGc,5695
|
|
5
5
|
esios/constants.py,sha256=pwB2UlBI96zYBA8wAbcCSHcm_E-aIj2hBarDA8t1Vp8,474
|
|
6
6
|
esios/exceptions.py,sha256=AiWLdRDWj50JEsld9CvVBsfLnZZKFmW62_bZmZ7Z_eA,899
|
|
7
|
-
esios/.agents/skills/esios/SKILL.md,sha256=
|
|
7
|
+
esios/.agents/skills/esios/SKILL.md,sha256=Z7ltdPCtJPA4cMAI8_94jy_12laq-356PJ78TqfY2NQ,6984
|
|
8
8
|
esios/cli/__init__.py,sha256=9gd5ZDIH1-yNP_xcd60ethOFXm9w6un0CJ9CX0Qvb2A,256
|
|
9
9
|
esios/cli/app.py,sha256=JL-4QWlzu5UKd6mq3-bEmWn8YR463edGRNUcv6rv8L8,1370
|
|
10
|
-
esios/cli/archives.py,sha256=
|
|
10
|
+
esios/cli/archives.py,sha256=Re9ZMauTiJlHdmiE7F3ZlV2wfaEyShS0C7Z4M2X4Ra8,7715
|
|
11
11
|
esios/cli/cache_cmd.py,sha256=TWOn1UrkxK1paq_bbDHfvBd_OrQYcmoYtUG65evohOA,3165
|
|
12
12
|
esios/cli/config.py,sha256=uYnCEy6NPkzX5tCrwYia4L24J-G9qw-rgTrN8KLDBVQ,1676
|
|
13
13
|
esios/cli/config_cmd.py,sha256=KWZ6Uc4VvmP4wHl0CukqMjZirTjf_FiwFEuP8XI-lNA,923
|
|
@@ -21,7 +21,7 @@ esios/data/catalogs/archives/refresh.py,sha256=VxuUHdtF4rQgN-jq5ewiOge-J3ok7Fiva
|
|
|
21
21
|
esios/managers/__init__.py,sha256=-1AwL7arUf7WEZn1RSiK_DZhY3j6U4GE9_dqjbukCJc,268
|
|
22
22
|
esios/managers/archives.py,sha256=7mK3BSYEPNygMO2pQCXYqNE90QTqfOGY4byBNn76ikQ,9386
|
|
23
23
|
esios/managers/base.py,sha256=7XcdrUtUOPuqfHYlz4w562TD8o9cNdBWOgs4CHHonoo,835
|
|
24
|
-
esios/managers/indicators.py,sha256=
|
|
24
|
+
esios/managers/indicators.py,sha256=GEyHq09TCPnf3ARULS7olJYC6iiom2XrcyhDAf946po,16653
|
|
25
25
|
esios/managers/offer_indicators.py,sha256=0MjEKkj77YC2fRSHVTEc7FW6E8AuwwciAXK-bOVEL5Q,4187
|
|
26
26
|
esios/models/__init__.py,sha256=oppuTASpf0Dh2KbGMXInULT0F4sELjeo-9UhPiPOZiA,289
|
|
27
27
|
esios/models/archive.py,sha256=P2LaT7_ff4ujwqVn_ofgQP3dbpf7jqON0R22dKwSJ_w,1062
|
|
@@ -29,10 +29,10 @@ esios/models/indicator.py,sha256=u1AJyEA3YeOqQFjV08_lzyMaofuCiMoLPjvosls9gfE,111
|
|
|
29
29
|
esios/models/offer_indicator.py,sha256=nA80Y7Yp0utDaDOdZ-ObcWTsAdhvuXlfJjJBpdVQ7Lo,758
|
|
30
30
|
esios/processing/__init__.py,sha256=1kLt_gO_wDhXM1BbY0zTyfAYo-CjYKW1ljgRRDZ7USM,278
|
|
31
31
|
esios/processing/dataframes.py,sha256=OitzBvAerssGP2VXNC-sSO48XsHdIB2nKTUgByN5eYQ,2524
|
|
32
|
-
esios/processing/i90.py,sha256=
|
|
32
|
+
esios/processing/i90.py,sha256=k4RH4lIwIm04ASYnubdQwJ3WM98iLj5l14zwxXBQEBo,10443
|
|
33
33
|
esios/processing/zip.py,sha256=12LbFHJTdX_h3JG-clEgQ4Haj-kw0UjfopGLlCRXfGM,1913
|
|
34
|
-
python_esios-2.0.
|
|
35
|
-
python_esios-2.0.
|
|
36
|
-
python_esios-2.0.
|
|
37
|
-
python_esios-2.0.
|
|
38
|
-
python_esios-2.0.
|
|
34
|
+
python_esios-2.0.2.dist-info/METADATA,sha256=zGyHPDmU_YJLEry8ltNFkDzw07hpBqVJuvumQoxeKNM,2803
|
|
35
|
+
python_esios-2.0.2.dist-info/WHEEL,sha256=QccIxa26bgl1E6uMy58deGWi-0aeIkkangHcxk2kWfw,87
|
|
36
|
+
python_esios-2.0.2.dist-info/entry_points.txt,sha256=7ngseyIyvJ4buTHFL9htaZ4tTFHpG4zzJNkc8B5Jr8U,40
|
|
37
|
+
python_esios-2.0.2.dist-info/licenses/LICENSE,sha256=LorLs1-VeBW70Wo9fLAtLJN7nNd6Poy0xzvqdWVqFlE,35128
|
|
38
|
+
python_esios-2.0.2.dist-info/RECORD,,
|
|
File without changes
|
|
File without changes
|
|
File without changes
|