python-entsoe 0.6.1__tar.gz → 0.6.3__tar.gz

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (64) hide show
  1. python_entsoe-0.6.3/.github/workflows/publish.yml +26 -0
  2. python_entsoe-0.6.3/.gitlab-ci.yml +22 -0
  3. {python_entsoe-0.6.1 → python_entsoe-0.6.3}/CHANGELOG.md +18 -0
  4. {python_entsoe-0.6.1 → python_entsoe-0.6.3}/CLAUDE.md +8 -4
  5. {python_entsoe-0.6.1 → python_entsoe-0.6.3}/PKG-INFO +11 -8
  6. {python_entsoe-0.6.1 → python_entsoe-0.6.3}/README.md +5 -2
  7. {python_entsoe-0.6.1 → python_entsoe-0.6.3}/docs/data-availability.md +24 -0
  8. {python_entsoe-0.6.1 → python_entsoe-0.6.3}/pyproject.toml +5 -5
  9. {python_entsoe-0.6.1 → python_entsoe-0.6.3}/src/entsoe/_xml.py +56 -7
  10. {python_entsoe-0.6.1 → python_entsoe-0.6.3}/src/entsoe/cache.py +41 -10
  11. {python_entsoe-0.6.1 → python_entsoe-0.6.3}/src/entsoe/cli/_resolve.py +3 -3
  12. {python_entsoe-0.6.1 → python_entsoe-0.6.3}/src/entsoe/cli/prices.py +2 -1
  13. {python_entsoe-0.6.1 → python_entsoe-0.6.3}/src/entsoe/namespaces/_base.py +33 -24
  14. {python_entsoe-0.6.1 → python_entsoe-0.6.3}/src/entsoe/namespaces/prices.py +20 -1
  15. python_entsoe-0.6.3/tests/test_cache_layout.py +23 -0
  16. python_entsoe-0.6.3/tests/test_cli_periods.py +15 -0
  17. python_entsoe-0.6.3/tests/test_xml_day_ahead.py +113 -0
  18. {python_entsoe-0.6.1 → python_entsoe-0.6.3}/uv.lock +2 -2
  19. python_entsoe-0.6.1/.github/workflows/publish.yml +0 -42
  20. python_entsoe-0.6.1/.release-please-manifest.json +0 -3
  21. python_entsoe-0.6.1/release-please-config.json +0 -19
  22. {python_entsoe-0.6.1 → python_entsoe-0.6.3}/.gitignore +0 -0
  23. {python_entsoe-0.6.1 → python_entsoe-0.6.3}/.python-version +0 -0
  24. {python_entsoe-0.6.1 → python_entsoe-0.6.3}/examples/balancing.ipynb +0 -0
  25. {python_entsoe-0.6.1 → python_entsoe-0.6.3}/examples/generation.ipynb +0 -0
  26. {python_entsoe-0.6.1 → python_entsoe-0.6.3}/examples/generation_per_plant.ipynb +0 -0
  27. {python_entsoe-0.6.1 → python_entsoe-0.6.3}/examples/load.ipynb +0 -0
  28. {python_entsoe-0.6.1 → python_entsoe-0.6.3}/examples/prices.ipynb +0 -0
  29. {python_entsoe-0.6.1 → python_entsoe-0.6.3}/examples/transmission.ipynb +0 -0
  30. {python_entsoe-0.6.1 → python_entsoe-0.6.3}/scripts/generate_notebooks.py +0 -0
  31. {python_entsoe-0.6.1 → python_entsoe-0.6.3}/src/entsoe/.agents/skills/entsoe/SKILL.md +0 -0
  32. {python_entsoe-0.6.1 → python_entsoe-0.6.3}/src/entsoe/__init__.py +0 -0
  33. {python_entsoe-0.6.1 → python_entsoe-0.6.3}/src/entsoe/_http.py +0 -0
  34. {python_entsoe-0.6.1 → python_entsoe-0.6.3}/src/entsoe/_mappings.py +0 -0
  35. {python_entsoe-0.6.1 → python_entsoe-0.6.3}/src/entsoe/catalog.py +0 -0
  36. {python_entsoe-0.6.1 → python_entsoe-0.6.3}/src/entsoe/cli/__init__.py +0 -0
  37. {python_entsoe-0.6.1 → python_entsoe-0.6.3}/src/entsoe/cli/_output.py +0 -0
  38. {python_entsoe-0.6.1 → python_entsoe-0.6.3}/src/entsoe/cli/app.py +0 -0
  39. {python_entsoe-0.6.1 → python_entsoe-0.6.3}/src/entsoe/cli/balancing.py +0 -0
  40. {python_entsoe-0.6.1 → python_entsoe-0.6.3}/src/entsoe/cli/cache_cmd.py +0 -0
  41. {python_entsoe-0.6.1 → python_entsoe-0.6.3}/src/entsoe/cli/catalog_cmd.py +0 -0
  42. {python_entsoe-0.6.1 → python_entsoe-0.6.3}/src/entsoe/cli/config.py +0 -0
  43. {python_entsoe-0.6.1 → python_entsoe-0.6.3}/src/entsoe/cli/config_cmd.py +0 -0
  44. {python_entsoe-0.6.1 → python_entsoe-0.6.3}/src/entsoe/cli/exec_cmd.py +0 -0
  45. {python_entsoe-0.6.1 → python_entsoe-0.6.3}/src/entsoe/cli/generation.py +0 -0
  46. {python_entsoe-0.6.1 → python_entsoe-0.6.3}/src/entsoe/cli/load.py +0 -0
  47. {python_entsoe-0.6.1 → python_entsoe-0.6.3}/src/entsoe/cli/transmission.py +0 -0
  48. {python_entsoe-0.6.1 → python_entsoe-0.6.3}/src/entsoe/client.py +0 -0
  49. {python_entsoe-0.6.1 → python_entsoe-0.6.3}/src/entsoe/exceptions.py +0 -0
  50. {python_entsoe-0.6.1 → python_entsoe-0.6.3}/src/entsoe/namespaces/__init__.py +0 -0
  51. {python_entsoe-0.6.1 → python_entsoe-0.6.3}/src/entsoe/namespaces/balancing.py +0 -0
  52. {python_entsoe-0.6.1 → python_entsoe-0.6.3}/src/entsoe/namespaces/generation.py +0 -0
  53. {python_entsoe-0.6.1 → python_entsoe-0.6.3}/src/entsoe/namespaces/load.py +0 -0
  54. {python_entsoe-0.6.1 → python_entsoe-0.6.3}/src/entsoe/namespaces/transmission.py +0 -0
  55. {python_entsoe-0.6.1 → python_entsoe-0.6.3}/src/entsoe/py.typed +0 -0
  56. {python_entsoe-0.6.1 → python_entsoe-0.6.3}/tests/__init__.py +0 -0
  57. {python_entsoe-0.6.1 → python_entsoe-0.6.3}/tests/conftest.py +0 -0
  58. {python_entsoe-0.6.1 → python_entsoe-0.6.3}/tests/test_balancing.py +0 -0
  59. {python_entsoe-0.6.1 → python_entsoe-0.6.3}/tests/test_generation.py +0 -0
  60. {python_entsoe-0.6.1 → python_entsoe-0.6.3}/tests/test_load.py +0 -0
  61. {python_entsoe-0.6.1 → python_entsoe-0.6.3}/tests/test_multi.py +0 -0
  62. {python_entsoe-0.6.1 → python_entsoe-0.6.3}/tests/test_prices.py +0 -0
  63. {python_entsoe-0.6.1 → python_entsoe-0.6.3}/tests/test_transmission.py +0 -0
  64. {python_entsoe-0.6.1 → python_entsoe-0.6.3}/tests/test_validation.py +0 -0
