Skip to main content

baltic-py

CI PyPI Python License: MIT

A small Python client for the Baltic Transparency Dashboard (BTD) open API — the balancing market data published by the Baltic TSOs AST, Elering and Litgrid.

  • No dependencies. Standard library only (plus tzdata on Windows).
  • No setup. The API is public and unauthenticated — import baltic and go.
  • Discoverable. The 65 report IDs ship with the package, searchable by name and category, with typo suggestions.
  • Long ranges just work. Multi-year queries are split into windows and stitched back together for you.
  • Table-shaped data out. The API's column/value matrix is parsed into labelled rows, ready for pandas, polars or csv.

Quickstart

Install

pip install baltic-py

Requires Python 3.11 or newer.

Query data

import baltic

# What can I ask for?
baltic.reports("imbalance")
# [Report(id='imbalance_prices', title='Imbalance prices', ...),
#  Report(id='imbalance_volumes_v2', title='Imbalance volumes', ...), ...]

export = baltic.export("imbalance_prices", start="2024-01-01", end="2024-01-02")

export.title  # 'Imbalance prices'
export.unit  # 'EUR/MWh'
export.resolution  # 'PT15M'
len(export)  # 96

export.rows()[0]
# {'start': datetime(2024, 1, 1, 0, 0, tzinfo=UTC),
#  'end': datetime(2024, 1, 1, 0, 15, tzinfo=UTC),
#  'Estonia / Final': 118.03, 'Estonia / Preliminary': None,
#  'Latvia / Final': 118.03, 'Latvia / Preliminary': None,
#  'Lithuania / Final': 118.03, 'Lithuania / Preliminary': None}

Into a dataframe:

import pandas as pd

df = pd.DataFrame(export.rows()).set_index("start")
tidy = pd.DataFrame(export.records())  # long format instead

Guide

Finding reports

Every export is identified by a report ID. The catalog ships with the package, so browsing it costs no network call:

baltic.REPORT_IDS  # all 65 IDs
baltic.CATEGORIES  # ('Activations', 'Balancing', 'Bids', 'Capacities', ...)

baltic.reports(category="Reserves")
baltic.reports("mfrr bid")
baltic.report("imbalance_prices").resolutions  # ('PT15M',)

Unknown IDs are rejected before any request is made, with suggestions:

baltic.export("imbalance_price", start="2024-01-01", end="2024-01-02")
# ValueError: unknown report 'imbalance_price', did you mean imbalance_prices
# or imbalance_volumes or balancing_energy_prices?

Several reports in one round trip — the API allows up to four and returns them as a ZIP archive, which is unpacked for you:

exports = baltic.export_many(
    ["imbalance_prices", "imbalance_volumes_v2"],
    start="2024-01-01",
    end="2024-01-02",
)
exports["imbalance_prices"].rows()

Dates and time zones

start and end accept a string, a date or a datetime. end is exclusive, so start="2024-01-01", end="2024-02-01" is exactly January.

The tz argument ("UTC", "EET" or "CET", default "UTC") is the export time zone. It decides how naive values are read, and how csv/xlsx timestamps are rendered. Aware datetimes are converted into it:

from datetime import date, datetime

# 1 January 2024, 00:00 Baltic time
baltic.export("imbalance_prices", start="2024-01-01", end=date(2024, 1, 2), tz="EET")

# aware input is converted to the export time zone for you
baltic.export(
    "imbalance_prices", start=datetime(2024, 1, 1, tzinfo=UTC), end=..., tz="EET"
)

Regardless of tz, the start/end of every returned interval is a timezone-aware UTC instant, which is what the API emits.

export() vs download()

  • export() parses the JSON payload into an Export.
  • download() returns the response bytes untouched, for csv, xlsx or json, exactly as the dashboard's download button produces them.
from pathlib import Path

Path("prices.xlsx").write_bytes(
    baltic.download(
        "imbalance_prices",
        start="2024-01-01",
        end="2024-02-01",
        output_format="xlsx",
        tz="EET",
    )
)

