Metadata-Version: 2.4
Name: enappsys
Version: 0.3.0
Summary: The EnAppSys Python client provides a light-weight client that allows for simple access to EnAppSys' API services.
Author-email: Silvan Murre <silvan.murre@montel.energy>, Andras Rozs <andras.rozs@montel.energy>
License-Expression: MIT
Classifier: Programming Language :: Python :: 3
Classifier: Operating System :: OS Independent
Requires-Python: >=3.10
Description-Content-Type: text/markdown
License-File: LICENSE
Requires-Dist: requests
Provides-Extra: async
Requires-Dist: aiohttp; extra == "async"
Provides-Extra: pandas
Requires-Dist: pandas; extra == "pandas"
Requires-Dist: tzdata; extra == "pandas"
Provides-Extra: dev
Requires-Dist: enappsys[async]; extra == "dev"
Requires-Dist: enappsys[pandas]; extra == "dev"
Requires-Dist: pytest-asyncio; extra == "dev"
Requires-Dist: pytest-benchmark; extra == "dev"
Requires-Dist: pytest; extra == "dev"
Requires-Dist: ruff; extra == "dev"
Dynamic: license-file

<picture align="center">
  <a href="https://app.enappsys.com/">
    <img alt="EnAppSys" src="docs/assets/enappsys.svg">
  </a>
</picture>

# EnAppSys Python Client