@@ -0,0 +1,26 @@
1
+ name: Publish to PyPI
2
+
3
+ on:
4
+ push:
5
+ tags: ["v*"]
6
+
7
+ jobs:
8
+ publish:
9
+ runs-on: ubuntu-latest
10
+ permissions:
11
+ id-token: write
12
+ steps:
13
+ - uses: actions/checkout@v4
14
+
15
+ - uses: actions/setup-python@v5
16
+ with:
17
+ python-version: "3.12"
18
+
19
+ - name: Install build tools
20
+ run: pip install build
21
+
22
+ - name: Build package
23
+ run: python -m build
24
+
25
+ - name: Publish to PyPI
26
+ uses: pypa/gh-action-pypi-publish@release/v1
@@ -0,0 +1,22 @@
1
+ stages:
2
+ - build
3
+ - publish
4
+
5
+ default:
6
+ image: ghcr.io/astral-sh/uv:python3.13-bookworm
7
+
8
+ build:
9
+ stage: build
10
+ script:
11
+ - uv build --no-sources
12
+ artifacts:
13
+ paths:
14
+ - dist/
15
+
16
+ publish:
17
+ stage: publish
18
+ rules:
19
+ - if: '$CI_COMMIT_TAG =~ /^v[0-9]+\.[0-9]+\.[0-9]+/'
20
+ script:
21
+ - uv build --no-sources
22
+ - uv publish --publish-url "$CI_API_V4_URL/projects/$CI_PROJECT_ID/packages/pypi" --check-url "$CI_API_V4_URL/projects/$CI_PROJECT_ID/packages/pypi/simple" --username gitlab-ci-token --password "$CI_JOB_TOKEN"
@@ -1,5 +1,23 @@
1
1
  # Changelog
