python-entsoe 0.3.0__tar.gz → 0.4.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.
Files changed (54) hide show
  1. python_entsoe-0.4.0/.github/workflows/publish.yml +28 -0
  2. {python_entsoe-0.3.0 → python_entsoe-0.4.0}/PKG-INFO +7 -2
  3. {python_entsoe-0.3.0 → python_entsoe-0.4.0}/pyproject.toml +10 -2
  4. python_entsoe-0.4.0/skills/entsoe/SKILL.md +202 -0
  5. {python_entsoe-0.3.0 → python_entsoe-0.4.0}/src/entsoe/__init__.py +1 -1
  6. {python_entsoe-0.3.0 → python_entsoe-0.4.0}/src/entsoe/_http.py +2 -0
  7. {python_entsoe-0.3.0 → python_entsoe-0.4.0}/src/entsoe/_mappings.py +2 -0
  8. {python_entsoe-0.3.0 → python_entsoe-0.4.0}/src/entsoe/_xml.py +2 -0
  9. python_entsoe-0.4.0/src/entsoe/cli/__init__.py +5 -0
  10. python_entsoe-0.4.0/src/entsoe/cli/_output.py +100 -0
  11. python_entsoe-0.4.0/src/entsoe/cli/app.py +56 -0
  12. python_entsoe-0.4.0/src/entsoe/cli/balancing.py +47 -0
  13. python_entsoe-0.4.0/src/entsoe/cli/config.py +63 -0
  14. python_entsoe-0.4.0/src/entsoe/cli/config_cmd.py +34 -0
  15. python_entsoe-0.4.0/src/entsoe/cli/exec_cmd.py +113 -0
  16. python_entsoe-0.4.0/src/entsoe/cli/generation.py +94 -0
  17. python_entsoe-0.4.0/src/entsoe/cli/load.py +47 -0
  18. python_entsoe-0.4.0/src/entsoe/cli/prices.py +29 -0
  19. python_entsoe-0.4.0/src/entsoe/cli/transmission.py +75 -0
  20. {python_entsoe-0.3.0 → python_entsoe-0.4.0}/src/entsoe/exceptions.py +2 -0
  21. {python_entsoe-0.3.0 → python_entsoe-0.4.0}/src/entsoe/namespaces/_base.py +48 -2
  22. {python_entsoe-0.3.0 → python_entsoe-0.4.0}/src/entsoe/namespaces/balancing.py +23 -17
  23. python_entsoe-0.4.0/src/entsoe/namespaces/generation.py +154 -0
  24. {python_entsoe-0.3.0 → python_entsoe-0.4.0}/src/entsoe/namespaces/load.py +25 -17
  25. {python_entsoe-0.3.0 → python_entsoe-0.4.0}/src/entsoe/namespaces/prices.py +15 -11
  26. python_entsoe-0.4.0/src/entsoe/namespaces/transmission.py +131 -0
  27. python_entsoe-0.4.0/tests/test_multi.py +128 -0
  28. {python_entsoe-0.3.0 → python_entsoe-0.4.0}/uv.lock +693 -24
  29. python_entsoe-0.3.0/.github/workflows/publish.yml +0 -23
  30. python_entsoe-0.3.0/src/entsoe/namespaces/generation.py +0 -129
  31. python_entsoe-0.3.0/src/entsoe/namespaces/transmission.py +0 -93
  32. {python_entsoe-0.3.0 → python_entsoe-0.4.0}/.gitignore +0 -0
  33. {python_entsoe-0.3.0 → python_entsoe-0.4.0}/.python-version +0 -0
  34. {python_entsoe-0.3.0 → python_entsoe-0.4.0}/CLAUDE.md +0 -0
  35. {python_entsoe-0.3.0 → python_entsoe-0.4.0}/README.md +0 -0
  36. {python_entsoe-0.3.0 → python_entsoe-0.4.0}/docs/data-availability.md +0 -0
  37. {python_entsoe-0.3.0 → python_entsoe-0.4.0}/examples/balancing.ipynb +0 -0
  38. {python_entsoe-0.3.0 → python_entsoe-0.4.0}/examples/generation.ipynb +0 -0
  39. {python_entsoe-0.3.0 → python_entsoe-0.4.0}/examples/generation_per_plant.ipynb +0 -0
  40. {python_entsoe-0.3.0 → python_entsoe-0.4.0}/examples/load.ipynb +0 -0
  41. {python_entsoe-0.3.0 → python_entsoe-0.4.0}/examples/prices.ipynb +0 -0
  42. {python_entsoe-0.3.0 → python_entsoe-0.4.0}/examples/transmission.ipynb +0 -0
  43. {python_entsoe-0.3.0 → python_entsoe-0.4.0}/scripts/generate_notebooks.py +0 -0
  44. {python_entsoe-0.3.0 → python_entsoe-0.4.0}/src/entsoe/client.py +0 -0
  45. {python_entsoe-0.3.0 → python_entsoe-0.4.0}/src/entsoe/namespaces/__init__.py +0 -0
  46. {python_entsoe-0.3.0 → python_entsoe-0.4.0}/src/entsoe/py.typed +0 -0
  47. {python_entsoe-0.3.0 → python_entsoe-0.4.0}/tests/__init__.py +0 -0
  48. {python_entsoe-0.3.0 → python_entsoe-0.4.0}/tests/conftest.py +0 -0
  49. {python_entsoe-0.3.0 → python_entsoe-0.4.0}/tests/test_balancing.py +0 -0
  50. {python_entsoe-0.3.0 → python_entsoe-0.4.0}/tests/test_generation.py +0 -0
  51. {python_entsoe-0.3.0 → python_entsoe-0.4.0}/tests/test_load.py +0 -0
  52. {python_entsoe-0.3.0 → python_entsoe-0.4.0}/tests/test_prices.py +0 -0
  53. {python_entsoe-0.3.0 → python_entsoe-0.4.0}/tests/test_transmission.py +0 -0
  54. {python_entsoe-0.3.0 → python_entsoe-0.4.0}/tests/test_validation.py +0 -0
