python-esios 2.4.1__tar.gz → 2.4.2__tar.gz

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.
Files changed (83) hide show
  1. python_esios-2.4.2/.release-please-manifest.json +3 -0
  2. {python_esios-2.4.1 → python_esios-2.4.2}/CHANGELOG.md +7 -0
  3. {python_esios-2.4.1 → python_esios-2.4.2}/PKG-INFO +1 -1
  4. {python_esios-2.4.1 → python_esios-2.4.2}/pyproject.toml +1 -1
  5. {python_esios-2.4.1 → python_esios-2.4.2}/src/esios/processing/i90.py +25 -5
  6. {python_esios-2.4.1 → python_esios-2.4.2}/tests/test_i90.py +46 -0
  7. python_esios-2.4.1/.release-please-manifest.json +0 -3
  8. {python_esios-2.4.1 → python_esios-2.4.2}/.github/workflows/release-please.yml +0 -0
  9. {python_esios-2.4.1 → python_esios-2.4.2}/.gitignore +0 -0
  10. {python_esios-2.4.1 → python_esios-2.4.2}/CLAUDE.md +0 -0
  11. {python_esios-2.4.1 → python_esios-2.4.2}/LICENSE +0 -0
  12. {python_esios-2.4.1 → python_esios-2.4.2}/README.md +0 -0
  13. {python_esios-2.4.1 → python_esios-2.4.2}/examples/.gitignore +0 -0
  14. {python_esios-2.4.1 → python_esios-2.4.2}/examples/01_Quickstart/01_Setup and First Query.ipynb +0 -0
  15. {python_esios-2.4.1 → python_esios-2.4.2}/examples/02_Indicators/01_Search Indicators.ipynb +0 -0
  16. {python_esios-2.4.1 → python_esios-2.4.2}/examples/02_Indicators/02_Historical Data.ipynb +0 -0
  17. {python_esios-2.4.1 → python_esios-2.4.2}/examples/02_Indicators/03_Multi-Geography Indicators.ipynb +0 -0
  18. {python_esios-2.4.1 → python_esios-2.4.2}/examples/02_Indicators/04_Compare Indicators.ipynb +0 -0
  19. {python_esios-2.4.1 → python_esios-2.4.2}/examples/02_Indicators/05_Market Prices.ipynb +0 -0
  20. {python_esios-2.4.1 → python_esios-2.4.2}/examples/02_Indicators/06_Generation and Demand.ipynb +0 -0
  21. {python_esios-2.4.1 → python_esios-2.4.2}/examples/03_Archives/01_Download Archives.ipynb +0 -0
  22. {python_esios-2.4.1 → python_esios-2.4.2}/examples/03_Archives/02_I90 Settlement Files.ipynb +0 -0
  23. {python_esios-2.4.1 → python_esios-2.4.2}/examples/04_Caching/01_Cache Management.ipynb +0 -0
  24. {python_esios-2.4.1 → python_esios-2.4.2}/examples/05_Advanced/01_Ad-hoc Pandas Expressions.ipynb +0 -0
  25. {python_esios-2.4.1 → python_esios-2.4.2}/examples/05_Advanced/02_Async Client.ipynb +0 -0
  26. {python_esios-2.4.1 → python_esios-2.4.2}/examples/_specs/01_Quickstart/01_Setup and First Query.yaml +0 -0
  27. {python_esios-2.4.1 → python_esios-2.4.2}/examples/_specs/02_Indicators/01_Search Indicators.yaml +0 -0
  28. {python_esios-2.4.1 → python_esios-2.4.2}/examples/_specs/02_Indicators/02_Historical Data.yaml +0 -0
  29. {python_esios-2.4.1 → python_esios-2.4.2}/examples/_specs/02_Indicators/03_Multi-Geography Indicators.yaml +0 -0
  30. {python_esios-2.4.1 → python_esios-2.4.2}/examples/_specs/02_Indicators/04_Compare Indicators.yaml +0 -0
  31. {python_esios-2.4.1 → python_esios-2.4.2}/examples/_specs/02_Indicators/05_Market Prices.yaml +0 -0
  32. {python_esios-2.4.1 → python_esios-2.4.2}/examples/_specs/02_Indicators/06_Generation and Demand.yaml +0 -0
  33. {python_esios-2.4.1 → python_esios-2.4.2}/examples/_specs/03_Archives/01_Download Archives.yaml +0 -0
  34. {python_esios-2.4.1 → python_esios-2.4.2}/examples/_specs/03_Archives/02_I90 Settlement Files.yaml +0 -0
  35. {python_esios-2.4.1 → python_esios-2.4.2}/examples/_specs/04_Caching/01_Cache Management.yaml +0 -0
  36. {python_esios-2.4.1 → python_esios-2.4.2}/examples/_specs/05_Advanced/01_Ad-hoc Pandas Expressions.yaml +0 -0
  37. {python_esios-2.4.1 → python_esios-2.4.2}/examples/_specs/05_Advanced/02_Async Client.yaml +0 -0
  38. {python_esios-2.4.1 → python_esios-2.4.2}/examples/generate.py +0 -0
  39. {python_esios-2.4.1 → python_esios-2.4.2}/release-please-config.json +0 -0
  40. {python_esios-2.4.1 → python_esios-2.4.2}/src/esios/.agents/skills/esios/SKILL.md +0 -0
  41. {python_esios-2.4.1 → python_esios-2.4.2}/src/esios/__init__.py +0 -0
  42. {python_esios-2.4.1 → python_esios-2.4.2}/src/esios/async_client.py +0 -0
  43. {python_esios-2.4.1 → python_esios-2.4.2}/src/esios/cache.py +0 -0
  44. {python_esios-2.4.1 → python_esios-2.4.2}/src/esios/catalog.py +0 -0
  45. {python_esios-2.4.1 → python_esios-2.4.2}/src/esios/cli/__init__.py +0 -0
  46. {python_esios-2.4.1 → python_esios-2.4.2}/src/esios/cli/app.py +0 -0
  47. {python_esios-2.4.1 → python_esios-2.4.2}/src/esios/cli/archives.py +0 -0
  48. {python_esios-2.4.1 → python_esios-2.4.2}/src/esios/cli/cache_cmd.py +0 -0
  49. {python_esios-2.4.1 → python_esios-2.4.2}/src/esios/cli/catalog_cmd.py +0 -0
  50. {python_esios-2.4.1 → python_esios-2.4.2}/src/esios/cli/config.py +0 -0
  51. {python_esios-2.4.1 → python_esios-2.4.2}/src/esios/cli/config_cmd.py +0 -0
  52. {python_esios-2.4.1 → python_esios-2.4.2}/src/esios/cli/exec_cmd.py +0 -0
  53. {python_esios-2.4.1 → python_esios-2.4.2}/src/esios/cli/indicators.py +0 -0
  54. {python_esios-2.4.1 → python_esios-2.4.2}/src/esios/client.py +0 -0
  55. {python_esios-2.4.1 → python_esios-2.4.2}/src/esios/constants.py +0 -0
  56. {python_esios-2.4.1 → python_esios-2.4.2}/src/esios/data/__init__.py +0 -0
  57. {python_esios-2.4.1 → python_esios-2.4.2}/src/esios/data/archives.yaml +0 -0
  58. {python_esios-2.4.1 → python_esios-2.4.2}/src/esios/data/geos.yaml +0 -0
  59. {python_esios-2.4.1 → python_esios-2.4.2}/src/esios/data/indicators.yaml +0 -0
  60. {python_esios-2.4.1 → python_esios-2.4.2}/src/esios/data/magnitudes.yaml +0 -0
  61. {python_esios-2.4.1 → python_esios-2.4.2}/src/esios/data/time_periods.yaml +0 -0
  62. {python_esios-2.4.1 → python_esios-2.4.2}/src/esios/exceptions.py +0 -0
  63. {python_esios-2.4.1 → python_esios-2.4.2}/src/esios/managers/__init__.py +0 -0
  64. {python_esios-2.4.1 → python_esios-2.4.2}/src/esios/managers/archives.py +0 -0
  65. {python_esios-2.4.1 → python_esios-2.4.2}/src/esios/managers/base.py +0 -0
  66. {python_esios-2.4.1 → python_esios-2.4.2}/src/esios/managers/indicators.py +0 -0
  67. {python_esios-2.4.1 → python_esios-2.4.2}/src/esios/managers/offer_indicators.py +0 -0
  68. {python_esios-2.4.1 → python_esios-2.4.2}/src/esios/models/__init__.py +0 -0
  69. {python_esios-2.4.1 → python_esios-2.4.2}/src/esios/models/archive.py +0 -0
  70. {python_esios-2.4.1 → python_esios-2.4.2}/src/esios/models/indicator.py +0 -0
  71. {python_esios-2.4.1 → python_esios-2.4.2}/src/esios/models/offer_indicator.py +0 -0
  72. {python_esios-2.4.1 → python_esios-2.4.2}/src/esios/processing/__init__.py +0 -0
  73. {python_esios-2.4.1 → python_esios-2.4.2}/src/esios/processing/dataframes.py +0 -0
  74. {python_esios-2.4.1 → python_esios-2.4.2}/src/esios/processing/zip.py +0 -0
  75. {python_esios-2.4.1 → python_esios-2.4.2}/tests/__init__.py +0 -0
  76. {python_esios-2.4.1 → python_esios-2.4.2}/tests/conftest.py +0 -0
  77. {python_esios-2.4.1 → python_esios-2.4.2}/tests/test_cache.py +0 -0
  78. {python_esios-2.4.1 → python_esios-2.4.2}/tests/test_client.py +0 -0
  79. {python_esios-2.4.1 → python_esios-2.4.2}/tests/test_dataframes.py +0 -0
  80. {python_esios-2.4.1 → python_esios-2.4.2}/tests/test_exceptions.py +0 -0
  81. {python_esios-2.4.1 → python_esios-2.4.2}/tests/test_managers.py +0 -0
  82. {python_esios-2.4.1 → python_esios-2.4.2}/tests/test_models.py +0 -0
  83. {python_esios-2.4.1 → python_esios-2.4.2}/tests/test_zip.py +0 -0
