Balancing Services Python Client
Python client library for the Balancing Services REST API. Access European electricity balancing market data with a modern, type-safe Python interface.
Features
- Type-safe: Full type hints for better IDE support and code quality
- Modern Python: Built with httpx and attrs for async support and clean data models
- Auto-generated: Generated from the official OpenAPI specification
- Comprehensive: Access to all API endpoints and data models
- Well-documented: Inline documentation and usage examples
Installation
uv pip install balancing-services
Or with pip:
pip install balancing-services
For development, install from source:
cd clients/python
uv pip install -e .
Quick Start
from datetime import datetime
from balancing_services import AuthenticatedClient
from balancing_services.api.default import get_imbalance_prices
# Create an authenticated client
client = AuthenticatedClient(base_url="https://api.balancing.services/v2", token="YOUR_API_TOKEN")
# Get imbalance prices for Estonia. Enum-typed parameters take plain string
# values (e.g. area="EE") — see "Data Models" below.
response = get_imbalance_prices.sync_detailed(
client=client,
area="EE",
period_start_at=datetime.fromisoformat("2025-01-01T00:00:00Z"),
period_end_at=datetime.fromisoformat("2025-01-02T00:00:00Z"),
)
if response.status_code == 200:
data = response.parsed
print(f"Query period: {data.queried_period.start_at} to {data.queried_period.end_at}")
for imbalance_prices in data.data:
print(f"Area: {imbalance_prices.area}, Direction: {imbalance_prices.direction}")
for price in imbalance_prices.prices:
print(f" {price.period.start_at}: {price.price_per_mwh} {imbalance_prices.currency}")
Authentication
To obtain an API token:
- Email: info@balancing.services
- Website: https://balancing.services
Include your token when creating the client:
from balancing_services import AuthenticatedClient
client = AuthenticatedClient(base_url="https://api.balancing.services/v2", token="YOUR_API_TOKEN")
Usage Examples
Get Balancing Energy Bids with Pagination
from datetime import datetime
from balancing_services import AuthenticatedClient
from balancing_services.api.default import get_balancing_energy_bids
client = AuthenticatedClient(base_url="https://api.balancing.services/v2", token="YOUR_TOKEN")
# First page
response = get_balancing_energy_bids.sync_detailed(
client=client,
area="EE",
period_start_at=datetime.fromisoformat("2025-01-01T00:00:00Z"),
period_end_at=datetime.fromisoformat("2025-01-02T00:00:00Z"),
reserve_type="aFRR",
limit=100,
)
if response.status_code == 200:
data = response.parsed
print(f"Has more: {data.has_more}")
# Process first page. Each group carries one entry per delivery period.
for bid_group in data.data:
for period in bid_group.periods:
for bid in period.bids:
print(f"Bid: {bid.volume_in_mw} MW @ {bid.price_per_mwh} {bid_group.currency}")
# Get next page if available
if data.has_more and data.next_cursor:
next_response = get_balancing_energy_bids.sync_detailed(
client=client,
area="EE",
period_start_at=datetime.fromisoformat("2025-01-01T00:00:00Z"),
period_end_at=datetime.fromisoformat("2025-01-02T00:00:00Z"),
reserve_type="aFRR",
cursor=data.next_cursor,
limit=100,
)
Async Usage
import asyncio
from datetime import datetime
from balancing_services import AuthenticatedClient
from balancing_services.api.default import get_imbalance_prices
async def fetch_prices():
client = AuthenticatedClient(base_url="https://api.balancing.services/v2", token="YOUR_TOKEN")
response = await get_imbalance_prices.asyncio_detailed(
client=client,
area="EE",
period_start_at=datetime.fromisoformat("2025-01-01T00:00:00Z"),
period_end_at=datetime.fromisoformat("2025-01-02T00:00:00Z"),
)
if response.status_code == 200:
return response.parsed
return None
# Run async function
prices = asyncio.run(fetch_prices())
Error Handling
from datetime import datetime
from balancing_services import AuthenticatedClient
from balancing_services.api.default import get_imbalance_prices
client = AuthenticatedClient(base_url="https://api.balancing.services/v2", token="YOUR_TOKEN")
response = get_imbalance_prices.sync_detailed(
client=client,
area="EE",
period_start_at=datetime.fromisoformat("2025-01-01T00:00:00Z"),
period_end_at=datetime.fromisoformat("2025-01-02T00:00:00Z"),
)
if response.status_code == 200:
data = response.parsed
# Process successful response
elif response.status_code == 400:
error = response.parsed
print(f"Bad request: {error.detail}")
elif response.status_code == 401:
print("Authentication failed - check your API token")
elif response.status_code == 429:
print("Rate limited - please retry later")
else:
print(f"Error {response.status_code}: {response.content}")
Data Models
All response and request models are fully typed using attrs. Key models include:
ImbalancePricesResponse,ImbalanceTotalVolumesResponseBalancingEnergyVolumesResponse,BalancingEnergyPricesResponse,BalancingEnergyBidsResponseBalancingCapacityBidsResponse,BalancingCapacityPricesResponse,BalancingCapacityVolumesResponse- Enum types:
Area,ReserveType,Direction,Currency,ActivationType,BidStatus
Enum-typed fields and parameters are plain strings. Each enum is a Literal
string type (e.g. Area = Literal["EE", "FI", ...]) with a companion
*_VALUES set of accepted values — there is no Area.EE member access or
.value accessor. Pass the bare wire string (area="EE", reserve_type="aFRR")
and read fields back as strings.
Development
Regenerating the Client
The client is generated from the OpenAPI specification. To regenerate:
cd clients/python
./generate.sh
Running Tests
# Install development dependencies
uv pip install -e ".[dev]"
# Run tests
pytest
# Run tests with coverage
pytest --cov=balancing_services --cov-report=html
Code Quality
# Format code
ruff format .
# Lint code
ruff check .
# Type checking
mypy balancing_services
Automation Scripts
The Python client includes automation scripts for development and publishing:
generate-pyproject.sh
Generates pyproject.toml from the template with the current version from openapi.yaml.
Note: The version is stored in openapi.yaml (single source of truth). The pyproject.toml.draft template contains invalid TOML (version = __VERSION__ without quotes) to prevent accidental use without proper generation.
./generate-pyproject.sh
This script is automatically called by check.sh and test-and-publish.sh.
check.sh
Runs all quality checks in one command:
./check.sh
Performs:
- Generates
pyproject.tomlfrom draft - Runs tests with pytest
- Runs linting with ruff
- Runs type checking with mypy
- Verifies the package builds
test-and-publish.sh
Complete publishing workflow with TestPyPI validation:
Setup:
-
Copy
.env.sampleto.envand fill in your credentials:cp .env.sample .env # Edit .env and add: # BALANCING_SERVICES_API_KEY (required - for testing against live API) # UV_PUBLISH_TOKEN_TESTPYPI (required) # UV_PUBLISH_TOKEN_PYPI (required)
-
Run the publish script:
./test-and-publish.sh
Emergency option - skip API tests if the server is down:
./test-and-publish.sh --skip-api-tests
⚠️ This skips testing against the live API. Use only in emergencies!
Alternatively, export variables manually:
export BALANCING_SERVICES_API_KEY="your-api-key"
export UV_PUBLISH_TOKEN_TESTPYPI="your-testpypi-token"
export UV_PUBLISH_TOKEN_PYPI="your-pypi-token"
./test-and-publish.sh
What it does:
- Loads credentials from
.env(if present) - Verifies all required environment variables are set
- Runs quality checks
- Publishes to TestPyPI using
UV_PUBLISH_TOKEN_TESTPYPI - Creates isolated test sandbox
- Installs package from TestPyPI
- Runs all example scripts against live API using
BALANCING_SERVICES_API_KEY - Prompts to publish to PyPI using
UV_PUBLISH_TOKEN_PYPIif tests pass - Verifies production PyPI package by installing and running examples again
Get your credentials:
- API Key: https://balancing.services
- TestPyPI: https://test.pypi.org/manage/account/token/
- PyPI: https://pypi.org/manage/account/token/
For maintainers: See ../../scripts/README.md for the complete release workflow.
Troubleshooting
Authentication Errors (401)
Problem: Receiving 401 Unauthorized responses
Solutions:
- Verify your API token is correct
- Check that the token is being passed to
AuthenticatedClient - Ensure your token hasn't expired (contact support if needed)
Bad Request Errors (400)
Problem: Receiving 400 Bad Request responses
Common causes:
- Invalid date range (end date before start date)
- Date range too large
- Invalid area code or reserve type
- Malformed datetime strings
Solution: Check the error detail in the response for specific information:
if response.status_code == 400:
error = response.parsed
print(f"Error detail: {error.detail}")
Empty Results
Problem: Receiving 200 OK but no data
Possible reasons:
- No data available for the requested period
- Data not yet published for recent periods
- Requesting data for a period before data collection started
Solution: Try a different time period or check if data exists for your area
Timeout Issues
Problem: Requests timing out
Solutions:
- Increase the client timeout:
client = AuthenticatedClient(
base_url="https://api.balancing.services/v2",
token="YOUR_TOKEN",
timeout=30.0, # Increase from default
)
- Reduce the date range in your request
- Use pagination for large datasets
Type Errors with Enums
Problem: Type errors when passing area or reserve type
Solution: Pass the bare wire string value. Enum-typed parameters are
Literal string types, so the string itself is the type-safe value — there is
no enum member to import:
# Correct
area = "EE"
reserve_type = "aFRR"
The accepted values for each enum are available as a *_VALUES set if you need
to validate or enumerate them:
from balancing_services.models.area import AREA_VALUES
assert "EE" in AREA_VALUES
Documentation
- API Documentation: https://api.balancing.services/v2/documentation
- OpenAPI Spec: openapi.yaml
- Main Repository: https://github.com/balancing-services/rest-api
Support
- Website: https://balancing.services
- Email: info@balancing.services
- Issues: GitHub Issues
License
MIT License - see LICENSE for details.
Changelog
See CHANGELOG.md for version history and changes.
Metadata
Release files for balancing-services 2.5.0
For a detailed explanation of source distributions (sdists) and built distributions (wheels), please see the package formats documentation.
Source distribution (sdist)
| File | Size | Uploaded | |
|---|---|---|---|
| balancing_services-2.5.0.tar.gz | 63.4 kB | Details |
Built distribution (wheel)
| File | Interpreter | ABI | Platform | Reset |
|---|---|---|---|---|
| balancing_services-2.5.0-py3-none-any.whl | Python 3 | none | any | Details |
Total release size: 210.7 kB
Release files / balancing_services-2.5.0.tar.gz
| Download URL | balancing_services-2.5.0.tar.gz |
|---|---|
| Size | 63.4 kB |
| Tags | Source |
|
SHA-256 checksum How to use checksums |
9f7941747e60785d98393506fc2b6c2435f0f1a480818e3d6ab9cdf095aae973
|
|
BLAKE2b-256 checksum How to use checksums |
1778950bb049a373ae25512d021b0540c4c136ad452a9a73c7cd4f6339408b30
|
| Upload date | |
|
Uploaded using Trusted Publishing? What is trusted publishing? |
No |
| Uploaded via |
uv/0.12.0 {"installer":{"name":"uv","version":"0.12.0","subcommand":["publish"]},"python":null,"implementation":{"name":null,"version":null},"distro":{"name":"openSUSE Tumbleweed","version":"20260731","id":null,"libc":null},"system":{"name":null,"release":null},"cpu":null,"openssl_version":null,"setuptools_version":null,"rustc_version":null,"ci":null}
|
Release files / balancing_services-2.5.0-py3-none-any.whl
| Download URL | balancing_services-2.5.0-py3-none-any.whl |
|---|---|
| Size | 147.3 kB |
| Tags | Python 3 |
|
SHA-256 checksum How to use checksums |
acf29c57d1b5e0c4336b44632ac04fabf7c7755ebdf260e0a75d06d7d41642a4
|
|
BLAKE2b-256 checksum How to use checksums |
0cf4a1460a8612e2e29d46246a9e41d1d2163ac87d9a76ecb17ad659a94bdb77
|
| Upload date | |
|
Uploaded using Trusted Publishing? What is trusted publishing? |
No |
| Uploaded via |
uv/0.12.0 {"installer":{"name":"uv","version":"0.12.0","subcommand":["publish"]},"python":null,"implementation":{"name":null,"version":null},"distro":{"name":"openSUSE Tumbleweed","version":"20260731","id":null,"libc":null},"system":{"name":null,"release":null},"cpu":null,"openssl_version":null,"setuptools_version":null,"rustc_version":null,"ci":null}
|