python-esios 2.4.0__tar.gz → 2.4.1__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.
- python_esios-2.4.1/.release-please-manifest.json +3 -0
- {python_esios-2.4.0 → python_esios-2.4.1}/CHANGELOG.md +8 -0
- {python_esios-2.4.0 → python_esios-2.4.1}/PKG-INFO +4 -4
- {python_esios-2.4.0 → python_esios-2.4.1}/README.md +3 -3
- {python_esios-2.4.0 → python_esios-2.4.1}/pyproject.toml +1 -1
- {python_esios-2.4.0 → python_esios-2.4.1}/src/esios/.agents/skills/esios/SKILL.md +2 -2
- {python_esios-2.4.0 → python_esios-2.4.1}/src/esios/processing/i90.py +43 -0
- {python_esios-2.4.0 → python_esios-2.4.1}/tests/conftest.py +12 -12
- python_esios-2.4.1/tests/test_i90.py +319 -0
- {python_esios-2.4.0 → python_esios-2.4.1}/tests/test_managers.py +10 -10
- {python_esios-2.4.0 → python_esios-2.4.1}/tests/test_models.py +2 -2
- python_esios-2.4.0/.release-please-manifest.json +0 -3
- python_esios-2.4.0/tests/test_i90.py +0 -167
- {python_esios-2.4.0 → python_esios-2.4.1}/.github/workflows/release-please.yml +0 -0
- {python_esios-2.4.0 → python_esios-2.4.1}/.gitignore +0 -0
- {python_esios-2.4.0 → python_esios-2.4.1}/CLAUDE.md +0 -0
- {python_esios-2.4.0 → python_esios-2.4.1}/LICENSE +0 -0
- {python_esios-2.4.0 → python_esios-2.4.1}/examples/.gitignore +0 -0
- {python_esios-2.4.0 → python_esios-2.4.1}/examples/01_Quickstart/01_Setup and First Query.ipynb +0 -0
- {python_esios-2.4.0 → python_esios-2.4.1}/examples/02_Indicators/01_Search Indicators.ipynb +0 -0
- {python_esios-2.4.0 → python_esios-2.4.1}/examples/02_Indicators/02_Historical Data.ipynb +0 -0
- {python_esios-2.4.0 → python_esios-2.4.1}/examples/02_Indicators/03_Multi-Geography Indicators.ipynb +0 -0
- {python_esios-2.4.0 → python_esios-2.4.1}/examples/02_Indicators/04_Compare Indicators.ipynb +0 -0
- {python_esios-2.4.0 → python_esios-2.4.1}/examples/02_Indicators/05_Market Prices.ipynb +0 -0
- {python_esios-2.4.0 → python_esios-2.4.1}/examples/02_Indicators/06_Generation and Demand.ipynb +0 -0
- {python_esios-2.4.0 → python_esios-2.4.1}/examples/03_Archives/01_Download Archives.ipynb +0 -0
- {python_esios-2.4.0 → python_esios-2.4.1}/examples/03_Archives/02_I90 Settlement Files.ipynb +0 -0
- {python_esios-2.4.0 → python_esios-2.4.1}/examples/04_Caching/01_Cache Management.ipynb +0 -0
- {python_esios-2.4.0 → python_esios-2.4.1}/examples/05_Advanced/01_Ad-hoc Pandas Expressions.ipynb +0 -0
- {python_esios-2.4.0 → python_esios-2.4.1}/examples/05_Advanced/02_Async Client.ipynb +0 -0
- {python_esios-2.4.0 → python_esios-2.4.1}/examples/_specs/01_Quickstart/01_Setup and First Query.yaml +0 -0
- {python_esios-2.4.0 → python_esios-2.4.1}/examples/_specs/02_Indicators/01_Search Indicators.yaml +0 -0
- {python_esios-2.4.0 → python_esios-2.4.1}/examples/_specs/02_Indicators/02_Historical Data.yaml +0 -0
- {python_esios-2.4.0 → python_esios-2.4.1}/examples/_specs/02_Indicators/03_Multi-Geography Indicators.yaml +0 -0
- {python_esios-2.4.0 → python_esios-2.4.1}/examples/_specs/02_Indicators/04_Compare Indicators.yaml +0 -0
- {python_esios-2.4.0 → python_esios-2.4.1}/examples/_specs/02_Indicators/05_Market Prices.yaml +0 -0
- {python_esios-2.4.0 → python_esios-2.4.1}/examples/_specs/02_Indicators/06_Generation and Demand.yaml +0 -0
- {python_esios-2.4.0 → python_esios-2.4.1}/examples/_specs/03_Archives/01_Download Archives.yaml +0 -0
- {python_esios-2.4.0 → python_esios-2.4.1}/examples/_specs/03_Archives/02_I90 Settlement Files.yaml +0 -0
- {python_esios-2.4.0 → python_esios-2.4.1}/examples/_specs/04_Caching/01_Cache Management.yaml +0 -0
- {python_esios-2.4.0 → python_esios-2.4.1}/examples/_specs/05_Advanced/01_Ad-hoc Pandas Expressions.yaml +0 -0
- {python_esios-2.4.0 → python_esios-2.4.1}/examples/_specs/05_Advanced/02_Async Client.yaml +0 -0
- {python_esios-2.4.0 → python_esios-2.4.1}/examples/generate.py +0 -0
- {python_esios-2.4.0 → python_esios-2.4.1}/release-please-config.json +0 -0
- {python_esios-2.4.0 → python_esios-2.4.1}/src/esios/__init__.py +0 -0
- {python_esios-2.4.0 → python_esios-2.4.1}/src/esios/async_client.py +0 -0
- {python_esios-2.4.0 → python_esios-2.4.1}/src/esios/cache.py +0 -0
- {python_esios-2.4.0 → python_esios-2.4.1}/src/esios/catalog.py +0 -0
- {python_esios-2.4.0 → python_esios-2.4.1}/src/esios/cli/__init__.py +0 -0
- {python_esios-2.4.0 → python_esios-2.4.1}/src/esios/cli/app.py +0 -0
- {python_esios-2.4.0 → python_esios-2.4.1}/src/esios/cli/archives.py +0 -0
- {python_esios-2.4.0 → python_esios-2.4.1}/src/esios/cli/cache_cmd.py +0 -0
- {python_esios-2.4.0 → python_esios-2.4.1}/src/esios/cli/catalog_cmd.py +0 -0
- {python_esios-2.4.0 → python_esios-2.4.1}/src/esios/cli/config.py +0 -0
- {python_esios-2.4.0 → python_esios-2.4.1}/src/esios/cli/config_cmd.py +0 -0
- {python_esios-2.4.0 → python_esios-2.4.1}/src/esios/cli/exec_cmd.py +0 -0
- {python_esios-2.4.0 → python_esios-2.4.1}/src/esios/cli/indicators.py +0 -0
- {python_esios-2.4.0 → python_esios-2.4.1}/src/esios/client.py +0 -0
- {python_esios-2.4.0 → python_esios-2.4.1}/src/esios/constants.py +0 -0
- {python_esios-2.4.0 → python_esios-2.4.1}/src/esios/data/__init__.py +0 -0
- {python_esios-2.4.0 → python_esios-2.4.1}/src/esios/data/archives.yaml +0 -0
- {python_esios-2.4.0 → python_esios-2.4.1}/src/esios/data/geos.yaml +0 -0
- {python_esios-2.4.0 → python_esios-2.4.1}/src/esios/data/indicators.yaml +0 -0
- {python_esios-2.4.0 → python_esios-2.4.1}/src/esios/data/magnitudes.yaml +0 -0
- {python_esios-2.4.0 → python_esios-2.4.1}/src/esios/data/time_periods.yaml +0 -0
- {python_esios-2.4.0 → python_esios-2.4.1}/src/esios/exceptions.py +0 -0
- {python_esios-2.4.0 → python_esios-2.4.1}/src/esios/managers/__init__.py +0 -0
- {python_esios-2.4.0 → python_esios-2.4.1}/src/esios/managers/archives.py +0 -0
- {python_esios-2.4.0 → python_esios-2.4.1}/src/esios/managers/base.py +0 -0
- {python_esios-2.4.0 → python_esios-2.4.1}/src/esios/managers/indicators.py +0 -0
- {python_esios-2.4.0 → python_esios-2.4.1}/src/esios/managers/offer_indicators.py +0 -0
- {python_esios-2.4.0 → python_esios-2.4.1}/src/esios/models/__init__.py +0 -0
- {python_esios-2.4.0 → python_esios-2.4.1}/src/esios/models/archive.py +0 -0
- {python_esios-2.4.0 → python_esios-2.4.1}/src/esios/models/indicator.py +0 -0
- {python_esios-2.4.0 → python_esios-2.4.1}/src/esios/models/offer_indicator.py +0 -0
- {python_esios-2.4.0 → python_esios-2.4.1}/src/esios/processing/__init__.py +0 -0
- {python_esios-2.4.0 → python_esios-2.4.1}/src/esios/processing/dataframes.py +0 -0
- {python_esios-2.4.0 → python_esios-2.4.1}/src/esios/processing/zip.py +0 -0
- {python_esios-2.4.0 → python_esios-2.4.1}/tests/__init__.py +0 -0
- {python_esios-2.4.0 → python_esios-2.4.1}/tests/test_cache.py +0 -0
- {python_esios-2.4.0 → python_esios-2.4.1}/tests/test_client.py +0 -0
- {python_esios-2.4.0 → python_esios-2.4.1}/tests/test_dataframes.py +0 -0
- {python_esios-2.4.0 → python_esios-2.4.1}/tests/test_exceptions.py +0 -0
- {python_esios-2.4.0 → python_esios-2.4.1}/tests/test_zip.py +0 -0
|
@@ -1,5 +1,13 @@
|
|
|
1
1
|
# Changelog
|
|
2
2
|
|
|
3
|
+
## [2.4.1](https://github.com/datons/python-esios/compare/python-esios-v2.4.0...python-esios-v2.4.1) (2026-05-19)
|
|
4
|
+
|
|
5
|
+
|
|
6
|
+
### Bug Fixes
|
|
7
|
+
|
|
8
|
+
* **i90:** detect separator columns dynamically by header label ([1a9bf77](https://github.com/datons/python-esios/commit/1a9bf778eeb769aac836a945787c951c624f3806))
|
|
9
|
+
* **i90:** detect separator columns dynamically by header label ([d6c747f](https://github.com/datons/python-esios/commit/d6c747f4db44b5667abf12f075ef468eca3eec20))
|
|
10
|
+
|
|
3
11
|
## [2.4.0](https://github.com/datons/python-esios/compare/python-esios-v2.3.0...python-esios-v2.4.0) (2026-03-23)
|
|
4
12
|
|
|
5
13
|
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: python-esios
|
|
3
|
-
Version: 2.4.
|
|
3
|
+
Version: 2.4.1
|
|
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
|
|
@@ -85,7 +85,7 @@ from esios import ESIOSClient
|
|
|
85
85
|
client = ESIOSClient()
|
|
86
86
|
|
|
87
87
|
# Get indicator data as DataFrame
|
|
88
|
-
handle = client.indicators.get(600) #
|
|
88
|
+
handle = client.indicators.get(600) # Day-ahead spot price (OMIE)
|
|
89
89
|
df = handle.historical("2025-01-01", "2025-01-31")
|
|
90
90
|
|
|
91
91
|
# Search indicators
|
|
@@ -99,8 +99,8 @@ client.archives.download(1, start="2025-01-01", end="2025-01-31", output_dir="./
|
|
|
99
99
|
|
|
100
100
|
| ID | Name | Description |
|
|
101
101
|
|----|------|-------------|
|
|
102
|
-
| 600 |
|
|
103
|
-
| 1001 |
|
|
102
|
+
| 600 | Day-ahead price | OMIE spot market price |
|
|
103
|
+
| 1001 | PVPC | Voluntary price for small consumers (2.0TD) |
|
|
104
104
|
| 10033 | Demand | Real-time electricity demand |
|
|
105
105
|
| 10034 | Wind generation | Real-time wind generation |
|
|
106
106
|
| 10035 | Solar PV generation | Real-time solar generation |
|
|
@@ -50,7 +50,7 @@ from esios import ESIOSClient
|
|
|
50
50
|
client = ESIOSClient()
|
|
51
51
|
|
|
52
52
|
# Get indicator data as DataFrame
|
|
53
|
-
handle = client.indicators.get(600) #
|
|
53
|
+
handle = client.indicators.get(600) # Day-ahead spot price (OMIE)
|
|
54
54
|
df = handle.historical("2025-01-01", "2025-01-31")
|
|
55
55
|
|
|
56
56
|
# Search indicators
|
|
@@ -64,8 +64,8 @@ client.archives.download(1, start="2025-01-01", end="2025-01-31", output_dir="./
|
|
|
64
64
|
|
|
65
65
|
| ID | Name | Description |
|
|
66
66
|
|----|------|-------------|
|
|
67
|
-
| 600 |
|
|
68
|
-
| 1001 |
|
|
67
|
+
| 600 | Day-ahead price | OMIE spot market price |
|
|
68
|
+
| 1001 | PVPC | Voluntary price for small consumers (2.0TD) |
|
|
69
69
|
| 10033 | Demand | Real-time electricity demand |
|
|
70
70
|
| 10034 | Wind generation | Real-time wind generation |
|
|
71
71
|
| 10035 | Solar PV generation | Real-time solar generation |
|
|
@@ -60,8 +60,8 @@ print(sheet.frequency) # "hourly" or "hourly-quarterly"
|
|
|
60
60
|
|
|
61
61
|
| ID | Name | Description | Geos |
|
|
62
62
|
|----|------|-------------|------|
|
|
63
|
-
| 600 | Precio mercado
|
|
64
|
-
| 1001 |
|
|
63
|
+
| 600 | Precio mercado SPOT Diario | OMIE spot / day-ahead market price | ES, PT, FR, DE, BE, NL |
|
|
64
|
+
| 1001 | PVPC T. 2.0TD | Término de facturación de energía activa del PVPC (voluntary price for small consumers) | Península, Canarias, Baleares, Ceuta, Melilla |
|
|
65
65
|
| 10033 | Demanda real | Real-time electricity demand | ES |
|
|
66
66
|
| 10034 | Generación eólica | Real-time wind generation | ES |
|
|
67
67
|
| 10035 | Generación solar FV | Real-time solar PV generation | ES |
|
|
@@ -33,6 +33,43 @@ def _any_value_greater_than_30(series: np.ndarray) -> bool:
|
|
|
33
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
|
+
# Labels that REE uses for the cells sitting between the index columns and the
|
|
37
|
+
# per-period value columns of an I90 sheet. Match is exact (case-insensitive,
|
|
38
|
+
# trimmed). The count of these cells per sheet has varied across REE format
|
|
39
|
+
# revisions — historically 2 ("Hora" / "Cuarto de Hora del dia" + "Total");
|
|
40
|
+
# from Oct 2025 the MTU 15-min transition dropped "Total" on several sheets,
|
|
41
|
+
# leaving 1. Counting them dynamically keeps the parser resilient to either.
|
|
42
|
+
_SEPARATOR_LABELS = frozenset({
|
|
43
|
+
"cuarto de hora del dia",
|
|
44
|
+
"hora del dia",
|
|
45
|
+
"hora",
|
|
46
|
+
"total",
|
|
47
|
+
"indicadores",
|
|
48
|
+
})
|
|
49
|
+
|
|
50
|
+
|
|
51
|
+
def _count_header_separators(row: np.ndarray, idx_col_start: int) -> int:
|
|
52
|
+
"""Count separator cells immediately preceding the time-value block.
|
|
53
|
+
|
|
54
|
+
Walks ``row`` backwards from ``idx_col_start - 1``, incrementing on each
|
|
55
|
+
cell whose text matches a known separator label and stopping at the first
|
|
56
|
+
non-matching cell or NaN. A NaN cell signals the start of the index-column
|
|
57
|
+
placeholder zone in double-header layouts (where index labels live on the
|
|
58
|
+
other header row, leaving the date row blank under each index position).
|
|
59
|
+
"""
|
|
60
|
+
n = 0
|
|
61
|
+
for i in range(idx_col_start - 1, -1, -1):
|
|
62
|
+
cell = row[i]
|
|
63
|
+
if cell is None or (isinstance(cell, float) and np.isnan(cell)):
|
|
64
|
+
break
|
|
65
|
+
text = str(cell).strip().lower()
|
|
66
|
+
if text in _SEPARATOR_LABELS:
|
|
67
|
+
n += 1
|
|
68
|
+
continue
|
|
69
|
+
break
|
|
70
|
+
return n
|
|
71
|
+
|
|
72
|
+
|
|
36
73
|
class I90Book:
|
|
37
74
|
"""Represents an I90DIA workbook (XLS) with lazy sheet preprocessing.
|
|
38
75
|
|
|
@@ -221,6 +258,11 @@ class I90Sheet:
|
|
|
221
258
|
return pd.DataFrame()
|
|
222
259
|
|
|
223
260
|
columns_date = self._normalize_datetime_columns(columns_prior[idx_col_start:])
|
|
261
|
+
# _normalize_datetime_columns sets _n_columns_totals from the time-block
|
|
262
|
+
# content (NaN-filler vs sequential). Override with a header-label count
|
|
263
|
+
# so the index slice survives REE format revisions that add or drop a
|
|
264
|
+
# "Total" column without touching the time-axis encoding.
|
|
265
|
+
self._n_columns_totals = _count_header_separators(columns_prior, idx_col_start)
|
|
224
266
|
columns_variable = columns[idx_col_start:]
|
|
225
267
|
columns_index = columns[: idx_col_start - self._n_columns_totals]
|
|
226
268
|
|
|
@@ -230,6 +272,7 @@ class I90Sheet:
|
|
|
230
272
|
self, idx_col_start: int, columns: np.ndarray
|
|
231
273
|
) -> tuple[np.ndarray, np.ndarray, np.ndarray, None]:
|
|
232
274
|
columns_date = self._normalize_datetime_columns(columns[idx_col_start:])
|
|
275
|
+
self._n_columns_totals = _count_header_separators(columns, idx_col_start)
|
|
233
276
|
columns_index = columns[: idx_col_start - self._n_columns_totals]
|
|
234
277
|
return columns, columns_index, columns_date, None
|
|
235
278
|
|
|
@@ -26,12 +26,12 @@ def client(mock_httpx):
|
|
|
26
26
|
|
|
27
27
|
@pytest.fixture
|
|
28
28
|
def sample_indicator_response():
|
|
29
|
-
"""Sample API response for GET /indicators/
|
|
29
|
+
"""Sample API response for GET /indicators/1001."""
|
|
30
30
|
return {
|
|
31
31
|
"indicator": {
|
|
32
|
-
"id":
|
|
33
|
-
"name": "PVPC
|
|
34
|
-
"short_name": "PVPC",
|
|
32
|
+
"id": 1001,
|
|
33
|
+
"name": "Término de facturación de energía activa del PVPC 2.0TD",
|
|
34
|
+
"short_name": "PVPC T. 2.0TD",
|
|
35
35
|
"description": "<p>Precio voluntario para el pequeño consumidor</p>",
|
|
36
36
|
"values": [
|
|
37
37
|
{
|
|
@@ -39,16 +39,16 @@ def sample_indicator_response():
|
|
|
39
39
|
"datetime": "2025-01-01T00:00:00.000+01:00",
|
|
40
40
|
"datetime_utc": "2024-12-31T23:00:00Z",
|
|
41
41
|
"tz_time": "2025-01-01T00:00:00.000+01:00",
|
|
42
|
-
"geo_id":
|
|
43
|
-
"geo_name": "
|
|
42
|
+
"geo_id": 8741,
|
|
43
|
+
"geo_name": "Península",
|
|
44
44
|
},
|
|
45
45
|
{
|
|
46
46
|
"value": 115.3,
|
|
47
47
|
"datetime": "2025-01-01T01:00:00.000+01:00",
|
|
48
48
|
"datetime_utc": "2025-01-01T00:00:00Z",
|
|
49
49
|
"tz_time": "2025-01-01T01:00:00.000+01:00",
|
|
50
|
-
"geo_id":
|
|
51
|
-
"geo_name": "
|
|
50
|
+
"geo_id": 8741,
|
|
51
|
+
"geo_name": "Península",
|
|
52
52
|
},
|
|
53
53
|
],
|
|
54
54
|
}
|
|
@@ -61,10 +61,10 @@ def sample_indicators_list_response():
|
|
|
61
61
|
return {
|
|
62
62
|
"indicators": [
|
|
63
63
|
{
|
|
64
|
-
"id":
|
|
65
|
-
"name": "PVPC
|
|
66
|
-
"short_name": "PVPC",
|
|
67
|
-
"description": "<p>Precio voluntario</p>",
|
|
64
|
+
"id": 1001,
|
|
65
|
+
"name": "Término de facturación de energía activa del PVPC 2.0TD",
|
|
66
|
+
"short_name": "PVPC T. 2.0TD",
|
|
67
|
+
"description": "<p>Precio voluntario para el pequeño consumidor</p>",
|
|
68
68
|
},
|
|
69
69
|
{
|
|
70
70
|
"id": 10034,
|
|
@@ -0,0 +1,319 @@
|
|
|
1
|
+
"""Tests for I90 file processing — frequency detection and column normalisation."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
from unittest.mock import MagicMock
|
|
6
|
+
|
|
7
|
+
import numpy as np
|
|
8
|
+
import pytest
|
|
9
|
+
|
|
10
|
+
from esios.processing.i90 import (
|
|
11
|
+
I90Sheet,
|
|
12
|
+
_any_value_greater_than_30,
|
|
13
|
+
_count_header_separators,
|
|
14
|
+
)
|
|
15
|
+
|
|
16
|
+
|
|
17
|
+
# ---------------------------------------------------------------------------
|
|
18
|
+
# Helpers
|
|
19
|
+
# ---------------------------------------------------------------------------
|
|
20
|
+
|
|
21
|
+
|
|
22
|
+
def _make_sheet() -> I90Sheet:
|
|
23
|
+
"""Return a bare I90Sheet instance backed by mocked objects."""
|
|
24
|
+
wb = MagicMock()
|
|
25
|
+
sheet = MagicMock()
|
|
26
|
+
sheet.to_python.return_value = [[""]]
|
|
27
|
+
wb.get_sheet_by_name.return_value = sheet
|
|
28
|
+
return I90Sheet("test", wb, "I90DIA_20241001.xls", {})
|
|
29
|
+
|
|
30
|
+
|
|
31
|
+
def _full_nan_filler_columns(n_hours: int = 24) -> np.ndarray:
|
|
32
|
+
"""Build NaN-filler quarterly columns: [1, NaN, NaN, NaN, 2, NaN, …]."""
|
|
33
|
+
cols: list = []
|
|
34
|
+
for h in range(1, n_hours + 1):
|
|
35
|
+
cols.append(h)
|
|
36
|
+
cols.extend([np.nan, np.nan, np.nan])
|
|
37
|
+
return np.array(cols, dtype=object)
|
|
38
|
+
|
|
39
|
+
|
|
40
|
+
def _full_hq_columns(n_hours: int = 24) -> np.ndarray:
|
|
41
|
+
"""Build explicit H-Q columns: ['1-1', '1-2', '1-3', '1-4', '2-1', …]."""
|
|
42
|
+
return np.array(
|
|
43
|
+
[f"{h}-{q}" for h in range(1, n_hours + 1) for q in range(1, 5)],
|
|
44
|
+
dtype=object,
|
|
45
|
+
)
|
|
46
|
+
|
|
47
|
+
|
|
48
|
+
def _mixed_hq_columns(n_hours: int = 24) -> np.ndarray:
|
|
49
|
+
"""First quarter is unlabelled ('1', '2', …); rest carry '-Q' suffix."""
|
|
50
|
+
cols: list = []
|
|
51
|
+
for h in range(1, n_hours + 1):
|
|
52
|
+
cols.append(str(h))
|
|
53
|
+
for q in range(2, 5):
|
|
54
|
+
cols.append(f"{h}-{q}")
|
|
55
|
+
return np.array(cols, dtype=object)
|
|
56
|
+
|
|
57
|
+
|
|
58
|
+
# ---------------------------------------------------------------------------
|
|
59
|
+
# _any_value_greater_than_30
|
|
60
|
+
# ---------------------------------------------------------------------------
|
|
61
|
+
|
|
62
|
+
|
|
63
|
+
class TestAnyValueGreaterThan30:
|
|
64
|
+
def test_returns_true_for_value_above_30(self):
|
|
65
|
+
assert _any_value_greater_than_30(np.array([1, 31, 24])) is True
|
|
66
|
+
|
|
67
|
+
def test_returns_false_for_all_values_24_or_less(self):
|
|
68
|
+
assert _any_value_greater_than_30(np.arange(1, 25)) is False
|
|
69
|
+
|
|
70
|
+
def test_works_with_numpy_int64(self):
|
|
71
|
+
"""numpy 2.x broke isinstance(np.int64, int) — make sure we handle it."""
|
|
72
|
+
arr = np.array([1, 2, 31, 96], dtype=np.int64)
|
|
73
|
+
assert _any_value_greater_than_30(arr) is True
|
|
74
|
+
|
|
75
|
+
def test_ignores_nan(self):
|
|
76
|
+
arr = np.array([np.nan, 5.0, 20.0])
|
|
77
|
+
assert _any_value_greater_than_30(arr) is False
|
|
78
|
+
|
|
79
|
+
def test_sequential_quarterly_1_to_96(self):
|
|
80
|
+
assert _any_value_greater_than_30(np.arange(1, 97)) is True
|
|
81
|
+
|
|
82
|
+
|
|
83
|
+
# ---------------------------------------------------------------------------
|
|
84
|
+
# _normalize_datetime_columns — hourly (unchanged behaviour)
|
|
85
|
+
# ---------------------------------------------------------------------------
|
|
86
|
+
|
|
87
|
+
|
|
88
|
+
class TestNormalizeHourly:
|
|
89
|
+
def test_sequential_1_to_24(self):
|
|
90
|
+
s = _make_sheet()
|
|
91
|
+
cols = np.array([float(i) for i in range(1, 25)], dtype=object)
|
|
92
|
+
result = s._normalize_datetime_columns(cols)
|
|
93
|
+
assert list(result) == list(range(1, 25))
|
|
94
|
+
assert not _any_value_greater_than_30(result)
|
|
95
|
+
|
|
96
|
+
def test_n_columns_totals_is_2_when_no_nan(self):
|
|
97
|
+
s = _make_sheet()
|
|
98
|
+
cols = np.array([float(i) for i in range(1, 25)], dtype=object)
|
|
99
|
+
s._normalize_datetime_columns(cols)
|
|
100
|
+
assert s._n_columns_totals == 2
|
|
101
|
+
|
|
102
|
+
|
|
103
|
+
# ---------------------------------------------------------------------------
|
|
104
|
+
# _normalize_datetime_columns — quarterly (new behaviour)
|
|
105
|
+
# ---------------------------------------------------------------------------
|
|
106
|
+
|
|
107
|
+
|
|
108
|
+
class TestNormalizeQuarterlySequential:
|
|
109
|
+
"""Columns already in 1–96 sequential form (already worked before the fix)."""
|
|
110
|
+
|
|
111
|
+
def test_sequential_1_to_96(self):
|
|
112
|
+
s = _make_sheet()
|
|
113
|
+
cols = np.array([float(i) for i in range(1, 97)], dtype=object)
|
|
114
|
+
result = s._normalize_datetime_columns(cols)
|
|
115
|
+
assert list(result) == list(range(1, 97))
|
|
116
|
+
assert _any_value_greater_than_30(result)
|
|
117
|
+
|
|
118
|
+
|
|
119
|
+
class TestNormalizeQuarterlyHQFormat:
|
|
120
|
+
"""Columns in explicit 'H-Q' dash notation: '1-1', '1-2', …, '24-4'."""
|
|
121
|
+
|
|
122
|
+
def test_full_day_96_periods(self):
|
|
123
|
+
s = _make_sheet()
|
|
124
|
+
result = s._normalize_datetime_columns(_full_hq_columns())
|
|
125
|
+
assert len(result) == 96
|
|
126
|
+
assert list(result) == list(range(1, 97))
|
|
127
|
+
assert _any_value_greater_than_30(result)
|
|
128
|
+
|
|
129
|
+
def test_time_deltas_are_correct(self):
|
|
130
|
+
"""period 1 → 0 min (00:00), period 96 → 1425 min (23:45)."""
|
|
131
|
+
s = _make_sheet()
|
|
132
|
+
result = s._normalize_datetime_columns(_full_hq_columns())
|
|
133
|
+
time_deltas = (result - 1) * 15
|
|
134
|
+
assert time_deltas[0] == 0
|
|
135
|
+
assert time_deltas[-1] == 1425
|
|
136
|
+
|
|
137
|
+
def test_mixed_hq_first_quarter_unlabelled(self):
|
|
138
|
+
"""'1', '1-2', '1-3', '1-4', '2', '2-2', … is treated the same."""
|
|
139
|
+
s = _make_sheet()
|
|
140
|
+
result = s._normalize_datetime_columns(_mixed_hq_columns())
|
|
141
|
+
assert len(result) == 96
|
|
142
|
+
assert list(result) == list(range(1, 97))
|
|
143
|
+
assert _any_value_greater_than_30(result)
|
|
144
|
+
|
|
145
|
+
def test_n_columns_totals_is_2_for_explicit_hq(self):
|
|
146
|
+
s = _make_sheet()
|
|
147
|
+
s._normalize_datetime_columns(_full_hq_columns())
|
|
148
|
+
assert s._n_columns_totals == 2
|
|
149
|
+
|
|
150
|
+
|
|
151
|
+
class TestNormalizeQuarterlyNaNFiller:
|
|
152
|
+
"""Columns in NaN-filler form: [1, NaN, NaN, NaN, 2, NaN, …]."""
|
|
153
|
+
|
|
154
|
+
def test_full_day_96_periods(self):
|
|
155
|
+
s = _make_sheet()
|
|
156
|
+
result = s._normalize_datetime_columns(_full_nan_filler_columns())
|
|
157
|
+
assert len(result) == 96
|
|
158
|
+
assert list(result) == list(range(1, 97))
|
|
159
|
+
assert _any_value_greater_than_30(result)
|
|
160
|
+
|
|
161
|
+
def test_time_deltas_are_correct(self):
|
|
162
|
+
s = _make_sheet()
|
|
163
|
+
result = s._normalize_datetime_columns(_full_nan_filler_columns())
|
|
164
|
+
time_deltas = (result - 1) * 15
|
|
165
|
+
assert time_deltas[0] == 0
|
|
166
|
+
assert time_deltas[-1] == 1425
|
|
167
|
+
|
|
168
|
+
def test_n_columns_totals_is_3_when_nan_present(self):
|
|
169
|
+
s = _make_sheet()
|
|
170
|
+
s._normalize_datetime_columns(_full_nan_filler_columns())
|
|
171
|
+
assert s._n_columns_totals == 3
|
|
172
|
+
|
|
173
|
+
|
|
174
|
+
# ---------------------------------------------------------------------------
|
|
175
|
+
# _count_header_separators — header-label-based separator counting
|
|
176
|
+
#
|
|
177
|
+
# Headers below are calques of real REE I90 layouts. The counter walks back
|
|
178
|
+
# from idx_col_start, counting cells whose text equals a known separator
|
|
179
|
+
# label ('Cuarto de Hora del dia', 'Hora', 'Total', 'Indicadores', 'Hora del
|
|
180
|
+
# dia'), stopping at the first non-matching cell or NaN.
|
|
181
|
+
# ---------------------------------------------------------------------------
|
|
182
|
+
|
|
183
|
+
|
|
184
|
+
class TestCountHeaderSeparators:
|
|
185
|
+
def test_returns_zero_when_no_separators(self):
|
|
186
|
+
"""If the cell right before the time block is a real index column."""
|
|
187
|
+
row = np.array(["Redespacho", "Tipo", "1.0", "2.0"], dtype=object)
|
|
188
|
+
assert _count_header_separators(row, idx_col_start=2) == 0
|
|
189
|
+
|
|
190
|
+
def test_pre_mtu_rr_price_layout_returns_2(self):
|
|
191
|
+
"""I90DIA11 jun-2025: [Redespacho, Tipo, Cuarto de Hora del dia, Total, 1, …]."""
|
|
192
|
+
row = np.array(
|
|
193
|
+
["Redespacho", "Tipo", "Cuarto de Hora del dia", "Total", "1.0", "2.0"],
|
|
194
|
+
dtype=object,
|
|
195
|
+
)
|
|
196
|
+
assert _count_header_separators(row, idx_col_start=4) == 2
|
|
197
|
+
|
|
198
|
+
def test_post_mtu_rr_price_layout_returns_1(self):
|
|
199
|
+
"""I90DIA11 nov-2025: 'Total' dropped → [Redespacho, Cuarto de Hora del dia, 1, …]."""
|
|
200
|
+
row = np.array(
|
|
201
|
+
["Redespacho", "Cuarto de Hora del dia", "1.0", "2.0"],
|
|
202
|
+
dtype=object,
|
|
203
|
+
)
|
|
204
|
+
assert _count_header_separators(row, idx_col_start=2) == 1
|
|
205
|
+
|
|
206
|
+
def test_pre_2024_hourly_layout_returns_2(self):
|
|
207
|
+
"""I90DIA08 2014: [Redespacho, Tipo, …, Signo de Energía, Hora, Total, 00-01, …]."""
|
|
208
|
+
row = np.array(
|
|
209
|
+
[
|
|
210
|
+
"Redespacho", "Tipo", "Sentido", "Unidad de Programación",
|
|
211
|
+
"Tipo Oferta", "Tipo cálculo", "Tipo Restricción",
|
|
212
|
+
"Signo de Energía", "Hora", "Total", "00-01", "01-02",
|
|
213
|
+
],
|
|
214
|
+
dtype=object,
|
|
215
|
+
)
|
|
216
|
+
assert _count_header_separators(row, idx_col_start=10) == 2
|
|
217
|
+
|
|
218
|
+
def test_post_mtu_rtr_price_layout_returns_1(self):
|
|
219
|
+
"""I90DIA10 nov-2025: 'Total' dropped + index reordered."""
|
|
220
|
+
row = np.array(
|
|
221
|
+
[
|
|
222
|
+
"Redespacho", "Sentido", "Unidad de Programación", "Tipo Oferta",
|
|
223
|
+
"Tipo cálculo", "Signo de Energía", "Cuarto de Hora del dia",
|
|
224
|
+
"1.0", "2.0",
|
|
225
|
+
],
|
|
226
|
+
dtype=object,
|
|
227
|
+
)
|
|
228
|
+
assert _count_header_separators(row, idx_col_start=7) == 1
|
|
229
|
+
|
|
230
|
+
def test_double_index_date_row_with_nan_index_placeholders(self):
|
|
231
|
+
"""Double-header layout (e.g. I90DIA30 jun-2025): the date row carries
|
|
232
|
+
the separator labels; positions under each real index column are NaN.
|
|
233
|
+
The counter must stop at the first NaN (= start of the index zone).
|
|
234
|
+
"""
|
|
235
|
+
row = np.array(
|
|
236
|
+
[np.nan, np.nan, np.nan, np.nan,
|
|
237
|
+
"Cuarto de Hora del dia", "Total", "1.0", "2.0"],
|
|
238
|
+
dtype=object,
|
|
239
|
+
)
|
|
240
|
+
assert _count_header_separators(row, idx_col_start=6) == 2
|
|
241
|
+
|
|
242
|
+
def test_double_index_post_mtu_dropped_total(self):
|
|
243
|
+
"""I90DIA30 nov-2025 (single-index path post-MTU)."""
|
|
244
|
+
row = np.array(
|
|
245
|
+
["Redespacho", "Sentido", "Tipo QH",
|
|
246
|
+
"Cuarto de Hora del dia", "1.0", "2.0"],
|
|
247
|
+
dtype=object,
|
|
248
|
+
)
|
|
249
|
+
assert _count_header_separators(row, idx_col_start=4) == 1
|
|
250
|
+
|
|
251
|
+
def test_match_is_case_insensitive(self):
|
|
252
|
+
row = np.array(["Redespacho", "TOTAL", "1.0"], dtype=object)
|
|
253
|
+
assert _count_header_separators(row, idx_col_start=2) == 1
|
|
254
|
+
|
|
255
|
+
def test_match_is_whitespace_tolerant(self):
|
|
256
|
+
row = np.array(["Redespacho", " Total ", "1.0"], dtype=object)
|
|
257
|
+
assert _count_header_separators(row, idx_col_start=2) == 1
|
|
258
|
+
|
|
259
|
+
def test_indicadores_counts_as_separator(self):
|
|
260
|
+
"""Variable-row layout: [Redespacho, Tipo, Indicadores, Precio Marginal …, …]."""
|
|
261
|
+
row = np.array(
|
|
262
|
+
["Redespacho", "Tipo", "Indicadores", "Precio Marginal", "1.0"],
|
|
263
|
+
dtype=object,
|
|
264
|
+
)
|
|
265
|
+
# idx_col_start = 4 → walk back from i=3
|
|
266
|
+
# i=3 'Precio Marginal' is not a separator → stop, return 0
|
|
267
|
+
assert _count_header_separators(row, idx_col_start=4) == 0
|
|
268
|
+
# But when adjacent to time block it counts:
|
|
269
|
+
row2 = np.array(["Redespacho", "Tipo", "Indicadores", "1.0"], dtype=object)
|
|
270
|
+
assert _count_header_separators(row2, idx_col_start=3) == 1
|
|
271
|
+
|
|
272
|
+
def test_unknown_label_immediately_after_index_stops_counter(self):
|
|
273
|
+
"""Non-separator labels do not get absorbed even when next to time block."""
|
|
274
|
+
row = np.array(["Redespacho", "Foo Bar", "1.0"], dtype=object)
|
|
275
|
+
assert _count_header_separators(row, idx_col_start=2) == 0
|
|
276
|
+
|
|
277
|
+
def test_empty_string_breaks_counter(self):
|
|
278
|
+
row = np.array(["Redespacho", "", "Total", "1.0"], dtype=object)
|
|
279
|
+
# i=2 'Total' → sep, i=1 '' → break → returns 1
|
|
280
|
+
assert _count_header_separators(row, idx_col_start=3) == 1
|
|
281
|
+
|
|
282
|
+
|
|
283
|
+
# ---------------------------------------------------------------------------
|
|
284
|
+
# _preprocess_*_index — verify the dynamic counter overrides the heuristic
|
|
285
|
+
# from _normalize_datetime_columns when slicing the index portion.
|
|
286
|
+
# ---------------------------------------------------------------------------
|
|
287
|
+
|
|
288
|
+
|
|
289
|
+
class TestPreprocessOverrideIntegration:
|
|
290
|
+
def test_single_index_post_mtu_layout_keeps_redespacho_in_index(self):
|
|
291
|
+
"""I90DIA11 nov-2025 regression: pre-fix this consumed 'Redespacho' as
|
|
292
|
+
a 'Total' placeholder and returned an empty DataFrame. Post-fix the
|
|
293
|
+
index slice keeps it.
|
|
294
|
+
"""
|
|
295
|
+
s = _make_sheet()
|
|
296
|
+
# Plant a quarterly time block to match real shape (96 periods).
|
|
297
|
+
time_block = np.array([float(i) for i in range(1, 97)], dtype=object)
|
|
298
|
+
columns = np.concatenate([
|
|
299
|
+
np.array(["Redespacho", "Cuarto de Hora del dia"], dtype=object),
|
|
300
|
+
time_block,
|
|
301
|
+
])
|
|
302
|
+
result = s._preprocess_single_index(idx_col_start=2, columns=columns)
|
|
303
|
+
_, columns_index, _, _ = result
|
|
304
|
+
assert list(columns_index) == ["Redespacho"]
|
|
305
|
+
assert s._n_columns_totals == 1
|
|
306
|
+
|
|
307
|
+
def test_single_index_pre_mtu_layout_still_drops_total(self):
|
|
308
|
+
"""I90DIA11 jun-2025 (and 2014-2024): two separators (Cuarto/Hora + Total)."""
|
|
309
|
+
s = _make_sheet()
|
|
310
|
+
time_block = np.array([float(i) for i in range(1, 97)], dtype=object)
|
|
311
|
+
columns = np.concatenate([
|
|
312
|
+
np.array(["Redespacho", "Tipo", "Cuarto de Hora del dia", "Total"],
|
|
313
|
+
dtype=object),
|
|
314
|
+
time_block,
|
|
315
|
+
])
|
|
316
|
+
result = s._preprocess_single_index(idx_col_start=4, columns=columns)
|
|
317
|
+
_, columns_index, _, _ = result
|
|
318
|
+
assert list(columns_index) == ["Redespacho", "Tipo"]
|
|
319
|
+
assert s._n_columns_totals == 2
|
|
@@ -20,7 +20,7 @@ class TestIndicatorsManager:
|
|
|
20
20
|
df = client.indicators.list()
|
|
21
21
|
assert isinstance(df, pd.DataFrame)
|
|
22
22
|
assert len(df) == 3
|
|
23
|
-
assert "PVPC
|
|
23
|
+
assert "Término de facturación de energía activa del PVPC 2.0TD" in df["name"].values
|
|
24
24
|
|
|
25
25
|
def test_search(self, client, mock_httpx, sample_indicators_list_response):
|
|
26
26
|
response = MagicMock()
|
|
@@ -38,9 +38,9 @@ class TestIndicatorsManager:
|
|
|
38
38
|
response.json.return_value = sample_indicator_response
|
|
39
39
|
mock_httpx.get.return_value = response
|
|
40
40
|
|
|
41
|
-
handle = client.indicators.get(
|
|
42
|
-
assert handle.id ==
|
|
43
|
-
assert handle.name == "PVPC
|
|
41
|
+
handle = client.indicators.get(1001)
|
|
42
|
+
assert handle.id == 1001
|
|
43
|
+
assert handle.name == "Término de facturación de energía activa del PVPC 2.0TD"
|
|
44
44
|
|
|
45
45
|
def test_historical_returns_dataframe(
|
|
46
46
|
self, client, mock_httpx, sample_indicator_response
|
|
@@ -50,7 +50,7 @@ class TestIndicatorsManager:
|
|
|
50
50
|
response.json.return_value = sample_indicator_response
|
|
51
51
|
mock_httpx.get.return_value = response
|
|
52
52
|
|
|
53
|
-
handle = client.indicators.get(
|
|
53
|
+
handle = client.indicators.get(1001)
|
|
54
54
|
df = handle.historical("2025-01-01", "2025-01-01")
|
|
55
55
|
assert isinstance(df, pd.DataFrame)
|
|
56
56
|
assert len(df) == 2
|
|
@@ -134,15 +134,15 @@ class TestIndicatorsCaching:
|
|
|
134
134
|
mock_httpx.get.return_value = response
|
|
135
135
|
|
|
136
136
|
# First call: hits API
|
|
137
|
-
handle1 = cached_client.indicators.get(
|
|
137
|
+
handle1 = cached_client.indicators.get(1001)
|
|
138
138
|
assert mock_httpx.get.call_count == 1
|
|
139
|
-
assert handle1.id ==
|
|
139
|
+
assert handle1.id == 1001
|
|
140
140
|
|
|
141
141
|
# Second call: should use cached meta (no additional API call)
|
|
142
|
-
handle2 = cached_client.indicators.get(
|
|
142
|
+
handle2 = cached_client.indicators.get(1001)
|
|
143
143
|
assert mock_httpx.get.call_count == 1 # No new API call
|
|
144
|
-
assert handle2.id ==
|
|
145
|
-
assert handle2.name == "PVPC
|
|
144
|
+
assert handle2.id == 1001
|
|
145
|
+
assert handle2.name == "Término de facturación de energía activa del PVPC 2.0TD"
|
|
146
146
|
|
|
147
147
|
def test_get_persists_geos_to_registry(
|
|
148
148
|
self, cached_client, mock_httpx,
|
|
@@ -8,13 +8,13 @@ from esios.models.offer_indicator import OfferIndicator
|
|
|
8
8
|
class TestIndicator:
|
|
9
9
|
def test_from_api(self):
|
|
10
10
|
data = {
|
|
11
|
-
"id":
|
|
11
|
+
"id": 1001,
|
|
12
12
|
"name": "PVPC",
|
|
13
13
|
"short_name": "PVPC",
|
|
14
14
|
"description": "Precio voluntario",
|
|
15
15
|
}
|
|
16
16
|
ind = Indicator.from_api(data)
|
|
17
|
-
assert ind.id ==
|
|
17
|
+
assert ind.id == 1001
|
|
18
18
|
assert ind.name == "PVPC"
|
|
19
19
|
assert ind.raw == data
|
|
20
20
|
|
|
@@ -1,167 +0,0 @@
|
|
|
1
|
-
"""Tests for I90 file processing — frequency detection and column normalisation."""
|
|
2
|
-
|
|
3
|
-
from __future__ import annotations
|
|
4
|
-
|
|
5
|
-
from unittest.mock import MagicMock
|
|
6
|
-
|
|
7
|
-
import numpy as np
|
|
8
|
-
import pytest
|
|
9
|
-
|
|
10
|
-
from esios.processing.i90 import I90Sheet, _any_value_greater_than_30
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
# ---------------------------------------------------------------------------
|
|
14
|
-
# Helpers
|
|
15
|
-
# ---------------------------------------------------------------------------
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
def _make_sheet() -> I90Sheet:
|
|
19
|
-
"""Return a bare I90Sheet instance backed by mocked objects."""
|
|
20
|
-
wb = MagicMock()
|
|
21
|
-
sheet = MagicMock()
|
|
22
|
-
sheet.to_python.return_value = [[""]]
|
|
23
|
-
wb.get_sheet_by_name.return_value = sheet
|
|
24
|
-
return I90Sheet("test", wb, "I90DIA_20241001.xls", {})
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
def _full_nan_filler_columns(n_hours: int = 24) -> np.ndarray:
|
|
28
|
-
"""Build NaN-filler quarterly columns: [1, NaN, NaN, NaN, 2, NaN, …]."""
|
|
29
|
-
cols: list = []
|
|
30
|
-
for h in range(1, n_hours + 1):
|
|
31
|
-
cols.append(h)
|
|
32
|
-
cols.extend([np.nan, np.nan, np.nan])
|
|
33
|
-
return np.array(cols, dtype=object)
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
def _full_hq_columns(n_hours: int = 24) -> np.ndarray:
|
|
37
|
-
"""Build explicit H-Q columns: ['1-1', '1-2', '1-3', '1-4', '2-1', …]."""
|
|
38
|
-
return np.array(
|
|
39
|
-
[f"{h}-{q}" for h in range(1, n_hours + 1) for q in range(1, 5)],
|
|
40
|
-
dtype=object,
|
|
41
|
-
)
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
def _mixed_hq_columns(n_hours: int = 24) -> np.ndarray:
|
|
45
|
-
"""First quarter is unlabelled ('1', '2', …); rest carry '-Q' suffix."""
|
|
46
|
-
cols: list = []
|
|
47
|
-
for h in range(1, n_hours + 1):
|
|
48
|
-
cols.append(str(h))
|
|
49
|
-
for q in range(2, 5):
|
|
50
|
-
cols.append(f"{h}-{q}")
|
|
51
|
-
return np.array(cols, dtype=object)
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
# ---------------------------------------------------------------------------
|
|
55
|
-
# _any_value_greater_than_30
|
|
56
|
-
# ---------------------------------------------------------------------------
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
class TestAnyValueGreaterThan30:
|
|
60
|
-
def test_returns_true_for_value_above_30(self):
|
|
61
|
-
assert _any_value_greater_than_30(np.array([1, 31, 24])) is True
|
|
62
|
-
|
|
63
|
-
def test_returns_false_for_all_values_24_or_less(self):
|
|
64
|
-
assert _any_value_greater_than_30(np.arange(1, 25)) is False
|
|
65
|
-
|
|
66
|
-
def test_works_with_numpy_int64(self):
|
|
67
|
-
"""numpy 2.x broke isinstance(np.int64, int) — make sure we handle it."""
|
|
68
|
-
arr = np.array([1, 2, 31, 96], dtype=np.int64)
|
|
69
|
-
assert _any_value_greater_than_30(arr) is True
|
|
70
|
-
|
|
71
|
-
def test_ignores_nan(self):
|
|
72
|
-
arr = np.array([np.nan, 5.0, 20.0])
|
|
73
|
-
assert _any_value_greater_than_30(arr) is False
|
|
74
|
-
|
|
75
|
-
def test_sequential_quarterly_1_to_96(self):
|
|
76
|
-
assert _any_value_greater_than_30(np.arange(1, 97)) is True
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
# ---------------------------------------------------------------------------
|
|
80
|
-
# _normalize_datetime_columns — hourly (unchanged behaviour)
|
|
81
|
-
# ---------------------------------------------------------------------------
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
class TestNormalizeHourly:
|
|
85
|
-
def test_sequential_1_to_24(self):
|
|
86
|
-
s = _make_sheet()
|
|
87
|
-
cols = np.array([float(i) for i in range(1, 25)], dtype=object)
|
|
88
|
-
result = s._normalize_datetime_columns(cols)
|
|
89
|
-
assert list(result) == list(range(1, 25))
|
|
90
|
-
assert not _any_value_greater_than_30(result)
|
|
91
|
-
|
|
92
|
-
def test_n_columns_totals_is_2_when_no_nan(self):
|
|
93
|
-
s = _make_sheet()
|
|
94
|
-
cols = np.array([float(i) for i in range(1, 25)], dtype=object)
|
|
95
|
-
s._normalize_datetime_columns(cols)
|
|
96
|
-
assert s._n_columns_totals == 2
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
# ---------------------------------------------------------------------------
|
|
100
|
-
# _normalize_datetime_columns — quarterly (new behaviour)
|
|
101
|
-
# ---------------------------------------------------------------------------
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
class TestNormalizeQuarterlySequential:
|
|
105
|
-
"""Columns already in 1–96 sequential form (already worked before the fix)."""
|
|
106
|
-
|
|
107
|
-
def test_sequential_1_to_96(self):
|
|
108
|
-
s = _make_sheet()
|
|
109
|
-
cols = np.array([float(i) for i in range(1, 97)], dtype=object)
|
|
110
|
-
result = s._normalize_datetime_columns(cols)
|
|
111
|
-
assert list(result) == list(range(1, 97))
|
|
112
|
-
assert _any_value_greater_than_30(result)
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
class TestNormalizeQuarterlyHQFormat:
|
|
116
|
-
"""Columns in explicit 'H-Q' dash notation: '1-1', '1-2', …, '24-4'."""
|
|
117
|
-
|
|
118
|
-
def test_full_day_96_periods(self):
|
|
119
|
-
s = _make_sheet()
|
|
120
|
-
result = s._normalize_datetime_columns(_full_hq_columns())
|
|
121
|
-
assert len(result) == 96
|
|
122
|
-
assert list(result) == list(range(1, 97))
|
|
123
|
-
assert _any_value_greater_than_30(result)
|
|
124
|
-
|
|
125
|
-
def test_time_deltas_are_correct(self):
|
|
126
|
-
"""period 1 → 0 min (00:00), period 96 → 1425 min (23:45)."""
|
|
127
|
-
s = _make_sheet()
|
|
128
|
-
result = s._normalize_datetime_columns(_full_hq_columns())
|
|
129
|
-
time_deltas = (result - 1) * 15
|
|
130
|
-
assert time_deltas[0] == 0
|
|
131
|
-
assert time_deltas[-1] == 1425
|
|
132
|
-
|
|
133
|
-
def test_mixed_hq_first_quarter_unlabelled(self):
|
|
134
|
-
"""'1', '1-2', '1-3', '1-4', '2', '2-2', … is treated the same."""
|
|
135
|
-
s = _make_sheet()
|
|
136
|
-
result = s._normalize_datetime_columns(_mixed_hq_columns())
|
|
137
|
-
assert len(result) == 96
|
|
138
|
-
assert list(result) == list(range(1, 97))
|
|
139
|
-
assert _any_value_greater_than_30(result)
|
|
140
|
-
|
|
141
|
-
def test_n_columns_totals_is_2_for_explicit_hq(self):
|
|
142
|
-
s = _make_sheet()
|
|
143
|
-
s._normalize_datetime_columns(_full_hq_columns())
|
|
144
|
-
assert s._n_columns_totals == 2
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
class TestNormalizeQuarterlyNaNFiller:
|
|
148
|
-
"""Columns in NaN-filler form: [1, NaN, NaN, NaN, 2, NaN, …]."""
|
|
149
|
-
|
|
150
|
-
def test_full_day_96_periods(self):
|
|
151
|
-
s = _make_sheet()
|
|
152
|
-
result = s._normalize_datetime_columns(_full_nan_filler_columns())
|
|
153
|
-
assert len(result) == 96
|
|
154
|
-
assert list(result) == list(range(1, 97))
|
|
155
|
-
assert _any_value_greater_than_30(result)
|
|
156
|
-
|
|
157
|
-
def test_time_deltas_are_correct(self):
|
|
158
|
-
s = _make_sheet()
|
|
159
|
-
result = s._normalize_datetime_columns(_full_nan_filler_columns())
|
|
160
|
-
time_deltas = (result - 1) * 15
|
|
161
|
-
assert time_deltas[0] == 0
|
|
162
|
-
assert time_deltas[-1] == 1425
|
|
163
|
-
|
|
164
|
-
def test_n_columns_totals_is_3_when_nan_present(self):
|
|
165
|
-
s = _make_sheet()
|
|
166
|
-
s._normalize_datetime_columns(_full_nan_filler_columns())
|
|
167
|
-
assert s._n_columns_totals == 3
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
{python_esios-2.4.0 → python_esios-2.4.1}/examples/01_Quickstart/01_Setup and First Query.ipynb
RENAMED
|
File without changes
|
|
File without changes
|
|
File without changes
|
{python_esios-2.4.0 → python_esios-2.4.1}/examples/02_Indicators/03_Multi-Geography Indicators.ipynb
RENAMED
|
File without changes
|
{python_esios-2.4.0 → python_esios-2.4.1}/examples/02_Indicators/04_Compare Indicators.ipynb
RENAMED
|
File without changes
|
|
File without changes
|
{python_esios-2.4.0 → python_esios-2.4.1}/examples/02_Indicators/06_Generation and Demand.ipynb
RENAMED
|
File without changes
|
|
File without changes
|
{python_esios-2.4.0 → python_esios-2.4.1}/examples/03_Archives/02_I90 Settlement Files.ipynb
RENAMED
|
File without changes
|
|
File without changes
|
{python_esios-2.4.0 → python_esios-2.4.1}/examples/05_Advanced/01_Ad-hoc Pandas Expressions.ipynb
RENAMED
|
File without changes
|
|
File without changes
|
|
File without changes
|
{python_esios-2.4.0 → python_esios-2.4.1}/examples/_specs/02_Indicators/01_Search Indicators.yaml
RENAMED
|
File without changes
|
{python_esios-2.4.0 → python_esios-2.4.1}/examples/_specs/02_Indicators/02_Historical Data.yaml
RENAMED
|
File without changes
|
|
File without changes
|
{python_esios-2.4.0 → python_esios-2.4.1}/examples/_specs/02_Indicators/04_Compare Indicators.yaml
RENAMED
|
File without changes
|
{python_esios-2.4.0 → python_esios-2.4.1}/examples/_specs/02_Indicators/05_Market Prices.yaml
RENAMED
|
File without changes
|
|
File without changes
|
{python_esios-2.4.0 → python_esios-2.4.1}/examples/_specs/03_Archives/01_Download Archives.yaml
RENAMED
|
File without changes
|
{python_esios-2.4.0 → python_esios-2.4.1}/examples/_specs/03_Archives/02_I90 Settlement Files.yaml
RENAMED
|
File without changes
|
{python_esios-2.4.0 → python_esios-2.4.1}/examples/_specs/04_Caching/01_Cache Management.yaml
RENAMED
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|