elering-py 0.1.1__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.
- elering_py-0.1.1/LICENSE +21 -0
- elering_py-0.1.1/PKG-INFO +296 -0
- elering_py-0.1.1/README.md +274 -0
- elering_py-0.1.1/pyproject.toml +98 -0
- elering_py-0.1.1/pyproject.toml.orig +94 -0
- elering_py-0.1.1/src/elering/__init__.py +77 -0
- elering_py-0.1.1/src/elering/_client.py +180 -0
- elering_py-0.1.1/src/elering/_errors.py +44 -0
- elering_py-0.1.1/src/elering/_http.py +111 -0
- elering_py-0.1.1/src/elering/_params.py +151 -0
- elering_py-0.1.1/src/elering/_resources.py +402 -0
- elering_py-0.1.1/src/elering/py.typed +0 -0
elering_py-0.1.1/LICENSE
ADDED
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2026 Hooman FI
|
|
4
|
+
|
|
5
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
6
|
+
of this software and associated documentation files (the "Software"), to deal
|
|
7
|
+
in the Software without restriction, including without limitation the rights
|
|
8
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
9
|
+
copies of the Software, and to permit persons to whom the Software is
|
|
10
|
+
furnished to do so, subject to the following conditions:
|
|
11
|
+
|
|
12
|
+
The above copyright notice and this permission notice shall be included in all
|
|
13
|
+
copies or substantial portions of the Software.
|
|
14
|
+
|
|
15
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
16
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
17
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
18
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
19
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
20
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
21
|
+
SOFTWARE.
|
|
@@ -0,0 +1,296 @@
|
|
|
1
|
+
Metadata-Version: 2.4
|
|
2
|
+
Name: elering-py
|
|
3
|
+
Version: 0.1.1
|
|
4
|
+
Summary: Python client for the Elering open API
|
|
5
|
+
Keywords: elering,elering-py,py-elering,elering-api,elering API,elering open API,elering dashboard,estonia,energy
|
|
6
|
+
License-Expression: MIT
|
|
7
|
+
License-File: LICENSE
|
|
8
|
+
Classifier: Intended Audience :: Science/Research
|
|
9
|
+
Classifier: Operating System :: OS Independent
|
|
10
|
+
Classifier: Programming Language :: Python :: 3.11
|
|
11
|
+
Classifier: Programming Language :: Python :: 3.12
|
|
12
|
+
Classifier: Programming Language :: Python :: 3.13
|
|
13
|
+
Classifier: Programming Language :: Python :: 3.14
|
|
14
|
+
Classifier: Topic :: Scientific/Engineering
|
|
15
|
+
Classifier: Topic :: Software Development :: Libraries :: Python Modules
|
|
16
|
+
Classifier: Typing :: Typed
|
|
17
|
+
Requires-Python: >=3.11
|
|
18
|
+
Project-URL: Repository, https://github.com/hoofir/elering-py
|
|
19
|
+
Project-URL: Issues, https://github.com/hoofir/elering-py/issues
|
|
20
|
+
Project-URL: Elering dashboard API, https://dashboard.elering.ee/assets/swagger-ui/index.html
|
|
21
|
+
Description-Content-Type: text/markdown
|
|
22
|
+
|
|
23
|
+
# elering-py
|
|
24
|
+
|
|
25
|
+
[](https://pypi.org/project/elering-py/)
|
|
26
|
+
[](https://github.com/hoofir/elering-py/actions/workflows/ci.yml)
|
|
27
|
+
[](LICENSE)
|
|
28
|
+
[](https://github.com/astral-sh/ruff)
|
|
29
|
+
[](https://github.com/microsoft/ty)
|
|
30
|
+
[](https://github.com/fpgmaas/deptry)
|
|
31
|
+
[](https://github.com/pytest-dev/pytest)
|
|
32
|
+
|
|
33
|
+
|
|
34
|
+
A small Python client for the [Elering dashboard open API](https://dashboard.elering.ee/assets/swagger-ui/index.html) — Estonian electricity and gas system data published by Elering AS.
|
|
35
|
+
|
|
36
|
+
- **No dependencies.** Standard library only.
|
|
37
|
+
- **No setup.** The API is public and unauthenticated — `import elering` and go.
|
|
38
|
+
- **Readable calls.** Endpoint groups mirror the API docs, with snake case
|
|
39
|
+
arguments and flexible dates.
|
|
40
|
+
- **Long ranges just work.** The API caps a request at one year; longer queries
|
|
41
|
+
are split into windows and stitched back together for you.
|
|
42
|
+
- **Plain data out.** The `{"success": ..., "data": ...}` envelope is unwrapped
|
|
43
|
+
and every call returns a `list[dict]`, ready for `pandas`, `polars` or `csv`.
|
|
44
|
+
|
|
45
|
+
---
|
|
46
|
+
|
|
47
|
+
## Quickstart
|
|
48
|
+
|
|
49
|
+
### Install
|
|
50
|
+
|
|
51
|
+
```bash
|
|
52
|
+
pip install elering-py
|
|
53
|
+
```
|
|
54
|
+
|
|
55
|
+
Requires Python 3.11 or newer.
|
|
56
|
+
|
|
57
|
+
### Query data
|
|
58
|
+
|
|
59
|
+
```python
|
|
60
|
+
import elering
|
|
61
|
+
|
|
62
|
+
# Nord Pool day-ahead prices for every Baltic area plus Finland
|
|
63
|
+
prices = elering.nps.price(start="2024-01-01", end="2024-01-02")
|
|
64
|
+
|
|
65
|
+
prices.keys()
|
|
66
|
+
# dict_keys(['ee', 'fi', 'lv', 'lt'])
|
|
67
|
+
|
|
68
|
+
prices["ee"][0]
|
|
69
|
+
# {'timestamp': 1704067200, 'price': 28.46}
|
|
70
|
+
|
|
71
|
+
# Estonian power system, right now
|
|
72
|
+
elering.system.latest()
|
|
73
|
+
# [{'timestamp': 1788104700, 'production': 375.25, 'consumption': 780.73,
|
|
74
|
+
# 'losses': None, 'frequency': 50.02, 'system_balance': -405.47,
|
|
75
|
+
# 'ac_balance': 232.37, 'production_renewable': 96.29,
|
|
76
|
+
# 'solar_energy_production': None}]
|
|
77
|
+
```
|
|
78
|
+
|
|
79
|
+
Into a dataframe:
|
|
80
|
+
|
|
81
|
+
```python
|
|
82
|
+
import pandas as pd
|
|
83
|
+
|
|
84
|
+
df = pd.DataFrame(prices["ee"])
|
|
85
|
+
df["time"] = pd.to_datetime(df["timestamp"], unit="s", utc=True)
|
|
86
|
+
```
|
|
87
|
+
|
|
88
|
+
---
|
|
89
|
+
|
|
90
|
+
## Guide
|
|
91
|
+
|
|
92
|
+
### Dates and times
|
|
93
|
+
|
|
94
|
+
`start` and `end` accept a string, a `date` or a `datetime`. Naive values are
|
|
95
|
+
treated as UTC; aware values are converted to UTC. **`end` is inclusive**, so
|
|
96
|
+
`start="2024-01-01", end="2024-01-02"` covers both days' boundary hours.
|
|
97
|
+
|
|
98
|
+
```python
|
|
99
|
+
from datetime import date, datetime
|
|
100
|
+
|
|
101
|
+
elering.system.values(start="2024-01-01", end=date(2024, 2, 1))
|
|
102
|
+
elering.system.values(start=datetime(2024, 1, 1, 6), end="2024-01-01T12:00")
|
|
103
|
+
```
|
|
104
|
+
|
|
105
|
+
Omitting both gives whatever the API considers current — usually the last day or
|
|
106
|
+
two. Passing only one of them raises `ValueError`.
|
|
107
|
+
|
|
108
|
+
```python
|
|
109
|
+
elering.nps.price() # no range: the current window
|
|
110
|
+
```
|
|
111
|
+
|
|
112
|
+
### Timestamps
|
|
113
|
+
|
|
114
|
+
Every row is stamped with `timestamp`, a Unix time in seconds. Rows are returned
|
|
115
|
+
exactly as the API sends them; convert when you need to:
|
|
116
|
+
|
|
117
|
+
```python
|
|
118
|
+
from elering import from_timestamp
|
|
119
|
+
|
|
120
|
+
row = elering.system.latest()[0]
|
|
121
|
+
from_timestamp(row["timestamp"])
|
|
122
|
+
# datetime.datetime(2026, 4, 29, 5, 5, tzinfo=datetime.timezone.utc)
|
|
123
|
+
```
|
|
124
|
+
|
|
125
|
+
### Rows and groups
|
|
126
|
+
|
|
127
|
+
Most methods return a `list[dict]`. Endpoints that publish one series per area
|
|
128
|
+
return a `dict[str, list[dict]]` instead, keyed exactly as the API keys it:
|
|
129
|
+
|
|
130
|
+
| Method | Keys |
|
|
131
|
+
| --- | --- |
|
|
132
|
+
| `nps.price()`, `nps.turnover()` | `ee`, `fi`, `lv`, `lt` |
|
|
133
|
+
| `system.with_plan()` | `real`, `plan` |
|
|
134
|
+
| `transmission.capacity()` | `FI`, `LV`, `RU` |
|
|
135
|
+
| `gas_transmission.cross_border()` | `bc`, `karksi`, `misso`, `narva`, `varska` |
|
|
136
|
+
| `gas_trade.prices()` | `common`, `ee`, `fi`, `lv`, `lt` |
|
|
137
|
+
|
|
138
|
+
Where the API also offers a single-area variant, so does the client:
|
|
139
|
+
|
|
140
|
+
```python
|
|
141
|
+
elering.nps.price(start="2024-01-01", end="2024-01-02")["ee"]
|
|
142
|
+
elering.nps.price_latest("EE")
|
|
143
|
+
|
|
144
|
+
elering.transmission.capacity(start="2024-01-01", end="2024-01-02")["FI"]
|
|
145
|
+
elering.transmission.capacity_for("FI", start="2024-01-01", end="2024-01-02")
|
|
146
|
+
```
|
|
147
|
+
|
|
148
|
+
Area codes are case-insensitive and validated locally, so a typo raises
|
|
149
|
+
`ValueError` instead of costing a round trip.
|
|
150
|
+
|
|
151
|
+
### Long time ranges
|
|
152
|
+
|
|
153
|
+
The API rejects any request spanning more than a year with
|
|
154
|
+
`"Maximum period is 1 year"`. Ranges longer than `max_window` (default 365 days)
|
|
155
|
+
are therefore split into consecutive requests and concatenated in order. Because
|
|
156
|
+
`end` is inclusive, consecutive windows share one row; the duplicate is dropped
|
|
157
|
+
by `timestamp`, so the result is the same series you would get from a single
|
|
158
|
+
request:
|
|
159
|
+
|
|
160
|
+
```python
|
|
161
|
+
# transparently issued as several requests
|
|
162
|
+
rows = elering.transmission.cross_border_hourly(start="2015-01-01", end="2024-01-01")
|
|
163
|
+
```
|
|
164
|
+
|
|
165
|
+
### Urgent market messages
|
|
166
|
+
|
|
167
|
+
`umm.messages()` is the one paginated endpoint. It walks every page by default;
|
|
168
|
+
pass `page=` to fetch just one:
|
|
169
|
+
|
|
170
|
+
```python
|
|
171
|
+
elering.umm.messages(event_status="active", unavailability_type="planned")
|
|
172
|
+
elering.umm.messages(page=1)
|
|
173
|
+
|
|
174
|
+
# every message published for one event
|
|
175
|
+
elering.umm.event(2189)
|
|
176
|
+
```
|
|
177
|
+
|
|
178
|
+
### Errors
|
|
179
|
+
|
|
180
|
+
An empty result is returned as `[]` (or `{}`), not an error. Everything else
|
|
181
|
+
raises a subclass of `elering.EleringError`:
|
|
182
|
+
|
|
183
|
+
| Exception | Raised when |
|
|
184
|
+
| --- | --- |
|
|
185
|
+
| `EleringBadRequest` | HTTP 4xx — bad parameters. Exposes `.status` and `.messages` |
|
|
186
|
+
| `EleringServerError` | HTTP 5xx, after retries are exhausted |
|
|
187
|
+
| `EleringTransportError` | Network failure or timeout |
|
|
188
|
+
|
|
189
|
+
```python
|
|
190
|
+
try:
|
|
191
|
+
elering.Client(max_window=None).nps.price(start="2020-01-01", end="2024-01-01")
|
|
192
|
+
except elering.EleringBadRequest as exc:
|
|
193
|
+
print(exc.status) # 400
|
|
194
|
+
print(exc.messages) # ['Maximum period is 1 year']
|
|
195
|
+
```
|
|
196
|
+
|
|
197
|
+
Invalid arguments (an unknown area, a lone `start`, an unparseable date) raise
|
|
198
|
+
`ValueError` before any request is made.
|
|
199
|
+
|
|
200
|
+
### Configuring a client
|
|
201
|
+
|
|
202
|
+
The module-level helpers use a shared default client. Create your own to change
|
|
203
|
+
its behaviour:
|
|
204
|
+
|
|
205
|
+
```python
|
|
206
|
+
from datetime import timedelta
|
|
207
|
+
|
|
208
|
+
with elering.Client(timeout=120, retries=5, max_window=timedelta(days=90)) as client:
|
|
209
|
+
rows = client.gas_system.values(start="2024-01-01", end="2024-04-01")
|
|
210
|
+
```
|
|
211
|
+
|
|
212
|
+
| Argument | Default | Purpose |
|
|
213
|
+
| --- | --- | --- |
|
|
214
|
+
| `base_url` | `https://dashboard.elering.ee` | API root; must be http or https |
|
|
215
|
+
| `timeout` | `60.0` | Per-request socket timeout in seconds |
|
|
216
|
+
| `retries` | `3` | Extra attempts on transport errors and 5xx |
|
|
217
|
+
| `backoff` | `0.5` | Base delay for exponential retry backoff |
|
|
218
|
+
| `max_window` | `365 days` | Longest span per request; `None` disables splitting |
|
|
219
|
+
|
|
220
|
+
---
|
|
221
|
+
|
|
222
|
+
## Endpoints
|
|
223
|
+
|
|
224
|
+
| API group | Attribute | Methods |
|
|
225
|
+
| --- | --- | --- |
|
|
226
|
+
| Nord Pool | `nps` | `price()`, `price_latest()`, `price_current()`, `turnover()`, `turnover_latest()` |
|
|
227
|
+
| Power system | `system` | `values()`, `latest()`, `with_plan()` |
|
|
228
|
+
| Balance | `balance` | `balancing()`, `physical()`, `physical_latest()`, `commercial()`, `commercial_latest()` |
|
|
229
|
+
| Transmission | `transmission` | `cross_border()`, `cross_border_latest()`, `cross_border_hourly()`, `planned_trade()`, `planned_trade_latest()`, `capacity()`, `capacity_for()` |
|
|
230
|
+
| Gas system | `gas_system` | `values()`, `values_m3()`, `latest()`, `calorific_value()`, `calorific_value_25_0()` |
|
|
231
|
+
| Gas transmission | `gas_transmission` | `cross_border()`, `cross_border_latest()` |
|
|
232
|
+
| GET Baltic | `gas_trade` | `prices()`, `latest()` |
|
|
233
|
+
| Gas balance | `gas_balance` | `price()` |
|
|
234
|
+
| Gas border trade | `gas_border_trade` | `current()` |
|
|
235
|
+
| Gas capacity | `capacity` | `firm()`, `interruptible()` |
|
|
236
|
+
| Gas nominations | `nominations` | `values()`, `renominations()` |
|
|
237
|
+
| Green certificates | `green` | `certificates()` |
|
|
238
|
+
| Urgent market messages | `umm` | `messages()`, `event()`, `message()` |
|
|
239
|
+
|
|
240
|
+
Each attribute is available both on a `Client` instance and at module level
|
|
241
|
+
(`elering.nps.price(...)`).
|
|
242
|
+
|
|
243
|
+
### Not covered
|
|
244
|
+
|
|
245
|
+
- **CSV endpoints.** Every `/csv` path serves the same data as its JSON sibling,
|
|
246
|
+
so the client exposes the JSON one and leaves formatting to you.
|
|
247
|
+
- **RSS feeds.** `/umm/gas/rss` is XML; the same messages are available as data
|
|
248
|
+
through `umm.messages()`.
|
|
249
|
+
|
|
250
|
+
---
|
|
251
|
+
|
|
252
|
+
## Contributing
|
|
253
|
+
|
|
254
|
+
```bash
|
|
255
|
+
make setup # create the venv and install dev dependencies
|
|
256
|
+
make check # ruff lint, format check, ty type check, deptry
|
|
257
|
+
make test # fast offline tests
|
|
258
|
+
make test-live # end-to-end tests against the real API
|
|
259
|
+
```
|
|
260
|
+
|
|
261
|
+
`make test` never touches the network. The live suite is deselected by default
|
|
262
|
+
and exercises every endpoint group, the windowing logic and the response shapes.
|
|
263
|
+
|
|
264
|
+
### Keeping up with the API
|
|
265
|
+
|
|
266
|
+
The endpoint methods are hand-written. The published spec declares every schema
|
|
267
|
+
as an empty object, so it documents paths and parameters but says nothing about
|
|
268
|
+
the data — the response shapes here were verified against the live API.
|
|
269
|
+
|
|
270
|
+
```bash
|
|
271
|
+
make spec-check # fail if the published spec differs from spec/openapi.json
|
|
272
|
+
make spec # refresh the vendored spec
|
|
273
|
+
```
|
|
274
|
+
|
|
275
|
+
CI runs `make spec-check` weekly, so spec changes show up as a reviewable diff.
|
|
276
|
+
Adding a new endpoint is then a few lines in `src/elering/_resources.py`.
|
|
277
|
+
|
|
278
|
+
### Known spec defects
|
|
279
|
+
|
|
280
|
+
Worked around in the hand-written layer, each verified against the live API:
|
|
281
|
+
|
|
282
|
+
- Responses are wrapped in `{"success": ..., "data": ...}`, which no schema
|
|
283
|
+
mentions. The client unwraps it.
|
|
284
|
+
- `/api/balance/total` documents its first parameter as `fields`; it is `start`.
|
|
285
|
+
- `/api/umm/single/{id}` also requires `id` as a query parameter, and returns
|
|
286
|
+
HTTP 500 without it.
|
|
287
|
+
- `/api/umm/gas/messages` documents its parameter as `id`; it is `event_id`.
|
|
288
|
+
- `start` and `end` are documented as accepting `yyyy-MM-dd HH:mm`; only
|
|
289
|
+
offset-bearing ISO 8601 (`2024-01-01T00:00:00Z`) is actually parsed.
|
|
290
|
+
- The one-year limit and the inclusive `end` are undocumented.
|
|
291
|
+
|
|
292
|
+
---
|
|
293
|
+
|
|
294
|
+
## License
|
|
295
|
+
|
|
296
|
+
MIT — see [LICENSE](LICENSE). This project is not affiliated with Elering AS.
|
|
@@ -0,0 +1,274 @@
|
|
|
1
|
+
# elering-py
|
|
2
|
+
|
|
3
|
+
[](https://pypi.org/project/elering-py/)
|
|
4
|
+
[](https://github.com/hoofir/elering-py/actions/workflows/ci.yml)
|
|
5
|
+
[](LICENSE)
|
|
6
|
+
[](https://github.com/astral-sh/ruff)
|
|
7
|
+
[](https://github.com/microsoft/ty)
|
|
8
|
+
[](https://github.com/fpgmaas/deptry)
|
|
9
|
+
[](https://github.com/pytest-dev/pytest)
|
|
10
|
+
|
|
11
|
+
|
|
12
|
+
A small Python client for the [Elering dashboard open API](https://dashboard.elering.ee/assets/swagger-ui/index.html) — Estonian electricity and gas system data published by Elering AS.
|
|
13
|
+
|
|
14
|
+
- **No dependencies.** Standard library only.
|
|
15
|
+
- **No setup.** The API is public and unauthenticated — `import elering` and go.
|
|
16
|
+
- **Readable calls.** Endpoint groups mirror the API docs, with snake case
|
|
17
|
+
arguments and flexible dates.
|
|
18
|
+
- **Long ranges just work.** The API caps a request at one year; longer queries
|
|
19
|
+
are split into windows and stitched back together for you.
|
|
20
|
+
- **Plain data out.** The `{"success": ..., "data": ...}` envelope is unwrapped
|
|
21
|
+
and every call returns a `list[dict]`, ready for `pandas`, `polars` or `csv`.
|
|
22
|
+
|
|
23
|
+
---
|
|
24
|
+
|
|
25
|
+
## Quickstart
|
|
26
|
+
|
|
27
|
+
### Install
|
|
28
|
+
|
|
29
|
+
```bash
|
|
30
|
+
pip install elering-py
|
|
31
|
+
```
|
|
32
|
+
|
|
33
|
+
Requires Python 3.11 or newer.
|
|
34
|
+
|
|
35
|
+
### Query data
|
|
36
|
+
|
|
37
|
+
```python
|
|
38
|
+
import elering
|
|
39
|
+
|
|
40
|
+
# Nord Pool day-ahead prices for every Baltic area plus Finland
|
|
41
|
+
prices = elering.nps.price(start="2024-01-01", end="2024-01-02")
|
|
42
|
+
|
|
43
|
+
prices.keys()
|
|
44
|
+
# dict_keys(['ee', 'fi', 'lv', 'lt'])
|
|
45
|
+
|
|
46
|
+
prices["ee"][0]
|
|
47
|
+
# {'timestamp': 1704067200, 'price': 28.46}
|
|
48
|
+
|
|
49
|
+
# Estonian power system, right now
|
|
50
|
+
elering.system.latest()
|
|
51
|
+
# [{'timestamp': 1788104700, 'production': 375.25, 'consumption': 780.73,
|
|
52
|
+
# 'losses': None, 'frequency': 50.02, 'system_balance': -405.47,
|
|
53
|
+
# 'ac_balance': 232.37, 'production_renewable': 96.29,
|
|
54
|
+
# 'solar_energy_production': None}]
|
|
55
|
+
```
|
|
56
|
+
|
|
57
|
+
Into a dataframe:
|
|
58
|
+
|
|
59
|
+
```python
|
|
60
|
+
import pandas as pd
|
|
61
|
+
|
|
62
|
+
df = pd.DataFrame(prices["ee"])
|
|
63
|
+
df["time"] = pd.to_datetime(df["timestamp"], unit="s", utc=True)
|
|
64
|
+
```
|
|
65
|
+
|
|
66
|
+
---
|
|
67
|
+
|
|
68
|
+
## Guide
|
|
69
|
+
|
|
70
|
+
### Dates and times
|
|
71
|
+
|
|
72
|
+
`start` and `end` accept a string, a `date` or a `datetime`. Naive values are
|
|
73
|
+
treated as UTC; aware values are converted to UTC. **`end` is inclusive**, so
|
|
74
|
+
`start="2024-01-01", end="2024-01-02"` covers both days' boundary hours.
|
|
75
|
+
|
|
76
|
+
```python
|
|
77
|
+
from datetime import date, datetime
|
|
78
|
+
|
|
79
|
+
elering.system.values(start="2024-01-01", end=date(2024, 2, 1))
|
|
80
|
+
elering.system.values(start=datetime(2024, 1, 1, 6), end="2024-01-01T12:00")
|
|
81
|
+
```
|
|
82
|
+
|
|
83
|
+
Omitting both gives whatever the API considers current — usually the last day or
|
|
84
|
+
two. Passing only one of them raises `ValueError`.
|
|
85
|
+
|
|
86
|
+
```python
|
|
87
|
+
elering.nps.price() # no range: the current window
|
|
88
|
+
```
|
|
89
|
+
|
|
90
|
+
### Timestamps
|
|
91
|
+
|
|
92
|
+
Every row is stamped with `timestamp`, a Unix time in seconds. Rows are returned
|
|
93
|
+
exactly as the API sends them; convert when you need to:
|
|
94
|
+
|
|
95
|
+
```python
|
|
96
|
+
from elering import from_timestamp
|
|
97
|
+
|
|
98
|
+
row = elering.system.latest()[0]
|
|
99
|
+
from_timestamp(row["timestamp"])
|
|
100
|
+
# datetime.datetime(2026, 4, 29, 5, 5, tzinfo=datetime.timezone.utc)
|
|
101
|
+
```
|
|
102
|
+
|
|
103
|
+
### Rows and groups
|
|
104
|
+
|
|
105
|
+
Most methods return a `list[dict]`. Endpoints that publish one series per area
|
|
106
|
+
return a `dict[str, list[dict]]` instead, keyed exactly as the API keys it:
|
|
107
|
+
|
|
108
|
+
| Method | Keys |
|
|
109
|
+
| --- | --- |
|
|
110
|
+
| `nps.price()`, `nps.turnover()` | `ee`, `fi`, `lv`, `lt` |
|
|
111
|
+
| `system.with_plan()` | `real`, `plan` |
|
|
112
|
+
| `transmission.capacity()` | `FI`, `LV`, `RU` |
|
|
113
|
+
| `gas_transmission.cross_border()` | `bc`, `karksi`, `misso`, `narva`, `varska` |
|
|
114
|
+
| `gas_trade.prices()` | `common`, `ee`, `fi`, `lv`, `lt` |
|
|
115
|
+
|
|
116
|
+
Where the API also offers a single-area variant, so does the client:
|
|
117
|
+
|
|
118
|
+
```python
|
|
119
|
+
elering.nps.price(start="2024-01-01", end="2024-01-02")["ee"]
|
|
120
|
+
elering.nps.price_latest("EE")
|
|
121
|
+
|
|
122
|
+
elering.transmission.capacity(start="2024-01-01", end="2024-01-02")["FI"]
|
|
123
|
+
elering.transmission.capacity_for("FI", start="2024-01-01", end="2024-01-02")
|
|
124
|
+
```
|
|
125
|
+
|
|
126
|
+
Area codes are case-insensitive and validated locally, so a typo raises
|
|
127
|
+
`ValueError` instead of costing a round trip.
|
|
128
|
+
|
|
129
|
+
### Long time ranges
|
|
130
|
+
|
|
131
|
+
The API rejects any request spanning more than a year with
|
|
132
|
+
`"Maximum period is 1 year"`. Ranges longer than `max_window` (default 365 days)
|
|
133
|
+
are therefore split into consecutive requests and concatenated in order. Because
|
|
134
|
+
`end` is inclusive, consecutive windows share one row; the duplicate is dropped
|
|
135
|
+
by `timestamp`, so the result is the same series you would get from a single
|
|
136
|
+
request:
|
|
137
|
+
|
|
138
|
+
```python
|
|
139
|
+
# transparently issued as several requests
|
|
140
|
+
rows = elering.transmission.cross_border_hourly(start="2015-01-01", end="2024-01-01")
|
|
141
|
+
```
|
|
142
|
+
|
|
143
|
+
### Urgent market messages
|
|
144
|
+
|
|
145
|
+
`umm.messages()` is the one paginated endpoint. It walks every page by default;
|
|
146
|
+
pass `page=` to fetch just one:
|
|
147
|
+
|
|
148
|
+
```python
|
|
149
|
+
elering.umm.messages(event_status="active", unavailability_type="planned")
|
|
150
|
+
elering.umm.messages(page=1)
|
|
151
|
+
|
|
152
|
+
# every message published for one event
|
|
153
|
+
elering.umm.event(2189)
|
|
154
|
+
```
|
|
155
|
+
|
|
156
|
+
### Errors
|
|
157
|
+
|
|
158
|
+
An empty result is returned as `[]` (or `{}`), not an error. Everything else
|
|
159
|
+
raises a subclass of `elering.EleringError`:
|
|
160
|
+
|
|
161
|
+
| Exception | Raised when |
|
|
162
|
+
| --- | --- |
|
|
163
|
+
| `EleringBadRequest` | HTTP 4xx — bad parameters. Exposes `.status` and `.messages` |
|
|
164
|
+
| `EleringServerError` | HTTP 5xx, after retries are exhausted |
|
|
165
|
+
| `EleringTransportError` | Network failure or timeout |
|
|
166
|
+
|
|
167
|
+
```python
|
|
168
|
+
try:
|
|
169
|
+
elering.Client(max_window=None).nps.price(start="2020-01-01", end="2024-01-01")
|
|
170
|
+
except elering.EleringBadRequest as exc:
|
|
171
|
+
print(exc.status) # 400
|
|
172
|
+
print(exc.messages) # ['Maximum period is 1 year']
|
|
173
|
+
```
|
|
174
|
+
|
|
175
|
+
Invalid arguments (an unknown area, a lone `start`, an unparseable date) raise
|
|
176
|
+
`ValueError` before any request is made.
|
|
177
|
+
|
|
178
|
+
### Configuring a client
|
|
179
|
+
|
|
180
|
+
The module-level helpers use a shared default client. Create your own to change
|
|
181
|
+
its behaviour:
|
|
182
|
+
|
|
183
|
+
```python
|
|
184
|
+
from datetime import timedelta
|
|
185
|
+
|
|
186
|
+
with elering.Client(timeout=120, retries=5, max_window=timedelta(days=90)) as client:
|
|
187
|
+
rows = client.gas_system.values(start="2024-01-01", end="2024-04-01")
|
|
188
|
+
```
|
|
189
|
+
|
|
190
|
+
| Argument | Default | Purpose |
|
|
191
|
+
| --- | --- | --- |
|
|
192
|
+
| `base_url` | `https://dashboard.elering.ee` | API root; must be http or https |
|
|
193
|
+
| `timeout` | `60.0` | Per-request socket timeout in seconds |
|
|
194
|
+
| `retries` | `3` | Extra attempts on transport errors and 5xx |
|
|
195
|
+
| `backoff` | `0.5` | Base delay for exponential retry backoff |
|
|
196
|
+
| `max_window` | `365 days` | Longest span per request; `None` disables splitting |
|
|
197
|
+
|
|
198
|
+
---
|
|
199
|
+
|
|
200
|
+
## Endpoints
|
|
201
|
+
|
|
202
|
+
| API group | Attribute | Methods |
|
|
203
|
+
| --- | --- | --- |
|
|
204
|
+
| Nord Pool | `nps` | `price()`, `price_latest()`, `price_current()`, `turnover()`, `turnover_latest()` |
|
|
205
|
+
| Power system | `system` | `values()`, `latest()`, `with_plan()` |
|
|
206
|
+
| Balance | `balance` | `balancing()`, `physical()`, `physical_latest()`, `commercial()`, `commercial_latest()` |
|
|
207
|
+
| Transmission | `transmission` | `cross_border()`, `cross_border_latest()`, `cross_border_hourly()`, `planned_trade()`, `planned_trade_latest()`, `capacity()`, `capacity_for()` |
|
|
208
|
+
| Gas system | `gas_system` | `values()`, `values_m3()`, `latest()`, `calorific_value()`, `calorific_value_25_0()` |
|
|
209
|
+
| Gas transmission | `gas_transmission` | `cross_border()`, `cross_border_latest()` |
|
|
210
|
+
| GET Baltic | `gas_trade` | `prices()`, `latest()` |
|
|
211
|
+
| Gas balance | `gas_balance` | `price()` |
|
|
212
|
+
| Gas border trade | `gas_border_trade` | `current()` |
|
|
213
|
+
| Gas capacity | `capacity` | `firm()`, `interruptible()` |
|
|
214
|
+
| Gas nominations | `nominations` | `values()`, `renominations()` |
|
|
215
|
+
| Green certificates | `green` | `certificates()` |
|
|
216
|
+
| Urgent market messages | `umm` | `messages()`, `event()`, `message()` |
|
|
217
|
+
|
|
218
|
+
Each attribute is available both on a `Client` instance and at module level
|
|
219
|
+
(`elering.nps.price(...)`).
|
|
220
|
+
|
|
221
|
+
### Not covered
|
|
222
|
+
|
|
223
|
+
- **CSV endpoints.** Every `/csv` path serves the same data as its JSON sibling,
|
|
224
|
+
so the client exposes the JSON one and leaves formatting to you.
|
|
225
|
+
- **RSS feeds.** `/umm/gas/rss` is XML; the same messages are available as data
|
|
226
|
+
through `umm.messages()`.
|
|
227
|
+
|
|
228
|
+
---
|
|
229
|
+
|
|
230
|
+
## Contributing
|
|
231
|
+
|
|
232
|
+
```bash
|
|
233
|
+
make setup # create the venv and install dev dependencies
|
|
234
|
+
make check # ruff lint, format check, ty type check, deptry
|
|
235
|
+
make test # fast offline tests
|
|
236
|
+
make test-live # end-to-end tests against the real API
|
|
237
|
+
```
|
|
238
|
+
|
|
239
|
+
`make test` never touches the network. The live suite is deselected by default
|
|
240
|
+
and exercises every endpoint group, the windowing logic and the response shapes.
|
|
241
|
+
|
|
242
|
+
### Keeping up with the API
|
|
243
|
+
|
|
244
|
+
The endpoint methods are hand-written. The published spec declares every schema
|
|
245
|
+
as an empty object, so it documents paths and parameters but says nothing about
|
|
246
|
+
the data — the response shapes here were verified against the live API.
|
|
247
|
+
|
|
248
|
+
```bash
|
|
249
|
+
make spec-check # fail if the published spec differs from spec/openapi.json
|
|
250
|
+
make spec # refresh the vendored spec
|
|
251
|
+
```
|
|
252
|
+
|
|
253
|
+
CI runs `make spec-check` weekly, so spec changes show up as a reviewable diff.
|
|
254
|
+
Adding a new endpoint is then a few lines in `src/elering/_resources.py`.
|
|
255
|
+
|
|
256
|
+
### Known spec defects
|
|
257
|
+
|
|
258
|
+
Worked around in the hand-written layer, each verified against the live API:
|
|
259
|
+
|
|
260
|
+
- Responses are wrapped in `{"success": ..., "data": ...}`, which no schema
|
|
261
|
+
mentions. The client unwraps it.
|
|
262
|
+
- `/api/balance/total` documents its first parameter as `fields`; it is `start`.
|
|
263
|
+
- `/api/umm/single/{id}` also requires `id` as a query parameter, and returns
|
|
264
|
+
HTTP 500 without it.
|
|
265
|
+
- `/api/umm/gas/messages` documents its parameter as `id`; it is `event_id`.
|
|
266
|
+
- `start` and `end` are documented as accepting `yyyy-MM-dd HH:mm`; only
|
|
267
|
+
offset-bearing ISO 8601 (`2024-01-01T00:00:00Z`) is actually parsed.
|
|
268
|
+
- The one-year limit and the inclusive `end` are undocumented.
|
|
269
|
+
|
|
270
|
+
---
|
|
271
|
+
|
|
272
|
+
## License
|
|
273
|
+
|
|
274
|
+
MIT — see [LICENSE](LICENSE). This project is not affiliated with Elering AS.
|
|
@@ -0,0 +1,98 @@
|
|
|
1
|
+
[project]
|
|
2
|
+
name = "elering-py"
|
|
3
|
+
version = "0.1.1"
|
|
4
|
+
description = "Python client for the Elering open API"
|
|
5
|
+
readme = "README.md"
|
|
6
|
+
license = "MIT"
|
|
7
|
+
license-files = ["LICENSE"]
|
|
8
|
+
keywords = [
|
|
9
|
+
"elering",
|
|
10
|
+
"elering-py",
|
|
11
|
+
"py-elering",
|
|
12
|
+
"elering-api",
|
|
13
|
+
"elering API",
|
|
14
|
+
"elering open API",
|
|
15
|
+
"elering dashboard",
|
|
16
|
+
"estonia",
|
|
17
|
+
"energy",
|
|
18
|
+
]
|
|
19
|
+
classifiers = [
|
|
20
|
+
"Intended Audience :: Science/Research",
|
|
21
|
+
"Operating System :: OS Independent",
|
|
22
|
+
"Programming Language :: Python :: 3.11",
|
|
23
|
+
"Programming Language :: Python :: 3.12",
|
|
24
|
+
"Programming Language :: Python :: 3.13",
|
|
25
|
+
"Programming Language :: Python :: 3.14",
|
|
26
|
+
"Topic :: Scientific/Engineering",
|
|
27
|
+
"Topic :: Software Development :: Libraries :: Python Modules",
|
|
28
|
+
"Typing :: Typed",
|
|
29
|
+
]
|
|
30
|
+
requires-python = ">=3.11"
|
|
31
|
+
dependencies = []
|
|
32
|
+
|
|
33
|
+
[project.urls]
|
|
34
|
+
Repository = "https://github.com/hoofir/elering-py"
|
|
35
|
+
Issues = "https://github.com/hoofir/elering-py/issues"
|
|
36
|
+
"Elering dashboard API" = "https://dashboard.elering.ee/assets/swagger-ui/index.html"
|
|
37
|
+
|
|
38
|
+
[dependency-groups]
|
|
39
|
+
dev = [
|
|
40
|
+
"deptry>=0.25.1",
|
|
41
|
+
"ipykernel>=7.1.0",
|
|
42
|
+
"pytest>=9.0.2",
|
|
43
|
+
"pytest-cov>=7.0.0",
|
|
44
|
+
"ruff>=0.14.10",
|
|
45
|
+
"twine>=6.2.0",
|
|
46
|
+
"ty>=0.0.59",
|
|
47
|
+
]
|
|
48
|
+
|
|
49
|
+
[build-system]
|
|
50
|
+
requires = ["uv-build<0.12"]
|
|
51
|
+
build-backend = "uv_build"
|
|
52
|
+
|
|
53
|
+
[tool.uv.build-backend]
|
|
54
|
+
module-root = "src"
|
|
55
|
+
module-name = "elering"
|
|
56
|
+
|
|
57
|
+
[tool.pytest.ini_options]
|
|
58
|
+
testpaths = ["tests"]
|
|
59
|
+
addopts = "-m 'not live'"
|
|
60
|
+
markers = ["live: hits the real Elering API (deselected by default)"]
|
|
61
|
+
|
|
62
|
+
[tool.ruff.lint]
|
|
63
|
+
ignore = ["E501"]
|
|
64
|
+
select = [
|
|
65
|
+
"C",
|
|
66
|
+
"E",
|
|
67
|
+
"F",
|
|
68
|
+
"I",
|
|
69
|
+
]
|
|
70
|
+
extend-select = [
|
|
71
|
+
"ANN",
|
|
72
|
+
"PYI",
|
|
73
|
+
]
|
|
74
|
+
|
|
75
|
+
[tool.ruff.lint.per-file-ignores]
|
|
76
|
+
"src/elering/_http.py" = ["ANN401"]
|
|
77
|
+
"src/elering/_client.py" = ["ANN401"]
|
|
78
|
+
"tests/**" = ["ANN"]
|
|
79
|
+
|
|
80
|
+
[tool.ruff.format]
|
|
81
|
+
docstring-code-format = true
|
|
82
|
+
docstring-code-line-length = 72
|
|
83
|
+
|
|
84
|
+
[tool.ty.src]
|
|
85
|
+
include = [
|
|
86
|
+
"src",
|
|
87
|
+
"tests",
|
|
88
|
+
]
|
|
89
|
+
|
|
90
|
+
[tool.ty.terminal]
|
|
91
|
+
output-format = "concise"
|
|
92
|
+
|
|
93
|
+
[tool.ty.rules]
|
|
94
|
+
unused-ignore-comment = "warn"
|
|
95
|
+
possibly-unresolved-reference = "warn"
|
|
96
|
+
possibly-missing-attribute = "error"
|
|
97
|
+
possibly-missing-import = "error"
|
|
98
|
+
missing-type-argument = "error"
|