elering-py
A small Python client for the Elering dashboard open API — Estonian electricity and gas system data published by Elering AS.
- No dependencies. Standard library only.
- No setup. The API is public and unauthenticated —
import eleringand go. - Readable calls. Endpoint groups mirror the API docs, with snake case arguments and flexible dates.
- Long ranges just work. The API caps a request at one year; longer queries are split into windows and stitched back together for you.
- Plain data out. The
{"success": ..., "data": ...}envelope is unwrapped and every call returns alist[dict], ready forpandas,polarsorcsv.
Quickstart
Install
pip install elering-py
Requires Python 3.11 or newer.
Query data
import elering
# Nord Pool day-ahead prices for every Baltic area plus Finland
prices = elering.nps.price(start="2024-01-01", end="2024-01-02")
prices.keys()
# dict_keys(['ee', 'fi', 'lv', 'lt'])
prices["ee"][0]
# {'timestamp': 1704067200, 'price': 28.46}
# Estonian power system, right now
elering.system.latest()
# [{'timestamp': 1788104700, 'production': 375.25, 'consumption': 780.73,
# 'losses': None, 'frequency': 50.02, 'system_balance': -405.47,
# 'ac_balance': 232.37, 'production_renewable': 96.29,
# 'solar_energy_production': None}]
Into a dataframe:
import pandas as pd
df = pd.DataFrame(prices["ee"])
df["time"] = pd.to_datetime(df["timestamp"], unit="s", utc=True)
Guide
Dates and times
start and end accept a string, a date or a datetime. Naive values are
treated as UTC; aware values are converted to UTC. end is inclusive, so
start="2024-01-01", end="2024-01-02" covers both days' boundary hours.
from datetime import date, datetime
elering.system.values(start="2024-01-01", end=date(2024, 2, 1))
elering.system.values(start=datetime(2024, 1, 1, 6), end="2024-01-01T12:00")
Omitting both gives whatever the API considers current — usually the last day or
two. Passing only one of them raises ValueError.
elering.nps.price() # no range: the current window
Timestamps
Every row is stamped with timestamp, a Unix time in seconds. Rows are returned
exactly as the API sends them; convert when you need to:
from elering import from_timestamp
row = elering.system.latest()[0]
from_timestamp(row["timestamp"])
# datetime.datetime(2026, 4, 29, 5, 5, tzinfo=datetime.timezone.utc)
Rows and groups
Most methods return a list[dict]. Endpoints that publish one series per area
return a dict[str, list[dict]] instead, keyed exactly as the API keys it:
| Method | Keys |
|---|---|
nps.price(), nps.turnover() |
ee, fi, lv, lt |
system.with_plan() |
real, plan |
transmission.capacity() |
FI, LV, RU |
gas_transmission.cross_border() |
bc, karksi, misso, narva, varska |
gas_trade.prices() |
common, ee, fi, lv, lt |
Where the API also offers a single-area variant, so does the client:
elering.nps.price(start="2024-01-01", end="2024-01-02")["ee"]
elering.nps.price_latest("EE")
elering.transmission.capacity(start="2024-01-01", end="2024-01-02")["FI"]
elering.transmission.capacity_for("FI", start="2024-01-01", end="2024-01-02")
Area codes are case-insensitive and validated locally, so a typo raises
ValueError instead of costing a round trip.
Long time ranges
The API rejects any request spanning more than a year with
"Maximum period is 1 year". Ranges longer than max_window (default 365 days)
are therefore split into consecutive requests and concatenated in order. Because
end is inclusive, consecutive windows share one row; the duplicate is dropped
by timestamp, so the result is the same series you would get from a single
request:
# transparently issued as several requests
rows = elering.transmission.cross_border_hourly(start="2015-01-01", end="2024-01-01")
Urgent market messages
umm.messages() is the one paginated endpoint. It walks every page by default;
pass page= to fetch just one:
elering.umm.messages(event_status="active", unavailability_type="planned")
elering.umm.messages(page=1)
# every message published for one event
elering.umm.event(2189)
Errors
An empty result is returned as [] (or {}), not an error. Everything else
raises a subclass of elering.EleringError:
| Exception | Raised when |
|---|---|
EleringBadRequest |
HTTP 4xx — bad parameters. Exposes .status and .messages |
EleringServerError |
HTTP 5xx, after retries are exhausted |
EleringTransportError |
Network failure or timeout |
try:
elering.Client(max_window=None).nps.price(start="2020-01-01", end="2024-01-01")
except elering.EleringBadRequest as exc:
print(exc.status) # 400
print(exc.messages) # ['Maximum period is 1 year']
Invalid arguments (an unknown area, a lone start, an unparseable date) 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 elering.Client(timeout=120, retries=5, max_window=timedelta(days=90)) as client:
rows = client.gas_system.values(start="2024-01-01", end="2024-04-01")
| Argument | Default | Purpose |
|---|---|---|
base_url |
https://dashboard.elering.ee |
API root; must be http or https |
timeout |
60.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 |
max_window |
365 days |
Longest span per request; None disables splitting |
Endpoints
| API group | Attribute | Methods |
|---|---|---|
| Nord Pool | nps |
price(), price_latest(), price_current(), turnover(), turnover_latest() |
| Power system | system |
values(), latest(), with_plan() |
| Balance | balance |
balancing(), physical(), physical_latest(), commercial(), commercial_latest() |
| Transmission | transmission |
cross_border(), cross_border_latest(), cross_border_hourly(), planned_trade(), planned_trade_latest(), capacity(), capacity_for() |
| Gas system | gas_system |
values(), values_m3(), latest(), calorific_value(), calorific_value_25_0() |
| Gas transmission | gas_transmission |
cross_border(), cross_border_latest() |
| GET Baltic | gas_trade |
prices(), latest() |
| Gas balance | gas_balance |
price() |
| Gas border trade | gas_border_trade |
current() |
| Gas capacity | capacity |
firm(), interruptible() |
| Gas nominations | nominations |
values(), renominations() |
| Green certificates | green |
certificates() |
| Urgent market messages | umm |
messages(), event(), message() |
Each attribute is available both on a Client instance and at module level
(elering.nps.price(...)).
Not covered
- CSV endpoints. Every
/csvpath serves the same data as its JSON sibling, so the client exposes the JSON one and leaves formatting to you. - RSS feeds.
/umm/gas/rssis XML; the same messages are available as data throughumm.messages().
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 every endpoint group, the windowing logic and the response shapes.
Keeping up with the API
The endpoint methods are hand-written. The published spec declares every schema as an empty object, so it documents paths and parameters but says nothing about the data — the response shapes here were verified against the live API.
make spec-check # fail if the published spec differs from spec/openapi.json
make spec # refresh the vendored spec
CI runs make spec-check weekly, so spec changes show up as a reviewable diff.
Adding a new endpoint is then a few lines in src/elering/_resources.py.
Known spec defects
Worked around in the hand-written layer, each verified against the live API:
- Responses are wrapped in
{"success": ..., "data": ...}, which no schema mentions. The client unwraps it. /api/balance/totaldocuments its first parameter asfields; it isstart./api/umm/single/{id}also requiresidas a query parameter, and returns HTTP 500 without it./api/umm/gas/messagesdocuments its parameter asid; it isevent_id.startandendare documented as acceptingyyyy-MM-dd HH:mm; only offset-bearing ISO 8601 (2024-01-01T00:00:00Z) is actually parsed.- The one-year limit and the inclusive
endare undocumented.
License
MIT — see LICENSE. This project is not affiliated with Elering AS.
Release files for elering-py 0.1.1
For a detailed explanation of source distributions (sdists) and built distributions (wheels), please see the package formats documentation.
Source distribution (sdist)
| File | Size | Uploaded | |
|---|---|---|---|
| elering_py-0.1.1.tar.gz | 14.5 kB | Details |
Built distribution (wheel)
| File | Interpreter | ABI | Platform | Reset |
|---|---|---|---|---|
| elering_py-0.1.1-py3-none-any.whl | Python 3 | none | any | Details |
Total release size: 31.5 kB
Release files / elering_py-0.1.1.tar.gz
| Download URL | elering_py-0.1.1.tar.gz |
|---|---|
| Size | 14.5 kB |
| Tags | Source |
|
SHA-256 checksum How to use checksums |
1ef7043d4de75c7c0a4e207a22086b39db74852fe971373ae1626e86c351b086
|
|
BLAKE2b-256 checksum How to use checksums |
aed502019bc5869b3cb900596b01b37ff030c6105675067b4751cc6972d634f2
|
| 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 logRelease files / elering_py-0.1.1-py3-none-any.whl
| Download URL | elering_py-0.1.1-py3-none-any.whl |
|---|---|
| Size | 17.0 kB |
| Tags | Python 3 |
|
SHA-256 checksum How to use checksums |
ce1bd0e50b4cd86fd34c36b71cc4f67cd6b642fd7632c5661b865f4fa72dfd89
|
|
BLAKE2b-256 checksum How to use checksums |
634e93d4e780b15991eed20b8f44891c33a1cf2ec417a895cc20227f4127545b
|
| 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