2
2
 
3
+ ## [0.6.3](https://gitlab.com/datons.com/libraries/python-entsoe/-/tags/v0.6.3) (2026-10-15)
4
+
5
+
6
+ ### Bug Fixes
7
+
8
+ * **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.
9
+ * **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.
10
+
11
+ ## [0.6.2](https://gitlab.com/datons.com/libraries/python-entsoe/-/tags/v0.6.2) (2026-10-15)
12
+
13
+
14
+ ### Bug Fixes
15
+
16
+ * **prices:** `day_ahead` mixed the SDAC and EXAA auctions for DE_LU and AT, and the intraday auctions for ES, returning several prices per timestamp. It now returns SDAC; `sequence=2` returns EXAA (CLI: `--sequence 2`).
17
+ * **parser:** points omitted from A03 (variable-sized block) curves are filled with the previous value, so no interval is lost. Affects every endpoint.
18
+ * **parser:** TimeSeries that the API repeats verbatim no longer produce duplicate rows.
19
+ * **cache:** the cache moves to a versioned layout (`~/.cache/entsoe/v2/`). Data cached by earlier versions, which may hold the errors above, is deleted on first use and downloaded again.
20
+
3
21
  ## [0.6.1](https://github.com/datons/python-entsoe/compare/python-entsoe-v0.6.0...python-entsoe-v0.6.1) (2026-05-16)
4
22
 
5
23
 
@@ -9,19 +9,23 @@ Python client for the ENTSO-E Transparency Platform API.
9
9
  - Run scripts: `uv run python <script>`
10
10
  - Run tests: `uv run pytest`
11
11
 
12
- ## PyPI Publishing
12
+ ## Publishing
13
13
 
14
- Publishing is **tag-based only**. Pushing to `main` does NOT trigger a release.
14
+ The repo lives in GitLab (`datons.com/libraries/python-entsoe`, private). Publishing is **tag-based only**: pushing to `main` does NOT release. The CI publishes to the `datons.com/libraries` package registry, which `esios` and the other services consume through the `datons-libraries` index.
15
15
 
16
16
  ```bash
17
17
  # 1. Bump version in pyproject.toml
18
18
  # 2. Commit the version bump
19
19
  # 3. Tag and push
20
- git tag v0.3.0
20
+ git tag v0.6.2
21
21
  git push origin main --tags
22
22
  ```
23
23
 
24
- The GitHub Actions workflow (`.github/workflows/publish.yml`) uses OIDC trusted publishing — no tokens needed.
24
+ Public users install from PyPI, so a fix that matters to them is also released there: push `main` and the same tag to the `github` remote. `.github/workflows/publish.yml` builds and publishes on `v*` tags through PyPI trusted publishing (no token). GitLab stays the source of truth.
25
+
26
+ ```bash
27
+ git push github main --tags
28
+ ```
25
29
 
26
30
  ## Example Notebooks
27
31
 
@@ -1,11 +1,11 @@
1
- Metadata-Version: 2.4
1
+ Metadata-Version: 2.5
2
2
  Name: python-entsoe