The Python library for the [EnAppSys](https://app.enappsys.com) platform provides a light-weight, typed Python client to interact with EnAppSys' API services. Additionally, there is an asynchronous client for non-blocking operations.

## Project Status

This project is in active development (0.x release series).
While the core API structure is expected to remain stable,
minor breaking changes may occur between releases.

## Installation

Supports Python 3.10+

```bash
pip install enappsys[pandas,async]
```

The extras are optional:

- `pandas` required for converting API responses to DataFrames, e.g. via `client_response.to_df()`
- `async` required for using the `EnAppSysAsync` asynchronous client.

If you only need the synchronous client and raw responses, install without extras:

```bash
pip install enappsys
```

### Configuring credentials

Your EnAppSys username and secret are required to make API requests. You can obtain these as follows:

1. Go to any download page on EnAppSys and click **Copy API URL**.
2. In the copied URL:

    - The value after `user=` is your **username**.
    - The value after `pass=` is your **secret** (a long numeric string).


The client looks for credentials in the following order:

1. **Direct arguments** when creating the client:

    ```python
    from enappsys import EnAppSys

    client = EnAppSys(
        user="example_user",
        secret="123456789123456789123456789123456789"
    )
    ```
    
2. **Environment variables**:

    ```bash
    export ENAPPSYS_USER=example_user
    export ENAPPSYS_SECRET=123456789123456789123456789123456789
    ```

3. **Credentials file** at your home directory, the default location is: `~/.credentials/enappsys.json`:

    ```json
    {
        "user": "example_user",
        "secret": "123456789123456789123456789123456789"
    }
    ```

    You can also save and specify a custom path:

    ```python
    client = EnAppSys(credentials_file="path/to/credentials.json")
    ```

#### Credentials in logs

The platform authenticates with `user` and `pass` query parameters, so every
request URL contains them. Creating a client therefore attaches a logging filter
to `urllib3.connectionpool` and `aiohttp.client` that replaces the secret with
`<redacted>`, because those libraries log the request line they send and would
otherwise write it into any log with DEBUG enabled.

The username and the rest of the URL are left alone, so the logs stay useful and
a request can still be traced back to an account:

```
https://app.enappsys.com:443 "GET /csvapi?type=ENTSOE_DAY_AHEAD_PRICES&res=qh&user=your-username&pass=<redacted> HTTP/1.1" 200 None
```

## Usage

The EnAppSys client provides several download interfaces, depending on your user permissions.

### Bulk API

The Bulk API is a subscription service that allows you to retrieve time series data.
A **data type** represents a group of related series, and each individual series within that group is referred to as an **entity**.

The web interface for browsing available data types and entities is available at:
[https://app.enappsys.com/#dataservicecsv](https://app.enappsys.com/#dataservicecsv)

Retrieve the `DA_PRICE` and `DA_VOLUME` entities belonging to `EPEX_HR_AUCTION_RESULTS_DE` and convert them to a pandas `DataFrame`. When converting to a `DataFrame`, you can also rename the columns:

```python
day_ahead = client.bulk.get(
    "csv",
    data_type="EPEX_HR_AUCTION_RESULTS_DE",
    entities=["DA_PRICE", "DA_VOLUME"],
    start_dt="2025-01-01T00:00",
    end_dt="2025-01-02T00:00",
    resolution="qh",
    time_zone="CET",
)

df = day_ahead.to_df(rename_columns=["price", "volume"])
```

To retrieve **all entities** for a given `data_type`, omit the `entities` argument or pass `None`:

```python
day_ahead_all = client.bulk.get(
    "csv",
    data_type="EPEX_HR_AUCTION_RESULTS_DE",
    start_dt="2025-01-01T00:00",
    end_dt="2025-01-02T00:00",
    resolution="qh",
    time_zone="CET",
)

df_all = day_ahead_all.to_df()
```

The Bulk API supports multiple response formats:

- `"csv"`
- `"json"`
- `"json_map"`
- `"xml"`

The JSON-based formats optionally include metadata fields:

- `timestamp`: Indicates when the data was first entered into the database or created as a forecast (UTC).
- `last_updated`: Indicates the last time the data was updated in the database (UTC).

These fields can be included when converting the response to a DataFrame:

```python
data = client.bulk.get(
    "json",
    data_type="EPEX_HR_AUCTION_RESULTS_DE",
    start_dt="2025-01-01T00:00",
    end_dt="2025-01-02T00:00",
    resolution="qh",
    time_zone="CET",
)

df = data.to_df(timestamp=True, last_updated=True)
```

#### Units

Series with a unit assigned have a base unit, shown in the chart info tooltip on
the platform. They are returned in that unit unless you pass `units`, in which
case the platform converts them. For example, API2 coal is quoted in USD/teCOAL:

```python
coal = client.bulk.get(
    "csv",
    data_type="spectron_closing_prices_coal_api2_cif_ara_mid",
    entities=["PREVAILING"],
    start_dt="2026-09-01T00:00",
    end_dt="2026-09-02T00:00",
    resolution="daily",
    time_zone="CET",
    units="EUR/MWh",   # or units="USD/teCOAL" (the base unit, same as omitting it)
)

df = coal.to_df(unit_in_columns=True)   # column "PREVAILING (EUR/MWh)"
```

An unknown unit name fails with HTTP 400. A known unit that the series cannot be
converted to does not raise or warn: the data comes back in its base unit, but
labelled with the unit you asked for (EUR/MWh gas requested as `EUR/te` returns
the EUR/MWh values labelled `EUR/te`).

A mapping of datatype-entities to their base units is currently not available
through the API. You can read the base unit in either of these ways:

- **Charts:** open the chart information, select the series in the dropdown and
  read its Base Unit.
- **Bulk API:** download a short range without `units`. In CSV the unit is in the
  Units column (`to_df(unit_in_columns=True)` adds it to the column names). In
  JSON it is under `metadata.dataTypes["<DATA_TYPE>.<ENTITY>"].unit`, e.g.
  `"unit": "USD/teCOAL"` for `SPECTRON_CLOSING_PRICES_COAL_API2_CIF_ARA_MID.PREVAILING`.

#### Large requests

A wide date range can be split into several requests and stitched back
together, so a long history arrives as one result. It is off unless you ask for
it:

```python
data = client.bulk.get(
    "csv",
    data_type="ENTSOE_AGGREGATED_GENERATION_PER_TYPE",
    entities=["DE.GERMANY_SOLAR"],
    start_dt="2023-01-01T00:00",
    end_dt="2026-01-01T00:00",
    resolution="qh",
    time_zone="UTC",
    chunk_rows=True,
)
```

`chunk_rows` takes three forms:

| value | effect |
| --- | --- |
| omitted, `None` or `False` | one request |
| `True` | split at `CHUNK_ROWS` (5,000) |
| an `int` | split at that many rows |

`True` is the one to reach for first. Name a number instead when you want
fewer, larger requests, or more, smaller ones. Either way the count is cells
rather than rows, since requesting two entities returns two series worth of
data. On the asynchronous client the chunks are fetched concurrently.

Worth knowing before you opt in: several requests are not one request. For example, if the
data is updated while the chunks are in flight, the later ones return the new
values and the earlier ones the old, and a stitched result does not show that
it happened. Leave it off where that matters.

### Chart API

The Chart API extracts data directly from charts available on the EnAppSys platform.

Each chart is identified by a **code**, which can be found in the page URL.
For example:

```
https://app.enappsys.com/#de/elec/pricing/daprices/chart
```

The chart code is the part between `#` and `/chart`, in this case:

```
de/elec/pricing/daprices
```

Example usage:

```python
day_ahead_chart = client.chart.get(
    "csv",
    code="de/elec/pricing/daprices",
    start_dt="2025-01-01T00:00",
    end_dt="2025-01-02T00:00",
    resolution="qh",
    time_zone="CET",
)

df_day_ahead_chart = day_ahead_chart.to_df()
```

Some charts support the optional `enable_settlement_period` flag:

```python
settlement_chart = client.chart.get(
    "csv",
    code="gb/elec/pricing/daprices",
    resolution="hh",
    start_dt="2026-07-13T10:49",
    end_dt="2026-07-13T18:49",
    time_zone="WET",
    currency="GBP",
    enable_settlement_period=True,
)
df_settlement = settlement_chart.to_df()
```

Some charts also support alternative time window modes via `time_display`:

```python
rolling_chart = client.chart.get(
    "csv",
    code="gb/elec/pricing/daprice/ensemble/forecast",
    resolution="hh",
    time_zone="WET",
    currency="GBP",
    enable_settlement_period=True,
    time_display={
        "mode": "rolling",
        "periodback": "daily",
        "amountback": 4,
        "periodfor": "min",
        "amountfor": 4,
    },
)
df_rolling = rolling_chart.to_df()
```

CSV chart requests also take `units`, spelled as on the Bulk API
([Units](#units)). Only units offered in the chart's Units dropdown, in the
requested `currency`, are applied; anything else raises `ValidationError`
instead of silently returning the base unit:

```python
coal_chart = client.chart.get(
    "csv",
    code="de/elec/spectron/coal",
    start_dt="2026-09-01T00:00",
    end_dt="2026-09-02T00:00",
    resolution="daily",
    time_zone="CET",
    currency="EUR",
    units="EUR/MWh 55% Eff",
)
```

> [!NOTE]
> Some charts contain non-timeseries data and may have a different structure.
> Below chart types are supported. If you encounter a chart that is not yet supported, please open an issue and include a link to the chart.

### Price Volume Curves

The Price Volume Curve API retrieves auction price-volume curve data for a given timestamp.

```python
hu_price_volume_curve = client.price_volume_curve.get(
    "csv",
    code="hu/elec/ancillary/capacity/afrr/daily/up",
    dt="2025-01-01T00:00",
    time_zone="CET",
    currency="EUR",
)

df_curve = hu_price_volume_curve.to_df()
```

The `dt` parameter represents the auction timestamp for which the curve should be retrieved.

### EPEX Trade Evolution And Contracts

The EPEX service uses the `apxdownload` endpoint and is exposed as `client.epex`. It currently supports settlement trade evolution downloads and contract evolution downloads in `"csv"`, `"json"`, and `"xml"` formats.

Settlement trade evolution is available under `client.epex.settlement`. You can identify a quarter-hour either with `settlement_date` plus `settlement_period`, or with a single `settlement_datetime`:

```python
trade_evo = client.epex.settlement.get(
    "csv",
    code="nl/elec/epex/tradeevo/download",
    settlement_datetime="2025-01-01T12:07",
    max_points=200,
    time_zone="CET",
)

trade_evo_df = trade_evo.to_df(
    rename_columns=["qh_price", "qh_vol", "hh_price", "hh_vol", "hr_price", "hr_vol"],
    unit_in_columns=True,
)
```

Contract evolution is available under `client.epex.contract`:

```python
contract_evo = client.epex.contract.get(
    "csv",
    code="nl/elec/spectron/power/baseload/evolution/download",
    contract="DEC-25",
    max_points="all",
    time_zone="CET",
)

contract_evo_df = contract_evo.to_df(unit_in_columns=True)
```

## Asynchronous

An asynchronous client (`EnAppSysAsync`) is available for non-blocking and concurrent request execution.

The asynchronous interface is currently under active development.
Usage examples and extended documentation will be added in a future release.

## License

This project is licensed under the terms of the MIT license.