# several reports in one ZIP
Path("bundle.zip").write_bytes(
    baltic.download_many(["imbalance_prices", "neutrality_component"], ...)
)

Long time ranges

A year of 15-minute data is ~35 000 intervals and ~4 MB, so wide queries are slow. export() and export_many() therefore split any range longer than max_window (default 366 days) into consecutive requests and concatenate the results. Because end is exclusive, the windows do not overlap and no interval is duplicated:

# transparently issued as several requests
export = baltic.export("imbalance_prices", start="2015-01-01", end="2024-01-01")

Minute-resolution reports (current_balancing_state_v2) are ~500 000 intervals per year, so they are capped at 92 days per request instead.

download() is never split — CSV and XLSX files cannot be concatenated safely.

The Export object

Attribute Meaning
id, title, description Report identity; description is the dashboard's HTML blurb
unit Measurement unit, e.g. 'EUR/MWh'
resolution, timezone, local_timezone 'PT15M', 'EET', 'Europe/Tallinn'
created When the API built the export
columns tuple[Column, ...], each with index, label, groups, name
intervals tuple[Interval, ...], each with start, end, values

values is positional with respect to columns, so two flattening helpers are provided:

export.rows()  # wide: one dict per interval, one key per column
export.records()  # long: one dict per interval *and* column

Column names join the API's group levels with the leaf label, e.g. "Baltics / Upward / Min bid":

[c.name for c in export.columns]
export.columns[0].groups  # ('Baltics', 'Upward')

Errors

Everything raises a subclass of baltic.BalticError:

Exception Raised when
BalticBadRequest HTTP 4xx, or a 200 with error: true. Exposes .status and .messages
BalticServerError HTTP 5xx, after retries are exhausted
BalticTransportError Network failure or timeout
try:
    baltic.export("imbalance_prices", start="2024-01-01", end="2024-01-02", tz="UTC")
except baltic.BalticBadRequest as exc:
    print(exc.status)  # 400
    print(exc.messages)  # ['Invalid value for `start_date`: "…"']

Invalid arguments (an unknown report, an unknown tz, an unparseable date, more than four reports) raise ValueError before any request is made.

Configuring a client

The module-level helpers use a shared default client. Create your own to change its behaviour:

from datetime import timedelta

with baltic.Client(
    timeout=300, retries=5, tz="EET", max_window=timedelta(days=90)
) as client:
    export = client.export("mfrr_bid_prices", start="2024-01-01", end="2024-04-01")
Argument Default Purpose
base_url https://api-baltic.transparency-dashboard.eu API root; must be http or https
timeout 120.0 Per-request socket timeout in seconds
retries 3 Extra attempts on transport errors and 5xx
backoff 0.5 Base delay for exponential retry backoff
tz "UTC" Default export time zone
max_window 366 days Longest span per request; None disables splitting

API surface

API endpoint Method Returns
GET /api/v1/export export() Export
GET /api/v1/export download() bytes (csv, xlsx or json)
GET /api/v1/export-multiple export_many() dict[str, Export]
GET /api/v1/export-multiple download_many() bytes (ZIP archive)
— reports(), report() Offline report catalog

Each method is available both on a Client instance and at module level (baltic.export(...)).

The API's json_header_groups flag is not exposed: it only adds table-header spans, and the same grouping is already available as Column.groups.


Contributing

make setup     # create the venv and install dev dependencies
make check     # ruff lint, format check, ty type check, deptry
make test      # fast offline tests
make test-live # end-to-end tests against the real API

make test never touches the network. The live suite is deselected by default and exercises both endpoints, every output format, the windowing logic and the report catalog against the real API.

Keeping up with the API

Only src/baltic/_catalog.py is generated; the client is hand-written, because the API has just two endpoints and its Swagger 2.0 definition describes the response shape only through an example.

make spec-check   # fail if the published metadata differs from spec/
make spec         # refresh spec/openapi.json and spec/reports.json
make catalog      # regenerate src/baltic/_catalog.py from them