3
- Version: 0.6.1
3
+ Version: 0.6.3
4
4
  Summary: Python client for the ENTSO-E Transparency Platform API
5
- Project-URL: Homepage, https://github.com/datons/python-entsoe
6
- Project-URL: Repository, https://github.com/datons/python-entsoe
7
- Project-URL: Issues, https://github.com/datons/python-entsoe/issues
8
- Project-URL: Changelog, https://github.com/datons/python-entsoe/releases
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
@@ -93,11 +93,14 @@ df = client.load.forecast(start, end, country="FR")
93
93
 
94
94
  | Method | Description | Parameters |
95
95
  |--------|-------------|------------|
96
- | `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 |
97
97
 
98
98
  ```python
99
99
  df = client.prices.day_ahead(start, end, country="FR")
100
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)
101
104
  ```
102
105
 
103
106
  ### `client.generation`
@@ -251,7 +254,7 @@ See the [`examples/`](examples/) directory for Jupyter notebooks with plotly vis
251
254
  ## Development
252
255
 
253
256
  ```bash
254
- git clone https://github.com/jsulopzs/python-entsoe.git
257
+ git clone git@gitlab.com:datons.com/libraries/python-entsoe.git
255
258
  cd python-entsoe
256
259
  uv sync
257
260
 
@@ -65,11 +65,14 @@ df = client.load.forecast(start, end, country="FR")
65
65
 
66
66
  | Method | Description | Parameters |
67
67
  |--------|-------------|------------|
68
- | `day_ahead(start, end, country)` | Day-ahead market prices (EUR/MWh) | `country`: ISO code |
68
+ | `day_ahead(start, end, country, sequence=1)` | Day-ahead market prices (EUR/MWh) | `country`: ISO code; `sequence`: `1` SDAC, `2` EXAA |
69
69
 
70
70
  ```python
71
71
  df = client.prices.day_ahead(start, end, country="FR")
72
72
  # Returns: timestamp, value, currency, price_unit
73
+
74
+ # DE_LU and AT also publish the separate EXAA auction (10:15 CET)
75
+ exaa = client.prices.day_ahead(start, end, country="DE_LU", sequence=2)
73
76
  ```
74
77
 
75
78
  ### `client.generation`
@@ -223,7 +226,7 @@ See the [`examples/`](examples/) directory for Jupyter notebooks with plotly vis
223
226
  ## Development
224
227
 
225
228
  ```bash
226
- git clone https://github.com/jsulopzs/python-entsoe.git
229
+ git clone git@gitlab.com:datons.com/libraries/python-entsoe.git
227
230
  cd python-entsoe
228
231
  uv sync
229
232
 
@@ -92,6 +92,28 @@ environment variable.
92
92
  export $(cat .env | xargs) && uv run jupyter nbconvert --execute ...
93
93
  ```
94
94
 
95
+ ## 7. Day-ahead documents (A44) mix several auctions
96
+
97
+ **Affected endpoints:** `prices.day_ahead`
98
+
99
+ **Problem:** One A44 response can hold several price series for the same hours:
100
+
101
+ - DE_LU and AT publish the SDAC auction (gate closure 12:00 CET) as `classificationSequence_AttributeInstanceComponent.position` 1 and the EXAA auction (10:15 CET) as position 2.
102
+ - ES also returns the intraday auctions (IDA1–3) under `contract_MarketAgreement.type` A07, numbered by the same sequence element.
103
+ - Some zones (FR, AT) repeat an identical TimeSeries.
104
+
105
+ Parsing every series gives two or more prices per timestamp.
106
+
107
+ **Solution:** Keep only daily contracts (`A01`, or no contract type) whose sequence matches the requested one, treating a missing sequence as 1. `day_ahead(..., sequence=2)` returns EXAA. Identical rows are dropped.
108
+
109
+ ## 8. Curve type A03 omits repeated points
110
+
111
+ **Affected endpoints:** all (prices, load, generation, transmission)
112
+
113
+ **Problem:** With `<curveType>A03</curveType>` (variable-sized blocks), a Point applies until the next Point, and positions whose value equals the previous one are left out. Reading only the Points present loses those intervals: a day of quarter-hourly prices can come back with 95 rows, and a flat generation series with a single row.
114
+
115
+ **Solution:** Fill every position up to the Period's `timeInterval/end` with the last value seen. The Period end tracks published data, so recent partial days are not extended.
116
+
95
117
  ## Summary table