@@ -0,0 +1,28 @@
1
+ name: Publish to PyPI
2
+
3
+ on:
4
+ push:
5
+ branches: [main]
6
+
7
+ jobs:
8
+ publish:
9
+ runs-on: ubuntu-latest
10
+ permissions:
11
+ id-token: write # Required for trusted publishing
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
27
+ with:
28
+ skip-existing: true
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: python-entsoe
3
- Version: 0.3.0
3
+ Version: 0.4.0
4
4
  Summary: Python client for the ENTSO-E Transparency Platform API
5
5
  Project-URL: Repository, https://github.com/datons/python-entsoe
6
6
  Author-email: jsulopzs <jesus.lopez@datons.com>
@@ -10,11 +10,16 @@ Classifier: Development Status :: 3 - Alpha
10
10
  Classifier: Intended Audience :: Science/Research
11
11
  Classifier: License :: OSI Approved :: MIT License
12
12
  Classifier: Programming Language :: Python :: 3
13
+ Classifier: Programming Language :: Python :: 3.10
14
+ Classifier: Programming Language :: Python :: 3.11
15
+ Classifier: Programming Language :: Python :: 3.12
13
16
  Classifier: Programming Language :: Python :: 3.13
14
17
  Classifier: Topic :: Scientific/Engineering
15
- Requires-Python: >=3.13
18
+ Requires-Python: >=3.10
16
19
  Requires-Dist: pandas>=2.0
17
20
  Requires-Dist: requests>=2.28
21
+ Requires-Dist: rich>=13.0
22
+ Requires-Dist: typer>=0.9
18
23
  Description-Content-Type: text/markdown
19
24
 
20
25
  # python-entsoe
@@ -1,27 +1,35 @@
1
1
  [project]
2
2
  name = "python-entsoe"
3
- version = "0.3.0"
3
+ version = "0.4.0"
4
4
  description = "Python client for the ENTSO-E Transparency Platform API"
5
5
  readme = "README.md"
6
6
  license = "MIT"
7
7
  authors = [
8
8
  { name = "jsulopzs", email = "jesus.lopez@datons.com" },
9
9
  ]
