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.
Files changed (27) hide show
  1. balancing_services_cli-1.6.0/.env.sample +15 -0
  2. balancing_services_cli-1.6.0/.gitignore +72 -0
  3. balancing_services_cli-1.6.0/PKG-INFO +108 -0
  4. balancing_services_cli-1.6.0/README.md +80 -0
  5. balancing_services_cli-1.6.0/balancing_services_cli/__init__.py +5 -0
  6. balancing_services_cli-1.6.0/balancing_services_cli/__main__.py +5 -0
  7. balancing_services_cli-1.6.0/balancing_services_cli/client_factory.py +23 -0
  8. balancing_services_cli-1.6.0/balancing_services_cli/commands/__init__.py +0 -0
  9. balancing_services_cli-1.6.0/balancing_services_cli/commands/capacity.py +186 -0
  10. balancing_services_cli-1.6.0/balancing_services_cli/commands/energy.py +186 -0
  11. balancing_services_cli-1.6.0/balancing_services_cli/commands/imbalance.py +77 -0
  12. balancing_services_cli-1.6.0/balancing_services_cli/flatten.py +119 -0
  13. balancing_services_cli-1.6.0/balancing_services_cli/main.py +65 -0
  14. balancing_services_cli-1.6.0/balancing_services_cli/output.py +60 -0
  15. balancing_services_cli-1.6.0/balancing_services_cli/pagination.py +54 -0
  16. balancing_services_cli-1.6.0/balancing_services_cli/types.py +25 -0
  17. balancing_services_cli-1.6.0/check.sh +56 -0
  18. balancing_services_cli-1.6.0/generate-pyproject.sh +83 -0
  19. balancing_services_cli-1.6.0/pyproject.toml +62 -0
  20. balancing_services_cli-1.6.0/pyproject.toml.draft +62 -0
  21. balancing_services_cli-1.6.0/test-and-publish.sh +418 -0
  22. balancing_services_cli-1.6.0/tests/__init__.py +0 -0
  23. balancing_services_cli-1.6.0/tests/stubs.py +156 -0
  24. balancing_services_cli-1.6.0/tests/test_commands.py +209 -0
  25. balancing_services_cli-1.6.0/tests/test_flatten.py +195 -0
  26. balancing_services_cli-1.6.0/tests/test_output.py +75 -0
  27. 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,5 @@
1
+ """Balancing Services CLI - command-line access to European electricity balancing market data."""
2
+
3
+ from importlib.metadata import version
4
+
5
+ __version__ = version("balancing-services-cli")
@@ -0,0 +1,5 @@
1
+ """Allow running as python -m balancing_services_cli."""
2
+
3
+ from balancing_services_cli.main import cli
4
+
5
+ cli()
@@ -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)
@@ -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"])