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.
@@ -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
- 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
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.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")
137
181
 
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"]
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)
@@ -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
- series = series.str.split("-").str[0]
175
- return series.astype(float).astype(int).values
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.1
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=XFamz2-8QbM95gTh-wry9YSE5qc-58RWx9CqwFzY_co,5412
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=gGlPGhVuRddWBWwiaKtL-tFTgNrQMbZdFIiresoEOHc,2100
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=xnTdyvLsRi3DO-7_CckNQuxtq4ghA1ttzPnYQZUoOoA,16387
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=2MaevdYECAj55ShqpHehoD0c2rLQ97avUfM2qBw7eso,9499
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.1.dist-info/METADATA,sha256=kirS790d42WBJrzA_aaAXT61w7ycKAzNGeT27sea_XQ,2803
35
- python_esios-2.0.1.dist-info/WHEEL,sha256=QccIxa26bgl1E6uMy58deGWi-0aeIkkangHcxk2kWfw,87
36
- python_esios-2.0.1.dist-info/entry_points.txt,sha256=7ngseyIyvJ4buTHFL9htaZ4tTFHpG4zzJNkc8B5Jr8U,40
37
- python_esios-2.0.1.dist-info/licenses/LICENSE,sha256=LorLs1-VeBW70Wo9fLAtLJN7nNd6Poy0xzvqdWVqFlE,35128
38
- python_esios-2.0.1.dist-info/RECORD,,
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,,