python-esios 2.3.0__py3-none-any.whl → 2.4.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.
esios/constants.py CHANGED
@@ -14,7 +14,17 @@ MAX_RETRIES = 3
14
14
  RETRY_MIN_WAIT = 2 # seconds
15
15
  RETRY_MAX_WAIT = 10 # seconds
16
16
 
17
- # ESIOS API limits responses to ~3 weeks of data per request
18
- CHUNK_SIZE_DAYS = 21
17
+ # ESIOS API chunk sizes for historical data fetching.
18
+ # High-geo indicators (40+ geos) timeout (504) at >21 days.
19
+ # Low-geo indicators handle 6+ months per request in <0.1s.
20
+ CHUNK_SIZE_DAYS = 21 # Legacy default, kept for backward compat
21
+ CHUNK_SIZE_DAYS_LOW_GEO = 180 # 6 months for indicators with few geos
22
+ CHUNK_SIZE_DAYS_HIGH_GEO = 21 # Conservative for indicators with many geos
23
+ HIGH_GEO_THRESHOLD = 15 # Indicators with >= this many geos use smaller chunks
24
+
25
+ # Concurrent chunk fetching within a single indicator.
26
+ # 4 workers gives ~17-95x speedup over sequential with no errors.
27
+ # Diminishing returns past 4 (ESIOS server becomes the bottleneck).
28
+ DEFAULT_CHUNK_WORKERS = 4
19
29
 
20
30
  TIMEZONE = "Europe/Madrid"
@@ -3,13 +3,20 @@
3
3
  from __future__ import annotations
4
4
 
5
5
  import logging
6
+ from concurrent.futures import ThreadPoolExecutor, as_completed
6
7
  from datetime import timedelta
7
8
  from typing import Any
8
9
 
9
10
  import pandas as pd
10
11
 
11
12
  from esios.cache import CacheStore
12
- from esios.constants import CHUNK_SIZE_DAYS, TIMEZONE
13
+ from esios.constants import (
14
+ CHUNK_SIZE_DAYS_HIGH_GEO,
15
+ CHUNK_SIZE_DAYS_LOW_GEO,
16
+ DEFAULT_CHUNK_WORKERS,
17
+ HIGH_GEO_THRESHOLD,
18
+ TIMEZONE,
19
+ )
13
20
  from esios.managers.base import BaseManager
14
21
  from esios.models.indicator import Indicator
15
22
  from esios.processing.dataframes import to_dataframe
@@ -131,6 +138,88 @@ class IndicatorHandle:
131
138
  f"Available: {', '.join(available)}"
132
139
  )
133
140
 
141
+ @property
142
+ def _chunk_days(self) -> int:
143
+ """Choose chunk size based on indicator's geo count.
144
+
145
+ ESIOS API times out (504) for high-geo indicators (40+ geos) with
146
+ windows larger than ~3 weeks. Low-geo indicators handle 6+ months
147
+ per request in <0.1s.
148
+
149
+ When geos are unknown (empty metadata), uses the conservative
150
+ chunk size to avoid timeouts on first fetch.
151
+ """
152
+ geo_count = len(self.geos)
153
+ if geo_count == 0:
154
+ # Unknown geo count — be conservative
155
+ return CHUNK_SIZE_DAYS_HIGH_GEO
156
+ if geo_count >= HIGH_GEO_THRESHOLD:
157
+ return CHUNK_SIZE_DAYS_HIGH_GEO
158
+ return CHUNK_SIZE_DAYS_LOW_GEO
159
+
160
+ def _fetch_one(
161
+ self, start: str, end: str, base_params: dict[str, Any],
162
+ ) -> list[dict]:
163
+ """Fetch a single date-range chunk from the ESIOS API."""
164
+ params = {
165
+ **base_params,
166
+ "start_date": start,
167
+ "end_date": end + "T23:59:59",
168
+ }
169
+ logger.debug("Fetch %s → %s", start, end)
170
+ data = self._manager._get(f"indicators/{self.id}", params=params)
171
+ return data.get("indicator", {}).get("values", [])
172
+
173
+ def _fetch_chunks(
174
+ self,
175
+ gaps: list,
176
+ base_params: dict[str, Any],
177
+ max_workers: int = DEFAULT_CHUNK_WORKERS,
178
+ ) -> list[dict]:
179
+ """Fetch all gap chunks concurrently, return values in order.
180
+
181
+ Builds a list of (start, end) chunks from the gaps, then fetches
182
+ them in parallel using a thread pool. Results are reassembled in
183
+ chronological order.
184
+ """
185
+ chunk_delta = timedelta(days=self._chunk_days)
186
+
187
+ # Build chunk list
188
+ chunks: list[tuple[str, str]] = []
189
+ for gap in gaps:
190
+ current = gap.start
191
+ while current <= gap.end:
192
+ chunk_end = min(current + chunk_delta, gap.end)
193
+ chunks.append((
194
+ current.strftime("%Y-%m-%d"),
195
+ chunk_end.strftime("%Y-%m-%d"),
196
+ ))
197
+ current = chunk_end + timedelta(days=1)
198
+
199
+ if not chunks:
200
+ return []
201
+
202
+ if len(chunks) == 1:
203
+ return self._fetch_one(chunks[0][0], chunks[0][1], base_params)
204
+
205
+ # Fetch concurrently, preserve order
206
+ results: list[list[dict] | None] = [None] * len(chunks)
207
+ with ThreadPoolExecutor(max_workers=max_workers) as pool:
208
+ futures = {
209
+ pool.submit(self._fetch_one, s, e, base_params): i
210
+ for i, (s, e) in enumerate(chunks)
211
+ }
212
+ for future in as_completed(futures):
213
+ idx = futures[future]
214
+ results[idx] = future.result()
215
+
216
+ # Flatten in chronological order
217
+ all_values: list[dict] = []
218
+ for chunk_values in results:
219
+ if chunk_values:
220
+ all_values.extend(chunk_values)
221
+ return all_values
222
+
134
223
  def historical(
135
224
  self,
136
225
  start: str,
@@ -143,11 +232,15 @@ class IndicatorHandle:
143
232
  time_trunc: str | None = None,
144
233
  geo_trunc: str | None = None,
145
234
  column_name: str | None = None,
235
+ chunk_workers: int = DEFAULT_CHUNK_WORKERS,
146
236
  ) -> pd.DataFrame:
