python-entsoe 0.6.2__tar.gz → 0.7.0__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_entsoe-0.6.2 → python_entsoe-0.7.0}/CHANGELOG.md +14 -0
- {python_entsoe-0.6.2 → python_entsoe-0.7.0}/PKG-INFO +7 -3
- {python_entsoe-0.6.2 → python_entsoe-0.7.0}/README.md +6 -2
- {python_entsoe-0.6.2 → python_entsoe-0.7.0}/pyproject.toml +1 -1
- {python_entsoe-0.6.2 → python_entsoe-0.7.0}/src/entsoe/_mappings.py +24 -0
- {python_entsoe-0.6.2 → python_entsoe-0.7.0}/src/entsoe/cli/_output.py +8 -2
- {python_entsoe-0.6.2 → python_entsoe-0.7.0}/src/entsoe/cli/_resolve.py +3 -3
- {python_entsoe-0.6.2 → python_entsoe-0.7.0}/src/entsoe/cli/app.py +4 -3
- {python_entsoe-0.6.2 → python_entsoe-0.7.0}/src/entsoe/cli/balancing.py +4 -2
- {python_entsoe-0.6.2 → python_entsoe-0.7.0}/src/entsoe/cli/config_cmd.py +5 -1
- {python_entsoe-0.6.2 → python_entsoe-0.7.0}/src/entsoe/cli/exec_cmd.py +10 -5
- {python_entsoe-0.6.2 → python_entsoe-0.7.0}/src/entsoe/cli/generation.py +8 -4
- {python_entsoe-0.6.2 → python_entsoe-0.7.0}/src/entsoe/cli/load.py +4 -2
- {python_entsoe-0.6.2 → python_entsoe-0.7.0}/src/entsoe/cli/prices.py +2 -1
- {python_entsoe-0.6.2 → python_entsoe-0.7.0}/src/entsoe/cli/transmission.py +6 -3
- {python_entsoe-0.6.2 → python_entsoe-0.7.0}/src/entsoe/client.py +12 -5
- {python_entsoe-0.6.2 → python_entsoe-0.7.0}/src/entsoe/namespaces/_base.py +61 -12
- {python_entsoe-0.6.2 → python_entsoe-0.7.0}/src/entsoe/namespaces/balancing.py +12 -4
- {python_entsoe-0.6.2 → python_entsoe-0.7.0}/src/entsoe/namespaces/generation.py +23 -6
- {python_entsoe-0.6.2 → python_entsoe-0.7.0}/src/entsoe/namespaces/load.py +12 -4
- {python_entsoe-0.6.2 → python_entsoe-0.7.0}/src/entsoe/namespaces/prices.py +6 -2
- {python_entsoe-0.6.2 → python_entsoe-0.7.0}/src/entsoe/namespaces/transmission.py +21 -8
- python_entsoe-0.7.0/tests/test_cli_periods.py +15 -0
- python_entsoe-0.7.0/tests/test_output_tz.py +109 -0
- {python_entsoe-0.6.2 → python_entsoe-0.7.0}/tests/test_xml_day_ahead.py +14 -0
- {python_entsoe-0.6.2 → python_entsoe-0.7.0}/uv.lock +1 -1
- {python_entsoe-0.6.2 → python_entsoe-0.7.0}/.github/workflows/publish.yml +0 -0
- {python_entsoe-0.6.2 → python_entsoe-0.7.0}/.gitignore +0 -0
- {python_entsoe-0.6.2 → python_entsoe-0.7.0}/.gitlab-ci.yml +0 -0
- {python_entsoe-0.6.2 → python_entsoe-0.7.0}/.python-version +0 -0
- /python_entsoe-0.6.2/CLAUDE.md → /python_entsoe-0.7.0/AGENTS.md +0 -0
- {python_entsoe-0.6.2 → python_entsoe-0.7.0}/docs/data-availability.md +0 -0
- {python_entsoe-0.6.2 → python_entsoe-0.7.0}/examples/balancing.ipynb +0 -0
- {python_entsoe-0.6.2 → python_entsoe-0.7.0}/examples/generation.ipynb +0 -0
- {python_entsoe-0.6.2 → python_entsoe-0.7.0}/examples/generation_per_plant.ipynb +0 -0
- {python_entsoe-0.6.2 → python_entsoe-0.7.0}/examples/load.ipynb +0 -0
- {python_entsoe-0.6.2 → python_entsoe-0.7.0}/examples/prices.ipynb +0 -0
- {python_entsoe-0.6.2 → python_entsoe-0.7.0}/examples/transmission.ipynb +0 -0
- {python_entsoe-0.6.2 → python_entsoe-0.7.0}/scripts/generate_notebooks.py +0 -0
- {python_entsoe-0.6.2 → python_entsoe-0.7.0}/src/entsoe/.agents/skills/entsoe/SKILL.md +0 -0
- {python_entsoe-0.6.2 → python_entsoe-0.7.0}/src/entsoe/__init__.py +0 -0
- {python_entsoe-0.6.2 → python_entsoe-0.7.0}/src/entsoe/_http.py +0 -0
- {python_entsoe-0.6.2 → python_entsoe-0.7.0}/src/entsoe/_xml.py +0 -0
- {python_entsoe-0.6.2 → python_entsoe-0.7.0}/src/entsoe/cache.py +0 -0
- {python_entsoe-0.6.2 → python_entsoe-0.7.0}/src/entsoe/catalog.py +0 -0
- {python_entsoe-0.6.2 → python_entsoe-0.7.0}/src/entsoe/cli/__init__.py +0 -0
- {python_entsoe-0.6.2 → python_entsoe-0.7.0}/src/entsoe/cli/cache_cmd.py +0 -0
- {python_entsoe-0.6.2 → python_entsoe-0.7.0}/src/entsoe/cli/catalog_cmd.py +0 -0
- {python_entsoe-0.6.2 → python_entsoe-0.7.0}/src/entsoe/cli/config.py +0 -0
- {python_entsoe-0.6.2 → python_entsoe-0.7.0}/src/entsoe/exceptions.py +0 -0
- {python_entsoe-0.6.2 → python_entsoe-0.7.0}/src/entsoe/namespaces/__init__.py +0 -0
- {python_entsoe-0.6.2 → python_entsoe-0.7.0}/src/entsoe/py.typed +0 -0
- {python_entsoe-0.6.2 → python_entsoe-0.7.0}/tests/__init__.py +0 -0
- {python_entsoe-0.6.2 → python_entsoe-0.7.0}/tests/conftest.py +0 -0
- {python_entsoe-0.6.2 → python_entsoe-0.7.0}/tests/test_balancing.py +0 -0
- {python_entsoe-0.6.2 → python_entsoe-0.7.0}/tests/test_cache_layout.py +0 -0
- {python_entsoe-0.6.2 → python_entsoe-0.7.0}/tests/test_generation.py +0 -0
- {python_entsoe-0.6.2 → python_entsoe-0.7.0}/tests/test_load.py +0 -0
- {python_entsoe-0.6.2 → python_entsoe-0.7.0}/tests/test_multi.py +0 -0
- {python_entsoe-0.6.2 → python_entsoe-0.7.0}/tests/test_prices.py +0 -0
- {python_entsoe-0.6.2 → python_entsoe-0.7.0}/tests/test_transmission.py +0 -0
- {python_entsoe-0.6.2 → python_entsoe-0.7.0}/tests/test_validation.py +0 -0
|
@@ -1,5 +1,19 @@
|
|
|
1
1
|
# Changelog
|
|
2
2
|
|
|
3
|
+
## [0.7.0](https://gitlab.com/datons.com/libraries/python-entsoe/-/tags/v0.7.0) (2026-11-02)
|
|
4
|
+
|
|
5
|
+
### Features
|
|
6
|
+
|
|
7
|
+
* **timezones:** configure result timestamps as UTC (default), local area time, or a fixed IANA timezone through the client, per-call override, or CLI `--tz` and persistent `output-tz` setting.
|
|
8
|
+
|
|
9
|
+
## [0.6.3](https://gitlab.com/datons.com/libraries/python-entsoe/-/tags/v0.6.3) (2026-10-15)
|
|
10
|
+
|
|
11
|
+
|
|
12
|
+
### Bug Fixes
|
|
13
|
+
|
|
14
|
+
* **cli:** `--period today` and `--period yesterday` produced an empty range and always failed with "start must be before end"; each now covers the full day.
|
|
15
|
+
* **cache:** a failed download (timeout, HTTP error) no longer returns an empty or partial DataFrame with only a warning in the log; the error is raised.
|
|
16
|
+
|
|
3
17
|
## [0.6.2](https://gitlab.com/datons.com/libraries/python-entsoe/-/tags/v0.6.2) (2026-10-15)
|
|
4
18
|
|
|
5
19
|
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.5
|
|
2
2
|
Name: python-entsoe
|
|
3
|
-
Version: 0.
|
|
3
|
+
Version: 0.7.0
|
|
4
4
|
Summary: Python client for the ENTSO-E Transparency Platform API
|
|
5
5
|
Project-URL: Homepage, https://gitlab.com/datons.com/libraries/python-entsoe
|
|
6
6
|
Project-URL: Repository, https://gitlab.com/datons.com/libraries/python-entsoe
|
|
@@ -57,7 +57,7 @@ start = pd.Timestamp("2024-06-01", tz="Europe/Paris")
|
|
|
57
57
|
df = client.load.actual(start, "2024-06-08", country="FR")
|
|
58
58
|
```
|
|
59
59
|
|
|
60
|
-
Every method returns a `pandas.DataFrame` with a `timestamp` column
|
|
60
|
+
Every method returns a `pandas.DataFrame` with a timezone-aware `timestamp` column and a `value` column.
|
|
61
61
|
|
|
62
62
|
## Authentication
|
|
63
63
|
|
|
@@ -216,7 +216,11 @@ df = client.load.actual("2024-01-01", pd.Timestamp("2024-01-07", tz="UTC"), coun
|
|
|
216
216
|
start = pd.Timestamp("2024-01-01") # ← no tz, will error
|
|
217
217
|
```
|
|
218
218
|
|
|
219
|
-
|
|
219
|
+
## Timezones
|
|
220
|
+
|
|
221
|
+
The client `tz` setting interprets input date strings (default `Europe/Brussels`); it does not control returned timestamps. The separate `output_tz` setting controls results: `"UTC"` (default, unchanged behavior), `"local"` (the requested area's timezone), or a fixed IANA timezone such as `"Europe/Madrid"`. Every namespace method also accepts `output_tz=` for one call. The CLI accepts `--tz` on data commands, or `entsoe config set output-tz local` as a persistent default; `--tz` takes precedence.
|
|
222
|
+
|
|
223
|
+
For a multi-area request, `"local"` requires matching clock offsets throughout the returned period (for example, FR and ES); otherwise choose `"UTC"` or a fixed IANA zone. Cross-border results use the first importing (`in_domain`) zone. During the October DST fall-back, local wall-clock times repeat; each row retains its offset, but timestamps cease to be unique if the timezone is stripped.
|
|
220
224
|
|
|
221
225
|
## Features
|
|
222
226
|
|
|
@@ -29,7 +29,7 @@ start = pd.Timestamp("2024-06-01", tz="Europe/Paris")
|
|
|
29
29
|
df = client.load.actual(start, "2024-06-08", country="FR")
|
|
30
30
|
```
|
|
31
31
|
|
|
32
|
-
Every method returns a `pandas.DataFrame` with a `timestamp` column
|
|
32
|
+
Every method returns a `pandas.DataFrame` with a timezone-aware `timestamp` column and a `value` column.
|
|
33
33
|
|
|
34
34
|
## Authentication
|
|
35
35
|
|
|
@@ -188,7 +188,11 @@ df = client.load.actual("2024-01-01", pd.Timestamp("2024-01-07", tz="UTC"), coun
|
|
|
188
188
|
start = pd.Timestamp("2024-01-01") # ← no tz, will error
|
|
189
189
|
```
|
|
190
190
|
|
|
191
|
-
|
|
191
|
+
## Timezones
|
|
192
|
+
|
|
193
|
+
The client `tz` setting interprets input date strings (default `Europe/Brussels`); it does not control returned timestamps. The separate `output_tz` setting controls results: `"UTC"` (default, unchanged behavior), `"local"` (the requested area's timezone), or a fixed IANA timezone such as `"Europe/Madrid"`. Every namespace method also accepts `output_tz=` for one call. The CLI accepts `--tz` on data commands, or `entsoe config set output-tz local` as a persistent default; `--tz` takes precedence.
|
|
194
|
+
|
|
195
|
+
For a multi-area request, `"local"` requires matching clock offsets throughout the returned period (for example, FR and ES); otherwise choose `"UTC"` or a fixed IANA zone. Cross-border results use the first importing (`in_domain`) zone. During the October DST fall-back, local wall-clock times repeat; each row retains its offset, but timestamps cease to be unique if the timezone is stripped.
|
|
192
196
|
|
|
193
197
|
## Features
|
|
194
198
|
|
|
@@ -64,6 +64,30 @@ AREA_CODES: dict[str, str] = {
|
|
|
64
64
|
"XK": "10Y1001C--00100H",
|
|
65
65
|
}
|
|
66
66
|
|
|
67
|
+
# IANA timezone of each ENTSO-E bidding area. Keys intentionally mirror AREA_CODES.
|
|
68
|
+
AREA_TZ: dict[str, str] = {
|
|
69
|
+
"AL": "Europe/Tirane", "AT": "Europe/Vienna", "BA": "Europe/Sarajevo",
|
|
70
|
+
"BE": "Europe/Brussels", "BG": "Europe/Sofia", "CH": "Europe/Zurich",
|
|
71
|
+
"CZ": "Europe/Prague", "DE": "Europe/Berlin", "DE_LU": "Europe/Berlin",
|
|
72
|
+
"DE_AT_LU": "Europe/Berlin", "DK": "Europe/Copenhagen",
|
|
73
|
+
"DK_1": "Europe/Copenhagen", "DK_2": "Europe/Copenhagen",
|
|
74
|
+
"EE": "Europe/Tallinn", "ES": "Europe/Madrid", "FI": "Europe/Helsinki",
|
|
75
|
+
"FR": "Europe/Paris", "GB": "Europe/London", "GR": "Europe/Athens",
|
|
76
|
+
"HR": "Europe/Zagreb", "HU": "Europe/Budapest", "IE": "Europe/Dublin",
|
|
77
|
+
"IE_SEM": "Europe/Dublin", "IT": "Europe/Rome", "IT_NORTH": "Europe/Rome",
|
|
78
|
+
"IT_CNOR": "Europe/Rome", "IT_CSUD": "Europe/Rome", "IT_SUD": "Europe/Rome",
|
|
79
|
+
"IT_SICI": "Europe/Rome", "IT_SARD": "Europe/Rome", "LT": "Europe/Vilnius",
|
|
80
|
+
"LU": "Europe/Luxembourg", "LV": "Europe/Riga", "ME": "Europe/Podgorica",
|
|
81
|
+
"MK": "Europe/Skopje", "MT": "Europe/Malta", "NL": "Europe/Amsterdam",
|
|
82
|
+
"NO": "Europe/Oslo", "NO_1": "Europe/Oslo", "NO_2": "Europe/Oslo",
|
|
83
|
+
"NO_3": "Europe/Oslo", "NO_4": "Europe/Oslo", "NO_5": "Europe/Oslo",
|
|
84
|
+
"PL": "Europe/Warsaw", "PT": "Europe/Lisbon", "RO": "Europe/Bucharest",
|
|
85
|
+
"RS": "Europe/Belgrade", "SE": "Europe/Stockholm", "SE_1": "Europe/Stockholm",
|
|
86
|
+
"SE_2": "Europe/Stockholm", "SE_3": "Europe/Stockholm", "SE_4": "Europe/Stockholm",
|
|
87
|
+
"SI": "Europe/Ljubljana", "SK": "Europe/Bratislava", "TR": "Europe/Istanbul",
|
|
88
|
+
"UA": "Europe/Kyiv", "UK": "Europe/London", "XK": "Europe/Belgrade",
|
|
89
|
+
}
|
|
90
|
+
|
|
67
91
|
# ISO code → human-readable country/area name
|
|
68
92
|
COUNTRY_NAMES: dict[str, str] = {
|
|
69
93
|
"AL": "Albania",
|
|
@@ -34,7 +34,10 @@ def output(df, format: str, title: str = "", output_path: str | None = None):
|
|
|
34
34
|
typer.echo(text)
|
|
35
35
|
|
|
36
36
|
elif format == "json":
|
|
37
|
-
|
|
37
|
+
display = df.copy()
|
|
38
|
+
if "timestamp" in display:
|
|
39
|
+
display["timestamp"] = display["timestamp"].map(lambda value: value.isoformat())
|
|
40
|
+
text = display.to_json(orient="records", indent=2)
|
|
38
41
|
if output_path:
|
|
39
42
|
with open(output_path, "w") as f:
|
|
40
43
|
f.write(text)
|
|
@@ -72,7 +75,10 @@ def render_result(result, format: str, output_path: str | None) -> None:
|
|
|
72
75
|
if format == "csv":
|
|
73
76
|
text = result.to_csv()
|
|
74
77
|
elif format == "json":
|
|
75
|
-
|
|
78
|
+
display = result.copy()
|
|
79
|
+
if "timestamp" in display:
|
|
80
|
+
display["timestamp"] = display["timestamp"].map(lambda value: value.isoformat())
|
|
81
|
+
text = display.to_json(orient="records", indent=2)
|
|
76
82
|
else:
|
|
77
83
|
table = Table()
|
|
78
84
|
idx_name = result.index.name or ""
|
|
@@ -100,11 +100,11 @@ def _expand_period(period: str, today: date) -> tuple[date, date]:
|
|
|
100
100
|
|
|
101
101
|
p = period.lower().strip()
|
|
102
102
|
|
|
103
|
+
# The library's end is exclusive, so a single day ends on the next one.
|
|
103
104
|
if p == "today":
|
|
104
|
-
return today, today
|
|
105
|
+
return today, today + timedelta(days=1)
|
|
105
106
|
if p == "yesterday":
|
|
106
|
-
|
|
107
|
-
return yesterday, yesterday
|
|
107
|
+
return today - timedelta(days=1), today
|
|
108
108
|
if p == "week":
|
|
109
109
|
monday = today - timedelta(days=today.weekday())
|
|
110
110
|
return monday, today
|
|
@@ -6,7 +6,7 @@ from typing import Optional
|
|
|
6
6
|
|
|
7
7
|
import typer
|
|
8
8
|
|
|
9
|
-
from entsoe.cli.config import get_api_key
|
|
9
|
+
from entsoe.cli.config import get_api_key, get_config
|
|
10
10
|
|
|
11
11
|
app = typer.Typer(
|
|
12
12
|
name="entsoe",
|
|
@@ -15,7 +15,7 @@ app = typer.Typer(
|
|
|
15
15
|
)
|
|
16
16
|
|
|
17
17
|
|
|
18
|
-
def get_client(api_key: str | None = None):
|
|
18
|
+
def get_client(api_key: str | None = None, output_tz: str | None = None):
|
|
19
19
|
"""Lazy import + construct client."""
|
|
20
20
|
from entsoe.client import ENTSOEClient
|
|
21
21
|
|
|
@@ -26,7 +26,8 @@ def get_client(api_key: str | None = None):
|
|
|
26
26
|
err=True,
|
|
27
27
|
)
|
|
28
28
|
raise typer.Exit(1)
|
|
29
|
-
|
|
29
|
+
configured_tz = get_config("output-tz") if output_tz is None else output_tz
|
|
30
|
+
return ENTSOEClient(api_key=resolved, output_tz="UTC" if configured_tz is None else configured_tz)
|
|
30
31
|
|
|
31
32
|
|
|
32
33
|
# -- Register sub-commands ---------------------------------------------------
|
|
@@ -21,12 +21,13 @@ def prices(
|
|
|
21
21
|
format: str = typer.Option("table", "--format", "-f", help="Output format: table, csv, json"),
|
|
22
22
|
output_path: Optional[str] = typer.Option(None, "--output", "-o", help="Output file path"),
|
|
23
23
|
api_key: Optional[str] = typer.Option(None, "--api-key", help="ENTSO-E API key"),
|
|
24
|
+
tz: Optional[str] = typer.Option(None, "--tz", help="Output timezone: UTC, local, or IANA name"),
|
|
24
25
|
):
|
|
25
26
|
"""Query imbalance prices."""
|
|
26
27
|
from entsoe.cli.app import get_client
|
|
27
28
|
|
|
28
29
|
s, e = resolve_dates(start, end, period)
|
|
29
|
-
client = get_client(api_key)
|
|
30
|
+
client = get_client(api_key, tz)
|
|
30
31
|
countries = resolve_many_required(country)
|
|
31
32
|
df = client.balancing.imbalance_prices(s, e, country=countries)
|
|
32
33
|
output(df, format, title="Imbalance Prices", output_path=output_path)
|
|
@@ -41,12 +42,13 @@ def volumes(
|
|
|
41
42
|
format: str = typer.Option("table", "--format", "-f", help="Output format: table, csv, json"),
|
|
42
43
|
output_path: Optional[str] = typer.Option(None, "--output", "-o", help="Output file path"),
|
|
43
44
|
api_key: Optional[str] = typer.Option(None, "--api-key", help="ENTSO-E API key"),
|
|
45
|
+
tz: Optional[str] = typer.Option(None, "--tz", help="Output timezone: UTC, local, or IANA name"),
|
|
44
46
|
):
|
|
45
47
|
"""Query imbalance volumes."""
|
|
46
48
|
from entsoe.cli.app import get_client
|
|
47
49
|
|
|
48
50
|
s, e = resolve_dates(start, end, period)
|
|
49
|
-
client = get_client(api_key)
|
|
51
|
+
client = get_client(api_key, tz)
|
|
50
52
|
countries = resolve_many_required(country)
|
|
51
53
|
df = client.balancing.imbalance_volumes(s, e, country=countries)
|
|
52
54
|
output(df, format, title="Imbalance Volumes", output_path=output_path)
|
|
@@ -9,12 +9,16 @@ config_app = typer.Typer(no_args_is_help=True)
|
|
|
9
9
|
|
|
10
10
|
@config_app.command("set")
|
|
11
11
|
def config_set(
|
|
12
|
-
key: str = typer.Argument(..., help="Configuration key (
|
|
12
|
+
key: str = typer.Argument(..., help="Configuration key (api-key or output-tz)"),
|
|
13
13
|
value: str = typer.Argument(..., help="Configuration value"),
|
|
14
14
|
):
|
|
15
15
|
"""Set a configuration value."""
|
|
16
16
|
from entsoe.cli.config import set_config
|
|
17
17
|
|
|
18
|
+
if key == "output-tz":
|
|
19
|
+
from entsoe.namespaces._base import validate_output_tz
|
|
20
|
+
validate_output_tz(value)
|
|
21
|
+
|
|
18
22
|
set_config(key, value)
|
|
19
23
|
typer.echo(f"Set {key} = {'***' if 'key' in key.lower() else value}")
|
|
20
24
|
|
|
@@ -62,6 +62,7 @@ def exec_prices(
|
|
|
62
62
|
format: str = typer.Option("table", "--format", "-f", help="Output format: table, csv, json"),
|
|
63
63
|
output_path: Optional[str] = typer.Option(None, "--output", "-o", help="Output file path"),
|
|
64
64
|
api_key: Optional[str] = typer.Option(None, "--api-key", help="ENTSO-E API key"),
|
|
65
|
+
tz: Optional[str] = typer.Option(None, "--tz", help="Output timezone: UTC, local, or IANA name"),
|
|
65
66
|
):
|
|
66
67
|
"""Fetch price data and evaluate a Python expression.
|
|
67
68
|
|
|
@@ -73,7 +74,7 @@ def exec_prices(
|
|
|
73
74
|
|
|
74
75
|
method_name = _resolve_method(method, _PRICES_METHODS)
|
|
75
76
|
s, e = resolve_dates(start, end, period)
|
|
76
|
-
client = get_client(api_key)
|
|
77
|
+
client = get_client(api_key, tz)
|
|
77
78
|
fn = getattr(client.prices, method_name)
|
|
78
79
|
df = fn(s, e, country=resolve_many_required(country))
|
|
79
80
|
_eval_and_render(df, expr, format, output_path)
|
|
@@ -96,6 +97,7 @@ def exec_load(
|
|
|
96
97
|
format: str = typer.Option("table", "--format", "-f", help="Output format: table, csv, json"),
|
|
97
98
|
output_path: Optional[str] = typer.Option(None, "--output", "-o", help="Output file path"),
|
|
98
99
|
api_key: Optional[str] = typer.Option(None, "--api-key", help="ENTSO-E API key"),
|
|
100
|
+
tz: Optional[str] = typer.Option(None, "--tz", help="Output timezone: UTC, local, or IANA name"),
|
|
99
101
|
):
|
|
100
102
|
"""Fetch load data and evaluate a Python expression.
|
|
101
103
|
|
|
@@ -107,7 +109,7 @@ def exec_load(
|
|
|
107
109
|
|
|
108
110
|
method_name = _resolve_method(method, _LOAD_METHODS)
|
|
109
111
|
s, e = resolve_dates(start, end, period)
|
|
110
|
-
client = get_client(api_key)
|
|
112
|
+
client = get_client(api_key, tz)
|
|
111
113
|
fn = getattr(client.load, method_name)
|
|
112
114
|
df = fn(s, e, country=resolve_many_required(country))
|
|
113
115
|
_eval_and_render(df, expr, format, output_path)
|
|
@@ -136,6 +138,7 @@ def exec_generation(
|
|
|
136
138
|
format: str = typer.Option("table", "--format", "-f", help="Output format: table, csv, json"),
|
|
137
139
|
output_path: Optional[str] = typer.Option(None, "--output", "-o", help="Output file path"),
|
|
138
140
|
api_key: Optional[str] = typer.Option(None, "--api-key", help="ENTSO-E API key"),
|
|
141
|
+
tz: Optional[str] = typer.Option(None, "--tz", help="Output timezone: UTC, local, or IANA name"),
|
|
139
142
|
):
|
|
140
143
|
"""Fetch generation data and evaluate a Python expression.
|
|
141
144
|
|
|
@@ -147,7 +150,7 @@ def exec_generation(
|
|
|
147
150
|
|
|
148
151
|
method_name = _resolve_method(method, _GENERATION_METHODS)
|
|
149
152
|
s, e = resolve_dates(start, end, period)
|
|
150
|
-
client = get_client(api_key)
|
|
153
|
+
client = get_client(api_key, tz)
|
|
151
154
|
fn = getattr(client.generation, method_name)
|
|
152
155
|
df = fn(s, e, country=resolve_many_required(country), psr_type=resolve_many(psr))
|
|
153
156
|
_eval_and_render(df, expr, format, output_path)
|
|
@@ -176,6 +179,7 @@ def exec_transmission(
|
|
|
176
179
|
format: str = typer.Option("table", "--format", "-f", help="Output format: table, csv, json"),
|
|
177
180
|
output_path: Optional[str] = typer.Option(None, "--output", "-o", help="Output file path"),
|
|
178
181
|
api_key: Optional[str] = typer.Option(None, "--api-key", help="ENTSO-E API key"),
|
|
182
|
+
tz: Optional[str] = typer.Option(None, "--tz", help="Output timezone: UTC, local, or IANA name"),
|
|
179
183
|
):
|
|
180
184
|
"""Fetch transmission data and evaluate a Python expression.
|
|
181
185
|
|
|
@@ -200,7 +204,7 @@ def exec_transmission(
|
|
|
200
204
|
typer.echo("Error: Provide --from/--to or --border.", err=True)
|
|
201
205
|
raise typer.Exit(1)
|
|
202
206
|
|
|
203
|
-
client = get_client(api_key)
|
|
207
|
+
client = get_client(api_key, tz)
|
|
204
208
|
fn = getattr(client.transmission, method_name)
|
|
205
209
|
df = fn(s, e, **kwargs)
|
|
206
210
|
_eval_and_render(df, expr, format, output_path)
|
|
@@ -223,6 +227,7 @@ def exec_balancing(
|
|
|
223
227
|
format: str = typer.Option("table", "--format", "-f", help="Output format: table, csv, json"),
|
|
224
228
|
output_path: Optional[str] = typer.Option(None, "--output", "-o", help="Output file path"),
|
|
225
229
|
api_key: Optional[str] = typer.Option(None, "--api-key", help="ENTSO-E API key"),
|
|
230
|
+
tz: Optional[str] = typer.Option(None, "--tz", help="Output timezone: UTC, local, or IANA name"),
|
|
226
231
|
):
|
|
227
232
|
"""Fetch balancing data and evaluate a Python expression.
|
|
228
233
|
|
|
@@ -234,7 +239,7 @@ def exec_balancing(
|
|
|
234
239
|
|
|
235
240
|
method_name = _resolve_method(method, _BALANCING_METHODS)
|
|
236
241
|
s, e = resolve_dates(start, end, period)
|
|
237
|
-
client = get_client(api_key)
|
|
242
|
+
client = get_client(api_key, tz)
|
|
238
243
|
fn = getattr(client.balancing, method_name)
|
|
239
244
|
df = fn(s, e, country=resolve_many_required(country))
|
|
240
245
|
_eval_and_render(df, expr, format, output_path)
|
|
@@ -22,12 +22,13 @@ def actual(
|
|
|
22
22
|
format: str = typer.Option("table", "--format", "-f", help="Output format: table, csv, json"),
|
|
23
23
|
output_path: Optional[str] = typer.Option(None, "--output", "-o", help="Output file path"),
|
|
24
24
|
api_key: Optional[str] = typer.Option(None, "--api-key", help="ENTSO-E API key"),
|
|
25
|
+
tz: Optional[str] = typer.Option(None, "--tz", help="Output timezone: UTC, local, or IANA name"),
|
|
25
26
|
):
|
|
26
27
|
"""Query actual generation output per type."""
|
|
27
28
|
from entsoe.cli.app import get_client
|
|
28
29
|
|
|
29
30
|
s, e = resolve_dates(start, end, period)
|
|
30
|
-
client = get_client(api_key)
|
|
31
|
+
client = get_client(api_key, tz)
|
|
31
32
|
df = client.generation.actual(s, e, country=resolve_many_required(country), psr_type=resolve_many(psr))
|
|
32
33
|
output(df, format, title="Actual Generation", output_path=output_path)
|
|
33
34
|
|
|
@@ -42,12 +43,13 @@ def forecast(
|
|
|
42
43
|
format: str = typer.Option("table", "--format", "-f", help="Output format: table, csv, json"),
|
|
43
44
|
output_path: Optional[str] = typer.Option(None, "--output", "-o", help="Output file path"),
|
|
44
45
|
api_key: Optional[str] = typer.Option(None, "--api-key", help="ENTSO-E API key"),
|
|
46
|
+
tz: Optional[str] = typer.Option(None, "--tz", help="Output timezone: UTC, local, or IANA name"),
|
|
45
47
|
):
|
|
46
48
|
"""Query day-ahead generation forecast (wind/solar)."""
|
|
47
49
|
from entsoe.cli.app import get_client
|
|
48
50
|
|
|
49
51
|
s, e = resolve_dates(start, end, period)
|
|
50
|
-
client = get_client(api_key)
|
|
52
|
+
client = get_client(api_key, tz)
|
|
51
53
|
df = client.generation.forecast(s, e, country=resolve_many_required(country), psr_type=resolve_many(psr))
|
|
52
54
|
output(df, format, title="Generation Forecast", output_path=output_path)
|
|
53
55
|
|
|
@@ -62,12 +64,13 @@ def capacity(
|
|
|
62
64
|
format: str = typer.Option("table", "--format", "-f", help="Output format: table, csv, json"),
|
|
63
65
|
output_path: Optional[str] = typer.Option(None, "--output", "-o", help="Output file path"),
|
|
64
66
|
api_key: Optional[str] = typer.Option(None, "--api-key", help="ENTSO-E API key"),
|
|
67
|
+
tz: Optional[str] = typer.Option(None, "--tz", help="Output timezone: UTC, local, or IANA name"),
|
|
65
68
|
):
|
|
66
69
|
"""Query installed generation capacity per type."""
|
|
67
70
|
from entsoe.cli.app import get_client
|
|
68
71
|
|
|
69
72
|
s, e = resolve_dates(start, end, period)
|
|
70
|
-
client = get_client(api_key)
|
|
73
|
+
client = get_client(api_key, tz)
|
|
71
74
|
df = client.generation.installed_capacity(s, e, country=resolve_many_required(country), psr_type=resolve_many(psr))
|
|
72
75
|
output(df, format, title="Installed Capacity", output_path=output_path)
|
|
73
76
|
|
|
@@ -82,12 +85,13 @@ def per_plant(
|
|
|
82
85
|
format: str = typer.Option("table", "--format", "-f", help="Output format: table, csv, json"),
|
|
83
86
|
output_path: Optional[str] = typer.Option(None, "--output", "-o", help="Output file path"),
|
|
84
87
|
api_key: Optional[str] = typer.Option(None, "--api-key", help="ENTSO-E API key"),
|
|
88
|
+
tz: Optional[str] = typer.Option(None, "--tz", help="Output timezone: UTC, local, or IANA name"),
|
|
85
89
|
):
|
|
86
90
|
"""Query actual generation per production unit."""
|
|
87
91
|
from entsoe.cli.app import get_client
|
|
88
92
|
|
|
89
93
|
s, e = resolve_dates(start, end, period)
|
|
90
|
-
client = get_client(api_key)
|
|
94
|
+
client = get_client(api_key, tz)
|
|
91
95
|
df = client.generation.per_plant(s, e, country=resolve_many_required(country), psr_type=resolve_many(psr))
|
|
92
96
|
output(df, format, title="Generation Per Plant", output_path=output_path)
|
|
93
97
|
|
|
@@ -21,12 +21,13 @@ def actual(
|
|
|
21
21
|
format: str = typer.Option("table", "--format", "-f", help="Output format: table, csv, json"),
|
|
22
22
|
output_path: Optional[str] = typer.Option(None, "--output", "-o", help="Output file path"),
|
|
23
23
|
api_key: Optional[str] = typer.Option(None, "--api-key", help="ENTSO-E API key"),
|
|
24
|
+
tz: Optional[str] = typer.Option(None, "--tz", help="Output timezone: UTC, local, or IANA name"),
|
|
24
25
|
):
|
|
25
26
|
"""Query actual total system load."""
|
|
26
27
|
from entsoe.cli.app import get_client
|
|
27
28
|
|
|
28
29
|
s, e = resolve_dates(start, end, period)
|
|
29
|
-
client = get_client(api_key)
|
|
30
|
+
client = get_client(api_key, tz)
|
|
30
31
|
countries = resolve_many_required(country)
|
|
31
32
|
df = client.load.actual(s, e, country=countries)
|
|
32
33
|
output(df, format, title="Actual Load", output_path=output_path)
|
|
@@ -41,12 +42,13 @@ def forecast(
|
|
|
41
42
|
format: str = typer.Option("table", "--format", "-f", help="Output format: table, csv, json"),
|
|
42
43
|
output_path: Optional[str] = typer.Option(None, "--output", "-o", help="Output file path"),
|
|
43
44
|
api_key: Optional[str] = typer.Option(None, "--api-key", help="ENTSO-E API key"),
|
|
45
|
+
tz: Optional[str] = typer.Option(None, "--tz", help="Output timezone: UTC, local, or IANA name"),
|
|
44
46
|
):
|
|
45
47
|
"""Query day-ahead load forecast."""
|
|
46
48
|
from entsoe.cli.app import get_client
|
|
47
49
|
|
|
48
50
|
s, e = resolve_dates(start, end, period)
|
|
49
|
-
client = get_client(api_key)
|
|
51
|
+
client = get_client(api_key, tz)
|
|
50
52
|
countries = resolve_many_required(country)
|
|
51
53
|
df = client.load.forecast(s, e, country=countries)
|
|
52
54
|
output(df, format, title="Load Forecast", output_path=output_path)
|
|
@@ -22,12 +22,13 @@ def day_ahead(
|
|
|
22
22
|
format: str = typer.Option("table", "--format", "-f", help="Output format: table, csv, json"),
|
|
23
23
|
output_path: Optional[str] = typer.Option(None, "--output", "-o", help="Output file path"),
|
|
24
24
|
api_key: Optional[str] = typer.Option(None, "--api-key", help="ENTSO-E API key"),
|
|
25
|
+
tz: Optional[str] = typer.Option(None, "--tz", help="Output timezone: UTC, local, or IANA name"),
|
|
25
26
|
):
|
|
26
27
|
"""Query day-ahead electricity prices."""
|
|
27
28
|
from entsoe.cli.app import get_client
|
|
28
29
|
|
|
29
30
|
s, e = resolve_dates(start, end, period)
|
|
30
|
-
client = get_client(api_key)
|
|
31
|
+
client = get_client(api_key, tz)
|
|
31
32
|
countries = resolve_many_required(country)
|
|
32
33
|
df = client.prices.day_ahead(s, e, country=countries, sequence=sequence)
|
|
33
34
|
output(df, format, title="Day-Ahead Prices", output_path=output_path)
|
|
@@ -43,13 +43,14 @@ def flows(
|
|
|
43
43
|
format: str = typer.Option("table", "--format", "-f", help="Output format: table, csv, json"),
|
|
44
44
|
output_path: Optional[str] = typer.Option(None, "--output", "-o", help="Output file path"),
|
|
45
45
|
api_key: Optional[str] = typer.Option(None, "--api-key", help="ENTSO-E API key"),
|
|
46
|
+
tz: Optional[str] = typer.Option(None, "--tz", help="Output timezone: UTC, local, or IANA name"),
|
|
46
47
|
):
|
|
47
48
|
"""Query physical cross-border flows."""
|
|
48
49
|
from entsoe.cli.app import get_client
|
|
49
50
|
|
|
50
51
|
s, e = resolve_dates(start, end, period)
|
|
51
52
|
kwargs = _resolve_borders_or_pairs(country_from, country_to, border)
|
|
52
|
-
client = get_client(api_key)
|
|
53
|
+
client = get_client(api_key, tz)
|
|
53
54
|
df = client.transmission.crossborder_flows(s, e, **kwargs)
|
|
54
55
|
output(df, format, title="Cross-Border Flows", output_path=output_path)
|
|
55
56
|
|
|
@@ -65,13 +66,14 @@ def exchanges(
|
|
|
65
66
|
format: str = typer.Option("table", "--format", "-f", help="Output format: table, csv, json"),
|
|
66
67
|
output_path: Optional[str] = typer.Option(None, "--output", "-o", help="Output file path"),
|
|
67
68
|
api_key: Optional[str] = typer.Option(None, "--api-key", help="ENTSO-E API key"),
|
|
69
|
+
tz: Optional[str] = typer.Option(None, "--tz", help="Output timezone: UTC, local, or IANA name"),
|
|
68
70
|
):
|
|
69
71
|
"""Query scheduled commercial exchanges (day-ahead)."""
|
|
70
72
|
from entsoe.cli.app import get_client
|
|
71
73
|
|
|
72
74
|
s, e = resolve_dates(start, end, period)
|
|
73
75
|
kwargs = _resolve_borders_or_pairs(country_from, country_to, border)
|
|
74
|
-
client = get_client(api_key)
|
|
76
|
+
client = get_client(api_key, tz)
|
|
75
77
|
df = client.transmission.scheduled_exchanges(s, e, **kwargs)
|
|
76
78
|
output(df, format, title="Scheduled Exchanges", output_path=output_path)
|
|
77
79
|
|
|
@@ -87,13 +89,14 @@ def capacity(
|
|
|
87
89
|
format: str = typer.Option("table", "--format", "-f", help="Output format: table, csv, json"),
|
|
88
90
|
output_path: Optional[str] = typer.Option(None, "--output", "-o", help="Output file path"),
|
|
89
91
|
api_key: Optional[str] = typer.Option(None, "--api-key", help="ENTSO-E API key"),
|
|
92
|
+
tz: Optional[str] = typer.Option(None, "--tz", help="Output timezone: UTC, local, or IANA name"),
|
|
90
93
|
):
|
|
91
94
|
"""Query day-ahead net transfer capacity."""
|
|
92
95
|
from entsoe.cli.app import get_client
|
|
93
96
|
|
|
94
97
|
s, e = resolve_dates(start, end, period)
|
|
95
98
|
kwargs = _resolve_borders_or_pairs(country_from, country_to, border)
|
|
96
|
-
client = get_client(api_key)
|
|
99
|
+
client = get_client(api_key, tz)
|
|
97
100
|
df = client.transmission.net_transfer_capacity(s, e, **kwargs)
|
|
98
101
|
output(df, format, title="Net Transfer Capacity", output_path=output_path)
|
|
99
102
|
|
|
@@ -7,6 +7,7 @@ from pathlib import Path
|
|
|
7
7
|
|
|
8
8
|
from ._http import HttpClient
|
|
9
9
|
from .cache import CacheConfig, CacheStore
|
|
10
|
+
from .namespaces._base import validate_output_tz
|
|
10
11
|
from .namespaces import (
|
|
11
12
|
BalancingNamespace,
|
|
12
13
|
GenerationNamespace,
|
|
@@ -38,6 +39,8 @@ class ENTSOEClient:
|
|
|
38
39
|
|
|
39
40
|
# Override default timezone
|
|
40
41
|
client = ENTSOEClient(tz="UTC")
|
|
42
|
+
client = ENTSOEClient(output_tz="local") # results use each zone's local time
|
|
43
|
+
client = ENTSOEClient(output_tz="Europe/Madrid") # fixed output zone
|
|
41
44
|
|
|
42
45
|
# Disable cache
|
|
43
46
|
client = ENTSOEClient(cache=False)
|
|
@@ -60,6 +63,7 @@ class ENTSOEClient:
|
|
|
60
63
|
cache: bool = True,
|
|
61
64
|
cache_dir: str | Path | None = None,
|
|
62
65
|
cache_recent_ttl: int = 48,
|
|
66
|
+
output_tz: str = "UTC",
|
|
63
67
|
) -> None:
|
|
64
68
|
"""Initialize the client.
|
|
65
69
|
|
|
@@ -71,10 +75,13 @@ class ENTSOEClient:
|
|
|
71
75
|
cache: Whether to enable local parquet caching. Defaults to True.
|
|
72
76
|
cache_dir: Custom cache directory. Defaults to ``~/.cache/entsoe/``.
|
|
73
77
|
cache_recent_ttl: Hours before data is considered final (default 48).
|
|
78
|
+
output_tz: Result timezone: ``UTC`` (default), ``local`` for the requested area, or an IANA name.
|
|
74
79
|
|
|
75
80
|
Raises:
|
|
76
81
|
ValueError: If no API key is found.
|
|
82
|
+
InvalidParameterError: If output_tz is not UTC, local, or a valid IANA timezone.
|
|
77
83
|
"""
|
|
84
|
+
output_tz = validate_output_tz(output_tz)
|
|
78
85
|
resolved_key = api_key or os.environ.get("ENTSOE_API_KEY")
|
|
79
86
|
if not resolved_key:
|
|
80
87
|
raise ValueError(
|
|
@@ -94,11 +101,11 @@ class ENTSOEClient:
|
|
|
94
101
|
|
|
95
102
|
cache_store = CacheStore(cache_config) if cache else None
|
|
96
103
|
|
|
97
|
-
self.load = LoadNamespace(http, tz=tz, cache=cache_store)
|
|
98
|
-
self.prices = PricesNamespace(http, tz=tz, cache=cache_store)
|
|
99
|
-
self.generation = GenerationNamespace(http, tz=tz, cache=cache_store)
|
|
100
|
-
self.transmission = TransmissionNamespace(http, tz=tz, cache=cache_store)
|
|
101
|
-
self.balancing = BalancingNamespace(http, tz=tz, cache=cache_store)
|
|
104
|
+
self.load = LoadNamespace(http, tz=tz, output_tz=output_tz, cache=cache_store)
|
|
105
|
+
self.prices = PricesNamespace(http, tz=tz, output_tz=output_tz, cache=cache_store)
|
|
106
|
+
self.generation = GenerationNamespace(http, tz=tz, output_tz=output_tz, cache=cache_store)
|
|
107
|
+
self.transmission = TransmissionNamespace(http, tz=tz, output_tz=output_tz, cache=cache_store)
|
|
108
|
+
self.balancing = BalancingNamespace(http, tz=tz, output_tz=output_tz, cache=cache_store)
|
|
102
109
|
|
|
103
110
|
# Expose for CLI cache commands
|
|
104
111
|
self._cache = cache_store
|
|
@@ -3,12 +3,13 @@
|
|
|
3
3
|
from __future__ import annotations
|
|
4
4
|
|
|
5
5
|
import logging
|
|
6
|
+
from zoneinfo import ZoneInfo, ZoneInfoNotFoundError
|
|
6
7
|
from typing import TYPE_CHECKING, Callable, Optional, Union
|
|
7
8
|
|
|
8
9
|
import pandas as pd
|
|
9
10
|
|
|
10
|
-
from ..exceptions import InvalidParameterError
|
|
11
|
-
from .._mappings import country_name, lookup_area
|
|
11
|
+
from ..exceptions import InvalidParameterError
|
|
12
|
+
from .._mappings import AREA_TZ, EIC_TO_ISO, country_name, lookup_area
|
|
12
13
|
|
|
13
14
|
if TYPE_CHECKING:
|
|
14
15
|
import xml.etree.ElementTree as ET
|
|
@@ -28,6 +29,52 @@ OneOrMany = Union[str, list[str]]
|
|
|
28
29
|
TimeSeriesFilter = Optional[Callable[["ET.Element"], bool]]
|
|
29
30
|
|
|
30
31
|
|
|
32
|
+
def validate_output_tz(value: str) -> str:
|
|
33
|
+
"""Validate an output timezone without changing its spelling."""
|
|
34
|
+
if value == "local":
|
|
35
|
+
return value
|
|
36
|
+
if not isinstance(value, str):
|
|
37
|
+
raise InvalidParameterError("output_tz must be 'UTC', 'local', or an IANA timezone.")
|
|
38
|
+
try:
|
|
39
|
+
ZoneInfo(value)
|
|
40
|
+
except (ZoneInfoNotFoundError, ValueError):
|
|
41
|
+
raise InvalidParameterError(
|
|
42
|
+
f"Invalid output_tz {value!r}; use 'UTC', 'local', or an IANA timezone."
|
|
43
|
+
) from None
|
|
44
|
+
return value
|
|
45
|
+
|
|
46
|
+
|
|
47
|
+
def _to_output_tz(df: pd.DataFrame, zones: OneOrMany, output_tz: str) -> pd.DataFrame:
|
|
48
|
+
"""Convert a UTC result at the namespace boundary without mutating cached data.
|
|
49
|
+
|
|
50
|
+
For cross-border requests, callers pass the first in_domain zone; local time then follows that zone.
|
|
51
|
+
"""
|
|
52
|
+
target = validate_output_tz(output_tz)
|
|
53
|
+
if target == "UTC":
|
|
54
|
+
return df
|
|
55
|
+
if target == "local":
|
|
56
|
+
requested = [zones] if isinstance(zones, str) else zones
|
|
57
|
+
timezones = [AREA_TZ[EIC_TO_ISO[lookup_area(zone)]] for zone in requested]
|
|
58
|
+
instants = df["timestamp"].drop_duplicates() if "timestamp" in df else []
|
|
59
|
+
if not len(instants):
|
|
60
|
+
instants = [pd.Timestamp("2024-01-01", tz="UTC"), pd.Timestamp("2024-07-01", tz="UTC")]
|
|
61
|
+
signatures = {
|
|
62
|
+
zone: tuple(instant.to_pydatetime().astimezone(ZoneInfo(zone)).utcoffset() for instant in instants)
|
|
63
|
+
for zone in set(timezones)
|
|
64
|
+
}
|
|
65
|
+
if len(set(signatures.values())) != 1:
|
|
66
|
+
raise InvalidParameterError(
|
|
67
|
+
f"Local output spans different timezones: {', '.join(sorted(set(timezones)))}. "
|
|
68
|
+
"Use 'UTC' or a fixed IANA timezone."
|
|
69
|
+
)
|
|
70
|
+
target = timezones[0]
|
|
71
|
+
if df.empty or "timestamp" not in df:
|
|
72
|
+
return df.copy()
|
|
73
|
+
result = df.copy()
|
|
74
|
+
result["timestamp"] = result["timestamp"].dt.tz_convert(target)
|
|
75
|
+
return result
|
|
76
|
+
|
|
77
|
+
|
|
31
78
|
class BaseNamespace:
|
|
32
79
|
"""Base class for domain namespaces.
|
|
33
80
|
|
|
@@ -40,12 +87,17 @@ class BaseNamespace:
|
|
|
40
87
|
http: HttpClient,
|
|
41
88
|
*,
|
|
42
89
|
tz: str = "Europe/Brussels",
|
|
90
|
+
output_tz: str = "UTC",
|
|
43
91
|
cache: CacheStore | None = None,
|
|
44
92
|
) -> None:
|
|
45
93
|
self._http = http
|
|
46
94
|
self._tz = tz
|
|
95
|
+
self._output_tz = validate_output_tz(output_tz)
|
|
47
96
|
self._cache = cache
|
|
48
97
|
|
|
98
|
+
def _output(self, df: pd.DataFrame, zones: OneOrMany, output_tz: str | None) -> pd.DataFrame:
|
|
99
|
+
return _to_output_tz(df, zones, self._output_tz if output_tz is None else output_tz)
|
|
100
|
+
|
|
49
101
|
# ------------------------------------------------------------------
|
|
50
102
|
# Timestamp coercion
|
|
51
103
|
# ------------------------------------------------------------------
|
|
@@ -232,17 +284,14 @@ class BaseNamespace:
|
|
|
232
284
|
)
|
|
233
285
|
new_wide_frames = []
|
|
234
286
|
gap_meta: dict = {}
|
|
287
|
+
# A failed request propagates: returning the cache without the gap
|
|
288
|
+
# would silently hand back partial or empty data.
|
|
235
289
|
for gap in valid_gaps:
|
|
236
|
-
|
|
237
|
-
|
|
238
|
-
|
|
239
|
-
|
|
240
|
-
|
|
241
|
-
gap_meta.update(meta)
|
|
242
|
-
except (InvalidParameterError, NoDataError):
|
|
243
|
-
raise
|
|
244
|
-
except Exception as exc:
|
|
245
|
-
logger.warning("Failed to fetch gap %s→%s: %s", gap.start, gap.end, exc)
|
|
290
|
+
raw_df = self._query(params, gap.start, gap.end, ts_filter=ts_filter)
|
|
291
|
+
wide, meta = self._to_wide(raw_df, pivot_col, value_col)
|
|
292
|
+
new_wide_frames.append(wide)
|
|
293
|
+
if meta:
|
|
294
|
+
gap_meta.update(meta)
|
|
246
295
|
|
|
247
296
|
# 4. Merge new data with cache and persist
|
|
248
297
|
if new_wide_frames:
|