96
118
 
97
119
  | Issue | Endpoint | Symptom | Fix |
@@ -102,3 +124,5 @@ export $(cat .env | xargs) && uv run jupyter nbconvert --execute ...
102
124
  | Route availability | net_transfer_capacity | `NoDataError` for FR→DE_LU | Use FR→ES, FR→GB, FR→IT_NORTH |
103
125
  | Value tag name | imbalance_prices | All values are `None` | Parse `imbalance_Price.amount` tag |
104
126
  | Env vars in notebooks | all (via nbconvert) | `ValueError: API key required` | Export env vars before execution |
127
+ | Mixed auctions | prices.day_ahead | Duplicate timestamps (DE_LU, AT, ES) | Filter by contract type and sequence |
128
+ | A03 omitted points | all | Missing intervals | Forward-fill to Period end |
@@ -1,6 +1,6 @@
1
1
  [project]
2
2
  name = "python-entsoe"
3
- version = "0.6.1"
3
+ version = "0.6.3"
4
4
  description = "Python client for the ENTSO-E Transparency Platform API"
5
5
  readme = "README.md"
6
6
  license = "MIT"
@@ -32,10 +32,10 @@ dependencies = [
32
32
  entsoe = "entsoe.cli:app"
33
33
 
34
34
  [project.urls]
35
- Homepage = "https://github.com/datons/python-entsoe"
36
- Repository = "https://github.com/datons/python-entsoe"
37
- Issues = "https://github.com/datons/python-entsoe/issues"
38
- Changelog = "https://github.com/datons/python-entsoe/releases"
35
+ Homepage = "https://gitlab.com/datons.com/libraries/python-entsoe"
36
+ Repository = "https://gitlab.com/datons.com/libraries/python-entsoe"
37
+ Issues = "https://gitlab.com/datons.com/libraries/python-entsoe/-/issues"
38
+ Changelog = "https://gitlab.com/datons.com/libraries/python-entsoe/-/releases"
39
39
 
40
40
  [build-system]
41
41
  requires = ["hatchling"]
@@ -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
- def parse_timeseries(xml_text: str) -> pd.DataFrame:
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
- position = int(position_text)
172
- value = _extract_point_value(point)
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
@@ -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: {cache_dir}/{domain}/{topic}/{partition}/data.parquet"""
79
- return self.config.cache_dir / domain / topic / partition / "data.parquet"
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: {cache_dir}/{domain}/{topic}/{partition}/meta.json"""
83
- return self.config.cache_dir / domain / topic / partition / "meta.json"
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.config.cache_dir / domain / topic / partition
325
+ target = self.root / domain / topic / partition
295
326
  elif domain and topic:
296
- target = self.config.cache_dir / domain / topic
327
+ target = self.root / domain / topic
297
328
  elif domain:
298
- target = self.config.cache_dir / domain
329
+ target = self.root / domain
299
330
  else:
300
- target = self.config.cache_dir
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.config.cache_dir
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
 
@@ -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
- yesterday = today - timedelta(days=1)
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
@@ -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
 
@@ -3,14 +3,16 @@
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
 
10
- from ..exceptions import InvalidParameterError, NoDataError
10
+ from ..exceptions import InvalidParameterError
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)
@@ -223,17 +232,14 @@ class BaseNamespace:
223
232
  )
224
233
  new_wide_frames = []
225
234
  gap_meta: dict = {}
235
+ # A failed request propagates: returning the cache without the gap
236
+ # would silently hand back partial or empty data.
226
237
  for gap in valid_gaps:
227
- try:
228
- raw_df = self._query(params, gap.start, gap.end)
229
- wide, meta = self._to_wide(raw_df, pivot_col, value_col)
230
- new_wide_frames.append(wide)
231
- if meta:
232
- gap_meta.update(meta)
233
- except (InvalidParameterError, NoDataError):
234
- raise
235
- except Exception as exc:
236
- logger.warning("Failed to fetch gap %s→%s: %s", gap.start, gap.end, exc)
238
+ raw_df = self._query(params, gap.start, gap.end, ts_filter=ts_filter)
239
+ wide, meta = self._to_wide(raw_df, pivot_col, value_col)
240
+ new_wide_frames.append(wide)
241
+ if meta:
242
+ gap_meta.update(meta)
237
243
 
