baltic-py 0.1.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.
- baltic_py-0.1.0/LICENSE +21 -0
- baltic_py-0.1.0/PKG-INFO +336 -0
- baltic_py-0.1.0/README.md +313 -0
- baltic_py-0.1.0/pyproject.toml +102 -0
- baltic_py-0.1.0/pyproject.toml.orig +102 -0
- baltic_py-0.1.0/src/baltic/__init__.py +67 -0
- baltic_py-0.1.0/src/baltic/_catalog.py +404 -0
- baltic_py-0.1.0/src/baltic/_client.py +260 -0
- baltic_py-0.1.0/src/baltic/_errors.py +52 -0
- baltic_py-0.1.0/src/baltic/_http.py +128 -0
- baltic_py-0.1.0/src/baltic/_params.py +97 -0
- baltic_py-0.1.0/src/baltic/_reports.py +72 -0
- baltic_py-0.1.0/src/baltic/models.py +192 -0
- baltic_py-0.1.0/src/baltic/py.typed +0 -0
baltic_py-0.1.0/LICENSE
ADDED
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2026
|
|
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.
|
baltic_py-0.1.0/PKG-INFO
ADDED
|
@@ -0,0 +1,336 @@
|
|
|
1
|
+
Metadata-Version: 2.4
|
|
2
|
+
Name: baltic-py
|
|
3
|
+
Version: 0.1.0
|
|
4
|
+
Summary: Python client for the Baltic Transparency Dashboard (BTD) open API
|
|
5
|
+
Keywords: baltic,baltic-py,py-baltic,btd,btd-api,baltic transparency dashboard,transparency dashboard,elering,litgrid,ast
|
|
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-Dist: tzdata ; sys_platform == 'win32'
|
|
18
|
+
Requires-Python: >=3.11
|
|
19
|
+
Project-URL: Repository, https://github.com/hoofir/baltic-py
|
|
20
|
+
Project-URL: Issues, https://github.com/hoofir/baltic-py/issues
|
|
21
|
+
Project-URL: BTD open API, https://baltic.transparency-dashboard.eu/documentation/api
|
|
22
|
+
Description-Content-Type: text/markdown
|
|
23
|
+
|
|
24
|
+
# baltic-py
|
|
25
|
+
|
|
26
|
+
[](https://github.com/hoofir/baltic-py/actions/workflows/ci.yml)
|
|
27
|
+
[](https://pypi.org/project/baltic-py/)
|
|
28
|
+
[](https://pypi.org/project/baltic-py/)
|
|
29
|
+
[](LICENSE)
|
|
30
|
+
|
|
31
|
+
A small Python client for the [Baltic Transparency Dashboard](https://baltic.transparency-dashboard.eu/) (BTD) [open API](https://baltic.transparency-dashboard.eu/documentation/api) — the balancing market data published by the Baltic TSOs *AST*, *Elering* and *Litgrid*.
|
|
32
|
+
|
|
33
|
+
- **No dependencies.** Standard library only (plus `tzdata` on Windows).
|
|
34
|
+
- **No setup.** The API is public and unauthenticated — `import baltic` and go.
|
|
35
|
+
- **Discoverable.** The 65 report IDs ship with the package, searchable by name
|
|
36
|
+
and category, with typo suggestions.
|
|
37
|
+
- **Long ranges just work.** Multi-year queries are split into windows and
|
|
38
|
+
stitched back together for you.
|
|
39
|
+
- **Table-shaped data out.** The API's column/value matrix is parsed into
|
|
40
|
+
labelled rows, ready for `pandas`, `polars` or `csv`.
|
|
41
|
+
|
|
42
|
+
---
|
|
43
|
+
|
|
44
|
+
## Quickstart
|
|
45
|
+
|
|
46
|
+
### Install
|
|
47
|
+
|
|
48
|
+
```bash
|
|
49
|
+
pip install baltic-py
|
|
50
|
+
```
|
|
51
|
+
|
|
52
|
+
Requires Python 3.11 or newer.
|
|
53
|
+
|
|
54
|
+
### Query data
|
|
55
|
+
|
|
56
|
+
```python
|
|
57
|
+
import baltic
|
|
58
|
+
|
|
59
|
+
# What can I ask for?
|
|
60
|
+
baltic.reports("imbalance")
|
|
61
|
+
# [Report(id='imbalance_prices', title='Imbalance prices', ...),
|
|
62
|
+
# Report(id='imbalance_volumes_v2', title='Imbalance volumes', ...), ...]
|
|
63
|
+
|
|
64
|
+
export = baltic.export("imbalance_prices", start="2024-01-01", end="2024-01-02")
|
|
65
|
+
|
|
66
|
+
export.title # 'Imbalance prices'
|
|
67
|
+
export.unit # 'EUR/MWh'
|
|
68
|
+
export.resolution # 'PT15M'
|
|
69
|
+
len(export) # 96
|
|
70
|
+
|
|
71
|
+
export.rows()[0]
|
|
72
|
+
# {'start': datetime(2024, 1, 1, 0, 0, tzinfo=UTC),
|
|
73
|
+
# 'end': datetime(2024, 1, 1, 0, 15, tzinfo=UTC),
|
|
74
|
+
# 'Estonia / Final': 118.03, 'Estonia / Preliminary': None,
|
|
75
|
+
# 'Latvia / Final': 118.03, 'Latvia / Preliminary': None,
|
|
76
|
+
# 'Lithuania / Final': 118.03, 'Lithuania / Preliminary': None}
|
|
77
|
+
```
|
|
78
|
+
|
|
79
|
+
Into a dataframe:
|
|
80
|
+
|
|
81
|
+
```python
|
|
82
|
+
import pandas as pd
|
|
83
|
+
|
|
84
|
+
df = pd.DataFrame(export.rows()).set_index("start")
|
|
85
|
+
tidy = pd.DataFrame(export.records()) # long format instead
|
|
86
|
+
```
|
|
87
|
+
|
|
88
|
+
---
|
|
89
|
+
|
|
90
|
+
## Guide
|
|
91
|
+
|
|
92
|
+
### Finding reports
|
|
93
|
+
|
|
94
|
+
Every export is identified by a report ID. The catalog ships with the package,
|
|
95
|
+
so browsing it costs no network call:
|
|
96
|
+
|
|
97
|
+
```python
|
|
98
|
+
baltic.REPORT_IDS # all 65 IDs
|
|
99
|
+
baltic.CATEGORIES # ('Activations', 'Balancing', 'Bids', 'Capacities', ...)
|
|
100
|
+
|
|
101
|
+
baltic.reports(category="Reserves")
|
|
102
|
+
baltic.reports("mfrr bid")
|
|
103
|
+
baltic.report("imbalance_prices").resolutions # ('PT15M',)
|
|
104
|
+
```
|
|
105
|
+
|
|
106
|
+
Unknown IDs are rejected before any request is made, with suggestions:
|
|
107
|
+
|
|
108
|
+
```python
|
|
109
|
+
baltic.export("imbalance_price", start="2024-01-01", end="2024-01-02")
|
|
110
|
+
# ValueError: unknown report 'imbalance_price', did you mean imbalance_prices
|
|
111
|
+
# or imbalance_volumes or balancing_energy_prices?
|
|
112
|
+
```
|
|
113
|
+
|
|
114
|
+
Several reports in one round trip — the API allows up to four and returns them
|
|
115
|
+
as a ZIP archive, which is unpacked for you:
|
|
116
|
+
|
|
117
|
+
```python
|
|
118
|
+
exports = baltic.export_many(
|
|
119
|
+
["imbalance_prices", "imbalance_volumes_v2"],
|
|
120
|
+
start="2024-01-01",
|
|
121
|
+
end="2024-01-02",
|
|
122
|
+
)
|
|
123
|
+
exports["imbalance_prices"].rows()
|
|
124
|
+
```
|
|
125
|
+
|
|
126
|
+
### Dates and time zones
|
|
127
|
+
|
|
128
|
+
`start` and `end` accept a string, a `date` or a `datetime`. **`end` is
|
|
129
|
+
exclusive**, so `start="2024-01-01", end="2024-02-01"` is exactly January.
|
|
130
|
+
|
|
131
|
+
The `tz` argument (`"UTC"`, `"EET"` or `"CET"`, default `"UTC"`) is the *export*
|
|
132
|
+
time zone. It decides how naive values are read, and how `csv`/`xlsx`
|
|
133
|
+
timestamps are rendered. Aware datetimes are converted into it:
|
|
134
|
+
|
|
135
|
+
```python
|
|
136
|
+
from datetime import date, datetime
|
|
137
|
+
|
|
138
|
+
# 1 January 2024, 00:00 Baltic time
|
|
139
|
+
baltic.export("imbalance_prices", start="2024-01-01", end=date(2024, 1, 2), tz="EET")
|
|
140
|
+
|
|
141
|
+
# aware input is converted to the export time zone for you
|
|
142
|
+
baltic.export(
|
|
143
|
+
"imbalance_prices", start=datetime(2024, 1, 1, tzinfo=UTC), end=..., tz="EET"
|
|
144
|
+
)
|
|
145
|
+
```
|
|
146
|
+
|
|
147
|
+
Regardless of `tz`, the `start`/`end` of every returned interval is a
|
|
148
|
+
timezone-aware UTC instant, which is what the API emits.
|
|
149
|
+
|
|
150
|
+
### `export()` vs `download()`
|
|
151
|
+
|
|
152
|
+
- `export()` parses the JSON payload into an [`Export`](#the-export-object).
|
|
153
|
+
- `download()` returns the response bytes untouched, for `csv`, `xlsx` or
|
|
154
|
+
`json`, exactly as the dashboard's download button produces them.
|
|
155
|
+
|
|
156
|
+
```python
|
|
157
|
+
from pathlib import Path
|
|
158
|
+
|
|
159
|
+
Path("prices.xlsx").write_bytes(
|
|
160
|
+
baltic.download(
|
|
161
|
+
"imbalance_prices",
|
|
162
|
+
start="2024-01-01",
|
|
163
|
+
end="2024-02-01",
|
|
164
|
+
output_format="xlsx",
|
|
165
|
+
tz="EET",
|
|
166
|
+
)
|
|
167
|
+
)
|
|
168
|
+
|
|
169
|
+
# several reports in one ZIP
|
|
170
|
+
Path("bundle.zip").write_bytes(
|
|
171
|
+
baltic.download_many(["imbalance_prices", "neutrality_component"], ...)
|
|
172
|
+
)
|
|
173
|
+
```
|
|
174
|
+
|
|
175
|
+
### Long time ranges
|
|
176
|
+
|
|
177
|
+
A year of 15-minute data is ~35 000 intervals and ~4 MB, so wide queries are
|
|
178
|
+
slow. `export()` and `export_many()` therefore split any range longer than
|
|
179
|
+
`max_window` (default 366 days) into consecutive requests and concatenate the
|
|
180
|
+
results. Because `end` is exclusive, the windows do not overlap and no interval
|
|
181
|
+
is duplicated:
|
|
182
|
+
|
|
183
|
+
```python
|
|
184
|
+
# transparently issued as several requests
|
|
185
|
+
export = baltic.export("imbalance_prices", start="2015-01-01", end="2024-01-01")
|
|
186
|
+
```
|
|
187
|
+
|
|
188
|
+
Minute-resolution reports (`current_balancing_state_v2`) are ~500 000 intervals
|
|
189
|
+
per year, so they are capped at 92 days per request instead.
|
|
190
|
+
|
|
191
|
+
`download()` is never split — CSV and XLSX files cannot be concatenated safely.
|
|
192
|
+
|
|
193
|
+
### The `Export` object
|
|
194
|
+
|
|
195
|
+
| Attribute | Meaning |
|
|
196
|
+
| --- | --- |
|
|
197
|
+
| `id`, `title`, `description` | Report identity; `description` is the dashboard's HTML blurb |
|
|
198
|
+
| `unit` | Measurement unit, e.g. `'EUR/MWh'` |
|
|
199
|
+
| `resolution`, `timezone`, `local_timezone` | `'PT15M'`, `'EET'`, `'Europe/Tallinn'` |
|
|
200
|
+
| `created` | When the API built the export |
|
|
201
|
+
| `columns` | `tuple[Column, ...]`, each with `index`, `label`, `groups`, `name` |
|
|
202
|
+
| `intervals` | `tuple[Interval, ...]`, each with `start`, `end`, `values` |
|
|
203
|
+
|
|
204
|
+
`values` is positional with respect to `columns`, so two flattening helpers are
|
|
205
|
+
provided:
|
|
206
|
+
|
|
207
|
+
```python
|
|
208
|
+
export.rows() # wide: one dict per interval, one key per column
|
|
209
|
+
export.records() # long: one dict per interval *and* column
|
|
210
|
+
```
|
|
211
|
+
|
|
212
|
+
Column names join the API's group levels with the leaf label, e.g.
|
|
213
|
+
`"Baltics / Upward / Min bid"`:
|
|
214
|
+
|
|
215
|
+
```python
|
|
216
|
+
[c.name for c in export.columns]
|
|
217
|
+
export.columns[0].groups # ('Baltics', 'Upward')
|
|
218
|
+
```
|
|
219
|
+
|
|
220
|
+
### Errors
|
|
221
|
+
|
|
222
|
+
Everything raises a subclass of `baltic.BalticError`:
|
|
223
|
+
|
|
224
|
+
| Exception | Raised when |
|
|
225
|
+
| --- | --- |
|
|
226
|
+
| `BalticBadRequest` | HTTP 4xx, or a `200` with `error: true`. Exposes `.status` and `.messages` |
|
|
227
|
+
| `BalticServerError` | HTTP 5xx, after retries are exhausted |
|
|
228
|
+
| `BalticTransportError` | Network failure or timeout |
|
|
229
|
+
|
|
230
|
+
```python
|
|
231
|
+
try:
|
|
232
|
+
baltic.export("imbalance_prices", start="2024-01-01", end="2024-01-02", tz="UTC")
|
|
233
|
+
except baltic.BalticBadRequest as exc:
|
|
234
|
+
print(exc.status) # 400
|
|
235
|
+
print(exc.messages) # ['Invalid value for `start_date`: "…"']
|
|
236
|
+
```
|
|
237
|
+
|
|
238
|
+
Invalid arguments (an unknown report, an unknown `tz`, an unparseable date, more
|
|
239
|
+
than four reports) raise `ValueError` before any request is made.
|
|
240
|
+
|
|
241
|
+
### Configuring a client
|
|
242
|
+
|
|
243
|
+
The module-level helpers use a shared default client. Create your own to change
|
|
244
|
+
its behaviour:
|
|
245
|
+
|
|
246
|
+
```python
|
|
247
|
+
from datetime import timedelta
|
|
248
|
+
|
|
249
|
+
with baltic.Client(
|
|
250
|
+
timeout=300, retries=5, tz="EET", max_window=timedelta(days=90)
|
|
251
|
+
) as client:
|
|
252
|
+
export = client.export("mfrr_bid_prices", start="2024-01-01", end="2024-04-01")
|
|
253
|
+
```
|
|
254
|
+
|
|
255
|
+
| Argument | Default | Purpose |
|
|
256
|
+
| --- | --- | --- |
|
|
257
|
+
| `base_url` | `https://api-baltic.transparency-dashboard.eu` | API root; must be http or https |
|
|
258
|
+
| `timeout` | `120.0` | Per-request socket timeout in seconds |
|
|
259
|
+
| `retries` | `3` | Extra attempts on transport errors and 5xx |
|
|
260
|
+
| `backoff` | `0.5` | Base delay for exponential retry backoff |
|
|
261
|
+
| `tz` | `"UTC"` | Default export time zone |
|
|
262
|
+
| `max_window` | `366 days` | Longest span per request; `None` disables splitting |
|
|
263
|
+
|
|
264
|
+
---
|
|
265
|
+
|
|
266
|
+
## API surface
|
|
267
|
+
|
|
268
|
+
| API endpoint | Method | Returns |
|
|
269
|
+
| --- | --- | --- |
|
|
270
|
+
| `GET /api/v1/export` | `export()` | `Export` |
|
|
271
|
+
| `GET /api/v1/export` | `download()` | `bytes` (`csv`, `xlsx` or `json`) |
|
|
272
|
+
| `GET /api/v1/export-multiple` | `export_many()` | `dict[str, Export]` |
|
|
273
|
+
| `GET /api/v1/export-multiple` | `download_many()` | `bytes` (ZIP archive) |
|
|
274
|
+
| — | `reports()`, `report()` | Offline report catalog |
|
|
275
|
+
|
|
276
|
+
Each method is available both on a `Client` instance and at module level
|
|
277
|
+
(`baltic.export(...)`).
|
|
278
|
+
|
|
279
|
+
The API's `json_header_groups` flag is not exposed: it only adds table-header
|
|
280
|
+
spans, and the same grouping is already available as `Column.groups`.
|
|
281
|
+
|
|
282
|
+
---
|
|
283
|
+
|
|
284
|
+
## Contributing
|
|
285
|
+
|
|
286
|
+
```bash
|
|
287
|
+
make setup # create the venv and install dev dependencies
|
|
288
|
+
make check # ruff lint, format check, ty type check, deptry
|
|
289
|
+
make test # fast offline tests
|
|
290
|
+
make test-live # end-to-end tests against the real API
|
|
291
|
+
```
|
|
292
|
+
|
|
293
|
+
`make test` never touches the network. The live suite is deselected by default
|
|
294
|
+
and exercises both endpoints, every output format, the windowing logic and the
|
|
295
|
+
report catalog against the real API.
|
|
296
|
+
|
|
297
|
+
### Keeping up with the API
|
|
298
|
+
|
|
299
|
+
Only `src/baltic/_catalog.py` is generated; the client is hand-written, because
|
|
300
|
+
the API has just two endpoints and its Swagger 2.0 definition describes the
|
|
301
|
+
response shape only through an example.
|
|
302
|
+
|
|
303
|
+
```bash
|
|
304
|
+
make spec-check # fail if the published metadata differs from spec/
|
|
305
|
+
make spec # refresh spec/openapi.json and spec/reports.json
|
|
306
|
+
make catalog # regenerate src/baltic/_catalog.py from them
|
|
307
|
+
```
|
|
308
|
+
|
|
309
|
+
CI runs `make spec-check` weekly, so new or renamed reports show up as a
|
|
310
|
+
reviewable diff.
|
|
311
|
+
|
|
312
|
+
### Notes on the API
|
|
313
|
+
|
|
314
|
+
Behaviour verified against the live API and worked around in the client:
|
|
315
|
+
|
|
316
|
+
- Both endpoints answer with a `{"error", "message", "data"}` envelope and can
|
|
317
|
+
report failures with HTTP 200 and `error: true`.
|
|
318
|
+
- `export-multiple` needs its report IDs comma-separated in a single `id`
|
|
319
|
+
parameter; repeating `id=` silently keeps only the last one. It answers with a
|
|
320
|
+
ZIP archive, and rejects more than four reports with HTTP 422.
|
|
321
|
+
- `start_date` and `end_date` must be `yyyy-MM-ddTHH:mm` with no time zone; they
|
|
322
|
+
are read in `output_time_zone`.
|
|
323
|
+
- Interval timestamps are always emitted as UTC instants, whatever
|
|
324
|
+
`output_time_zone` was requested.
|
|
325
|
+
- A range that predates the report answers HTTP 500 instead of an empty export;
|
|
326
|
+
a range in the future returns intervals whose values are all `None`.
|
|
327
|
+
- Report titles, resolutions and categories are not part of the documented
|
|
328
|
+
spec; they are vendored in `spec/reports.json` from the dashboard's own report
|
|
329
|
+
listing, and only used at code-generation time.
|
|
330
|
+
|
|
331
|
+
---
|
|
332
|
+
|
|
333
|
+
## License
|
|
334
|
+
|
|
335
|
+
MIT — see [LICENSE](LICENSE). This project is not affiliated with AST, Elering,
|
|
336
|
+
Litgrid or Baltic Transparency Dashboard.
|
|
@@ -0,0 +1,313 @@
|
|
|
1
|
+
# baltic-py
|
|
2
|
+
|
|
3
|
+
[](https://github.com/hoofir/baltic-py/actions/workflows/ci.yml)
|
|
4
|
+
[](https://pypi.org/project/baltic-py/)
|
|
5
|
+
[](https://pypi.org/project/baltic-py/)
|
|
6
|
+
[](LICENSE)
|
|
7
|
+
|
|
8
|
+
A small Python client for the [Baltic Transparency Dashboard](https://baltic.transparency-dashboard.eu/) (BTD) [open API](https://baltic.transparency-dashboard.eu/documentation/api) — the balancing market data published by the Baltic TSOs *AST*, *Elering* and *Litgrid*.
|
|
9
|
+
|
|
10
|
+
- **No dependencies.** Standard library only (plus `tzdata` on Windows).
|
|
11
|
+
- **No setup.** The API is public and unauthenticated — `import baltic` and go.
|
|
12
|
+
- **Discoverable.** The 65 report IDs ship with the package, searchable by name
|
|
13
|
+
and category, with typo suggestions.
|
|
14
|
+
- **Long ranges just work.** Multi-year queries are split into windows and
|
|
15
|
+
stitched back together for you.
|
|
16
|
+
- **Table-shaped data out.** The API's column/value matrix is parsed into
|
|
17
|
+
labelled rows, ready for `pandas`, `polars` or `csv`.
|
|
18
|
+
|
|
19
|
+
---
|
|
20
|
+
|
|
21
|
+
## Quickstart
|
|
22
|
+
|
|
23
|
+
### Install
|
|
24
|
+
|
|
25
|
+
```bash
|
|
26
|
+
pip install baltic-py
|
|
27
|
+
```
|
|
28
|
+
|
|
29
|
+
Requires Python 3.11 or newer.
|
|
30
|
+
|
|
31
|
+
### Query data
|
|
32
|
+
|
|
33
|
+
```python
|
|
34
|
+
import baltic
|
|
35
|
+
|
|
36
|
+
# What can I ask for?
|
|
37
|
+
baltic.reports("imbalance")
|
|
38
|
+
# [Report(id='imbalance_prices', title='Imbalance prices', ...),
|
|
39
|
+
# Report(id='imbalance_volumes_v2', title='Imbalance volumes', ...), ...]
|
|
40
|
+
|
|
41
|
+
export = baltic.export("imbalance_prices", start="2024-01-01", end="2024-01-02")
|
|
42
|
+
|
|
43
|
+
export.title # 'Imbalance prices'
|
|
44
|
+
export.unit # 'EUR/MWh'
|
|
45
|
+
export.resolution # 'PT15M'
|
|
46
|
+
len(export) # 96
|
|
47
|
+
|
|
48
|
+
export.rows()[0]
|
|
49
|
+
# {'start': datetime(2024, 1, 1, 0, 0, tzinfo=UTC),
|
|
50
|
+
# 'end': datetime(2024, 1, 1, 0, 15, tzinfo=UTC),
|
|
51
|
+
# 'Estonia / Final': 118.03, 'Estonia / Preliminary': None,
|
|
52
|
+
# 'Latvia / Final': 118.03, 'Latvia / Preliminary': None,
|
|
53
|
+
# 'Lithuania / Final': 118.03, 'Lithuania / Preliminary': None}
|
|
54
|
+
```
|
|
55
|
+
|
|
56
|
+
Into a dataframe:
|
|
57
|
+
|
|
58
|
+
```python
|
|
59
|
+
import pandas as pd
|
|
60
|
+
|
|
61
|
+
df = pd.DataFrame(export.rows()).set_index("start")
|
|
62
|
+
tidy = pd.DataFrame(export.records()) # long format instead
|
|
63
|
+
```
|
|
64
|
+
|
|
65
|
+
---
|
|
66
|
+
|
|
67
|
+
## Guide
|
|
68
|
+
|
|
69
|
+
### Finding reports
|
|
70
|
+
|
|
71
|
+
Every export is identified by a report ID. The catalog ships with the package,
|
|
72
|
+
so browsing it costs no network call:
|
|
73
|
+
|
|
74
|
+
```python
|
|
75
|
+
baltic.REPORT_IDS # all 65 IDs
|
|
76
|
+
baltic.CATEGORIES # ('Activations', 'Balancing', 'Bids', 'Capacities', ...)
|
|
77
|
+
|
|
78
|
+
baltic.reports(category="Reserves")
|
|
79
|
+
baltic.reports("mfrr bid")
|
|
80
|
+
baltic.report("imbalance_prices").resolutions # ('PT15M',)
|
|
81
|
+
```
|
|
82
|
+
|
|
83
|
+
Unknown IDs are rejected before any request is made, with suggestions:
|
|
84
|
+
|
|
85
|
+
```python
|
|
86
|
+
baltic.export("imbalance_price", start="2024-01-01", end="2024-01-02")
|
|
87
|
+
# ValueError: unknown report 'imbalance_price', did you mean imbalance_prices
|
|
88
|
+
# or imbalance_volumes or balancing_energy_prices?
|
|
89
|
+
```
|
|
90
|
+
|
|
91
|
+
Several reports in one round trip — the API allows up to four and returns them
|
|
92
|
+
as a ZIP archive, which is unpacked for you:
|
|
93
|
+
|
|
94
|
+
```python
|
|
95
|
+
exports = baltic.export_many(
|
|
96
|
+
["imbalance_prices", "imbalance_volumes_v2"],
|
|
97
|
+
start="2024-01-01",
|
|
98
|
+
end="2024-01-02",
|
|
99
|
+
)
|
|
100
|
+
exports["imbalance_prices"].rows()
|
|
101
|
+
```
|
|
102
|
+
|
|
103
|
+
### Dates and time zones
|
|
104
|
+
|
|
105
|
+
`start` and `end` accept a string, a `date` or a `datetime`. **`end` is
|
|
106
|
+
exclusive**, so `start="2024-01-01", end="2024-02-01"` is exactly January.
|
|
107
|
+
|
|
108
|
+
The `tz` argument (`"UTC"`, `"EET"` or `"CET"`, default `"UTC"`) is the *export*
|
|
109
|
+
time zone. It decides how naive values are read, and how `csv`/`xlsx`
|
|
110
|
+
timestamps are rendered. Aware datetimes are converted into it:
|
|
111
|
+
|
|
112
|
+
```python
|
|
113
|
+
from datetime import date, datetime
|
|
114
|
+
|
|
115
|
+
# 1 January 2024, 00:00 Baltic time
|
|
116
|
+
baltic.export("imbalance_prices", start="2024-01-01", end=date(2024, 1, 2), tz="EET")
|
|
117
|
+
|
|
118
|
+
# aware input is converted to the export time zone for you
|
|
119
|
+
baltic.export(
|
|
120
|
+
"imbalance_prices", start=datetime(2024, 1, 1, tzinfo=UTC), end=..., tz="EET"
|
|
121
|
+
)
|
|
122
|
+
```
|
|
123
|
+
|
|
124
|
+
Regardless of `tz`, the `start`/`end` of every returned interval is a
|
|
125
|
+
timezone-aware UTC instant, which is what the API emits.
|
|
126
|
+
|
|
127
|
+
### `export()` vs `download()`
|
|
128
|
+
|
|
129
|
+
- `export()` parses the JSON payload into an [`Export`](#the-export-object).
|
|
130
|
+
- `download()` returns the response bytes untouched, for `csv`, `xlsx` or
|
|
131
|
+
`json`, exactly as the dashboard's download button produces them.
|
|
132
|
+
|
|
133
|
+
```python
|
|
134
|
+
from pathlib import Path
|
|
135
|
+
|
|
136
|
+
Path("prices.xlsx").write_bytes(
|
|
137
|
+
baltic.download(
|
|
138
|
+
"imbalance_prices",
|
|
139
|
+
start="2024-01-01",
|
|
140
|
+
end="2024-02-01",
|
|
141
|
+
output_format="xlsx",
|
|
142
|
+
tz="EET",
|
|
143
|
+
)
|
|
144
|
+
)
|
|
145
|
+
|
|
146
|
+
# several reports in one ZIP
|
|
147
|
+
Path("bundle.zip").write_bytes(
|
|
148
|
+
baltic.download_many(["imbalance_prices", "neutrality_component"], ...)
|
|
149
|
+
)
|
|
150
|
+
```
|
|
151
|
+
|
|
152
|
+
### Long time ranges
|
|
153
|
+
|
|
154
|
+
A year of 15-minute data is ~35 000 intervals and ~4 MB, so wide queries are
|
|
155
|
+
slow. `export()` and `export_many()` therefore split any range longer than
|
|
156
|
+
`max_window` (default 366 days) into consecutive requests and concatenate the
|
|
157
|
+
results. Because `end` is exclusive, the windows do not overlap and no interval
|
|
158
|
+
is duplicated:
|
|
159
|
+
|
|
160
|
+
```python
|
|
161
|
+
# transparently issued as several requests
|
|
162
|
+
export = baltic.export("imbalance_prices", start="2015-01-01", end="2024-01-01")
|
|
163
|
+
```
|
|
164
|
+
|
|
165
|
+
Minute-resolution reports (`current_balancing_state_v2`) are ~500 000 intervals
|
|
166
|
+
per year, so they are capped at 92 days per request instead.
|
|
167
|
+
|
|
168
|
+
`download()` is never split — CSV and XLSX files cannot be concatenated safely.
|
|
169
|
+
|
|
170
|
+
### The `Export` object
|
|
171
|
+
|
|
172
|
+
| Attribute | Meaning |
|
|
173
|
+
| --- | --- |
|
|
174
|
+
| `id`, `title`, `description` | Report identity; `description` is the dashboard's HTML blurb |
|
|
175
|
+
| `unit` | Measurement unit, e.g. `'EUR/MWh'` |
|
|
176
|
+
| `resolution`, `timezone`, `local_timezone` | `'PT15M'`, `'EET'`, `'Europe/Tallinn'` |
|
|
177
|
+
| `created` | When the API built the export |
|
|
178
|
+
| `columns` | `tuple[Column, ...]`, each with `index`, `label`, `groups`, `name` |
|
|
179
|
+
| `intervals` | `tuple[Interval, ...]`, each with `start`, `end`, `values` |
|
|
180
|
+
|
|
181
|
+
`values` is positional with respect to `columns`, so two flattening helpers are
|
|
182
|
+
provided:
|
|
183
|
+
|
|
184
|
+
```python
|
|
185
|
+
export.rows() # wide: one dict per interval, one key per column
|
|
186
|
+
export.records() # long: one dict per interval *and* column
|
|
187
|
+
```
|
|
188
|
+
|
|
189
|
+
Column names join the API's group levels with the leaf label, e.g.
|
|
190
|
+
`"Baltics / Upward / Min bid"`:
|
|
191
|
+
|
|
192
|
+
```python
|
|
193
|
+
[c.name for c in export.columns]
|
|
194
|
+
export.columns[0].groups # ('Baltics', 'Upward')
|
|
195
|
+
```
|
|
196
|
+
|
|
197
|
+
### Errors
|
|
198
|
+
|
|
199
|
+
Everything raises a subclass of `baltic.BalticError`:
|
|
200
|
+
|
|
201
|
+
| Exception | Raised when |
|
|
202
|
+
| --- | --- |
|
|
203
|
+
| `BalticBadRequest` | HTTP 4xx, or a `200` with `error: true`. Exposes `.status` and `.messages` |
|
|
204
|
+
| `BalticServerError` | HTTP 5xx, after retries are exhausted |
|
|
205
|
+
| `BalticTransportError` | Network failure or timeout |
|
|
206
|
+
|
|
207
|
+
```python
|
|
208
|
+
try:
|
|
209
|
+
baltic.export("imbalance_prices", start="2024-01-01", end="2024-01-02", tz="UTC")
|
|
210
|
+
except baltic.BalticBadRequest as exc:
|
|
211
|
+
print(exc.status) # 400
|
|
212
|
+
print(exc.messages) # ['Invalid value for `start_date`: "…"']
|
|
213
|
+
```
|
|
214
|
+
|
|
215
|
+
Invalid arguments (an unknown report, an unknown `tz`, an unparseable date, more
|
|
216
|
+
than four reports) raise `ValueError` before any request is made.
|
|
217
|
+
|
|
218
|
+
### Configuring a client
|
|
219
|
+
|
|
220
|
+
The module-level helpers use a shared default client. Create your own to change
|
|
221
|
+
its behaviour:
|
|
222
|
+
|
|
223
|
+
```python
|
|
224
|
+
from datetime import timedelta
|
|
225
|
+
|
|
226
|
+
with baltic.Client(
|
|
227
|
+
timeout=300, retries=5, tz="EET", max_window=timedelta(days=90)
|
|
228
|
+
) as client:
|
|
229
|
+
export = client.export("mfrr_bid_prices", start="2024-01-01", end="2024-04-01")
|
|
230
|
+
```
|
|
231
|
+
|
|
232
|
+
| Argument | Default | Purpose |
|
|
233
|
+
| --- | --- | --- |
|
|
234
|
+
| `base_url` | `https://api-baltic.transparency-dashboard.eu` | API root; must be http or https |
|
|
235
|
+
| `timeout` | `120.0` | Per-request socket timeout in seconds |
|
|
236
|
+
| `retries` | `3` | Extra attempts on transport errors and 5xx |
|
|
237
|
+
| `backoff` | `0.5` | Base delay for exponential retry backoff |
|
|
238
|
+
| `tz` | `"UTC"` | Default export time zone |
|
|
239
|
+
| `max_window` | `366 days` | Longest span per request; `None` disables splitting |
|
|
240
|
+
|
|
241
|
+
---
|
|
242
|
+
|
|
243
|
+
## API surface
|
|
244
|
+
|
|
245
|
+
| API endpoint | Method | Returns |
|
|
246
|
+
| --- | --- | --- |
|
|
247
|
+
| `GET /api/v1/export` | `export()` | `Export` |
|
|
248
|
+
| `GET /api/v1/export` | `download()` | `bytes` (`csv`, `xlsx` or `json`) |
|
|
249
|
+
| `GET /api/v1/export-multiple` | `export_many()` | `dict[str, Export]` |
|
|
250
|
+
| `GET /api/v1/export-multiple` | `download_many()` | `bytes` (ZIP archive) |
|
|
251
|
+
| — | `reports()`, `report()` | Offline report catalog |
|
|
252
|
+
|
|
253
|
+
Each method is available both on a `Client` instance and at module level
|
|
254
|
+
(`baltic.export(...)`).
|
|
255
|
+
|
|
256
|
+
The API's `json_header_groups` flag is not exposed: it only adds table-header
|
|
257
|
+
spans, and the same grouping is already available as `Column.groups`.
|
|
258
|
+
|
|
259
|
+
---
|
|
260
|
+
|
|
261
|
+
## Contributing
|
|
262
|
+
|
|
263
|
+
```bash
|
|
264
|
+
make setup # create the venv and install dev dependencies
|
|
265
|
+
make check # ruff lint, format check, ty type check, deptry
|
|
266
|
+
make test # fast offline tests
|
|
267
|
+
make test-live # end-to-end tests against the real API
|
|
268
|
+
```
|
|
269
|
+
|
|
270
|
+
`make test` never touches the network. The live suite is deselected by default
|
|
271
|
+
and exercises both endpoints, every output format, the windowing logic and the
|
|
272
|
+
report catalog against the real API.
|
|
273
|
+
|
|
274
|
+
### Keeping up with the API
|
|
275
|
+
|
|
276
|
+
Only `src/baltic/_catalog.py` is generated; the client is hand-written, because
|
|
277
|
+
the API has just two endpoints and its Swagger 2.0 definition describes the
|
|
278
|
+
response shape only through an example.
|
|
279
|
+
|
|
280
|
+
```bash
|
|
281
|
+
make spec-check # fail if the published metadata differs from spec/
|
|
282
|
+
make spec # refresh spec/openapi.json and spec/reports.json
|
|
283
|
+
make catalog # regenerate src/baltic/_catalog.py from them
|
|
284
|
+
```
|
|
285
|
+
|
|
286
|
+
CI runs `make spec-check` weekly, so new or renamed reports show up as a
|
|
287
|
+
reviewable diff.
|
|
288
|
+
|
|
289
|
+
### Notes on the API
|
|
290
|
+
|
|
291
|
+
Behaviour verified against the live API and worked around in the client:
|
|
292
|
+
|
|
293
|
+
- Both endpoints answer with a `{"error", "message", "data"}` envelope and can
|
|
294
|
+
report failures with HTTP 200 and `error: true`.
|
|
295
|
+
- `export-multiple` needs its report IDs comma-separated in a single `id`
|
|
296
|
+
parameter; repeating `id=` silently keeps only the last one. It answers with a
|
|
297
|
+
ZIP archive, and rejects more than four reports with HTTP 422.
|
|
298
|
+
- `start_date` and `end_date` must be `yyyy-MM-ddTHH:mm` with no time zone; they
|
|
299
|
+
are read in `output_time_zone`.
|
|
300
|
+
- Interval timestamps are always emitted as UTC instants, whatever
|
|
301
|
+
`output_time_zone` was requested.
|
|
302
|
+
- A range that predates the report answers HTTP 500 instead of an empty export;
|
|
303
|
+
a range in the future returns intervals whose values are all `None`.
|
|
304
|
+
- Report titles, resolutions and categories are not part of the documented
|
|
305
|
+
spec; they are vendored in `spec/reports.json` from the dashboard's own report
|
|
306
|
+
listing, and only used at code-generation time.
|
|
307
|
+
|
|
308
|
+
---
|
|
309
|
+
|
|
310
|
+
## License
|
|
311
|
+
|
|
312
|
+
MIT — see [LICENSE](LICENSE). This project is not affiliated with AST, Elering,
|
|
313
|
+
Litgrid or Baltic Transparency Dashboard.
|