10
- requires-python = ">=3.13"
10
+ requires-python = ">=3.10"
11
11
  keywords = ["entsoe", "energy", "electricity", "api", "transparency"]
12
12
  classifiers = [
13
13
  "Development Status :: 3 - Alpha",
14
14
  "Intended Audience :: Science/Research",
15
15
  "License :: OSI Approved :: MIT License",
16
16
  "Programming Language :: Python :: 3",
17
+ "Programming Language :: Python :: 3.10",
18
+ "Programming Language :: Python :: 3.11",
19
+ "Programming Language :: Python :: 3.12",
17
20
  "Programming Language :: Python :: 3.13",
18
21
  "Topic :: Scientific/Engineering",
19
22
  ]
20
23
  dependencies = [
21
24
  "pandas>=2.0",
22
25
  "requests>=2.28",
26
+ "typer>=0.9",
27
+ "rich>=13.0",
23
28
  ]
24
29
 
30
+ [project.scripts]
31
+ entsoe = "entsoe.cli:app"
32
+
25
33
  [project.urls]
26
34
  Repository = "https://github.com/datons/python-entsoe"
27
35
 
@@ -0,0 +1,202 @@
1
+ ---
2
+ name: entsoe
3
+ description: Query European electricity market data (ENTSO-E Transparency Platform). Use when the user asks about electricity prices, load, generation, transmission, or balancing data for European countries.
4
+ version: 1.0.0
5
+ ---
6
+
7
+ # ENTSO-E Data Assistant
8
+
9
+ You have access to the `python-entsoe` CLI and library for querying the ENTSO-E Transparency Platform (European electricity market data).
10
+
11
+ ## CLI Reference
12
+
13
+ ### Prices
14
+
15
+ ```bash
16
+ # Day-ahead prices for one country
17
+ entsoe prices day-ahead -s 2024-06-01 -e 2024-06-08 --country FR
18
+
19
+ # Multi-country
20
+ entsoe prices day-ahead -s 2024-06-01 -e 2024-06-08 --country FR --country ES
21
+ ```
22
+
23
+ ### Load
24
+
25
+ ```bash
26
+ # Actual total system load
27
+ entsoe load actual -s 2024-06-01 -e 2024-06-08 --country FR
28
+
29
+ # Day-ahead load forecast
30
+ entsoe load forecast -s 2024-06-01 -e 2024-06-08 --country FR
31
+ ```
32
+
33
+ ### Generation
34
+
35
+ ```bash
36
+ # Actual generation per type (all types)
37
+ entsoe generation actual -s 2024-06-01 -e 2024-06-08 --country FR
38
+
39
+ # Filter by PSR type
40
+ entsoe generation actual -s 2024-06-01 -e 2024-06-08 --country FR --psr solar --psr wind_onshore
41
+
42
+ # Generation forecast (wind/solar)
43
+ entsoe generation forecast -s 2024-06-01 -e 2024-06-08 --country FR
44
+
45
+ # Installed capacity
46
+ entsoe generation capacity -s 2024-06-01 -e 2024-06-08 --country FR
47
+
48
+ # Per production unit
49
+ entsoe generation per-plant -s 2024-06-01 -e 2024-06-08 --country FR
50
+ ```
51
+
52
+ ### Transmission
53
+
54
+ ```bash
55
+ # Physical cross-border flows
56
+ entsoe transmission flows -s 2024-06-01 -e 2024-06-08 --from FR --to ES
57
+
58
+ # Scheduled commercial exchanges
59
+ entsoe transmission exchanges -s 2024-06-01 -e 2024-06-08 --from FR --to ES
60
+
61
+ # Net transfer capacity
62
+ entsoe transmission capacity -s 2024-06-01 -e 2024-06-08 --from FR --to ES
63
+ ```
64
+
65
+ ### Balancing
66
+
67
+ ```bash
68
+ # Imbalance prices
69
+ entsoe balancing prices -s 2024-06-01 -e 2024-06-08 --country FR
70
+
71
+ # Imbalance volumes
72
+ entsoe balancing volumes -s 2024-06-01 -e 2024-06-08 --country FR
73
+ ```
74
+
75
+ ### Exec (ad-hoc pandas expressions)
76
+
77
+ Run any pandas expression against fetched data:
78
+
79
+ ```bash
80
+ # Descriptive statistics on prices
81
+ entsoe exec prices day-ahead -s 2024-06-01 -e 2024-06-08 -c FR -x "df.describe()"
82
+
83
+ # Daily mean load
84
+ entsoe exec load actual -s 2024-06-01 -e 2024-06-08 -c FR -x "df.resample('D').mean()"
85
+
86
+ # Generation with PSR filter
87
+ entsoe exec generation actual -s 2024-06-01 -e 2024-06-08 -c FR --psr solar -x "df.head(20)"
88
+
89
+ # Transmission analysis
90
+ entsoe exec transmission flows -s 2024-06-01 -e 2024-06-08 --from FR --to ES -x "df.describe()"
91
+ ```
92
+
93
+ ### Output formats
94
+
95
+ All commands support:
96
+
97
+ ```bash
98
+ --format table # Default: Rich table in terminal
99
+ --format csv # CSV output
100
+ --format json # JSON output
101
+ --output file.csv # Write to file instead of stdout
102
+ ```
103
+
104
+ ## Country Codes
105
+
106
+ | Code | Country | Code | Country |
107
+ |------|---------|------|---------|
108
+ | AT | Austria | IT | Italy |
109
+ | BE | Belgium | LT | Lithuania |
110
+ | BG | Bulgaria | LU | Luxembourg |
111
+ | CH | Switzerland | LV | Latvia |
112
+ | CZ | Czech Republic | NL | Netherlands |
113
+ | DE_LU | Germany/Luxembourg | NO | Norway |
114
+ | DK | Denmark | PL | Poland |
115
+ | EE | Estonia | PT | Portugal |
116
+ | ES | Spain | RO | Romania |
117
+ | FI | Finland | RS | Serbia |
118
+ | FR | France | SE | Sweden |
119
+ | GB | Great Britain | SI | Slovenia |
120
+ | GR | Greece | SK | Slovakia |
121
+ | HR | Croatia | TR | Turkey |
122
+ | HU | Hungary | UA | Ukraine |
123
+
124
+ Bidding zones: `DK_1`, `DK_2`, `NO_1`–`NO_5`, `SE_1`–`SE_4`, `IT_NORTH`, `IT_CNOR`, `IT_CSUD`, `IT_SUD`, `IT_SICI`, `IT_SARD`, `DE_AT_LU`, `IE_SEM`.
125
+
126
+ ## PSR Types (Generation)
127
+
128
+ | Shorthand | Full Name | Code |
129
+ |-----------|-----------|------|
130
+ | solar | Solar | B16 |
131
+ | wind_onshore | Wind Onshore | B19 |
132
+ | wind_offshore | Wind Offshore | B18 |
133
+ | nuclear | Nuclear | B14 |
134
+ | gas | Fossil Gas | B04 |
135
+ | hard_coal | Fossil Hard coal | B05 |
136
+ | lignite | Fossil Brown coal/Lignite | B02 |
137
+ | hydro_reservoir | Hydro Water Reservoir | B12 |
138
+ | run_of_river | Hydro Run-of-river | B11 |
139
+ | pumped_storage | Hydro Pumped Storage | B10 |
140
+ | biomass | Biomass | B01 |
141
+ | oil | Fossil Oil | B06 |
142
+ | geothermal | Geothermal | B09 |
143
+ | waste | Waste | B17 |
144
+ | other | Other | B20 |
145
+
146
+ Use shorthands with `--psr` (e.g. `--psr solar --psr wind_onshore`), or ENTSO-E codes (e.g. `--psr B16`).
147
+
148
+ ## Python Library
149
+
150
+ ```python
151
+ from entsoe import Client
152
+
153
+ client = Client() # reads config file, then ENTSOE_API_KEY env var
154
+
155
+ # Prices
156
+ df = client.prices.day_ahead("2024-06-01", "2024-06-08", country="FR")
157
+ df = client.prices.day_ahead("2024-06-01", "2024-06-08", country=["FR", "ES"])
158
+
159
+ # Load
160
+ df = client.load.actual("2024-06-01", "2024-06-08", country="FR")
161
+ df = client.load.forecast("2024-06-01", "2024-06-08", country="FR")
162
+
163
+ # Generation
164
+ df = client.generation.actual("2024-06-01", "2024-06-08", country="FR")
165
+ df = client.generation.actual("2024-06-01", "2024-06-08", country="FR", psr_type="solar")
166
+ df = client.generation.actual("2024-06-01", "2024-06-08", country="FR", psr_type=["solar", "wind_onshore"])
167
+ df = client.generation.forecast("2024-06-01", "2024-06-08", country="FR")
168
+ df = client.generation.installed_capacity("2024-06-01", "2024-06-08", country="FR")
169
+ df = client.generation.per_plant("2024-06-01", "2024-06-08", country="FR")
170
+
171
+ # Transmission
172
+ df = client.transmission.crossborder_flows("2024-06-01", "2024-06-08", country_from="FR", country_to="ES")
173
+ df = client.transmission.scheduled_exchanges("2024-06-01", "2024-06-08", country_from="FR", country_to="ES")
174
+ df = client.transmission.net_transfer_capacity("2024-06-01", "2024-06-08", country_from="FR", country_to="ES")
175
+
176
+ # Balancing
177
+ df = client.balancing.imbalance_prices("2024-06-01", "2024-06-08", country="FR")
178
+ df = client.balancing.imbalance_volumes("2024-06-01", "2024-06-08", country="FR")
179
+ ```
180
+
181
+ ## Configuration
182
+
183
+ ```bash
184
+ # Store your API key persistently (recommended)
185
+ entsoe config set api-key YOUR_KEY
186
+
187
+ # Verify it's stored
188
+ entsoe config get api-key
189
+ ```
190
+
191
+ The config file is stored at `~/.config/entsoe/config.toml`.
192
+
193
+ ## Key Conventions
194
+
195
+ - **Timezone**: All string dates are interpreted in Europe/Brussels (CET) by default
196
+ - **Auto year-splitting**: Date ranges exceeding 1 year are automatically split into yearly API requests
197
+ - **Rate limiting**: ENTSO-E has a 400-request/minute limit; the library handles retries
198
+ - **Multi-value support**: Pass lists for country or psr_type to get combined results with label columns
199
+ - **Transmission**: Uses `--from`/`--to` instead of `--country`; multi-value adds a `border` column
200
+ - **API key resolution**: config file (`~/.config/entsoe/config.toml`) > `ENTSOE_API_KEY` env var
201
+ - **No caching**: Data is fetched fresh on each request (unlike python-esios)
202
+ - **Custom exceptions**: `ENTSOEError`, `AuthenticationError`, `APIResponseError`, `InvalidParameterError`, `NetworkError`
@@ -4,7 +4,7 @@ from .client import Client
4
4
  from .exceptions import ENTSOEError, InvalidParameterError, NoDataError, RateLimitError
