balancing-services-cli 1.6.0__tar.gz
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- balancing_services_cli-1.6.0/.env.sample +15 -0
- balancing_services_cli-1.6.0/.gitignore +72 -0
- balancing_services_cli-1.6.0/PKG-INFO +108 -0
- balancing_services_cli-1.6.0/README.md +80 -0
- balancing_services_cli-1.6.0/balancing_services_cli/__init__.py +5 -0
- balancing_services_cli-1.6.0/balancing_services_cli/__main__.py +5 -0
- balancing_services_cli-1.6.0/balancing_services_cli/client_factory.py +23 -0
- balancing_services_cli-1.6.0/balancing_services_cli/commands/__init__.py +0 -0
- balancing_services_cli-1.6.0/balancing_services_cli/commands/capacity.py +186 -0
- balancing_services_cli-1.6.0/balancing_services_cli/commands/energy.py +186 -0
- balancing_services_cli-1.6.0/balancing_services_cli/commands/imbalance.py +77 -0
- balancing_services_cli-1.6.0/balancing_services_cli/flatten.py +119 -0
- balancing_services_cli-1.6.0/balancing_services_cli/main.py +65 -0
- balancing_services_cli-1.6.0/balancing_services_cli/output.py +60 -0
- balancing_services_cli-1.6.0/balancing_services_cli/pagination.py +54 -0
- balancing_services_cli-1.6.0/balancing_services_cli/types.py +25 -0
- balancing_services_cli-1.6.0/check.sh +56 -0
- balancing_services_cli-1.6.0/generate-pyproject.sh +83 -0
- balancing_services_cli-1.6.0/pyproject.toml +62 -0
- balancing_services_cli-1.6.0/pyproject.toml.draft +62 -0
- balancing_services_cli-1.6.0/test-and-publish.sh +418 -0
- balancing_services_cli-1.6.0/tests/__init__.py +0 -0
- balancing_services_cli-1.6.0/tests/stubs.py +156 -0
- balancing_services_cli-1.6.0/tests/test_commands.py +209 -0
- balancing_services_cli-1.6.0/tests/test_flatten.py +195 -0
- balancing_services_cli-1.6.0/tests/test_output.py +75 -0
- balancing_services_cli-1.6.0/tests/test_pagination.py +68 -0
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
# Environment variables for development and publishing
|
|
2
|
+
# Copy this file to .env and fill in your actual tokens
|
|
3
|
+
# Never commit .env to version control!
|
|
4
|
+
|
|
5
|
+
# API key for Balancing Services API (for running examples/tests)
|
|
6
|
+
# Get your API key from: https://balancing.services
|
|
7
|
+
BALANCING_SERVICES_API_KEY=
|
|
8
|
+
|
|
9
|
+
# TestPyPI token (for testing releases)
|
|
10
|
+
# Get your token from: https://test.pypi.org/manage/account/token/
|
|
11
|
+
UV_PUBLISH_TOKEN_TESTPYPI=
|
|
12
|
+
|
|
13
|
+
# Production PyPI token (for production releases)
|
|
14
|
+
# Get your token from: https://pypi.org/manage/account/token/
|
|
15
|
+
UV_PUBLISH_TOKEN_PYPI=
|
|
@@ -0,0 +1,72 @@
|
|
|
1
|
+
# Byte-compiled / optimized / DLL files
|
|
2
|
+
__pycache__/
|
|
3
|
+
*.py[cod]
|
|
4
|
+
*$py.class
|
|
5
|
+
|
|
6
|
+
# C extensions
|
|
7
|
+
*.so
|
|
8
|
+
|
|
9
|
+
# Distribution / packaging
|
|
10
|
+
.Python
|
|
11
|
+
build/
|
|
12
|
+
develop-eggs/
|
|
13
|
+
dist/
|
|
14
|
+
downloads/
|
|
15
|
+
eggs/
|
|
16
|
+
.eggs/
|
|
17
|
+
lib/
|
|
18
|
+
lib64/
|
|
19
|
+
parts/
|
|
20
|
+
sdist/
|
|
21
|
+
var/
|
|
22
|
+
wheels/
|
|
23
|
+
pip-wheel-metadata/
|
|
24
|
+
share/python-wheels/
|
|
25
|
+
*.egg-info/
|
|
26
|
+
.installed.cfg
|
|
27
|
+
*.egg
|
|
28
|
+
MANIFEST
|
|
29
|
+
|
|
30
|
+
# PyInstaller
|
|
31
|
+
*.manifest
|
|
32
|
+
*.spec
|
|
33
|
+
|
|
34
|
+
# Unit test / coverage reports
|
|
35
|
+
htmlcov/
|
|
36
|
+
.tox/
|
|
37
|
+
.nox/
|
|
38
|
+
.coverage
|
|
39
|
+
.coverage.*
|
|
40
|
+
.cache
|
|
41
|
+
nosetests.xml
|
|
42
|
+
coverage.xml
|
|
43
|
+
*.cover
|
|
44
|
+
*.py,cover
|
|
45
|
+
.hypothesis/
|
|
46
|
+
.pytest_cache/
|
|
47
|
+
|
|
48
|
+
# Virtual environments
|
|
49
|
+
venv/
|
|
50
|
+
env/
|
|
51
|
+
ENV/
|
|
52
|
+
env.bak/
|
|
53
|
+
venv.bak/
|
|
54
|
+
.venv/
|
|
55
|
+
|
|
56
|
+
# IDEs
|
|
57
|
+
.vscode/
|
|
58
|
+
.idea/
|
|
59
|
+
*.swp
|
|
60
|
+
*.swo
|
|
61
|
+
*~
|
|
62
|
+
.DS_Store
|
|
63
|
+
|
|
64
|
+
# uv
|
|
65
|
+
.uv/
|
|
66
|
+
uv.lock
|
|
67
|
+
|
|
68
|
+
# Generated from pyproject.toml.draft
|
|
69
|
+
pyproject.toml
|
|
70
|
+
|
|
71
|
+
# Environment variables (contains secrets)
|
|
72
|
+
.env
|
|
@@ -0,0 +1,108 @@
|
|
|
1
|
+
Metadata-Version: 2.4
|
|
2
|
+
Name: balancing-services-cli
|
|
3
|
+
Version: 1.6.0
|
|
4
|
+
Summary: CLI for accessing European electricity balancing market data via the Balancing Services REST API
|
|
5
|
+
Project-URL: Homepage, https://balancing.services
|
|
6
|
+
Project-URL: Documentation, https://api.balancing.services/v1/documentation
|
|
7
|
+
Project-URL: Repository, https://github.com/balancing-services/rest-api
|
|
8
|
+
Author: Balancing Services Team
|
|
9
|
+
License: MIT
|
|
10
|
+
Keywords: api,balancing,cli,electricity,energy,europe,grid,imbalance,market,power
|
|
11
|
+
Classifier: Development Status :: 4 - Beta
|
|
12
|
+
Classifier: Intended Audience :: Developers
|
|
13
|
+
Classifier: License :: OSI Approved :: MIT License
|
|
14
|
+
Classifier: Programming Language :: Python :: 3
|
|
15
|
+
Classifier: Programming Language :: Python :: 3.10
|
|
16
|
+
Classifier: Programming Language :: Python :: 3.11
|
|
17
|
+
Classifier: Programming Language :: Python :: 3.12
|
|
18
|
+
Classifier: Programming Language :: Python :: 3.13
|
|
19
|
+
Classifier: Topic :: Software Development :: Libraries :: Python Modules
|
|
20
|
+
Requires-Python: >=3.10
|
|
21
|
+
Requires-Dist: balancing-services<1.7.0,>=1.6.0
|
|
22
|
+
Requires-Dist: click<9.0.0,>=8.0.0
|
|
23
|
+
Requires-Dist: pyarrow>=14.0.0
|
|
24
|
+
Provides-Extra: dev
|
|
25
|
+
Requires-Dist: pytest>=8.0.0; extra == 'dev'
|
|
26
|
+
Requires-Dist: ruff>=0.8.0; extra == 'dev'
|
|
27
|
+
Description-Content-Type: text/markdown
|
|
28
|
+
|
|
29
|
+
# balancing-services-cli
|
|
30
|
+
|
|
31
|
+
Command-line interface for the [Balancing Services REST API](https://balancing.services) - access European electricity balancing market data directly from your terminal.
|
|
32
|
+
|
|
33
|
+
## Installation
|
|
34
|
+
|
|
35
|
+
```bash
|
|
36
|
+
uv add balancing-services-cli
|
|
37
|
+
```
|
|
38
|
+
|
|
39
|
+
Or with pip:
|
|
40
|
+
|
|
41
|
+
```bash
|
|
42
|
+
pip install balancing-services-cli
|
|
43
|
+
```
|
|
44
|
+
|
|
45
|
+
Or run without installing via `uvx`:
|
|
46
|
+
|
|
47
|
+
```bash
|
|
48
|
+
uvx balancing-services-cli imbalance-prices --area EE --start 2025-01-01T00:00:00Z --end 2025-01-02T00:00:00Z
|
|
49
|
+
```
|
|
50
|
+
|
|
51
|
+
## Authentication
|
|
52
|
+
|
|
53
|
+
Provide your API token via `--token` or the `BALANCING_SERVICES_API_KEY` environment variable:
|
|
54
|
+
|
|
55
|
+
```bash
|
|
56
|
+
export BALANCING_SERVICES_API_KEY=your-token-here
|
|
57
|
+
bs-cli imbalance-prices --area EE --start 2025-01-01T00:00:00Z --end 2025-01-02T00:00:00Z
|
|
58
|
+
```
|
|
59
|
+
|
|
60
|
+
## Usage
|
|
61
|
+
|
|
62
|
+
```bash
|
|
63
|
+
# CSV to stdout (default)
|
|
64
|
+
bs-cli imbalance-prices --area EE --start 2025-01-01T00:00:00Z --end 2025-01-02T00:00:00Z
|
|
65
|
+
|
|
66
|
+
# Save to CSV file
|
|
67
|
+
bs-cli imbalance-prices --area EE --start 2025-01-01T00:00:00Z --end 2025-01-02T00:00:00Z -o prices.csv
|
|
68
|
+
|
|
69
|
+
# Save to Parquet
|
|
70
|
+
bs-cli imbalance-prices --area EE --start 2025-01-01T00:00:00Z --end 2025-01-02T00:00:00Z -o prices.parquet
|
|
71
|
+
|
|
72
|
+
# Balancing energy commands (require --reserve-type)
|
|
73
|
+
bs-cli energy-prices --area EE --start 2025-01-01T00:00:00Z --end 2025-01-02T00:00:00Z --reserve-type aFRR
|
|
74
|
+
bs-cli energy-bids --area EE --start 2025-01-01T00:00:00Z --end 2025-01-02T00:00:00Z --reserve-type mFRR
|
|
75
|
+
|
|
76
|
+
# Balancing capacity commands (require --reserve-type)
|
|
77
|
+
bs-cli capacity-prices --area EE --start 2025-01-01T00:00:00Z --end 2025-01-02T00:00:00Z --reserve-type aFRR
|
|
78
|
+
bs-cli capacity-bids --area EE --start 2025-01-01T00:00:00Z --end 2025-01-02T00:00:00Z --reserve-type FCR
|
|
79
|
+
```
|
|
80
|
+
|
|
81
|
+
## Commands
|
|
82
|
+
|
|
83
|
+
| Command | Description |
|
|
84
|
+
|---|---|
|
|
85
|
+
| `imbalance-prices` | Imbalance prices |
|
|
86
|
+
| `imbalance-volumes` | Imbalance total volumes |
|
|
87
|
+
| `energy-activated` | Balancing energy activated volumes |
|
|
88
|
+
| `energy-offered` | Balancing energy offered volumes |
|
|
89
|
+
| `energy-prices` | Balancing energy prices |
|
|
90
|
+
| `energy-bids` | Balancing energy bids (auto-paginates) |
|
|
91
|
+
| `capacity-bids` | Balancing capacity bids (auto-paginates) |
|
|
92
|
+
| `capacity-prices` | Balancing capacity prices |
|
|
93
|
+
| `capacity-procured` | Balancing capacity procured volumes |
|
|
94
|
+
| `capacity-cross-zonal` | Cross-zonal capacity allocation |
|
|
95
|
+
|
|
96
|
+
## Output Formats
|
|
97
|
+
|
|
98
|
+
- **CSV** (default): Written to stdout or file. Use with Excel, DuckDB, Polars, pandas, etc.
|
|
99
|
+
- **Parquet**: Must specify `-o file.parquet`.
|
|
100
|
+
|
|
101
|
+
|
|
102
|
+
## Global Options
|
|
103
|
+
|
|
104
|
+
| Option | Description |
|
|
105
|
+
|---|---|
|
|
106
|
+
| `--token` | API bearer token (or set `BALANCING_SERVICES_API_KEY`) |
|
|
107
|
+
| `-o, --output` | Output file path (auto-detects format from `.csv`/`.parquet` extension) |
|
|
108
|
+
| `-f, --format` | Override output format (`csv`, `parquet`) |
|
|
@@ -0,0 +1,80 @@
|
|
|
1
|
+
# balancing-services-cli
|
|
2
|
+
|
|
3
|
+
Command-line interface for the [Balancing Services REST API](https://balancing.services) - access European electricity balancing market data directly from your terminal.
|
|
4
|
+
|
|
5
|
+
## Installation
|
|
6
|
+
|
|
7
|
+
```bash
|
|
8
|
+
uv add balancing-services-cli
|
|
9
|
+
```
|
|
10
|
+
|
|
11
|
+
Or with pip:
|
|
12
|
+
|
|
13
|
+
```bash
|
|
14
|
+
pip install balancing-services-cli
|
|
15
|
+
```
|
|
16
|
+
|
|
17
|
+
Or run without installing via `uvx`:
|
|
18
|
+
|
|
19
|
+
```bash
|
|
20
|
+
uvx balancing-services-cli imbalance-prices --area EE --start 2025-01-01T00:00:00Z --end 2025-01-02T00:00:00Z
|
|
21
|
+
```
|
|
22
|
+
|
|
23
|
+
## Authentication
|
|
24
|
+
|
|
25
|
+
Provide your API token via `--token` or the `BALANCING_SERVICES_API_KEY` environment variable:
|
|
26
|
+
|
|
27
|
+
```bash
|
|
28
|
+
export BALANCING_SERVICES_API_KEY=your-token-here
|
|
29
|
+
bs-cli imbalance-prices --area EE --start 2025-01-01T00:00:00Z --end 2025-01-02T00:00:00Z
|
|
30
|
+
```
|
|
31
|
+
|
|
32
|
+
## Usage
|
|
33
|
+
|
|
34
|
+
```bash
|
|
35
|
+
# CSV to stdout (default)
|
|
36
|
+
bs-cli imbalance-prices --area EE --start 2025-01-01T00:00:00Z --end 2025-01-02T00:00:00Z
|
|
37
|
+
|
|
38
|
+
# Save to CSV file
|
|
39
|
+
bs-cli imbalance-prices --area EE --start 2025-01-01T00:00:00Z --end 2025-01-02T00:00:00Z -o prices.csv
|
|
40
|
+
|
|
41
|
+
# Save to Parquet
|
|
42
|
+
bs-cli imbalance-prices --area EE --start 2025-01-01T00:00:00Z --end 2025-01-02T00:00:00Z -o prices.parquet
|
|
43
|
+
|
|
44
|
+
# Balancing energy commands (require --reserve-type)
|
|
45
|
+
bs-cli energy-prices --area EE --start 2025-01-01T00:00:00Z --end 2025-01-02T00:00:00Z --reserve-type aFRR
|
|
46
|
+
bs-cli energy-bids --area EE --start 2025-01-01T00:00:00Z --end 2025-01-02T00:00:00Z --reserve-type mFRR
|
|
47
|
+
|
|
48
|
+
# Balancing capacity commands (require --reserve-type)
|
|
49
|
+
bs-cli capacity-prices --area EE --start 2025-01-01T00:00:00Z --end 2025-01-02T00:00:00Z --reserve-type aFRR
|
|
50
|
+
bs-cli capacity-bids --area EE --start 2025-01-01T00:00:00Z --end 2025-01-02T00:00:00Z --reserve-type FCR
|
|
51
|
+
```
|
|
52
|
+
|
|
53
|
+
## Commands
|
|
54
|
+
|
|
55
|
+
| Command | Description |
|
|
56
|
+
|---|---|
|
|
57
|
+
| `imbalance-prices` | Imbalance prices |
|
|
58
|
+
| `imbalance-volumes` | Imbalance total volumes |
|
|
59
|
+
| `energy-activated` | Balancing energy activated volumes |
|
|
60
|
+
| `energy-offered` | Balancing energy offered volumes |
|
|
61
|
+
| `energy-prices` | Balancing energy prices |
|
|
62
|
+
| `energy-bids` | Balancing energy bids (auto-paginates) |
|
|
63
|
+
| `capacity-bids` | Balancing capacity bids (auto-paginates) |
|
|
64
|
+
| `capacity-prices` | Balancing capacity prices |
|
|
65
|
+
| `capacity-procured` | Balancing capacity procured volumes |
|
|
66
|
+
| `capacity-cross-zonal` | Cross-zonal capacity allocation |
|
|
67
|
+
|
|
68
|
+
## Output Formats
|
|
69
|
+
|
|
70
|
+
- **CSV** (default): Written to stdout or file. Use with Excel, DuckDB, Polars, pandas, etc.
|
|
71
|
+
- **Parquet**: Must specify `-o file.parquet`.
|
|
72
|
+
|
|
73
|
+
|
|
74
|
+
## Global Options
|
|
75
|
+
|
|
76
|
+
| Option | Description |
|
|
77
|
+
|---|---|
|
|
78
|
+
| `--token` | API bearer token (or set `BALANCING_SERVICES_API_KEY`) |
|
|
79
|
+
| `-o, --output` | Output file path (auto-detects format from `.csv`/`.parquet` extension) |
|
|
80
|
+
| `-f, --format` | Override output format (`csv`, `parquet`) |
|
|
@@ -0,0 +1,23 @@
|
|
|
1
|
+
"""Create an authenticated API client from CLI options."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
import logging
|
|
6
|
+
import sys
|
|
7
|
+
|
|
8
|
+
import click
|
|
9
|
+
from balancing_services import AuthenticatedClient
|
|
10
|
+
|
|
11
|
+
BASE_URL = "https://api.balancing.services/v1"
|
|
12
|
+
|
|
13
|
+
log = logging.getLogger(__name__)
|
|
14
|
+
|
|
15
|
+
|
|
16
|
+
def make_client(ctx: click.Context) -> AuthenticatedClient:
|
|
17
|
+
"""Build an AuthenticatedClient from the Click context's global options."""
|
|
18
|
+
token: str | None = ctx.obj.get("token")
|
|
19
|
+
if not token:
|
|
20
|
+
click.echo("Error: API token is required. Use --token or set BALANCING_SERVICES_API_KEY.", err=True)
|
|
21
|
+
sys.exit(1)
|
|
22
|
+
log.debug("Creating client for %s", BASE_URL)
|
|
23
|
+
return AuthenticatedClient(base_url=BASE_URL, token=token)
|
|
File without changes
|
|
@@ -0,0 +1,186 @@
|
|
|
1
|
+
"""Balancing capacity subcommands: capacity-bids, capacity-prices, capacity-procured, capacity-cross-zonal."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
import logging
|
|
6
|
+
from datetime import datetime
|
|
7
|
+
|
|
8
|
+
import click
|
|
9
|
+
from balancing_services.api.default import (
|
|
10
|
+
get_balancing_capacity_bids,
|
|
11
|
+
get_balancing_capacity_prices,
|
|
12
|
+
get_balancing_capacity_procured_volumes,
|
|
13
|
+
get_cross_zonal_capacity_allocation,
|
|
14
|
+
)
|
|
15
|
+
from balancing_services.models import Area, ReserveType
|
|
16
|
+
|
|
17
|
+
from balancing_services_cli.client_factory import make_client
|
|
18
|
+
from balancing_services_cli.flatten import (
|
|
19
|
+
CAPACITY_BIDS,
|
|
20
|
+
CAPACITY_CROSS_ZONAL,
|
|
21
|
+
CAPACITY_PRICES,
|
|
22
|
+
CAPACITY_PROCURED,
|
|
23
|
+
flatten_response,
|
|
24
|
+
)
|
|
25
|
+
from balancing_services_cli.output import write_rows
|
|
26
|
+
from balancing_services_cli.pagination import fetch_all_pages
|
|
27
|
+
from balancing_services_cli.types import ISO8601
|
|
28
|
+
|
|
29
|
+
log = logging.getLogger(__name__)
|
|
30
|
+
|
|
31
|
+
AREA_CHOICES = [a.value for a in Area]
|
|
32
|
+
RESERVE_TYPE_CHOICES = [r.value for r in ReserveType]
|
|
33
|
+
|
|
34
|
+
|
|
35
|
+
@click.command("capacity-bids")
|
|
36
|
+
@click.option(
|
|
37
|
+
"--area",
|
|
38
|
+
required=True,
|
|
39
|
+
type=click.Choice(AREA_CHOICES, case_sensitive=False),
|
|
40
|
+
help="Area code.",
|
|
41
|
+
)
|
|
42
|
+
@click.option("--start", required=True, type=ISO8601, help="Period start (ISO 8601).")
|
|
43
|
+
@click.option("--end", required=True, type=ISO8601, help="Period end (ISO 8601).")
|
|
44
|
+
@click.option(
|
|
45
|
+
"--reserve-type",
|
|
46
|
+
required=True,
|
|
47
|
+
type=click.Choice(RESERVE_TYPE_CHOICES, case_sensitive=False),
|
|
48
|
+
help="Reserve type.",
|
|
49
|
+
)
|
|
50
|
+
@click.pass_context
|
|
51
|
+
def capacity_bids(ctx: click.Context, area: str, start: datetime, end: datetime, reserve_type: str) -> None:
|
|
52
|
+
"""Fetch balancing capacity bids (auto-paginates)."""
|
|
53
|
+
client = make_client(ctx)
|
|
54
|
+
log.debug(
|
|
55
|
+
"GET /balancing/capacity/bids area=%s start=%s end=%s reserve_type=%s",
|
|
56
|
+
area, start, end, reserve_type,
|
|
57
|
+
)
|
|
58
|
+
data = fetch_all_pages(
|
|
59
|
+
get_balancing_capacity_bids.sync_detailed,
|
|
60
|
+
verbose=ctx.obj["verbose"],
|
|
61
|
+
client=client,
|
|
62
|
+
area=Area(area),
|
|
63
|
+
period_start_at=start,
|
|
64
|
+
period_end_at=end,
|
|
65
|
+
reserve_type=ReserveType(reserve_type),
|
|
66
|
+
)
|
|
67
|
+
rows = flatten_response(data, CAPACITY_BIDS)
|
|
68
|
+
log.debug("Flattened to %d row(s)", len(rows))
|
|
69
|
+
write_rows(rows, ctx.obj["output"], ctx.obj["fmt"])
|
|
70
|
+
|
|
71
|
+
|
|
72
|
+
@click.command("capacity-prices")
|
|
73
|
+
@click.option(
|
|
74
|
+
"--area",
|
|
75
|
+
required=True,
|
|
76
|
+
type=click.Choice(AREA_CHOICES, case_sensitive=False),
|
|
77
|
+
help="Area code.",
|
|
78
|
+
)
|
|
79
|
+
@click.option("--start", required=True, type=ISO8601, help="Period start (ISO 8601).")
|
|
80
|
+
@click.option("--end", required=True, type=ISO8601, help="Period end (ISO 8601).")
|
|
81
|
+
@click.option(
|
|
82
|
+
"--reserve-type",
|
|
83
|
+
required=True,
|
|
84
|
+
type=click.Choice(RESERVE_TYPE_CHOICES, case_sensitive=False),
|
|
85
|
+
help="Reserve type.",
|
|
86
|
+
)
|
|
87
|
+
@click.pass_context
|
|
88
|
+
def capacity_prices(ctx: click.Context, area: str, start: datetime, end: datetime, reserve_type: str) -> None:
|
|
89
|
+
"""Fetch balancing capacity prices."""
|
|
90
|
+
client = make_client(ctx)
|
|
91
|
+
log.debug(
|
|
92
|
+
"GET /balancing/capacity/prices area=%s start=%s end=%s reserve_type=%s",
|
|
93
|
+
area, start, end, reserve_type,
|
|
94
|
+
)
|
|
95
|
+
response = get_balancing_capacity_prices.sync_detailed(
|
|
96
|
+
client=client,
|
|
97
|
+
area=Area(area),
|
|
98
|
+
period_start_at=start,
|
|
99
|
+
period_end_at=end,
|
|
100
|
+
reserve_type=ReserveType(reserve_type),
|
|
101
|
+
)
|
|
102
|
+
n_groups = len(response.parsed.data) if response.parsed else 0
|
|
103
|
+
log.debug("Response: HTTP %d, %d group(s)", response.status_code, n_groups)
|
|
104
|
+
if response.status_code != 200:
|
|
105
|
+
raise SystemExit(f"API error (HTTP {response.status_code}): {response.content.decode()}")
|
|
106
|
+
rows = flatten_response(response.parsed.data, CAPACITY_PRICES)
|
|
107
|
+
log.debug("Flattened to %d row(s)", len(rows))
|
|
108
|
+
write_rows(rows, ctx.obj["output"], ctx.obj["fmt"])
|
|
109
|
+
|
|
110
|
+
|
|
111
|
+
@click.command("capacity-procured")
|
|
112
|
+
@click.option(
|
|
113
|
+
"--area",
|
|
114
|
+
required=True,
|
|
115
|
+
type=click.Choice(AREA_CHOICES, case_sensitive=False),
|
|
116
|
+
help="Area code.",
|
|
117
|
+
)
|
|
118
|
+
@click.option("--start", required=True, type=ISO8601, help="Period start (ISO 8601).")
|
|
119
|
+
@click.option("--end", required=True, type=ISO8601, help="Period end (ISO 8601).")
|
|
120
|
+
@click.option(
|
|
121
|
+
"--reserve-type",
|
|
122
|
+
required=True,
|
|
123
|
+
type=click.Choice(RESERVE_TYPE_CHOICES, case_sensitive=False),
|
|
124
|
+
help="Reserve type.",
|
|
125
|
+
)
|
|
126
|
+
@click.pass_context
|
|
127
|
+
def capacity_procured(ctx: click.Context, area: str, start: datetime, end: datetime, reserve_type: str) -> None:
|
|
128
|
+
"""Fetch balancing capacity procured volumes."""
|
|
129
|
+
client = make_client(ctx)
|
|
130
|
+
log.debug(
|
|
131
|
+
"GET /balancing/capacity/procured-volumes area=%s start=%s end=%s reserve_type=%s",
|
|
132
|
+
area, start, end, reserve_type,
|
|
133
|
+
)
|
|
134
|
+
response = get_balancing_capacity_procured_volumes.sync_detailed(
|
|
135
|
+
client=client,
|
|
136
|
+
area=Area(area),
|
|
137
|
+
period_start_at=start,
|
|
138
|
+
period_end_at=end,
|
|
139
|
+
reserve_type=ReserveType(reserve_type),
|
|
140
|
+
)
|
|
141
|
+
n_groups = len(response.parsed.data) if response.parsed else 0
|
|
142
|
+
log.debug("Response: HTTP %d, %d group(s)", response.status_code, n_groups)
|
|
143
|
+
if response.status_code != 200:
|
|
144
|
+
raise SystemExit(f"API error (HTTP {response.status_code}): {response.content.decode()}")
|
|
145
|
+
rows = flatten_response(response.parsed.data, CAPACITY_PROCURED)
|
|
146
|
+
log.debug("Flattened to %d row(s)", len(rows))
|
|
147
|
+
write_rows(rows, ctx.obj["output"], ctx.obj["fmt"])
|
|
148
|
+
|
|
149
|
+
|
|
150
|
+
@click.command("capacity-cross-zonal")
|
|
151
|
+
@click.option(
|
|
152
|
+
"--area",
|
|
153
|
+
required=True,
|
|
154
|
+
type=click.Choice(AREA_CHOICES, case_sensitive=False),
|
|
155
|
+
help="Area code.",
|
|
156
|
+
)
|
|
157
|
+
@click.option("--start", required=True, type=ISO8601, help="Period start (ISO 8601).")
|
|
158
|
+
@click.option("--end", required=True, type=ISO8601, help="Period end (ISO 8601).")
|
|
159
|
+
@click.option(
|
|
160
|
+
"--reserve-type",
|
|
161
|
+
required=True,
|
|
162
|
+
type=click.Choice(RESERVE_TYPE_CHOICES, case_sensitive=False),
|
|
163
|
+
help="Reserve type.",
|
|
164
|
+
)
|
|
165
|
+
@click.pass_context
|
|
166
|
+
def capacity_cross_zonal(ctx: click.Context, area: str, start: datetime, end: datetime, reserve_type: str) -> None:
|
|
167
|
+
"""Fetch cross-zonal capacity allocation."""
|
|
168
|
+
client = make_client(ctx)
|
|
169
|
+
log.debug(
|
|
170
|
+
"GET /balancing/capacity/cross-zonal-allocation area=%s start=%s end=%s reserve_type=%s",
|
|
171
|
+
area, start, end, reserve_type,
|
|
172
|
+
)
|
|
173
|
+
response = get_cross_zonal_capacity_allocation.sync_detailed(
|
|
174
|
+
client=client,
|
|
175
|
+
area=Area(area),
|
|
176
|
+
period_start_at=start,
|
|
177
|
+
period_end_at=end,
|
|
178
|
+
reserve_type=ReserveType(reserve_type),
|
|
179
|
+
)
|
|
180
|
+
n_groups = len(response.parsed.data) if response.parsed else 0
|
|
181
|
+
log.debug("Response: HTTP %d, %d group(s)", response.status_code, n_groups)
|
|
182
|
+
if response.status_code != 200:
|
|
183
|
+
raise SystemExit(f"API error (HTTP {response.status_code}): {response.content.decode()}")
|
|
184
|
+
rows = flatten_response(response.parsed.data, CAPACITY_CROSS_ZONAL)
|
|
185
|
+
log.debug("Flattened to %d row(s)", len(rows))
|
|
186
|
+
write_rows(rows, ctx.obj["output"], ctx.obj["fmt"])
|
|
@@ -0,0 +1,186 @@
|
|
|
1
|
+
"""Balancing energy subcommands: energy-activated, energy-offered, energy-prices, energy-bids."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
import logging
|
|
6
|
+
from datetime import datetime
|
|
7
|
+
|
|
8
|
+
import click
|
|
9
|
+
from balancing_services.api.default import (
|
|
10
|
+
get_balancing_energy_activated_volumes,
|
|
11
|
+
get_balancing_energy_bids,
|
|
12
|
+
get_balancing_energy_offered_volumes,
|
|
13
|
+
get_balancing_energy_prices,
|
|
14
|
+
)
|
|
15
|
+
from balancing_services.models import Area, ReserveType
|
|
16
|
+
|
|
17
|
+
from balancing_services_cli.client_factory import make_client
|
|
18
|
+
from balancing_services_cli.flatten import (
|
|
19
|
+
ENERGY_ACTIVATED,
|
|
20
|
+
ENERGY_BIDS,
|
|
21
|
+
ENERGY_OFFERED,
|
|
22
|
+
ENERGY_PRICES,
|
|
23
|
+
flatten_response,
|
|
24
|
+
)
|
|
25
|
+
from balancing_services_cli.output import write_rows
|
|
26
|
+
from balancing_services_cli.pagination import fetch_all_pages
|
|
27
|
+
from balancing_services_cli.types import ISO8601
|
|
28
|
+
|
|
29
|
+
log = logging.getLogger(__name__)
|
|
30
|
+
|
|
31
|
+
AREA_CHOICES = [a.value for a in Area]
|
|
32
|
+
RESERVE_TYPE_CHOICES = [r.value for r in ReserveType]
|
|
33
|
+
|
|
34
|
+
|
|
35
|
+
@click.command("energy-activated")
|
|
36
|
+
@click.option(
|
|
37
|
+
"--area",
|
|
38
|
+
required=True,
|
|
39
|
+
type=click.Choice(AREA_CHOICES, case_sensitive=False),
|
|
40
|
+
help="Area code.",
|
|
41
|
+
)
|
|
42
|
+
@click.option("--start", required=True, type=ISO8601, help="Period start (ISO 8601).")
|
|
43
|
+
@click.option("--end", required=True, type=ISO8601, help="Period end (ISO 8601).")
|
|
44
|
+
@click.option(
|
|
45
|
+
"--reserve-type",
|
|
46
|
+
required=True,
|
|
47
|
+
type=click.Choice(RESERVE_TYPE_CHOICES, case_sensitive=False),
|
|
48
|
+
help="Reserve type.",
|
|
49
|
+
)
|
|
50
|
+
@click.pass_context
|
|
51
|
+
def energy_activated(ctx: click.Context, area: str, start: datetime, end: datetime, reserve_type: str) -> None:
|
|
52
|
+
"""Fetch balancing energy activated volumes."""
|
|
53
|
+
client = make_client(ctx)
|
|
54
|
+
log.debug(
|
|
55
|
+
"GET /balancing/energy/activated-volumes area=%s start=%s end=%s reserve_type=%s",
|
|
56
|
+
area, start, end, reserve_type,
|
|
57
|
+
)
|
|
58
|
+
response = get_balancing_energy_activated_volumes.sync_detailed(
|
|
59
|
+
client=client,
|
|
60
|
+
area=Area(area),
|
|
61
|
+
period_start_at=start,
|
|
62
|
+
period_end_at=end,
|
|
63
|
+
reserve_type=ReserveType(reserve_type),
|
|
64
|
+
)
|
|
65
|
+
n_groups = len(response.parsed.data) if response.parsed else 0
|
|
66
|
+
log.debug("Response: HTTP %d, %d group(s)", response.status_code, n_groups)
|
|
67
|
+
if response.status_code != 200:
|
|
68
|
+
raise SystemExit(f"API error (HTTP {response.status_code}): {response.content.decode()}")
|
|
69
|
+
rows = flatten_response(response.parsed.data, ENERGY_ACTIVATED)
|
|
70
|
+
log.debug("Flattened to %d row(s)", len(rows))
|
|
71
|
+
write_rows(rows, ctx.obj["output"], ctx.obj["fmt"])
|
|
72
|
+
|
|
73
|
+
|
|
74
|
+
@click.command("energy-offered")
|
|
75
|
+
@click.option(
|
|
76
|
+
"--area",
|
|
77
|
+
required=True,
|
|
78
|
+
type=click.Choice(AREA_CHOICES, case_sensitive=False),
|
|
79
|
+
help="Area code.",
|
|
80
|
+
)
|
|
81
|
+
@click.option("--start", required=True, type=ISO8601, help="Period start (ISO 8601).")
|
|
82
|
+
@click.option("--end", required=True, type=ISO8601, help="Period end (ISO 8601).")
|
|
83
|
+
@click.option(
|
|
84
|
+
"--reserve-type",
|
|
85
|
+
required=True,
|
|
86
|
+
type=click.Choice(RESERVE_TYPE_CHOICES, case_sensitive=False),
|
|
87
|
+
help="Reserve type.",
|
|
88
|
+
)
|
|
89
|
+
@click.pass_context
|
|
90
|
+
def energy_offered(ctx: click.Context, area: str, start: datetime, end: datetime, reserve_type: str) -> None:
|
|
91
|
+
"""Fetch balancing energy offered volumes."""
|
|
92
|
+
client = make_client(ctx)
|
|
93
|
+
log.debug(
|
|
94
|
+
"GET /balancing/energy/offered-volumes area=%s start=%s end=%s reserve_type=%s",
|
|
95
|
+
area, start, end, reserve_type,
|
|
96
|
+
)
|
|
97
|
+
response = get_balancing_energy_offered_volumes.sync_detailed(
|
|
98
|
+
client=client,
|
|
99
|
+
area=Area(area),
|
|
100
|
+
period_start_at=start,
|
|
101
|
+
period_end_at=end,
|
|
102
|
+
reserve_type=ReserveType(reserve_type),
|
|
103
|
+
)
|
|
104
|
+
n_groups = len(response.parsed.data) if response.parsed else 0
|
|
105
|
+
log.debug("Response: HTTP %d, %d group(s)", response.status_code, n_groups)
|
|
106
|
+
if response.status_code != 200:
|
|
107
|
+
raise SystemExit(f"API error (HTTP {response.status_code}): {response.content.decode()}")
|
|
108
|
+
rows = flatten_response(response.parsed.data, ENERGY_OFFERED)
|
|
109
|
+
log.debug("Flattened to %d row(s)", len(rows))
|
|
110
|
+
write_rows(rows, ctx.obj["output"], ctx.obj["fmt"])
|
|
111
|
+
|
|
112
|
+
|
|
113
|
+
@click.command("energy-prices")
|
|
114
|
+
@click.option(
|
|
115
|
+
"--area",
|
|
116
|
+
required=True,
|
|
117
|
+
type=click.Choice(AREA_CHOICES, case_sensitive=False),
|
|
118
|
+
help="Area code.",
|
|
119
|
+
)
|
|
120
|
+
@click.option("--start", required=True, type=ISO8601, help="Period start (ISO 8601).")
|
|
121
|
+
@click.option("--end", required=True, type=ISO8601, help="Period end (ISO 8601).")
|
|
122
|
+
@click.option(
|
|
123
|
+
"--reserve-type",
|
|
124
|
+
required=True,
|
|
125
|
+
type=click.Choice(RESERVE_TYPE_CHOICES, case_sensitive=False),
|
|
126
|
+
help="Reserve type.",
|
|
127
|
+
)
|
|
128
|
+
@click.pass_context
|
|
129
|
+
def energy_prices(ctx: click.Context, area: str, start: datetime, end: datetime, reserve_type: str) -> None:
|
|
130
|
+
"""Fetch balancing energy prices."""
|
|
131
|
+
client = make_client(ctx)
|
|
132
|
+
log.debug(
|
|
133
|
+
"GET /balancing/energy/prices area=%s start=%s end=%s reserve_type=%s",
|
|
134
|
+
area, start, end, reserve_type,
|
|
135
|
+
)
|
|
136
|
+
response = get_balancing_energy_prices.sync_detailed(
|
|
137
|
+
client=client,
|
|
138
|
+
area=Area(area),
|
|
139
|
+
period_start_at=start,
|
|
140
|
+
period_end_at=end,
|
|
141
|
+
reserve_type=ReserveType(reserve_type),
|
|
142
|
+
)
|
|
143
|
+
n_groups = len(response.parsed.data) if response.parsed else 0
|
|
144
|
+
log.debug("Response: HTTP %d, %d group(s)", response.status_code, n_groups)
|
|
145
|
+
if response.status_code != 200:
|
|
146
|
+
raise SystemExit(f"API error (HTTP {response.status_code}): {response.content.decode()}")
|
|
147
|
+
rows = flatten_response(response.parsed.data, ENERGY_PRICES)
|
|
148
|
+
log.debug("Flattened to %d row(s)", len(rows))
|
|
149
|
+
write_rows(rows, ctx.obj["output"], ctx.obj["fmt"])
|
|
150
|
+
|
|
151
|
+
|
|
152
|
+
@click.command("energy-bids")
|
|
153
|
+
@click.option(
|
|
154
|
+
"--area",
|
|
155
|
+
required=True,
|
|
156
|
+
type=click.Choice(AREA_CHOICES, case_sensitive=False),
|
|
157
|
+
help="Area code.",
|
|
158
|
+
)
|
|
159
|
+
@click.option("--start", required=True, type=ISO8601, help="Period start (ISO 8601).")
|
|
160
|
+
@click.option("--end", required=True, type=ISO8601, help="Period end (ISO 8601).")
|
|
161
|
+
@click.option(
|
|
162
|
+
"--reserve-type",
|
|
163
|
+
required=True,
|
|
164
|
+
type=click.Choice(RESERVE_TYPE_CHOICES, case_sensitive=False),
|
|
165
|
+
help="Reserve type.",
|
|
166
|
+
)
|
|
167
|
+
@click.pass_context
|
|
168
|
+
def energy_bids(ctx: click.Context, area: str, start: datetime, end: datetime, reserve_type: str) -> None:
|
|
169
|
+
"""Fetch balancing energy bids (auto-paginates)."""
|
|
170
|
+
client = make_client(ctx)
|
|
171
|
+
log.debug(
|
|
172
|
+
"GET /balancing/energy/bids area=%s start=%s end=%s reserve_type=%s",
|
|
173
|
+
area, start, end, reserve_type,
|
|
174
|
+
)
|
|
175
|
+
data = fetch_all_pages(
|
|
176
|
+
get_balancing_energy_bids.sync_detailed,
|
|
177
|
+
verbose=ctx.obj["verbose"],
|
|
178
|
+
client=client,
|
|
179
|
+
area=Area(area),
|
|
180
|
+
period_start_at=start,
|
|
181
|
+
period_end_at=end,
|
|
182
|
+
reserve_type=ReserveType(reserve_type),
|
|
183
|
+
)
|
|
184
|
+
rows = flatten_response(data, ENERGY_BIDS)
|
|
185
|
+
log.debug("Flattened to %d row(s)", len(rows))
|
|
186
|
+
write_rows(rows, ctx.obj["output"], ctx.obj["fmt"])
|