CI runs make spec-check weekly, so new or renamed reports show up as a reviewable diff.

Notes on the API

Behaviour verified against the live API and worked around in the client:

  • Both endpoints answer with a {"error", "message", "data"} envelope and can report failures with HTTP 200 and error: true.
  • export-multiple needs its report IDs comma-separated in a single id parameter; repeating id= silently keeps only the last one. It answers with a ZIP archive, and rejects more than four reports with HTTP 422.
  • start_date and end_date must be yyyy-MM-ddTHH:mm with no time zone; they are read in output_time_zone.
  • Interval timestamps are always emitted as UTC instants, whatever output_time_zone was requested.
  • A range that predates the report answers HTTP 500 instead of an empty export; a range in the future returns intervals whose values are all None.
  • Report titles, resolutions and categories are not part of the documented spec; they are vendored in spec/reports.json from the dashboard's own report listing, and only used at code-generation time.

License

MIT — see LICENSE. This project is not affiliated with AST, Elering, Litgrid or Baltic Transparency Dashboard.

Release files for baltic-py 0.1.0

For a detailed explanation of source distributions (sdists) and built distributions (wheels), please see the package formats documentation.

Source distribution (sdist)

Source distribution for baltic-py 0.1.0
File Size Uploaded
baltic_py-0.1.0.tar.gz 16.7 kB Details

Built distribution (wheel)

Table of built distributions (wheels) for baltic-py 0.1.0
File Interpreter ABI Platform
baltic_py-0.1.0-py3-none-any.whl Python 3 none any Details

Total release size: 35.9 kB

Release files / baltic_py-0.1.0.tar.gz

Download URL baltic_py-0.1.0.tar.gz
Size 16.7 kB
Tags Source
SHA-256 checksum
How to use checksums
dc59129a177420cf9d7ecab528ef75d64e76e1ab24035cef2af7b714ae5df227
BLAKE2b-256 checksum
How to use checksums
61bcd0a4cabc314cf98e0b7ae745a959e0a924ddf083f9459f5b140194cfe704
Upload date
Uploaded using Trusted Publishing?
What is trusted publishing?
Yes
Uploaded via twine/7.0.0 CPython/3.13.14

Provenance

Provenance describes where a file came from. On PyPI, provenance is shared via attestations, which provide a verifiable record of the build or publishing details. View details, limitations and caveats.

PyPI Publish Attestation

PyPI verified that this artifact, at this checksum, originated from the publisher listed below.

Signed by GitHub Actions, verified by PyPI on Sep 19, 2026.

Transparency log

Release files / baltic_py-0.1.0-py3-none-any.whl

Download URL baltic_py-0.1.0-py3-none-any.whl
Size 19.3 kB
Tags Python 3
SHA-256 checksum
How to use checksums
2e70d043bb0628f389ed714cc5e69400fbe3ce3c9cee43d6f5a16285d10e75d1
BLAKE2b-256 checksum
How to use checksums
0e97af17b06802b5d0754aa7aa7a69487ddc98c087a93e550c924696c6ad93c6
Upload date
Uploaded using Trusted Publishing?
What is trusted publishing?
Yes
Uploaded via twine/7.0.0 CPython/3.13.14

Provenance

Provenance describes where a file came from. On PyPI, provenance is shared via attestations, which provide a verifiable record of the build or publishing details. View details, limitations and caveats.

PyPI Publish Attestation

PyPI verified that this artifact, at this checksum, originated from the publisher listed below.

Signed by GitHub Actions, verified by PyPI on Sep 19, 2026.

Transparency log

Release history Release notifications | RSS feed

0.2.0

2 release files

This release

0.1.0 This release

2 release files

Anthropic, PBC Visionary sponsor Bloomberg Visionary sponsor Hudson River Trading Visionary sponsor Meta Visionary sponsor NVIDIA Visionary sponsor Microsoft Sustainability sponsor Depot Continuous Integration AWS Cloud computing and Security Sponsor Datadog Monitoring Fastly CDN Google Download Analytics Sentry Error logging StatusPage Status page