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.
@@ -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.
@@ -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
+ [![CI](https://github.com/hoofir/baltic-py/actions/workflows/ci.yml/badge.svg)](https://github.com/hoofir/baltic-py/actions/workflows/ci.yml)
27
+ [![PyPI](https://img.shields.io/pypi/v/baltic-py.svg)](https://pypi.org/project/baltic-py/)
28
+ [![Python](https://img.shields.io/pypi/pyversions/baltic-py.svg)](https://pypi.org/project/baltic-py/)
29
+ [![License: MIT](https://img.shields.io/badge/license-MIT-blue.svg)](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
+ [![CI](https://github.com/hoofir/baltic-py/actions/workflows/ci.yml/badge.svg)](https://github.com/hoofir/baltic-py/actions/workflows/ci.yml)
4
+ [![PyPI](https://img.shields.io/pypi/v/baltic-py.svg)](https://pypi.org/project/baltic-py/)
5
+ [![Python](https://img.shields.io/pypi/pyversions/baltic-py.svg)](https://pypi.org/project/baltic-py/)
6
+ [![License: MIT](https://img.shields.io/badge/license-MIT-blue.svg)](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.