@@ -0,0 +1,3 @@
1
+ {
2
+ ".": "2.4.2"
3
+ }
@@ -1,5 +1,12 @@
1
1
  # Changelog
2
2
 
3
+ ## [2.4.2](https://github.com/datons/python-esios/compare/python-esios-v2.4.1...python-esios-v2.4.2) (2026-06-11)
4
+
5
+
6
+ ### Bug Fixes
7
+
8
+ * **i90:** pair MW and price per quarter-hour in multi-measure sheets ([06efdb6](https://github.com/datons/python-esios/commit/06efdb660077cc36c4435f0e36ae5f24e68e9189))
9
+
3
10
  ## [2.4.1](https://github.com/datons/python-esios/compare/python-esios-v2.4.0...python-esios-v2.4.1) (2026-05-19)
4
11
 
5
12
 
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: python-esios
3
- Version: 2.4.1
3
+ Version: 2.4.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,7 +4,7 @@ build-backend = "hatchling.build"
4
4
 
5
5
  [project]
6
6
  name = "python-esios"
7
- version = "2.4.1"
7
+ version = "2.4.2"
8
8
  description = "A Python wrapper for the ESIOS API (Spanish electricity market)"
9
9
  readme = "README.md"
10
10
  license = "GPL-3.0-only"
@@ -200,18 +200,29 @@ class I90Sheet:
200
200
  arr[arr == ""] = np.nan
201
201
  return arr
202
202
 
203
- def _normalize_datetime_columns(self, columns: np.ndarray) -> np.ndarray:
203
+ def _normalize_datetime_columns(
204
+ self, columns: np.ndarray, n_measures: int = 1
205
+ ) -> np.ndarray:
204
206
  """Normalize time column headers to integer period indices.
205
207
 
206
- Handles four column formats found in I90 files:
208
+ Handles five column formats found in I90 files:
207
209
 
208
210
  1. Sequential integers: 1–24 (hourly) or 1–96 (quarterly)
209
211
  2. H-Q format: "1-1", "1-2", "1-3", "1-4", "2-1", …
210
- 3. NaN-filler format: [1, NaN, NaN, NaN, 2, …]
212
+ 3. NaN-filler format: [1, NaN, NaN, NaN, 2, …] — one period number per
213
+ hour, three NaN fillers for the remaining quarters (single measure).
211
214
  4. Range format (DST days): "00-01", "01-02", "02-03a", "02-03b", …
212
215
  where the first number is the start hour and a/b suffix marks
213
216
  the repeated hour on fall-back days. Detected by the first
214
217
  column starting with "0" (e.g. "00-01").
218
+ 5. Multi-measure format: [1, NaN, 2, NaN, …] — each period number labels
219
+ a block of `n_measures` sub-columns (e.g. MW + €/MWh paired per
220
+ quarter-hour in offer sheets), the NaN fillers being the extra
221
+ measures. The period number is on every n_measures-th column.
222
+
223
+ `n_measures` is the count of distinct measure sub-columns per period
224
+ (1 for single-measure sheets). It disambiguates format 3 from format 5,
225
+ which share the same NaN-filler shape but mean opposite things.
215
226
  """
216
227
  if any(pd.isna(columns)):
217
228
  self._n_columns_totals = 3
@@ -240,6 +251,10 @@ class I90Sheet:
240
251
  )
241
252
  return ((hours - 1) * 4 + quarters).values
242
253
 
254
+ # Multi-measure format: each period number labels a block of n_measures sub-columns (e.g. MW + €/MWh). The ffilled value already IS the period index and the duplication is the measures — keep it so every measure of a period shares one datetime (paired), instead of spreading them across sequential slots.
255
+ if n_measures > 1:
256
+ return hours.values
257
+
243
258
  # NaN-filler quarterly format: after ffill the same hour number repeats
244
259
  # four times (quarters share the hour label). Assign sequential indices.
245
260
  if hours.duplicated().any():
@@ -257,13 +272,18 @@ class I90Sheet:
257
272
  if idx_col_start == -1:
258
273
  return pd.DataFrame()
259
274
 
260
- columns_date = self._normalize_datetime_columns(columns_prior[idx_col_start:])
275
+ columns_variable = columns[idx_col_start:]
276
+ # Distinct measure labels per period (e.g. {MW, €/MWh} -> 2). Disambiguates the NaN-filler time header (single measure, expand to sequential quarters) from the multi-measure paired layout (keep the period index so measures pair).
277
+ measure_labels = pd.Series(columns_variable).dropna()
278
+ n_measures = int(measure_labels.nunique()) if len(measure_labels) else 1
279
+ columns_date = self._normalize_datetime_columns(
280
+ columns_prior[idx_col_start:], n_measures=n_measures
281
+ )
261
282
  # _normalize_datetime_columns sets _n_columns_totals from the time-block
262
283
  # content (NaN-filler vs sequential). Override with a header-label count
263
284
  # so the index slice survives REE format revisions that add or drop a
264
285
  # "Total" column without touching the time-axis encoding.
265
286
  self._n_columns_totals = _count_header_separators(columns_prior, idx_col_start)
266
- columns_variable = columns[idx_col_start:]
267
287
  columns_index = columns[: idx_col_start - self._n_columns_totals]
268
288
 
269
289
  return columns, columns_index, columns_date, columns_variable
@@ -55,6 +55,17 @@ def _mixed_hq_columns(n_hours: int = 24) -> np.ndarray:
55
55
  return np.array(cols, dtype=object)
56
56
 
57
57
 
58
+ def _multi_measure_columns(n_periods: int = 96, n_measures: int = 2) -> np.ndarray:
59
+ """Multi-measure paired columns: the period number labels a block of
60
+ n_measures sub-columns (e.g. MW + €/MWh), the rest NaN: [1, NaN, 2, NaN, …].
61
+ """
62
+ cols: list = []
63
+ for p in range(1, n_periods + 1):
64
+ cols.append(p)
65
+ cols.extend([np.nan] * (n_measures - 1))
66
+ return np.array(cols, dtype=object)
67
+
68
+
58
69
  # ---------------------------------------------------------------------------
59
70
  # _any_value_greater_than_30
60
71
  # ---------------------------------------------------------------------------
@@ -317,3 +328,38 @@ class TestPreprocessOverrideIntegration:
317
328
  _, columns_index, _, _ = result
318
329
  assert list(columns_index) == ["Redespacho", "Tipo"]
319
330
  assert s._n_columns_totals == 2
331
+
332
+
333
+ class TestNormalizeMultiMeasure:
334
+ """Multi-measure paired columns ([1, NaN, 2, NaN, …]) share the same
335
+ NaN-filler shape as the single-measure format but mean the opposite: the
336
+ duplication is the measures (MW + €/MWh), so the period index must be kept
337
+ (paired), not expanded to sequential slots. Regression for offer sheets
338
+ (OFFER_BT / OFFER_BS_*) whose MW and price were otherwise scattered across
339
+ 8 slots/hour instead of paired into 4 quarter-hours.
340
+ """
341
+
342
+ def test_two_measures_keep_period_index(self):
343
+ s = _make_sheet()
344
+ result = s._normalize_datetime_columns(
345
+ _multi_measure_columns(4, n_measures=2), n_measures=2
346
+ )
347
+ # 4 quarters x 2 measures -> each quarter index repeated for its measures
348
+ assert list(result) == [1, 1, 2, 2, 3, 3, 4, 4]
349
+
350
+ def test_full_day_paired_96_periods(self):
351
+ s = _make_sheet()
352
+ result = s._normalize_datetime_columns(
353
+ _multi_measure_columns(96, n_measures=2), n_measures=2
354
+ )
355
+ assert len(result) == 192
356
+ assert result.max() == 96
357
+ _, counts = np.unique(result, return_counts=True)
358
+ assert set(counts.tolist()) == {2} # every period appears once per measure
359
+ assert _any_value_greater_than_30(result)
360
+
361
+ def test_single_measure_default_still_sequential(self):
362
+ # n_measures defaults to 1 -> old NaN-filler behaviour is preserved
363
+ s = _make_sheet()
364
+ result = s._normalize_datetime_columns(_full_nan_filler_columns())
365
+ assert list(result) == list(range(1, 97))
@@ -1,3 +0,0 @@
1
- {
2
- ".": "2.4.1"
3
- }
File without changes
File without changes
File without changes
File without changes