238
244
  # 4. Merge new data with cache and persist
239
245
  if new_wide_frames:
@@ -279,27 +285,30 @@ class BaseNamespace:
279
285
  value_col: str = "value",
280
286
  label_fn: Callable[[str], str] = country_name,
281
287
  label_col: str = "country",
288
+ ts_filter: TimeSeriesFilter = None,
289
+ partition_fn: Callable[[str], str] = str,
282
290
  ) -> pd.DataFrame:
283
291
  """Execute a cached query for one or many values.
284
292
 
285
- Each value is its own cache partition. Multi-value queries add
286
- a label column, same as :meth:`_query_multi`.
293
+ Each value is its own cache partition, named by *partition_fn(value)*.
294
+ Multi-value queries add a label column, same as :meth:`_query_multi`.
287
295
  """
288
296
  if self._cache is None:
289
297
  return self._query_multi(build_params, values, start, end,
290
- label_fn=label_fn, label_col=label_col)
298
+ label_fn=label_fn, label_col=label_col,
299
+ ts_filter=ts_filter)
291
300
 
292
301
  if isinstance(values, str):
293
302
  return self._query_cached(
294
- domain, topic, values, build_params(values), start, end,
295
- pivot_col=pivot_col, value_col=value_col,
303
+ domain, topic, partition_fn(values), build_params(values), start, end,
304
+ pivot_col=pivot_col, value_col=value_col, ts_filter=ts_filter,
296
305
  )
297
306
 
298
307
  frames = []
299
308
  for val in values:
300
309
  df = self._query_cached(
301
- domain, topic, val, build_params(val), start, end,
302
- pivot_col=pivot_col, value_col=value_col,
310
+ domain, topic, partition_fn(val), build_params(val), start, end,
311
+ pivot_col=pivot_col, value_col=value_col, ts_filter=ts_filter,
303
312
  )
304
313
  df[label_col] = label_fn(val)
305
314
  frames.append(df)
@@ -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
  )