147
237
  """Fetch historical values as a DataFrame with DatetimeIndex.
148
238
 
149
239
  Uses local parquet cache when enabled. Only fetches missing date ranges
150
- from the API. Automatically chunks requests exceeding ~3 weeks.
240
+ from the API. Automatically chunks requests and fetches concurrently.
241
+
242
+ Chunk size adapts to the indicator's geo count: 180 days for low-geo
243
+ indicators, 21 days for high-geo (≥15 geos) to avoid ESIOS timeouts.
151
244
 
152
245
  When multiple geo_ids are present (e.g. indicator 600 returns data for
153
246
  several countries), the result is pivoted so each geo becomes a column
@@ -158,6 +251,8 @@ class IndicatorHandle:
158
251
  Useful for single-column results where a stable name like
159
252
  ``"value"`` is preferred over the default geo_name or
160
253
  indicator ID.
254
+ chunk_workers: Number of concurrent threads for fetching chunks.
255
+ Defaults to 4. Set to 1 for sequential fetching.
161
256
  """
162
257
  base_params: dict[str, Any] = {
163
258
  "locale": locale,
@@ -211,24 +306,8 @@ class IndicatorHandle:
211
306
  from esios.cache import DateRange
212
307
  gaps = [DateRange(start_date, end_date)]
213
308
 
214
- # -- Fetch missing ranges ----------------------------------------------
215
- all_values: list[dict] = []
216
- chunk_delta = timedelta(days=CHUNK_SIZE_DAYS)
217
-
218
- for gap in gaps:
219
- current = gap.start
220
- gap_end = gap.end
221
- while current <= gap_end:
222
- chunk_end = min(current + chunk_delta, gap_end)
223
- params = {
224
- **base_params,
225
- "start_date": current.strftime("%Y-%m-%d"),
226
- "end_date": chunk_end.strftime("%Y-%m-%d") + "T23:59:59",
227
- }
228
- logger.debug("Fetch %s → %s", params["start_date"], params["end_date"])
229
- data = self._manager._get(f"indicators/{self.id}", params=params)
230
- all_values.extend(data.get("indicator", {}).get("values", []))
231
- current = chunk_end + timedelta(days=1)
309
+ # -- Fetch missing ranges (concurrent + adaptive chunk size) -----------
310
+ all_values = self._fetch_chunks(gaps, base_params, max_workers=chunk_workers)
232
311
 
233
312
  # Learn any new geo mappings from the response
234
313
  self._enrich_geo_map(all_values)
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: python-esios
3
- Version: 2.3.0
3
+ Version: 2.4.0
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
@@ -3,7 +3,7 @@ esios/async_client.py,sha256=OVNNZwFbvPyUnu7LVr7X5MdXlk_-AJ1lfkUE0OODlbQ,3452
3
3
  esios/cache.py,sha256=GgbrL9Rc9aLrEWHvXtQOCGQRgq2T4m6VBJDvBJfWMTk,18920
4
4
  esios/catalog.py,sha256=xWwMx5I32m34npjAXHh-Ua4e_0pfG89yxUC_Vy9VlAA,16811
5
5
  esios/client.py,sha256=rLgdyPFII6CC_TJwgkHaScJ7nBUpt85N94mujKAn0d0,5825
6
- esios/constants.py,sha256=pwB2UlBI96zYBA8wAbcCSHcm_E-aIj2hBarDA8t1Vp8,474
6
+ esios/constants.py,sha256=yfxSNG37i4dkpa7x0CBvXTroyddn5jhNTuWGDhAq3-0,1074
7
7
  esios/exceptions.py,sha256=AiWLdRDWj50JEsld9CvVBsfLnZZKFmW62_bZmZ7Z_eA,899
8
8
  esios/.agents/skills/esios/SKILL.md,sha256=_5wCzMMB8FHWcAPeMA5vGklZFEGBEvU5wBOryNIogzM,6252
9
9
  esios/cli/__init__.py,sha256=9gd5ZDIH1-yNP_xcd60ethOFXm9w6un0CJ9CX0Qvb2A,256
@@ -24,7 +24,7 @@ esios/data/time_periods.yaml,sha256=oyisKYYyOGA57eEAqkFFx6B3x9rdSl0DokZe5gNZfMw,
24
24
  esios/managers/__init__.py,sha256=-1AwL7arUf7WEZn1RSiK_DZhY3j6U4GE9_dqjbukCJc,268
25
25
  esios/managers/archives.py,sha256=PG-1gQYEiJUVQQtTKIZeEoWIsS-gkWT3ZHy89c8tTW8,9293
26
26
  esios/managers/base.py,sha256=7XcdrUtUOPuqfHYlz4w562TD8o9cNdBWOgs4CHHonoo,835
27
- esios/managers/indicators.py,sha256=nbmKsvBTPO2w3FlcVYv9WOtCTU8xMjFvT_d1AcA2sbg,17506
27
+ esios/managers/indicators.py,sha256=4f1wLhT33Fc93ixHr51DIzIBqzznJSaoeLfWOT-2EQ0,20260
28
28
  esios/managers/offer_indicators.py,sha256=0MjEKkj77YC2fRSHVTEc7FW6E8AuwwciAXK-bOVEL5Q,4187
29
29
  esios/models/__init__.py,sha256=oppuTASpf0Dh2KbGMXInULT0F4sELjeo-9UhPiPOZiA,289
30
30
  esios/models/archive.py,sha256=P2LaT7_ff4ujwqVn_ofgQP3dbpf7jqON0R22dKwSJ_w,1062
@@ -34,8 +34,8 @@ esios/processing/__init__.py,sha256=1kLt_gO_wDhXM1BbY0zTyfAYo-CjYKW1ljgRRDZ7USM,
34
34
  esios/processing/dataframes.py,sha256=OitzBvAerssGP2VXNC-sSO48XsHdIB2nKTUgByN5eYQ,2524
35
35
  esios/processing/i90.py,sha256=fI8DfY8CD2kF1_ZrAzuEDxN0m7Vh3CV3dIn32lxKffA,11687
36
36
  esios/processing/zip.py,sha256=12LbFHJTdX_h3JG-clEgQ4Haj-kw0UjfopGLlCRXfGM,1913
37
- python_esios-2.3.0.dist-info/METADATA,sha256=KtWOIGA-o9z7mxZD8vryWTOntVZbxNm_LIVaXYhFD3g,3169
38
- python_esios-2.3.0.dist-info/WHEEL,sha256=QccIxa26bgl1E6uMy58deGWi-0aeIkkangHcxk2kWfw,87
39
- python_esios-2.3.0.dist-info/entry_points.txt,sha256=7ngseyIyvJ4buTHFL9htaZ4tTFHpG4zzJNkc8B5Jr8U,40
40
- python_esios-2.3.0.dist-info/licenses/LICENSE,sha256=LorLs1-VeBW70Wo9fLAtLJN7nNd6Poy0xzvqdWVqFlE,35128
41
- python_esios-2.3.0.dist-info/RECORD,,
37
+ python_esios-2.4.0.dist-info/METADATA,sha256=STVMDUwpgk6ZOx79KXOMPwn-t1aIvhB8MdsBmQtfdkk,3169
38
+ python_esios-2.4.0.dist-info/WHEEL,sha256=QccIxa26bgl1E6uMy58deGWi-0aeIkkangHcxk2kWfw,87
39
+ python_esios-2.4.0.dist-info/entry_points.txt,sha256=7ngseyIyvJ4buTHFL9htaZ4tTFHpG4zzJNkc8B5Jr8U,40
40
+ python_esios-2.4.0.dist-info/licenses/LICENSE,sha256=LorLs1-VeBW70Wo9fLAtLJN7nNd6Poy0xzvqdWVqFlE,35128
41
+ python_esios-2.4.0.dist-info/RECORD,,