Metadata-Version: 2.4
Name: esett-py
Version: 0.1.0
Summary: Python client for the eSett open data API
Keywords: esett,esett-py,py-esett,esett-api,esett API,esett open API,esett avoindata
License-Expression: MIT
License-File: LICENSE
Classifier: Intended Audience :: Science/Research
Classifier: Operating System :: OS Independent
Classifier: Programming Language :: Python :: 3.11
Classifier: Programming Language :: Python :: 3.12
Classifier: Programming Language :: Python :: 3.13
Classifier: Programming Language :: Python :: 3.14
Classifier: Topic :: Scientific/Engineering
Classifier: Topic :: Software Development :: Libraries :: Python Modules
Classifier: Typing :: Typed
Requires-Python: >=3.11
Project-URL: Repository, https://github.com/hoofir/esett-py
Project-URL: Issues, https://github.com/hoofir/esett-py/issues
Project-URL: eSett open data API, https://api.opendata.esett.com/
Description-Content-Type: text/markdown

# esett-py

[![PyPI](https://img.shields.io/pypi/v/esett-py?label=PyPI)](https://pypi.org/project/esett-py/)
[![CI](https://github.com/hoofir/esett-py/actions/workflows/ci.yml/badge.svg)](https://github.com/hoofir/esett-py/actions/workflows/ci.yml)
[![License: MIT](https://img.shields.io/badge/license-MIT-blue.svg)](LICENSE)
[![Ruff](https://img.shields.io/badge/lint-ruff-purple)](https://github.com/astral-sh/ruff)
[![Ruff](https://img.shields.io/badge/format-ruff-purple)](https://github.com/astral-sh/ruff)
[![ty](https://img.shields.io/badge/type-ty-purple)](https://github.com/microsoft/ty)
[![Deptry](https://img.shields.io/badge/deps-deptry-tomato)](https://github.com/fpgmaas/deptry)
[![Pytest](https://img.shields.io/badge/tests-pytest-yellow)](https://github.com/pytest-dev/pytest)


A small Python client for the [eSett open data API](https://api.opendata.esett.com/) — the Nordic imbalance settlement data published by eSett Oy.

- **No dependencies.** Standard library only.
- **No setup.** The API is public and unauthenticated — `import esett` and go.
- **Readable calls.** Endpoint groups mirror the API docs, with snake case
  arguments and flexible dates.
- **Long ranges just work.** Multi-year queries are split into windows and
  stitched back together for you.
- **Plain data out.** Every call returns a `list[dict]`, ready for `pandas`,
  `polars` or `csv`.

--- 

## Quickstart

### Install

```bash
pip install esett-py
```

Requires Python 3.11 or newer.

### Query data

```python
import esett

# Which balance areas can I ask about?
esett.production.mba_options()

# Nordic production volumes for Finland, 15-minute resolution
rows = esett.production.values(
    start="2024-01-01",
    end="2024-01-02",
    mba="10YFI_1________U",
)

rows[0]
# {'timestamp': '2024-01-01T01:00:00', 'timestampUTC': '2024-01-01T00:00:00Z',
#  'mba': 'FI', 'hydro': 300.63, 'nuclear': 1086.84, 'solar': 0.11,
#  'thermal': 456.49, 'wind': 699.42, 'windOffshore': None,
#  'energyStorage': None, 'other': 33.51, 'total': 2577.0}
```

Into a dataframe:

```python
import pandas as pd

df = pd.DataFrame(rows)
```

---

## Guide

### Finding MBA codes

Most endpoints are filtered by *metering balance area*, identified by an EIC
code rather than a friendly name. Each endpoint group lists the codes it
accepts:

```python
for country in esett.production.mba_options():
    for area in country["mbas"]:
        print(country["countryCode"], area["name"], area["code"])

# DK DK1 10YDK-1--------W
# FI FI  10YFI_1________U
# SE SE1 10Y1001A1001A44P
# ...
```

Pass one code, or several to have the API **sum** them into a single combined
series (the `mba` field of each row becomes e.g. `"SE1,SE2"`). Query the areas
separately if you want one series per area:

```python
esett.prices.values(start="2024-01-01", end="2024-01-08", mba="10YFI_1________U")

# one combined series covering both areas, not two series
esett.prices.values(
    start="2024-01-01",
    end="2024-01-08",
    mba=["10Y1001A1001A44P", "10Y1001A1001A45N"],
)
```

The API refuses any query whose result would exceed **100 000 rows**, raising
`ESettBadRequest` with a `rowLimit` violation. Narrow the time range or the
filters if you hit it.

### 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 exclusive**, so
`start="2024-01-01", end="2024-02-01"` is exactly the month of January.

```python
from datetime import date, datetime

esett.consumption.values(start="2024-01-01", end=date(2024, 2, 1), mba=FI)
esett.consumption.values(start=datetime(2024, 1, 1, 6), end="2024-01-01T12:00", mba=FI)
```

### `values()` vs `aggregate()`

Every time-series group exposes the same two methods:

- `values()` — the series at the API's native resolution.
- `aggregate()` — the same data resampled server-side, via
  `resolution="year" | "month" | "week" | "day" | "hour"`.

```python
esett.production.values(start="2024-01-01", end="2024-02-01", mba=FI)
esett.production.aggregate(start="2024-01-01", end="2024-02-01", mba=FI, resolution="day")
```

### Long time ranges

A year of 15-minute data is ~35 000 rows and ~9 MB, so wide queries are slow and
easy to time out. `values()` therefore splits any range longer than
`max_window` (default 366 days) into consecutive requests and concatenates the
results in order. Because `end` is exclusive, the windows do not overlap and no
row is duplicated:

```python
# transparently issued as several requests
rows = esett.prices.values(start="2015-01-01", end="2024-01-01", mba=FI)
```

`aggregate()` is never split — the server builds the buckets, so cutting the
range would change the answer.

### Typed models

Responses are dictionaries with the API's original camelCase keys. If you prefer
attribute access and snake case, every response shape is also available as a
frozen dataclass:

```python
from esett.models import ProductionVolumes

volumes = [ProductionVolumes.from_dict(row) for row in rows]
volumes[0].wind_offshore
volumes[0].timestamp_utc
```

### Errors

An empty result (HTTP 204) is returned as `[]`, not an error. Everything else
raises a subclass of `esett.ESettError`:

| Exception | Raised when |
| --- | --- |
| `ESettBadRequest` | HTTP 4xx — bad parameters. Exposes `.status` and `.violations` |
| `ESettServerError` | HTTP 5xx, after retries are exhausted |
| `ESettTransportError` | Network failure or timeout |

```python
try:
    esett.fees.history(country="XX", fee="NOPE")
except esett.ESettBadRequest as exc:
    print(exc.status)      # 400
    print(exc.violations)  # [{'field': ..., 'message': 'Unknown country: XX...'}]
```

Invalid arguments (an unknown `resolution`, an empty `mba` list, 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:

```python
from datetime import timedelta

with esett.Client(timeout=120, retries=5, max_window=timedelta(days=90)) as client:
    rows = client.load_profile.values(start="2024-01-01", end="2024-04-01", mba=SE1)
```

| Argument | Default | Purpose |
| --- | --- | --- |
| `base_url` | `https://api.opendata.esett.com` | 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` | `366 days` | Longest span per request; `None` disables splitting |

---

## Endpoints

| API group | Attribute | Methods |
| --- | --- | --- |
| EXP01 Market Parties | `market_parties` | `balance_responsible_parties()`, `balance_service_providers()`, `distribution_system_operators()`, `retailers()` |
| EXP03 Metering Grid Areas | `metering_grid_areas` | `areas()`, `mba_options()` |
| EXP04 Retailer Balance Responsibility | `retailer_balance_responsibility` | `responsibilities()`, `mba_options()` |
| EXP05 Fees | `fees` | `options()`, `all()`, `history()`, `latest()` |
| EXP06 Settlement Banks | — | `settlement_banks()` |
| EXP08 Historical Two Balance Prices | `two_balance_prices` | `values()`, `mba_options()` |
| EXP09 Historical Two Balance Volumes | `two_balance_volumes` | `values()`, `mba_options()` |
| EXP13 Imbalance Volumes | `imbalance_volumes` | `values()`, `aggregate()`, `mba_options()` |
| EXP14 Prices | `prices` | `values()`, `aggregate()`, `mba_options()` |
| EXP15 Consumption | `consumption` | `values()`, `aggregate()`, `mba_options()` |
| EXP16 Production | `production` | `values()`, `aggregate()`, `mba_options()` |
| EXP17 Reconciliation Prices | `reconciliation_prices` | `values()`, `aggregate()`, `mba_options()` |
| EXP18 Load Profile | `load_profile` | `values()`, `aggregate()`, `mba_options()` |

Each attribute is available both on a `Client` instance and at module level
(`esett.production.values(...)`).

Master-data endpoints take optional filters instead of a time range:

```python
esett.market_parties.retailers(country="FI", name="Helen")
esett.metering_grid_areas.areas(mba=FI, mga_type="Consumption")
esett.retailer_balance_responsibility.responsibilities(mba=FI, brp_name="Fortum")
esett.fees.latest(country="FI")
esett.settlement_banks()
```

---

## Contributing

```bash
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
against the vendored spec.

### Keeping up with the API

Only `src/esett/models.py` is generated; the endpoint methods are hand-written.
That split is deliberate — the eSett spec defines no `operationId`, so generated
method names would be unusable, and several response schemas do not match the
live API.

```bash
make spec-check   # fail if the published spec differs from spec/openapi.json
make spec         # refresh the vendored spec
make models       # regenerate src/esett/models.py from it
```

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/esett/_resources.py`.

### Known spec defects

Worked around in the hand-written layer, each verified against the live API:

- `EXP13/Aggregate` declares a `200` with no schema; it returns `ImbalanceVolumeDTO[]`.
- `EXP17/Aggregate` declares a single object; it returns an array.
- `EXP18/MBAOptions` declares a nested array; it returns a flat array.
- Timestamps are typed as plain `string` and `resolution` has no enum. The
  accepted values and the required `yyyy-MM-dd'T'HH:mm:ss.SSSX` format were
  determined from the live API.

---

## License

MIT — see [LICENSE](LICENSE). This project is not affiliated with eSett Oy.
