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.
@@ -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
+ [![PyPI](https://img.shields.io/pypi/v/elering-py.svg)](https://pypi.org/project/elering-py/)
26
+ [![CI](https://github.com/hoofir/elering-py/actions/workflows/ci.yml/badge.svg)](https://github.com/hoofir/elering-py/actions/workflows/ci.yml)
27
+ [![License: MIT](https://img.shields.io/badge/license-MIT-blue.svg)](LICENSE)
28
+ [![Ruff](https://img.shields.io/badge/checks-ruff-purple)](https://github.com/astral-sh/ruff)
29
+ [![ty](https://img.shields.io/badge/types-ty-purple)](https://github.com/microsoft/ty)
30
+ [![Deptry](https://img.shields.io/badge/deps-deptry-tomato)](https://github.com/fpgmaas/deptry)
31
+ [![Pytest](https://img.shields.io/badge/tests-pytest-yellow)](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
+ [![PyPI](https://img.shields.io/pypi/v/elering-py.svg)](https://pypi.org/project/elering-py/)
4
+ [![CI](https://github.com/hoofir/elering-py/actions/workflows/ci.yml/badge.svg)](https://github.com/hoofir/elering-py/actions/workflows/ci.yml)
5
+ [![License: MIT](https://img.shields.io/badge/license-MIT-blue.svg)](LICENSE)
6
+ [![Ruff](https://img.shields.io/badge/checks-ruff-purple)](https://github.com/astral-sh/ruff)
7
+ [![ty](https://img.shields.io/badge/types-ty-purple)](https://github.com/microsoft/ty)
8
+ [![Deptry](https://img.shields.io/badge/deps-deptry-tomato)](https://github.com/fpgmaas/deptry)
9
+ [![Pytest](https://img.shields.io/badge/tests-pytest-yellow)](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"