5
5
  from ._mappings import COUNTRY_NAMES, PSR_CODES, country_name, psr_name
6
6
 
7
- __version__ = "0.3.0"
7
+ __version__ = "0.4.0"
8
8
  __all__ = [
9
9
  "Client",
10
10
  "COUNTRY_NAMES",
@@ -7,6 +7,8 @@ Handles:
7
7
  - Response validation
8
8
  """
9
9
 
10
+ from __future__ import annotations
11
+
10
12
  import io
11
13
  import logging
12
14
  import time
@@ -1,5 +1,7 @@
1
1
  """ENTSO-E area codes, document types, and PSR type mappings."""
2
2
 
3
+ from __future__ import annotations
4
+
3
5
  # Country code (ISO 3166-1 alpha-2) → ENTSO-E EIC area code
4
6
  AREA_CODES: dict[str, str] = {
5
7
  "AL": "10YAL-KESH-----5",
@@ -10,6 +10,8 @@ Handles the various document types returned by the API:
10
10
  All share the TimeSeries > Period > Point structure.
11
11
  """
12
12
 
13
+ from __future__ import annotations
14
+
13
15
  import re
14
16
  import xml.etree.ElementTree as ET
15
17
  from datetime import timedelta
@@ -0,0 +1,5 @@
1
+ """ENTSO-E CLI — command-line interface for the ENTSO-E Transparency Platform API."""
2
+
3
+ from entsoe.cli.app import app
4
+
5
+ __all__ = ["app"]
@@ -0,0 +1,100 @@
1
+ """Shared output helpers for the ENTSO-E CLI."""
2
+
3
+ from __future__ import annotations
4
+
5
+ import typer
6
+ from rich.console import Console
7
+ from rich.table import Table
8
+
9
+ console = Console()
10
+
11
+
12
+ def _fmt(val) -> str:
13
+ """Format a value for table display."""
14
+ if isinstance(val, float):
15
+ return f"{val:.4f}"
16
+ return str(val)
17
+
18
+
19
+ def output(df, format: str, title: str = "", output_path: str | None = None):
20
+ """Render DataFrame in the requested format."""
21
+ import pandas as pd
22
+
23
+ if df.empty:
24
+ typer.echo("No data.")
25
+ return
26
+
27
+ if format == "csv":
28
+ text = df.to_csv()
29
+ if output_path:
30
+ with open(output_path, "w") as f:
31
+ f.write(text)
32
+ typer.echo(f"Written to {output_path}")
33
+ else:
34
+ typer.echo(text)
35
+
36
+ elif format == "json":
37
+ text = df.to_json(orient="records", indent=2, date_format="iso")
38
+ if output_path:
39
+ with open(output_path, "w") as f:
40
+ f.write(text)
41
+ typer.echo(f"Written to {output_path}")
42
+ else:
43
+ typer.echo(text)
44
+
45
+ else: # table
46
+ table = Table(title=title)
47
+ cols = list(df.columns)[:10] # Limit columns for readability
48
+ if df.index.name:
49
+ table.add_column(df.index.name, style="cyan")
50
+ for col in cols:
51
+ table.add_column(str(col))
52
+
53
+ for idx, row in df.head(50).iterrows():
54
+ values = [str(idx)] if df.index.name else []
55
+ values += [_fmt(row[c]) for c in cols]
56
+ table.add_row(*values)
57
+
58
+ if len(df) > 50:
59
+ table.caption = f"Showing 50 of {len(df)} rows"
60
+
61
+ console.print(table)
62
+
63
+
64
+ def render_result(result, format: str, output_path: str | None) -> None:
65
+ """Render an eval result (DataFrame, Series, or scalar)."""
66
+ import pandas as pd
67
+
68
+ if isinstance(result, pd.Series):
69
+ result = result.to_frame()
70
+
71
+ if isinstance(result, pd.DataFrame):
72
+ if format == "csv":
73
+ text = result.to_csv()
74
+ elif format == "json":
75
+ text = result.to_json(orient="records", indent=2, date_format="iso")
76
+ else:
77
+ table = Table()
78
+ idx_name = result.index.name or ""
79
+ table.add_column(str(idx_name), style="cyan")
80
+ for col in result.columns:
81
+ table.add_column(str(col))
82
+
83
+ for idx, row in result.head(100).iterrows():
84
+ values = [str(idx)]
85
+ values += [_fmt(row[c]) for c in result.columns]
86
+ table.add_row(*values)
87
+
88
+ if len(result) > 100:
89
+ table.caption = f"Showing 100 of {len(result)} rows"
90
+ console.print(table)
91
+ return
92
+
93
+ if output_path:
94
+ with open(output_path, "w") as f:
95
+ f.write(text)
96
+ typer.echo(f"Written to {output_path}")
97
+ else:
98
+ typer.echo(text)
99
+ else:
100
+ typer.echo(result)
@@ -0,0 +1,56 @@
1
+ """ENTSO-E CLI — main Typer application."""
2
+
3
+ from __future__ import annotations
4
+
5
+ from typing import Optional
6
+
7
+ import typer
8
+
9
+ from entsoe.cli.config import get_api_key
10
+
11
+ app = typer.Typer(
12
+ name="entsoe",
13
+ help="CLI for the ENTSO-E Transparency Platform (European electricity market data).",
14
+ no_args_is_help=True,
15
+ )
16
+
17
+
18
+ def get_client(api_key: str | None = None):
19
+ """Lazy import + construct client."""
20
+ from entsoe.client import Client
21
+
22
+ resolved = api_key or get_api_key()
23
+ if not resolved:
24
+ typer.echo(
25
+ "Error: No API key. Set ENTSOE_API_KEY or run: entsoe config set api-key <KEY>",
26
+ err=True,
27
+ )
28
+ raise typer.Exit(1)
29
+ return Client(api_key=resolved)
30
+
31
+
32
+ # -- Register sub-commands ---------------------------------------------------
33
+
34
+ from entsoe.cli.prices import prices_app # noqa: E402
35
+ from entsoe.cli.load import load_app # noqa: E402
36
+ from entsoe.cli.generation import generation_app # noqa: E402
37
+ from entsoe.cli.transmission import transmission_app # noqa: E402
38
+ from entsoe.cli.balancing import balancing_app # noqa: E402
39
+ from entsoe.cli.exec_cmd import exec_app # noqa: E402
40
+ from entsoe.cli.config_cmd import config_app # noqa: E402
41
+
42
+ app.add_typer(prices_app, name="prices", help="Day-ahead electricity prices")
43
+ app.add_typer(load_app, name="load", help="Actual load and load forecasts")
44
+ app.add_typer(generation_app, name="generation", help="Generation data (actual, forecast, capacity, per-plant)")
45
+ app.add_typer(transmission_app, name="transmission", help="Cross-border flows and exchanges")
46
+ app.add_typer(balancing_app, name="balancing", help="Imbalance prices and volumes")
47
+ app.add_typer(exec_app, name="exec", help="Fetch data and evaluate a pandas expression")
48
+ app.add_typer(config_app, name="config", help="Configuration management")
49
+
50
+
51
+ def main() -> None:
52
+ app()
53
+
54
+
55
+ if __name__ == "__main__":
56
+ main()
@@ -0,0 +1,47 @@
1
+ """CLI subcommands for balancing namespace."""
2
+
3
+ from __future__ import annotations
4
+
5
+ from typing import Optional
6
+
7
+ import typer
8
+
9
+ from entsoe.cli._output import output
10
+
11
+ balancing_app = typer.Typer(no_args_is_help=True)
12
+
13
+
14
+ @balancing_app.command("prices")
15
+ def prices(
16
+ start: str = typer.Option(..., "--start", "-s", help="Start date (YYYY-MM-DD)"),
17
+ end: str = typer.Option(..., "--end", "-e", help="End date (YYYY-MM-DD)"),
18
+ country: list[str] = typer.Option(..., "--country", "-c", help="Country code (e.g. FR, NL). Repeat for multiple."),
19
+ format: str = typer.Option("table", "--format", "-f", help="Output format: table, csv, json"),
20
+ output_path: Optional[str] = typer.Option(None, "--output", "-o", help="Output file path"),
21
+ api_key: Optional[str] = typer.Option(None, "--api-key", help="ENTSO-E API key"),
22
+ ):
23
+ """Query imbalance prices."""
24
+ from entsoe.cli.app import get_client
25
+
26
+ client = get_client(api_key)
27
+ countries = country[0] if len(country) == 1 else country
28
+ df = client.balancing.imbalance_prices(start, end, country=countries)
29
+ output(df, format, title="Imbalance Prices", output_path=output_path)
30
+
31
+
32
+ @balancing_app.command("volumes")
33
+ def volumes(
34
+ start: str = typer.Option(..., "--start", "-s", help="Start date (YYYY-MM-DD)"),
35
+ end: str = typer.Option(..., "--end", "-e", help="End date (YYYY-MM-DD)"),
36
+ country: list[str] = typer.Option(..., "--country", "-c", help="Country code (e.g. FR, NL). Repeat for multiple."),
37
+ format: str = typer.Option("table", "--format", "-f", help="Output format: table, csv, json"),
38
+ output_path: Optional[str] = typer.Option(None, "--output", "-o", help="Output file path"),
39
+ api_key: Optional[str] = typer.Option(None, "--api-key", help="ENTSO-E API key"),
40
+ ):
41
+ """Query imbalance volumes."""
42
+ from entsoe.cli.app import get_client
43
+
44
+ client = get_client(api_key)
45
+ countries = country[0] if len(country) == 1 else country
46
+ df = client.balancing.imbalance_volumes(start, end, country=countries)
47
+ output(df, format, title="Imbalance Volumes", output_path=output_path)
@@ -0,0 +1,63 @@
1
+ """Configuration management — read/write config.toml and env vars."""
2
+
3
+ from __future__ import annotations
4
+
5
+ import os
6
+ from pathlib import Path
7
+
8
+ CONFIG_DIR = Path.home() / ".config" / "entsoe"
9
+ CONFIG_FILE = CONFIG_DIR / "config.toml"
10
+
11
+
12
+ def get_api_key() -> str | None:
13
+ """Resolve API key: config file > env var."""
14
+ # Try config file first
15
+ if CONFIG_FILE.exists():
16
+ try:
17
+ import tomllib
18
+ except ImportError:
19
+ import tomli as tomllib # type: ignore[no-redef]
20
+
21
+ with open(CONFIG_FILE, "rb") as f:
22
+ config = tomllib.load(f)
23
+ key = config.get("api-key")
24
+ if key:
25
+ return key
26
+
27
+ # Fall back to environment variable
28
+ return os.getenv("ENTSOE_API_KEY")
29
+
30
+
31
+ def set_config(key: str, value: str) -> None:
32
+ """Write a key-value pair to config.toml."""
33
+ CONFIG_DIR.mkdir(parents=True, exist_ok=True)
34
+
35
+ config: dict = {}
36
+ if CONFIG_FILE.exists():
37
+ try:
38
+ import tomllib
39
+ except ImportError:
40
+ import tomli as tomllib # type: ignore[no-redef]
41
+
42
+ with open(CONFIG_FILE, "rb") as f:
43
+ config = tomllib.load(f)
44
+
45
+ config[key] = value
46
+
47
+ # Write as simple TOML
48
+ lines = [f'{k} = "{v}"' for k, v in config.items()]
49
+ CONFIG_FILE.write_text("\n".join(lines) + "\n")
50
+
51
+
52
+ def get_config(key: str) -> str | None:
53
+ """Read a value from config.toml."""
54
+ if not CONFIG_FILE.exists():
55
+ return None
56
+ try:
57
+ import tomllib
58
+ except ImportError:
59
+ import tomli as tomllib # type: ignore[no-redef]
60
+
61
+ with open(CONFIG_FILE, "rb") as f:
62
+ config = tomllib.load(f)
63
+ return config.get(key)
@@ -0,0 +1,34 @@
1
+ """CLI subcommands for configuration management."""
2
+
3
+ from __future__ import annotations
4
+
5
+ import typer
6
+
7
+ config_app = typer.Typer(no_args_is_help=True)
8
+
9
+
10
+ @config_app.command("set")
11
+ def config_set(
12
+ key: str = typer.Argument(..., help="Configuration key (e.g., 'api-key')"),
13
+ value: str = typer.Argument(..., help="Configuration value"),
14
+ ):
15
+ """Set a configuration value."""
16
+ from entsoe.cli.config import set_config
17
+
18
+ set_config(key, value)
19
+ typer.echo(f"Set {key} = {'***' if 'key' in key.lower() else value}")
20
+
21
+
22
+ @config_app.command("get")
23
+ def config_get(
24
+ key: str = typer.Argument(..., help="Configuration key to read"),
25
+ ):
26
+ """Get a configuration value."""
27
+ from entsoe.cli.config import get_config
28
+
29
+ val = get_config(key)
30
+ if val is None:
31
+ typer.echo(f"{key}: (not set)")
32
+ else:
33
+ display = "***" if "key" in key.lower() else val
34
+ typer.echo(f"{key} = {display}")