@@ -0,0 +1,23 @@
1
+ """Offline tests for cache layout versioning."""
2
+
3
+ from entsoe.cache import CACHE_LAYOUT_VERSION, CacheConfig, CacheStore
4
+
5
+
6
+ def test_outdated_layouts_are_purged(tmp_path):
7
+ legacy = tmp_path / "prices" / "day_ahead" / "DE_LU"
8
+ legacy.mkdir(parents=True)
9
+ (legacy / "data.parquet").write_bytes(b"stale")
10
+ old_version = tmp_path / "v1" / "load"
11
+ old_version.mkdir(parents=True)
12
+ current = tmp_path / f"v{CACHE_LAYOUT_VERSION}" / "prices"
13
+ current.mkdir(parents=True)
14
+ unrelated = tmp_path / "notes.txt"
15
+ unrelated.write_text("keep")
16
+
17
+ store = CacheStore(CacheConfig(cache_dir=tmp_path))
18
+
19
+ assert not (tmp_path / "prices").exists()
20
+ assert not (tmp_path / "v1").exists()
21
+ assert current.exists()
22
+ assert unrelated.exists()
23
+ assert store.root == tmp_path / f"v{CACHE_LAYOUT_VERSION}"
@@ -0,0 +1,15 @@
1
+ """Offline tests for CLI period shorthands."""
2
+
3
+ from datetime import date
4
+
5
+ from entsoe.cli._resolve import _expand_period
6
+
7
+ TODAY = date(2026, 10, 15)
8
+
9
+
10
+ def test_today_is_one_full_day():
11
+ assert _expand_period("today", TODAY) == (date(2026, 10, 15), date(2026, 10, 16))
12
+
13
+
14
+ def test_yesterday_is_one_full_day():
15
+ assert _expand_period("yesterday", TODAY) == (date(2026, 10, 14), date(2026, 10, 15))
@@ -0,0 +1,113 @@
1
+ """Offline tests for day-ahead parsing: auction selection, A03 gaps, duplicates."""
2
+
3
+ import pandas as pd
4
+ import pytest
5
+
6
+ from entsoe._xml import parse_timeseries
7
+ from entsoe.namespaces.prices import PricesNamespace
8
+
9
+ NS = "urn:iec62325.351:tc57wg16:451-3:publicationdocument:7:3"
10
+
11
+
12
+ def _series(mrid, prices, *, contract="A01", sequence=None, start="2026-10-12T22:00Z",
13
+ end="2026-10-12T23:00Z"):
14
+ seq = (
15
+ "<classificationSequence_AttributeInstanceComponent.position>"
16
+ f"{sequence}</classificationSequence_AttributeInstanceComponent.position>"
17
+ if sequence is not None else ""
18
+ )
19
+ points = "".join(
20
+ f"<Point><position>{pos}</position><price.amount>{val}</price.amount></Point>"
21
+ for pos, val in prices.items()
22
+ )
23
+ return (
24
+ f"<TimeSeries><mRID>{mrid}</mRID><auction.type>A01</auction.type>"
25
+ f"<contract_MarketAgreement.type>{contract}</contract_MarketAgreement.type>"
26
+ "<currency_Unit.name>EUR</currency_Unit.name>"
27
+ f"<price_Measure_Unit.name>MWH</price_Measure_Unit.name>{seq}"
28
+ "<curveType>A03</curveType>"
29
+ f"<Period><timeInterval><start>{start}</start><end>{end}</end></timeInterval>"
30
+ f"<resolution>PT15M</resolution>{points}</Period></TimeSeries>"
31
+ )
32
+
33
+
34
+ def _doc(*series):
35
+ return f'<Publication_MarketDocument xmlns="{NS}">{"".join(series)}</Publication_MarketDocument>'
36
+
37
+
38
+ class _FakeHttp:
39
+ def __init__(self, xml):
40
+ self.xml = xml
41
+
42
+ def query(self, params, start, end):
43
+ return self.xml
44
+
45
+
46
+ def _day_ahead(xml, **kwargs):
47
+ ns = PricesNamespace(_FakeHttp(xml), cache=None)
48
+ return ns.day_ahead("2026-10-13", "2026-10-14", country="DE_LU", **kwargs)
49
+
50
+
51
+ # Shaped like the live DE_LU response: EXAA (sequence 2) listed before SDAC (1).
52
+ DE_LU = _doc(
53
+ _series(1, {1: 189.61, 2: 180.0, 3: 175.0, 4: 170.0}, sequence=2),
54
+ _series(2, {1: 191.89, 2: 185.0, 3: 178.0, 4: 172.0}, sequence=1),
55
+ )
56
+
57
+ # Shaped like the live ES response: unsequenced SDAC plus IDA intraday auctions.
58
+ ES = _doc(
59
+ _series(1, {1: 220.0, 2: 210.0, 3: 205.0, 4: 200.0}),
60
+ _series(2, {1: 220.49, 2: 211.0, 3: 206.0, 4: 201.0}, contract="A07", sequence=1),
61
+ _series(3, {1: 219.0, 2: 209.0, 3: 204.0, 4: 199.0}, contract="A07", sequence=2),
62
+ )
63
+
64
+
65
+ def test_de_lu_defaults_to_sdac():
66
+ df = _day_ahead(DE_LU)
67
+ assert df["timestamp"].is_unique
68
+ assert df["value"].tolist() == [191.89, 185.0, 178.0, 172.0]
69
+
70
+
71
+ def test_de_lu_sequence_2_is_exaa():
72
+ df = _day_ahead(DE_LU, sequence=2)
73
+ assert df["value"].tolist() == [189.61, 180.0, 175.0, 170.0]
74
+
75
+
76
+ def test_spain_drops_intraday_auctions():
77
+ df = _day_ahead(ES)
78
+ assert df["value"].tolist() == [220.0, 210.0, 205.0, 200.0]
79
+
80
+
81
+ def test_a03_omitted_point_repeats_previous_value():
82
+ # Position 3 omitted: it carries position 2's value. Trailing slot 4 too.
83
+ df = parse_timeseries(_doc(_series(1, {1: 10.0, 2: 20.0})))
84
+ assert len(df) == 4
85
+ assert df["value"].tolist() == [10.0, 20.0, 20.0, 20.0]
86
+ assert df["timestamp"].iloc[-1] == pd.Timestamp("2026-10-12T22:45Z")
87
+
88
+
89
+ def test_verbatim_duplicate_series_dropped():
90
+ s = {1: 1.0, 2: 2.0, 3: 3.0, 4: 4.0}
91
+ df = parse_timeseries(_doc(_series(1, s), _series(1, s)))
92
+ assert len(df) == 4
93
+
94
+
95
+ def test_unknown_sequence_raises_no_data():
96
+ from entsoe.exceptions import NoDataError
97
+
98
+ with pytest.raises(NoDataError):
99
+ _day_ahead(ES, sequence=2)
100
+
101
+
102
+ def test_cached_query_raises_on_network_error(tmp_path):
103
+ import requests
104
+
105
+ from entsoe.cache import CacheConfig, CacheStore
106
+
107
+ class _DownHttp:
108
+ def query(self, params, start, end):
109
+ raise requests.exceptions.ConnectTimeout("down")
110
+
111
+ ns = PricesNamespace(_DownHttp(), cache=CacheStore(CacheConfig(cache_dir=tmp_path)))
112
+ with pytest.raises(requests.exceptions.ConnectTimeout):
113
+ ns.day_ahead("2026-10-13", "2026-10-14", country="DE_LU")
@@ -1,5 +1,5 @@
1
1
  version = 1
