python-entsoe 0.6.0__py3-none-any.whl → 0.6.2__py3-none-any.whl
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- entsoe/_mappings.py +0 -1
- entsoe/_xml.py +56 -7
- entsoe/cache.py +41 -10
- entsoe/catalog.py +191 -14
- entsoe/cli/prices.py +2 -1
- entsoe/client.py +0 -3
- entsoe/namespaces/_base.py +26 -14
- entsoe/namespaces/prices.py +20 -1
- {python_entsoe-0.6.0.dist-info → python_entsoe-0.6.2.dist-info}/METADATA +11 -9
- {python_entsoe-0.6.0.dist-info → python_entsoe-0.6.2.dist-info}/RECORD +12 -12
- {python_entsoe-0.6.0.dist-info → python_entsoe-0.6.2.dist-info}/WHEEL +1 -1
- {python_entsoe-0.6.0.dist-info → python_entsoe-0.6.2.dist-info}/entry_points.txt +0 -0
entsoe/_mappings.py
CHANGED
|
@@ -399,7 +399,6 @@ NEIGHBOURS: dict[str, set[str]] = {
|
|
|
399
399
|
"BG": {"RO", "RS", "MK", "GR", "TR"},
|
|
400
400
|
"MK": {"RS", "BG", "GR", "AL"},
|
|
401
401
|
"GR": {"BG", "MK", "AL", "IT_SUD", "TR"},
|
|
402
|
-
"IT_SUD": {"GR"},
|
|
403
402
|
"AL": {"RS", "MK", "GR", "ME"},
|
|
404
403
|
"ME": {"RS", "BA", "AL"},
|
|
405
404
|
"TR": {"BG", "GR"},
|
entsoe/_xml.py
CHANGED
|
@@ -15,6 +15,7 @@ from __future__ import annotations
|
|
|
15
15
|
import re
|
|
16
16
|
import xml.etree.ElementTree as ET
|
|
17
17
|
from datetime import timedelta
|
|
18
|
+
from typing import Callable
|
|
18
19
|
|
|
19
20
|
import pandas as pd
|
|
20
21
|
|
|
@@ -79,12 +80,42 @@ def _extract_point_value(point: ET.Element) -> float | None:
|
|
|
79
80
|
return None
|
|
80
81
|
|
|
81
82
|
|
|
82
|
-
|
|
83
|
+
#: Element holding the sequence of a TimeSeries within a document, e.g. the
|
|
84
|
+
#: SDAC (1) and EXAA (2) day-ahead auctions both published for DE_LU and AT.
|
|
85
|
+
SEQUENCE_TAG = "classificationSequence_AttributeInstanceComponent.position"
|
|
86
|
+
|
|
87
|
+
|
|
88
|
+
def timeseries_sequence(ts: ET.Element) -> int | None:
|
|
89
|
+
"""Return the classification sequence of a TimeSeries, or None if absent."""
|
|
90
|
+
for el in ts.iter():
|
|
91
|
+
if _strip_ns(el.tag) == SEQUENCE_TAG and el.text:
|
|
92
|
+
return int(el.text)
|
|
93
|
+
return None
|
|
94
|
+
|
|
95
|
+
|
|
96
|
+
def timeseries_contract_type(ts: ET.Element) -> str | None:
|
|
97
|
+
"""Return the contract_MarketAgreement.type of a TimeSeries (A01 daily, A07 intraday)."""
|
|
98
|
+
return _find_text(ts, "contract_MarketAgreement.type")
|
|
99
|
+
|
|
100
|
+
|
|
101
|
+
def parse_timeseries(
|
|
102
|
+
xml_text: str,
|
|
103
|
+
ts_filter: Callable[[ET.Element], bool] | None = None,
|
|
104
|
+
) -> pd.DataFrame:
|
|
83
105
|
"""Parse an ENTSO-E XML response into a DataFrame.
|
|
84
106
|
|
|
85
107
|
Handles multiple TimeSeries (e.g., generation per type) and multiple
|
|
86
108
|
Periods within each TimeSeries (e.g., multi-day responses).
|
|
87
109
|
|
|
110
|
+
Points omitted from a curveType A03 (variable-sized block) Period carry
|
|
111
|
+
the previous point's value, so they are filled in. TimeSeries the API
|
|
112
|
+
repeats verbatim produce duplicate rows, which are dropped.
|
|
113
|
+
|
|
114
|
+
Args:
|
|
115
|
+
xml_text: Raw XML response.
|
|
116
|
+
ts_filter: Optional predicate on each TimeSeries element; series for
|
|
117
|
+
which it returns False are skipped.
|
|
118
|
+
|
|
88
119
|
Returns a DataFrame with columns:
|
|
89
120
|
- timestamp: tz-aware UTC datetime
|
|
90
121
|
- value: numeric value (MW or EUR/MWh depending on query)
|
|
@@ -107,6 +138,9 @@ def parse_timeseries(xml_text: str) -> pd.DataFrame:
|
|
|
107
138
|
rows: list[dict] = []
|
|
108
139
|
|
|
109
140
|
for ts in timeseries_list:
|
|
141
|
+
if ts_filter is not None and not ts_filter(ts):
|
|
142
|
+
continue
|
|
143
|
+
|
|
110
144
|
# Extract TimeSeries metadata
|
|
111
145
|
ts_meta: dict = {}
|
|
112
146
|
|
|
@@ -163,23 +197,38 @@ def parse_timeseries(xml_text: str) -> pd.DataFrame:
|
|
|
163
197
|
period_start = pd.Timestamp(start_text)
|
|
164
198
|
resolution = _parse_resolution(resolution_text)
|
|
165
199
|
|
|
200
|
+
values: dict[int, float | None] = {}
|
|
166
201
|
for point in _findall(period, "Point"):
|
|
167
202
|
position_text = _find_text(point, "position")
|
|
168
203
|
if position_text is None:
|
|
169
204
|
continue
|
|
205
|
+
values[int(position_text)] = _extract_point_value(point)
|
|
170
206
|
|
|
171
|
-
|
|
172
|
-
|
|
207
|
+
if not values:
|
|
208
|
+
continue
|
|
173
209
|
|
|
210
|
+
positions = sorted(values)
|
|
211
|
+
if _find_text(ts, "curveType") == "A03":
|
|
212
|
+
end_text = _find_text(time_interval, "end")
|
|
213
|
+
if end_text is not None:
|
|
214
|
+
n_slots = int((pd.Timestamp(end_text) - period_start) / resolution)
|
|
215
|
+
else:
|
|
216
|
+
n_slots = positions[-1]
|
|
217
|
+
filled: dict[int, float | None] = {}
|
|
218
|
+
last = None
|
|
219
|
+
for pos in range(positions[0], max(n_slots, positions[-1]) + 1):
|
|
220
|
+
last = values.get(pos, last)
|
|
221
|
+
filled[pos] = last
|
|
222
|
+
values, positions = filled, list(filled)
|
|
223
|
+
|
|
224
|
+
for position in positions:
|
|
174
225
|
timestamp = period_start + resolution * (position - 1)
|
|
175
|
-
|
|
176
|
-
row = {"timestamp": timestamp, "value": value, **ts_meta}
|
|
177
|
-
rows.append(row)
|
|
226
|
+
rows.append({"timestamp": timestamp, "value": values[position], **ts_meta})
|
|
178
227
|
|
|
179
228
|
if not rows:
|
|
180
229
|
raise NoDataError()
|
|
181
230
|
|
|
182
|
-
df = pd.DataFrame(rows)
|
|
231
|
+
df = pd.DataFrame(rows).drop_duplicates()
|
|
183
232
|
df["timestamp"] = pd.to_datetime(df["timestamp"], utc=True)
|
|
184
233
|
df = df.sort_values("timestamp").reset_index(drop=True)
|
|
185
234
|
return df
|
entsoe/cache.py
CHANGED
|
@@ -6,7 +6,7 @@ published (~48h), so caching is safe and enabled by default.
|
|
|
6
6
|
|
|
7
7
|
Storage layout::
|
|
8
8
|
|
|
9
|
-
{cache_dir}/
|
|
9
|
+
{cache_dir}/v{CACHE_LAYOUT_VERSION}/
|
|
10
10
|
├── prices/day_ahead/{ES,FR,...}/data.parquet
|
|
11
11
|
├── load/{actual,forecast}/{ES,FR,...}/data.parquet
|
|
12
12
|
├── generation/{actual,forecast,capacity}/{ES,...}/data.parquet
|
|
@@ -18,6 +18,9 @@ Each partition stores a single wide-format parquet file: the DatetimeIndex
|
|
|
18
18
|
holds timestamps, and columns are values (e.g. "value", or per-type
|
|
19
19
|
columns like "solar", "wind_onshore").
|
|
20
20
|
|
|
21
|
+
The version directory changes whenever cached values from earlier releases
|
|
22
|
+
are known to be wrong; data from older layouts is removed on first use.
|
|
23
|
+
|
|
21
24
|
Gap detection works per-column: requesting specific columns checks whether
|
|
22
25
|
those columns have data for the requested range, not whether any column does.
|
|
23
26
|
"""
|
|
@@ -42,6 +45,13 @@ _DEFAULT_CACHE_DIR = Path(
|
|
|
42
45
|
os.environ.get("XDG_CACHE_HOME", Path.home() / ".cache")
|
|
43
46
|
) / "entsoe"
|
|
44
47
|
|
|
48
|
+
# Bumped when previously cached values are known to be wrong.
|
|
49
|
+
# v2 (0.6.2): A03 curves lost their omitted points; day-ahead mixed auctions.
|
|
50
|
+
CACHE_LAYOUT_VERSION = 2
|
|
51
|
+
|
|
52
|
+
# Domain directories written by the unversioned (<= 0.6.1) layout.
|
|
53
|
+
_LEGACY_DOMAINS = ("prices", "load", "generation", "transmission", "balancing")
|
|
54
|
+
|
|
45
55
|
# Data older than this (hours) is considered final and won't be re-fetched
|
|
46
56
|
_DEFAULT_RECENT_TTL_HOURS = 48
|
|
47
57
|
|
|
@@ -71,16 +81,37 @@ class CacheStore:
|
|
|
71
81
|
|
|
72
82
|
def __init__(self, config: CacheConfig):
|
|
73
83
|
self.config = config
|
|
84
|
+
self._purge_stale_layouts()
|
|
85
|
+
|
|
86
|
+
@property
|
|
87
|
+
def root(self) -> Path:
|
|
88
|
+
"""Directory holding the current cache layout."""
|
|
89
|
+
return self.config.cache_dir / f"v{CACHE_LAYOUT_VERSION}"
|
|
90
|
+
|
|
91
|
+
def _purge_stale_layouts(self) -> None:
|
|
92
|
+
"""Delete data cached by older layouts, which may hold wrong values."""
|
|
93
|
+
base = self.config.cache_dir
|
|
94
|
+
if not base.is_dir():
|
|
95
|
+
return
|
|
96
|
+
stale = [base / d for d in _LEGACY_DOMAINS]
|
|
97
|
+
stale += [
|
|
98
|
+
p for p in base.glob("v*")
|
|
99
|
+
if p.name[1:].isdigit() and int(p.name[1:]) < CACHE_LAYOUT_VERSION
|
|
100
|
+
]
|
|
101
|
+
for path in stale:
|
|
102
|
+
if path.is_dir():
|
|
103
|
+
logger.info("Removing outdated ENTSO-E cache: %s", path)
|
|
104
|
+
shutil.rmtree(path, ignore_errors=True)
|
|
74
105
|
|
|
75
106
|
# -- Path resolution -------------------------------------------------------
|
|
76
107
|
|
|
77
108
|
def _parquet_path(self, domain: str, topic: str, partition: str) -> Path:
|
|
78
|
-
"""Data file: {
|
|
79
|
-
return self.
|
|
109
|
+
"""Data file: {root}/{domain}/{topic}/{partition}/data.parquet"""
|
|
110
|
+
return self.root / domain / topic / partition / "data.parquet"
|
|
80
111
|
|
|
81
112
|
def _meta_path(self, domain: str, topic: str, partition: str) -> Path:
|
|
82
|
-
"""Metadata file: {
|
|
83
|
-
return self.
|
|
113
|
+
"""Metadata file: {root}/{domain}/{topic}/{partition}/meta.json"""
|
|
114
|
+
return self.root / domain / topic / partition / "meta.json"
|
|
84
115
|
|
|
85
116
|
# -- Data Read / Write -----------------------------------------------------
|
|
86
117
|
|
|
@@ -291,13 +322,13 @@ class CacheStore:
|
|
|
291
322
|
count = 0
|
|
292
323
|
|
|
293
324
|
if domain and topic and partition:
|
|
294
|
-
target = self.
|
|
325
|
+
target = self.root / domain / topic / partition
|
|
295
326
|
elif domain and topic:
|
|
296
|
-
target = self.
|
|
327
|
+
target = self.root / domain / topic
|
|
297
328
|
elif domain:
|
|
298
|
-
target = self.
|
|
329
|
+
target = self.root / domain
|
|
299
330
|
else:
|
|
300
|
-
target = self.
|
|
331
|
+
target = self.root
|
|
301
332
|
|
|
302
333
|
if target.exists():
|
|
303
334
|
count = sum(1 for f in target.rglob("*") if f.is_file())
|
|
@@ -307,7 +338,7 @@ class CacheStore:
|
|
|
307
338
|
|
|
308
339
|
def status(self) -> dict:
|
|
309
340
|
"""Return cache statistics."""
|
|
310
|
-
cache_dir = self.
|
|
341
|
+
cache_dir = self.root
|
|
311
342
|
if not cache_dir.exists():
|
|
312
343
|
return {"path": str(cache_dir), "files": 0, "size_mb": 0.0, "domains": {}}
|
|
313
344
|
|
entsoe/catalog.py
CHANGED
|
@@ -7,25 +7,202 @@ consult external documentation every time.
|
|
|
7
7
|
|
|
8
8
|
from __future__ import annotations
|
|
9
9
|
|
|
10
|
-
|
|
11
|
-
from entsoe._models import Category, Endpoint
|
|
12
|
-
from entsoe.catalog_manager import ENTSOECatalogManager as _Manager
|
|
10
|
+
from dataclasses import dataclass, field
|
|
13
11
|
|
|
14
|
-
__all__ = ["Category", "Endpoint", "CATEGORIES", "get_category", "summary"]
|
|
15
12
|
|
|
16
|
-
|
|
13
|
+
@dataclass(frozen=True)
|
|
14
|
+
class Endpoint:
|
|
15
|
+
"""A queryable data endpoint."""
|
|
17
16
|
|
|
18
|
-
|
|
19
|
-
|
|
17
|
+
name: str
|
|
18
|
+
description: str
|
|
19
|
+
method: str # Python client method name
|
|
20
|
+
cli: str # CLI command
|
|
21
|
+
resolution: str # e.g. "15min", "hourly", "daily"
|
|
22
|
+
columns: tuple[str, ...] # key columns returned
|
|
23
|
+
units: str
|
|
24
|
+
notes: str = ""
|
|
20
25
|
|
|
21
|
-
CATEGORIES: dict[str, Category] = _cats
|
|
22
26
|
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
27
|
+
@dataclass(frozen=True)
|
|
28
|
+
class Category:
|
|
29
|
+
"""A top-level data category grouping related endpoints."""
|
|
30
|
+
|
|
31
|
+
name: str
|
|
32
|
+
description: str
|
|
33
|
+
namespace: str # Python client namespace (e.g. "prices")
|
|
34
|
+
endpoints: tuple[Endpoint, ...]
|
|
35
|
+
|
|
36
|
+
|
|
37
|
+
# ── Prices ──────────────────────────────────────────────────────────────
|
|
38
|
+
|
|
39
|
+
PRICES = Category(
|
|
40
|
+
name="Prices",
|
|
41
|
+
description="Day-ahead electricity market prices",
|
|
42
|
+
namespace="prices",
|
|
43
|
+
endpoints=(
|
|
44
|
+
Endpoint(
|
|
45
|
+
name="Day-Ahead Prices",
|
|
46
|
+
description="Hourly day-ahead market clearing prices per bidding zone",
|
|
47
|
+
method="client.prices.day_ahead(start, end, country='FR')",
|
|
48
|
+
cli="entsoe prices day-ahead -s 2024-06-01 -e 2024-06-08 -c FR",
|
|
49
|
+
resolution="hourly",
|
|
50
|
+
columns=("timestamp", "country", "value", "currency", "quantity_unit"),
|
|
51
|
+
units="EUR/MWh",
|
|
52
|
+
),
|
|
53
|
+
),
|
|
54
|
+
)
|
|
55
|
+
|
|
56
|
+
# ── Load ────────────────────────────────────────────────────────────────
|
|
57
|
+
|
|
58
|
+
LOAD = Category(
|
|
59
|
+
name="Load",
|
|
60
|
+
description="Actual system load and day-ahead load forecasts",
|
|
61
|
+
namespace="load",
|
|
62
|
+
endpoints=(
|
|
63
|
+
Endpoint(
|
|
64
|
+
name="Actual Total Load",
|
|
65
|
+
description="Actual total electricity consumption per control area",
|
|
66
|
+
method="client.load.actual(start, end, country='FR')",
|
|
67
|
+
cli="entsoe load actual -s 2024-06-01 -e 2024-06-08 -c FR",
|
|
68
|
+
resolution="15min or hourly (varies by country)",
|
|
69
|
+
columns=("timestamp", "country", "value", "quantity_unit"),
|
|
70
|
+
units="MW",
|
|
71
|
+
),
|
|
72
|
+
Endpoint(
|
|
73
|
+
name="Day-Ahead Load Forecast",
|
|
74
|
+
description="Day-ahead forecast of total electricity consumption",
|
|
75
|
+
method="client.load.forecast(start, end, country='FR')",
|
|
76
|
+
cli="entsoe load forecast -s 2024-06-01 -e 2024-06-08 -c FR",
|
|
77
|
+
resolution="15min or hourly (varies by country)",
|
|
78
|
+
columns=("timestamp", "country", "value", "quantity_unit"),
|
|
79
|
+
units="MW",
|
|
80
|
+
),
|
|
81
|
+
),
|
|
82
|
+
)
|
|
83
|
+
|
|
84
|
+
# ── Generation ──────────────────────────────────────────────────────────
|
|
85
|
+
|
|
86
|
+
GENERATION = Category(
|
|
87
|
+
name="Generation",
|
|
88
|
+
description="Actual generation, forecasts, installed capacity, and per-plant output",
|
|
89
|
+
namespace="generation",
|
|
90
|
+
endpoints=(
|
|
91
|
+
Endpoint(
|
|
92
|
+
name="Actual Generation per Type",
|
|
93
|
+
description="Actual electricity generation broken down by fuel/technology type (PSR)",
|
|
94
|
+
method="client.generation.actual(start, end, country='FR')",
|
|
95
|
+
cli="entsoe generation actual -s 2024-06-01 -e 2024-06-08 -c FR",
|
|
96
|
+
resolution="15min or hourly",
|
|
97
|
+
columns=("timestamp", "country", "psr_type", "value", "quantity_unit"),
|
|
98
|
+
units="MW",
|
|
99
|
+
notes="Filter by PSR type: --psr solar --psr wind_onshore",
|
|
100
|
+
),
|
|
101
|
+
Endpoint(
|
|
102
|
+
name="Generation Forecast (Wind/Solar)",
|
|
103
|
+
description="Day-ahead forecast for wind and solar generation",
|
|
104
|
+
method="client.generation.forecast(start, end, country='FR')",
|
|
105
|
+
cli="entsoe generation forecast -s 2024-06-01 -e 2024-06-08 -c FR",
|
|
106
|
+
resolution="15min or hourly",
|
|
107
|
+
columns=("timestamp", "country", "psr_type", "value", "quantity_unit"),
|
|
108
|
+
units="MW",
|
|
109
|
+
),
|
|
110
|
+
Endpoint(
|
|
111
|
+
name="Installed Generation Capacity",
|
|
112
|
+
description="Installed generation capacity per type (updated annually or on change)",
|
|
113
|
+
method="client.generation.installed_capacity(start, end, country='FR')",
|
|
114
|
+
cli="entsoe generation capacity -s 2024-06-01 -e 2024-06-08 -c FR",
|
|
115
|
+
resolution="yearly (point-in-time)",
|
|
116
|
+
columns=("timestamp", "country", "psr_type", "value", "quantity_unit"),
|
|
117
|
+
units="MW",
|
|
118
|
+
),
|
|
119
|
+
Endpoint(
|
|
120
|
+
name="Actual Generation per Plant",
|
|
121
|
+
description="Generation output per individual production unit (>= 100 MW)",
|
|
122
|
+
method="client.generation.per_plant(start, end, country='FR')",
|
|
123
|
+
cli="entsoe generation per-plant -s 2024-06-01 -e 2024-06-08 -c FR",
|
|
124
|
+
resolution="15min or hourly",
|
|
125
|
+
columns=("timestamp", "country", "unit_name", "psr_type", "value", "quantity_unit"),
|
|
126
|
+
units="MW",
|
|
127
|
+
),
|
|
128
|
+
),
|
|
129
|
+
)
|
|
130
|
+
|
|
131
|
+
# ── Transmission ────────────────────────────────────────────────────────
|
|
132
|
+
|
|
133
|
+
TRANSMISSION = Category(
|
|
134
|
+
name="Transmission",
|
|
135
|
+
description="Cross-border physical flows, scheduled exchanges, and transfer capacity",
|
|
136
|
+
namespace="transmission",
|
|
137
|
+
endpoints=(
|
|
138
|
+
Endpoint(
|
|
139
|
+
name="Physical Cross-Border Flows",
|
|
140
|
+
description="Actual measured electricity flows between bidding zones",
|
|
141
|
+
method="client.transmission.crossborder_flows(start, end, country_from='FR', country_to='DE_LU')",
|
|
142
|
+
cli="entsoe transmission flows -s 2024-06-01 -e 2024-06-08 --from FR --to DE_LU",
|
|
143
|
+
resolution="15min or hourly",
|
|
144
|
+
columns=("timestamp", "in_domain", "value", "quantity_unit"),
|
|
145
|
+
units="MW",
|
|
146
|
+
notes="Use --border for multi-border queries: --border FR-*, --border iberian",
|
|
147
|
+
),
|
|
148
|
+
Endpoint(
|
|
149
|
+
name="Scheduled Commercial Exchanges",
|
|
150
|
+
description="Day-ahead scheduled commercial power exchanges between zones",
|
|
151
|
+
method="client.transmission.scheduled_exchanges(start, end, country_from='FR', country_to='ES')",
|
|
152
|
+
cli="entsoe transmission exchanges -s 2024-06-01 -e 2024-06-08 --from FR --to ES",
|
|
153
|
+
resolution="hourly",
|
|
154
|
+
columns=("timestamp", "in_domain", "value", "quantity_unit"),
|
|
155
|
+
units="MW",
|
|
156
|
+
),
|
|
157
|
+
Endpoint(
|
|
158
|
+
name="Net Transfer Capacity",
|
|
159
|
+
description="Available transfer capacity (NTC) between bidding zones",
|
|
160
|
+
method="client.transmission.net_transfer_capacity(start, end, country_from='FR', country_to='ES')",
|
|
161
|
+
cli="entsoe transmission capacity -s 2024-06-01 -e 2024-06-08 --from FR --to ES",
|
|
162
|
+
resolution="hourly",
|
|
163
|
+
columns=("timestamp", "in_domain", "value", "quantity_unit"),
|
|
164
|
+
units="MW",
|
|
165
|
+
),
|
|
166
|
+
),
|
|
167
|
+
)
|
|
168
|
+
|
|
169
|
+
# ── Balancing ───────────────────────────────────────────────────────────
|
|
170
|
+
|
|
171
|
+
BALANCING = Category(
|
|
172
|
+
name="Balancing",
|
|
173
|
+
description="System imbalance prices and volumes",
|
|
174
|
+
namespace="balancing",
|
|
175
|
+
endpoints=(
|
|
176
|
+
Endpoint(
|
|
177
|
+
name="Imbalance Prices",
|
|
178
|
+
description="System imbalance settlement prices",
|
|
179
|
+
method="client.balancing.imbalance_prices(start, end, country='FR')",
|
|
180
|
+
cli="entsoe balancing prices -s 2024-06-01 -e 2024-06-08 -c FR",
|
|
181
|
+
resolution="15min or hourly (varies by country)",
|
|
182
|
+
columns=("timestamp", "country", "value", "quantity_unit"),
|
|
183
|
+
units="EUR/MWh",
|
|
184
|
+
),
|
|
185
|
+
Endpoint(
|
|
186
|
+
name="Imbalance Volumes",
|
|
187
|
+
description="Net system imbalance volumes",
|
|
188
|
+
method="client.balancing.imbalance_volumes(start, end, country='FR')",
|
|
189
|
+
cli="entsoe balancing volumes -s 2024-06-01 -e 2024-06-08 -c FR",
|
|
190
|
+
resolution="15min or hourly (varies by country)",
|
|
191
|
+
columns=("timestamp", "country", "value", "quantity_unit"),
|
|
192
|
+
units="MW",
|
|
193
|
+
),
|
|
194
|
+
),
|
|
195
|
+
)
|
|
196
|
+
|
|
197
|
+
# ── Full catalog ────────────────────────────────────────────────────────
|
|
198
|
+
|
|
199
|
+
CATEGORIES: dict[str, Category] = {
|
|
200
|
+
"prices": PRICES,
|
|
201
|
+
"load": LOAD,
|
|
202
|
+
"generation": GENERATION,
|
|
203
|
+
"transmission": TRANSMISSION,
|
|
204
|
+
"balancing": BALANCING,
|
|
205
|
+
}
|
|
29
206
|
|
|
30
207
|
|
|
31
208
|
def get_category(name: str) -> Category:
|
entsoe/cli/prices.py
CHANGED
|
@@ -18,6 +18,7 @@ def day_ahead(
|
|
|
18
18
|
end: Optional[str] = typer.Option(None, "--end", "-e", help="End date (YYYY-MM-DD)"),
|
|
19
19
|
period: Optional[str] = typer.Option(None, "--period", "-P", help="Period shorthand: today, yesterday, week, month, ytd, <N>d"),
|
|
20
20
|
country: list[str] = typer.Option(..., "--country", "-c", help="Country code(s), comma-separated or repeated (e.g. ES,FR,DE_LU)"),
|
|
21
|
+
sequence: int = typer.Option(1, "--sequence", help="Auction: 1 = SDAC (default), 2 = EXAA (DE_LU, AT)"),
|
|
21
22
|
format: str = typer.Option("table", "--format", "-f", help="Output format: table, csv, json"),
|
|
22
23
|
output_path: Optional[str] = typer.Option(None, "--output", "-o", help="Output file path"),
|
|
23
24
|
api_key: Optional[str] = typer.Option(None, "--api-key", help="ENTSO-E API key"),
|
|
@@ -28,7 +29,7 @@ def day_ahead(
|
|
|
28
29
|
s, e = resolve_dates(start, end, period)
|
|
29
30
|
client = get_client(api_key)
|
|
30
31
|
countries = resolve_many_required(country)
|
|
31
|
-
df = client.prices.day_ahead(s, e, country=countries)
|
|
32
|
+
df = client.prices.day_ahead(s, e, country=countries, sequence=sequence)
|
|
32
33
|
output(df, format, title="Day-Ahead Prices", output_path=output_path)
|
|
33
34
|
|
|
34
35
|
|
entsoe/client.py
CHANGED
|
@@ -7,7 +7,6 @@ from pathlib import Path
|
|
|
7
7
|
|
|
8
8
|
from ._http import HttpClient
|
|
9
9
|
from .cache import CacheConfig, CacheStore
|
|
10
|
-
from .catalog_manager import ENTSOECatalogManager
|
|
11
10
|
from .namespaces import (
|
|
12
11
|
BalancingNamespace,
|
|
13
12
|
GenerationNamespace,
|
|
@@ -101,8 +100,6 @@ class ENTSOEClient:
|
|
|
101
100
|
self.transmission = TransmissionNamespace(http, tz=tz, cache=cache_store)
|
|
102
101
|
self.balancing = BalancingNamespace(http, tz=tz, cache=cache_store)
|
|
103
102
|
|
|
104
|
-
self.catalog = ENTSOECatalogManager(self)
|
|
105
|
-
|
|
106
103
|
# Expose for CLI cache commands
|
|
107
104
|
self._cache = cache_store
|
|
108
105
|
|
entsoe/namespaces/_base.py
CHANGED
|
@@ -3,7 +3,7 @@
|
|
|
3
3
|
from __future__ import annotations
|
|
4
4
|
|
|
5
5
|
import logging
|
|
6
|
-
from typing import TYPE_CHECKING, Callable, Union
|
|
6
|
+
from typing import TYPE_CHECKING, Callable, Optional, Union
|
|
7
7
|
|
|
8
8
|
import pandas as pd
|
|
9
9
|
|
|
@@ -11,6 +11,8 @@ from ..exceptions import InvalidParameterError, NoDataError
|
|
|
11
11
|
from .._mappings import country_name, lookup_area
|
|
12
12
|
|
|
13
13
|
if TYPE_CHECKING:
|
|
14
|
+
import xml.etree.ElementTree as ET
|
|
15
|
+
|
|
14
16
|
from .._http import HttpClient
|
|
15
17
|
from ..cache import CacheStore
|
|
16
18
|
|
|
@@ -22,6 +24,9 @@ Timestamp = Union[str, pd.Timestamp]
|
|
|
22
24
|
#: Parameters that accept a single value or a list.
|
|
23
25
|
OneOrMany = Union[str, list[str]]
|
|
24
26
|
|
|
27
|
+
#: Predicate deciding which TimeSeries of a response to keep.
|
|
28
|
+
TimeSeriesFilter = Optional[Callable[["ET.Element"], bool]]
|
|
29
|
+
|
|
25
30
|
|
|
26
31
|
class BaseNamespace:
|
|
27
32
|
"""Base class for domain namespaces.
|
|
@@ -61,6 +66,8 @@ class BaseNamespace:
|
|
|
61
66
|
params: dict,
|
|
62
67
|
start: Timestamp,
|
|
63
68
|
end: Timestamp,
|
|
69
|
+
*,
|
|
70
|
+
ts_filter: TimeSeriesFilter = None,
|
|
64
71
|
) -> pd.DataFrame:
|
|
65
72
|
"""Execute a query and parse the XML response into a DataFrame."""
|
|
66
73
|
from .._xml import parse_timeseries
|
|
@@ -71,10 +78,10 @@ class BaseNamespace:
|
|
|
71
78
|
|
|
72
79
|
# Handle multi-chunk responses (year-splitting)
|
|
73
80
|
if isinstance(result, list):
|
|
74
|
-
dfs = [parse_timeseries(xml) for xml in result]
|
|
81
|
+
dfs = [parse_timeseries(xml, ts_filter) for xml in result]
|
|
75
82
|
return pd.concat(dfs, ignore_index=True).sort_values("timestamp").reset_index(drop=True)
|
|
76
83
|
|
|
77
|
-
return parse_timeseries(result)
|
|
84
|
+
return parse_timeseries(result, ts_filter)
|
|
78
85
|
|
|
79
86
|
def _query_multi(
|
|
80
87
|
self,
|
|
@@ -85,6 +92,7 @@ class BaseNamespace:
|
|
|
85
92
|
*,
|
|
86
93
|
label_fn: Callable[[str], str] = country_name,
|
|
87
94
|
label_col: str = "country",
|
|
95
|
+
ts_filter: TimeSeriesFilter = None,
|
|
88
96
|
) -> pd.DataFrame:
|
|
89
97
|
"""Execute a query for one or many values, concatenating results.
|
|
90
98
|
|
|
@@ -110,11 +118,11 @@ class BaseNamespace:
|
|
|
110
118
|
queries with an additional label column.
|
|
111
119
|
"""
|
|
112
120
|
if isinstance(values, str):
|
|
113
|
-
return self._query(build_params(values), start, end)
|
|
121
|
+
return self._query(build_params(values), start, end, ts_filter=ts_filter)
|
|
114
122
|
|
|
115
123
|
frames = []
|
|
116
124
|
for val in values:
|
|
117
|
-
df = self._query(build_params(val), start, end)
|
|
125
|
+
df = self._query(build_params(val), start, end, ts_filter=ts_filter)
|
|
118
126
|
df[label_col] = label_fn(val)
|
|
119
127
|
frames.append(df)
|
|
120
128
|
return pd.concat(frames, ignore_index=True)
|
|
@@ -134,6 +142,7 @@ class BaseNamespace:
|
|
|
134
142
|
*,
|
|
135
143
|
pivot_col: str | None = None,
|
|
136
144
|
value_col: str = "value",
|
|
145
|
+
ts_filter: TimeSeriesFilter = None,
|
|
137
146
|
) -> pd.DataFrame:
|
|
138
147
|
"""Execute a query with parquet cache support.
|
|
139
148
|
|
|
@@ -174,7 +183,7 @@ class BaseNamespace:
|
|
|
174
183
|
raise InvalidParameterError("start must be before end.")
|
|
175
184
|
|
|
176
185
|
if self._cache is None:
|
|
177
|
-
return self._query(params, start_ts, end_ts)
|
|
186
|
+
return self._query(params, start_ts, end_ts, ts_filter=ts_filter)
|
|
178
187
|
|
|
179
188
|
# 1. Read cached wide data
|
|
180
189
|
cached_wide = self._cache.read(domain, topic, partition, start_ts, end_ts)
|
|
@@ -225,7 +234,7 @@ class BaseNamespace:
|
|
|
225
234
|
gap_meta: dict = {}
|
|
226
235
|
for gap in valid_gaps:
|
|
227
236
|
try:
|
|
228
|
-
raw_df = self._query(params, gap.start, gap.end)
|
|
237
|
+
raw_df = self._query(params, gap.start, gap.end, ts_filter=ts_filter)
|
|
229
238
|
wide, meta = self._to_wide(raw_df, pivot_col, value_col)
|
|
230
239
|
new_wide_frames.append(wide)
|
|
231
240
|
if meta:
|
|
@@ -279,27 +288,30 @@ class BaseNamespace:
|
|
|
279
288
|
value_col: str = "value",
|
|
280
289
|
label_fn: Callable[[str], str] = country_name,
|
|
281
290
|
label_col: str = "country",
|
|
291
|
+
ts_filter: TimeSeriesFilter = None,
|
|
292
|
+
partition_fn: Callable[[str], str] = str,
|
|
282
293
|
) -> pd.DataFrame:
|
|
283
294
|
"""Execute a cached query for one or many values.
|
|
284
295
|
|
|
285
|
-
Each value is its own cache partition
|
|
286
|
-
a label column, same as :meth:`_query_multi`.
|
|
296
|
+
Each value is its own cache partition, named by *partition_fn(value)*.
|
|
297
|
+
Multi-value queries add a label column, same as :meth:`_query_multi`.
|
|
287
298
|
"""
|
|
288
299
|
if self._cache is None:
|
|
289
300
|
return self._query_multi(build_params, values, start, end,
|
|
290
|
-
label_fn=label_fn, label_col=label_col
|
|
301
|
+
label_fn=label_fn, label_col=label_col,
|
|
302
|
+
ts_filter=ts_filter)
|
|
291
303
|
|
|
292
304
|
if isinstance(values, str):
|
|
293
305
|
return self._query_cached(
|
|
294
|
-
domain, topic, values, build_params(values), start, end,
|
|
295
|
-
pivot_col=pivot_col, value_col=value_col,
|
|
306
|
+
domain, topic, partition_fn(values), build_params(values), start, end,
|
|
307
|
+
pivot_col=pivot_col, value_col=value_col, ts_filter=ts_filter,
|
|
296
308
|
)
|
|
297
309
|
|
|
298
310
|
frames = []
|
|
299
311
|
for val in values:
|
|
300
312
|
df = self._query_cached(
|
|
301
|
-
domain, topic, val, build_params(val), start, end,
|
|
302
|
-
pivot_col=pivot_col, value_col=value_col,
|
|
313
|
+
domain, topic, partition_fn(val), build_params(val), start, end,
|
|
314
|
+
pivot_col=pivot_col, value_col=value_col, ts_filter=ts_filter,
|
|
303
315
|
)
|
|
304
316
|
df[label_col] = label_fn(val)
|
|
305
317
|
frames.append(df)
|
entsoe/namespaces/prices.py
CHANGED
|
@@ -2,11 +2,13 @@
|
|
|
2
2
|
|
|
3
3
|
from __future__ import annotations
|
|
4
4
|
|
|
5
|
+
import xml.etree.ElementTree as ET
|
|
6
|
+
|
|
5
7
|
import pandas as pd
|
|
6
8
|
|
|
9
|
+
from .._xml import timeseries_contract_type, timeseries_sequence
|
|
7
10
|
from ._base import BaseNamespace, OneOrMany, Timestamp
|
|
8
11
|
|
|
9
|
-
|
|
10
12
|
class PricesNamespace(BaseNamespace):
|
|
11
13
|
"""Access electricity price data.
|
|
12
14
|
|
|
@@ -19,14 +21,24 @@ class PricesNamespace(BaseNamespace):
|
|
|
19
21
|
start: Timestamp,
|
|
20
22
|
end: Timestamp,
|
|
21
23
|
country: OneOrMany,
|
|
24
|
+
sequence: int = 1,
|
|
22
25
|
) -> pd.DataFrame:
|
|
23
26
|
"""Query day-ahead electricity prices.
|
|
24
27
|
|
|
28
|
+
The A44 document mixes several auctions. Only daily contracts are
|
|
29
|
+
kept (intraday auctions such as the Spanish IDAs are dropped), and
|
|
30
|
+
among them the one with the requested classification sequence.
|
|
31
|
+
|
|
25
32
|
Args:
|
|
26
33
|
start: Period start — date string or tz-aware Timestamp.
|
|
27
34
|
end: Period end — date string or tz-aware Timestamp.
|
|
28
35
|
country: Country code or list of codes (e.g., "FR" or ["FR", "ES"]).
|
|
29
36
|
When a list is passed, results include a ``country`` column.
|
|
37
|
+
sequence: Which day-ahead auction to return. ``1`` (default) is
|
|
38
|
+
the Single Day-Ahead Coupling (SDAC, gate closure 12:00 CET).
|
|
39
|
+
``2`` is the separate EXAA auction (10:15 CET) published for
|
|
40
|
+
DE_LU and AT. Zones that publish a single auction without a
|
|
41
|
+
sequence count as ``1``.
|
|
30
42
|
|
|
31
43
|
Returns:
|
|
32
44
|
DataFrame with columns: timestamp, value (EUR/MWh).
|
|
@@ -41,6 +53,13 @@ class PricesNamespace(BaseNamespace):
|
|
|
41
53
|
"out_Domain": area,
|
|
42
54
|
}
|
|
43
55
|
|
|
56
|
+
def _keep(ts: ET.Element) -> bool:
|
|
57
|
+
if timeseries_contract_type(ts) not in (None, "A01"):
|
|
58
|
+
return False
|
|
59
|
+
return (timeseries_sequence(ts) or 1) == sequence
|
|
60
|
+
|
|
44
61
|
return self._query_multi_cached(
|
|
45
62
|
"prices", "day_ahead", _params, country, start, end,
|
|
63
|
+
ts_filter=_keep,
|
|
64
|
+
partition_fn=lambda code: code if sequence == 1 else f"{code}.seq{sequence}",
|
|
46
65
|
)
|
|
@@ -1,11 +1,11 @@
|
|
|
1
|
-
Metadata-Version: 2.
|
|
1
|
+
Metadata-Version: 2.5
|
|
2
2
|
Name: python-entsoe
|
|
3
|
-
Version: 0.6.
|
|
3
|
+
Version: 0.6.2
|
|
4
4
|
Summary: Python client for the ENTSO-E Transparency Platform API
|
|
5
|
-
Project-URL: Homepage, https://
|
|
6
|
-
Project-URL: Repository, https://
|
|
7
|
-
Project-URL: Issues, https://
|
|
8
|
-
Project-URL: Changelog, https://
|
|
5
|
+
Project-URL: Homepage, https://gitlab.com/datons.com/libraries/python-entsoe
|
|
6
|
+
Project-URL: Repository, https://gitlab.com/datons.com/libraries/python-entsoe
|
|
7
|
+
Project-URL: Issues, https://gitlab.com/datons.com/libraries/python-entsoe/-/issues
|
|
8
|
+
Project-URL: Changelog, https://gitlab.com/datons.com/libraries/python-entsoe/-/releases
|
|
9
9
|
Author-email: Jesús López <jesus.lopez@datons.com>
|
|
10
10
|
License-Expression: MIT
|
|
11
11
|
Keywords: api,electricity,energy,entsoe,transparency
|
|
@@ -21,7 +21,6 @@ Classifier: Topic :: Scientific/Engineering
|
|
|
21
21
|
Requires-Python: >=3.10
|
|
22
22
|
Requires-Dist: pandas>=2.0
|
|
23
23
|
Requires-Dist: pyarrow>=23.0.1
|
|
24
|
-
Requires-Dist: pyyaml>=6.0
|
|
25
24
|
Requires-Dist: requests>=2.28
|
|
26
25
|
Requires-Dist: rich>=13.0
|
|
27
26
|
Requires-Dist: typer>=0.9
|
|
@@ -94,11 +93,14 @@ df = client.load.forecast(start, end, country="FR")
|
|
|
94
93
|
|
|
95
94
|
| Method | Description | Parameters |
|
|
96
95
|
|--------|-------------|------------|
|
|
97
|
-
| `day_ahead(start, end, country)` | Day-ahead market prices (EUR/MWh) | `country`: ISO code |
|
|
96
|
+
| `day_ahead(start, end, country, sequence=1)` | Day-ahead market prices (EUR/MWh) | `country`: ISO code; `sequence`: `1` SDAC, `2` EXAA |
|
|
98
97
|
|
|
99
98
|
```python
|
|
100
99
|
df = client.prices.day_ahead(start, end, country="FR")
|
|
101
100
|
# Returns: timestamp, value, currency, price_unit
|
|
101
|
+
|
|
102
|
+
# DE_LU and AT also publish the separate EXAA auction (10:15 CET)
|
|
103
|
+
exaa = client.prices.day_ahead(start, end, country="DE_LU", sequence=2)
|
|
102
104
|
```
|
|
103
105
|
|
|
104
106
|
### `client.generation`
|
|
@@ -252,7 +254,7 @@ See the [`examples/`](examples/) directory for Jupyter notebooks with plotly vis
|
|
|
252
254
|
## Development
|
|
253
255
|
|
|
254
256
|
```bash
|
|
255
|
-
git clone
|
|
257
|
+
git clone git@gitlab.com:datons.com/libraries/python-entsoe.git
|
|
256
258
|
cd python-entsoe
|
|
257
259
|
uv sync
|
|
258
260
|
|
|
@@ -1,10 +1,10 @@
|
|
|
1
1
|
entsoe/__init__.py,sha256=9qzvMUzoD-CRfX1vTzWIBfpXBMdJ2MgRwJEgQQsw1fA,529
|
|
2
2
|
entsoe/_http.py,sha256=XhZZy059XHh8uAP2Vsm1e7BfRwQcmGLEYdREcgaF3Tk,6608
|
|
3
|
-
entsoe/_mappings.py,sha256=
|
|
4
|
-
entsoe/_xml.py,sha256=
|
|
5
|
-
entsoe/cache.py,sha256=
|
|
6
|
-
entsoe/catalog.py,sha256=
|
|
7
|
-
entsoe/client.py,sha256=
|
|
3
|
+
entsoe/_mappings.py,sha256=6-IKLtwChvM7_YG90oSRDxo3IfQUuz3Gnv17TmQgPjM,15534
|
|
4
|
+
entsoe/_xml.py,sha256=cJa4_nVIk27O9bCXkp2w-QR2fV3UrH-A80mX54KReVU,8439
|
|
5
|
+
entsoe/cache.py,sha256=3UIuhaSH9LVGk9Ony5eMsk_qOmRzzMEyskQS_cmY020,14547
|
|
6
|
+
entsoe/catalog.py,sha256=FUPg_WqLBkfl5sUgT9m8Bua7IP10NvBsakcndpUOBa0,9733
|
|
7
|
+
entsoe/client.py,sha256=zHB9y2TbCRjSNe9d5c9XVt_BNZ7b8KsvJDORc5V-ypE,3542
|
|
8
8
|
entsoe/exceptions.py,sha256=vqeu6_LxAtDvJziPnjbH52KV0fm63F8n2Y1lzSNp2Bs,1017
|
|
9
9
|
entsoe/py.typed,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
|
|
10
10
|
entsoe/.agents/skills/entsoe/SKILL.md,sha256=JsFECSkfnxyR6iFx55WBBcMJjAOaMu84vhcsPsTVWC0,6452
|
|
@@ -20,16 +20,16 @@ entsoe/cli/config_cmd.py,sha256=HfpMMv8S1kDUIid6kxX-cNirCvhDby2lSPNSD8o3qX8,923
|
|
|
20
20
|
entsoe/cli/exec_cmd.py,sha256=KiGwOTIjYQQPEg7v51oRbpk3Cj9-7hDDCbEYXfZ_lfQ,11308
|
|
21
21
|
entsoe/cli/generation.py,sha256=wSQUyZJ3vuhcHss4R8g__2BMkTSik6rYkr4FvwWvvrs,5681
|
|
22
22
|
entsoe/cli/load.py,sha256=wC8ZYGi8ce_b5OBBC7fdUPLCgUN_ssC79A0eZJuXGSQ,2685
|
|
23
|
-
entsoe/cli/prices.py,sha256=
|
|
23
|
+
entsoe/cli/prices.py,sha256=Gj_Irw4i5KJ7lUUUChkm9FqBDCHorP9tN4fFMnc5v0c,1729
|
|
24
24
|
entsoe/cli/transmission.py,sha256=nQMQwxNcvxHEdlldHnVY02oL2aUKONKRpz2M3bFKM1U,5592
|
|
25
25
|
entsoe/namespaces/__init__.py,sha256=ZVd80s5OVxegLo3A6nI64tHlBA1Iwe2ejqQilGNod5E,399
|
|
26
|
-
entsoe/namespaces/_base.py,sha256=
|
|
26
|
+
entsoe/namespaces/_base.py,sha256=euieKT3MZkOdIq9ER_hSHJuYPVXH2Ie_NDbzmiKZf6Y,15042
|
|
27
27
|
entsoe/namespaces/balancing.py,sha256=210Ft6dE7WXEuxtTJqyEvCIL960n-sDHT7tsqpSbXIw,2143
|
|
28
28
|
entsoe/namespaces/generation.py,sha256=V4DkvV5ai7aWYYSdZKwWyxTEv1ed4nCCq7DYyVwvc6M,6729
|
|
29
29
|
entsoe/namespaces/load.py,sha256=fbkwKWZifb8DOf5Rc_7c4_eFAXiqT_562r9WTMQwsuo,2160
|
|
30
|
-
entsoe/namespaces/prices.py,sha256=
|
|
30
|
+
entsoe/namespaces/prices.py,sha256=Dr1y5Y9wVdTOzTTWXlx11eiQLQc75chua3UoRpBYfxw,2311
|
|
31
31
|
entsoe/namespaces/transmission.py,sha256=V8zrs2CQXf1WPjJRqz7wc-ztnkfriIKXPLfYHBDJD9M,8067
|
|
32
|
-
python_entsoe-0.6.
|
|
33
|
-
python_entsoe-0.6.
|
|
34
|
-
python_entsoe-0.6.
|
|
35
|
-
python_entsoe-0.6.
|
|
32
|
+
python_entsoe-0.6.2.dist-info/METADATA,sha256=kdpKrENk7toC8X3fJQUfuyeP8Jv3VAsc-tDkHwGN5uI,8863
|
|
33
|
+
python_entsoe-0.6.2.dist-info/WHEEL,sha256=W3fkpkm7-wf9vBI5Z-7s0eWkeM-spu78I8Neb98DeEg,87
|
|
34
|
+
python_entsoe-0.6.2.dist-info/entry_points.txt,sha256=tYyCheggCsGRKOq2vIjpAxBksEM7K_iGI6vHkkQLmYg,42
|
|
35
|
+
python_entsoe-0.6.2.dist-info/RECORD,,
|
|
File without changes
|