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 balancing_services import AuthenticatedClient
from balancing_services.api.default import get_imbalance_prices
from balancing_services.models import Area
from datetime import datetime
# Create an authenticated client
client = AuthenticatedClient(
base_url="https://api.balancing.services/v1",
token="YOUR_API_TOKEN"
)
# Get imbalance prices for Estonia
response = get_imbalance_prices.sync_detailed(
client=client,
area=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} {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/v1",
token="YOUR_API_TOKEN"
)
Usage Examples
Get Balancing Energy Bids with Pagination
from balancing_services import AuthenticatedClient
from balancing_services.api.default import get_balancing_energy_bids
from balancing_services.models import Area, ReserveType
from datetime import datetime
client = AuthenticatedClient(base_url="https://api.balancing.services/v1", token="YOUR_TOKEN")
# First page
response = get_balancing_energy_bids.sync_detailed(
client=client,
area=Area.EE,
period_start_at=datetime.fromisoformat("2025-01-01T00:00:00Z"),
period_end_at=datetime.fromisoformat("2025-01-02T00:00:00Z"),
reserve_type=ReserveType.AFRR,
limit=100
)
if response.status_code == 200:
data = response.parsed
print(f"Has more: {data.has_more}")
# Process first page
for bid_group in data.data:
for bid in bid_group.bids:
print(f"Bid: {bid.volume} MW @ {bid.price} {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=Area.EE,
period_start_at=datetime.fromisoformat("2025-01-01T00:00:00Z"),
period_end_at=datetime.fromisoformat("2025-01-02T00:00:00Z"),
reserve_type=ReserveType.AFRR,
cursor=data.next_cursor,
limit=100
)
Async Usage
import asyncio
from balancing_services import AuthenticatedClient
from balancing_services.api.default import get_imbalance_prices
from balancing_services.models import Area
from datetime import datetime
async def fetch_prices():
client = AuthenticatedClient(
base_url="https://api.balancing.services/v1",
token="YOUR_TOKEN"
)
response = await get_imbalance_prices.asyncio_detailed(
client=client,
area=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 balancing_services import AuthenticatedClient
from balancing_services.api.default import get_imbalance_prices
from balancing_services.models import Area
from datetime import datetime
client = AuthenticatedClient(base_url="https://api.balancing.services/v1", token="YOUR_TOKEN")
response = get_imbalance_prices.sync_detailed(
client=client,
area=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- Enums:
Area,ReserveType,Direction,Currency,ActivationType,BidStatus
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/v1",
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: Use the provided enum classes:
from balancing_services.models import Area, ReserveType
# Correct
area=Area.EE
# Incorrect
area="EE" # String might work but not type-safe
Documentation
- API Documentation: https://api.balancing.services/v1/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 1.20.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-1.20.0.tar.gz | 49.1 kB | Details |
Built distribution (wheel)
| File | Interpreter | ABI | Platform | Reset |
|---|---|---|---|---|
| balancing_services-1.20.0-py3-none-any.whl | Python 3 | none | any | Details |
Total release size: 145.3 kB
Release files / balancing_services-1.20.0.tar.gz
| Download URL | balancing_services-1.20.0.tar.gz |
|---|---|
| Size | 49.1 kB |
| Tags | Source |
|
SHA-256 checksum How to use checksums |
4c1d52ef1db6c40e4a2973a5c9d61bff6072b8dc6d3ce70444c9cc8d3896f717
|
|
BLAKE2b-256 checksum How to use checksums |
14f6cb6d6edccde5b89204f88ccdea15b0b3d5e3598c35e01a3bab804976bbbb
|
| Upload date | |
|
Uploaded using Trusted Publishing? What is trusted publishing? |
No |
| Uploaded via |
uv/0.11.28 {"installer":{"name":"uv","version":"0.11.28","subcommand":["publish"]},"python":null,"implementation":{"name":null,"version":null},"distro":{"name":"openSUSE Tumbleweed","version":"20260718","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-1.20.0-py3-none-any.whl
| Download URL | balancing_services-1.20.0-py3-none-any.whl |
|---|---|
| Size | 96.2 kB |
| Tags | Python 3 |
|
SHA-256 checksum How to use checksums |
a9733f78fee52f14ee68c468b6ef074604718be37496840ab636b16f1421de22
|
|
BLAKE2b-256 checksum How to use checksums |
0800a809e80c3f0388dc5722564977091ad69c6c65ecfbf4de63ba95320036eb
|
| Upload date | |
|
Uploaded using Trusted Publishing? What is trusted publishing? |
No |
| Uploaded via |
uv/0.11.28 {"installer":{"name":"uv","version":"0.11.28","subcommand":["publish"]},"python":null,"implementation":{"name":null,"version":null},"distro":{"name":"openSUSE Tumbleweed","version":"20260718","id":null,"libc":null},"system":{"name":null,"release":null},"cpu":null,"openssl_version":null,"setuptools_version":null,"rustc_version":null,"ci":null}
|