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.
- python_entsoe-0.4.0/.github/workflows/publish.yml +28 -0
- {python_entsoe-0.3.0 → python_entsoe-0.4.0}/PKG-INFO +7 -2
- {python_entsoe-0.3.0 → python_entsoe-0.4.0}/pyproject.toml +10 -2
- python_entsoe-0.4.0/skills/entsoe/SKILL.md +202 -0
- {python_entsoe-0.3.0 → python_entsoe-0.4.0}/src/entsoe/__init__.py +1 -1
- {python_entsoe-0.3.0 → python_entsoe-0.4.0}/src/entsoe/_http.py +2 -0
- {python_entsoe-0.3.0 → python_entsoe-0.4.0}/src/entsoe/_mappings.py +2 -0
- {python_entsoe-0.3.0 → python_entsoe-0.4.0}/src/entsoe/_xml.py +2 -0
- python_entsoe-0.4.0/src/entsoe/cli/__init__.py +5 -0
- python_entsoe-0.4.0/src/entsoe/cli/_output.py +100 -0
- python_entsoe-0.4.0/src/entsoe/cli/app.py +56 -0
- python_entsoe-0.4.0/src/entsoe/cli/balancing.py +47 -0
- python_entsoe-0.4.0/src/entsoe/cli/config.py +63 -0
- python_entsoe-0.4.0/src/entsoe/cli/config_cmd.py +34 -0
- python_entsoe-0.4.0/src/entsoe/cli/exec_cmd.py +113 -0
- python_entsoe-0.4.0/src/entsoe/cli/generation.py +94 -0
- python_entsoe-0.4.0/src/entsoe/cli/load.py +47 -0
- python_entsoe-0.4.0/src/entsoe/cli/prices.py +29 -0
- python_entsoe-0.4.0/src/entsoe/cli/transmission.py +75 -0
- {python_entsoe-0.3.0 → python_entsoe-0.4.0}/src/entsoe/exceptions.py +2 -0
- {python_entsoe-0.3.0 → python_entsoe-0.4.0}/src/entsoe/namespaces/_base.py +48 -2
- {python_entsoe-0.3.0 → python_entsoe-0.4.0}/src/entsoe/namespaces/balancing.py +23 -17
- python_entsoe-0.4.0/src/entsoe/namespaces/generation.py +154 -0
- {python_entsoe-0.3.0 → python_entsoe-0.4.0}/src/entsoe/namespaces/load.py +25 -17
- {python_entsoe-0.3.0 → python_entsoe-0.4.0}/src/entsoe/namespaces/prices.py +15 -11
- python_entsoe-0.4.0/src/entsoe/namespaces/transmission.py +131 -0
- python_entsoe-0.4.0/tests/test_multi.py +128 -0
- {python_entsoe-0.3.0 → python_entsoe-0.4.0}/uv.lock +693 -24
- python_entsoe-0.3.0/.github/workflows/publish.yml +0 -23
- python_entsoe-0.3.0/src/entsoe/namespaces/generation.py +0 -129
- python_entsoe-0.3.0/src/entsoe/namespaces/transmission.py +0 -93
- {python_entsoe-0.3.0 → python_entsoe-0.4.0}/.gitignore +0 -0
- {python_entsoe-0.3.0 → python_entsoe-0.4.0}/.python-version +0 -0
- {python_entsoe-0.3.0 → python_entsoe-0.4.0}/CLAUDE.md +0 -0
- {python_entsoe-0.3.0 → python_entsoe-0.4.0}/README.md +0 -0
- {python_entsoe-0.3.0 → python_entsoe-0.4.0}/docs/data-availability.md +0 -0
- {python_entsoe-0.3.0 → python_entsoe-0.4.0}/examples/balancing.ipynb +0 -0
- {python_entsoe-0.3.0 → python_entsoe-0.4.0}/examples/generation.ipynb +0 -0
- {python_entsoe-0.3.0 → python_entsoe-0.4.0}/examples/generation_per_plant.ipynb +0 -0
- {python_entsoe-0.3.0 → python_entsoe-0.4.0}/examples/load.ipynb +0 -0
- {python_entsoe-0.3.0 → python_entsoe-0.4.0}/examples/prices.ipynb +0 -0
- {python_entsoe-0.3.0 → python_entsoe-0.4.0}/examples/transmission.ipynb +0 -0
- {python_entsoe-0.3.0 → python_entsoe-0.4.0}/scripts/generate_notebooks.py +0 -0
- {python_entsoe-0.3.0 → python_entsoe-0.4.0}/src/entsoe/client.py +0 -0
- {python_entsoe-0.3.0 → python_entsoe-0.4.0}/src/entsoe/namespaces/__init__.py +0 -0
- {python_entsoe-0.3.0 → python_entsoe-0.4.0}/src/entsoe/py.typed +0 -0
- {python_entsoe-0.3.0 → python_entsoe-0.4.0}/tests/__init__.py +0 -0
- {python_entsoe-0.3.0 → python_entsoe-0.4.0}/tests/conftest.py +0 -0
- {python_entsoe-0.3.0 → python_entsoe-0.4.0}/tests/test_balancing.py +0 -0
- {python_entsoe-0.3.0 → python_entsoe-0.4.0}/tests/test_generation.py +0 -0
- {python_entsoe-0.3.0 → python_entsoe-0.4.0}/tests/test_load.py +0 -0
- {python_entsoe-0.3.0 → python_entsoe-0.4.0}/tests/test_prices.py +0 -0
- {python_entsoe-0.3.0 → python_entsoe-0.4.0}/tests/test_transmission.py +0 -0
- {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
|
+
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.
|
|
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
|
+
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.
|
|
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.
|
|
7
|
+
__version__ = "0.4.0"
|
|
8
8
|
__all__ = [
|
|
9
9
|
"Client",
|
|
10
10
|
"COUNTRY_NAMES",
|
|
@@ -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}")
|