2
- revision = 2
2
+ revision = 3
3
3
  requires-python = ">=3.10"
4
4
  resolution-markers = [
5
5
  "python_full_version >= '3.14' and sys_platform == 'win32'",
@@ -1705,7 +1705,7 @@ wheels = [
1705
1705
 
1706
1706
  [[package]]
1707
1707
  name = "python-entsoe"
1708
- version = "0.4.1"
1708
+ version = "0.6.3"
1709
1709
  source = { editable = "." }
1710
1710
  dependencies = [
1711
1711
  { name = "pandas", version = "2.3.3", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version < '3.11'" },
@@ -1,42 +0,0 @@
1
- name: Release Please
2
-
3
- on:
4
- push:
5
- branches: [main]
6
-
7
- permissions:
8
- contents: write
9
- pull-requests: write
10
-
11
- jobs:
12
- release-please:
13
- runs-on: ubuntu-latest
14
- outputs:
15
- release_created: ${{ steps.release.outputs.release_created }}
16
- steps:
17
- - uses: googleapis/release-please-action@v4
18
- id: release
19
- with:
20
- token: ${{ secrets.GITHUB_TOKEN }}
21
-
22
- publish:
23
- needs: release-please
24
- if: needs.release-please.outputs.release_created == 'true'
25
- runs-on: ubuntu-latest
26
- permissions:
27
- id-token: write
28
- steps:
29
- - uses: actions/checkout@v4
30
-
31
- - uses: actions/setup-python@v5
32
- with:
33
- python-version: "3.12"
34
-
35
- - name: Install build tools
36
- run: pip install build
37
-
38
- - name: Build package
39
- run: python -m build
40
-
41
- - name: Publish to PyPI
42
- uses: pypa/gh-action-pypi-publish@release/v1
@@ -1,3 +0,0 @@
1
- {
2
- ".": "0.6.1"
3
- }
@@ -1,19 +0,0 @@
1
- {
2
- "packages": {
3
- ".": {
4
- "release-type": "python",
5
- "package-name": "python-entsoe",
6
- "extra-files": [
7
- "pyproject.toml"
8
- ],
9
- "changelog-sections": [
10
- { "type": "feat", "section": "Features" },
11
- { "type": "fix", "section": "Bug Fixes" },
12
- { "type": "perf", "section": "Performance" },
13
- { "type": "docs", "section": "Documentation", "hidden": true },
14
- { "type": "chore", "section": "Miscellaneous", "hidden": true }
15
- ]
16
- }
17
- },
18
- "$schema": "https://raw.githubusercontent.com/googleapis/release-please/main/schemas/config.json"
19
- }
File without changes