frequenz-gridpool 0.7.0__tar.gz → 0.7.1__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.
- {frequenz_gridpool-0.7.0/src/frequenz_gridpool.egg-info → frequenz_gridpool-0.7.1}/PKG-INFO +27 -2
- {frequenz_gridpool-0.7.0 → frequenz_gridpool-0.7.1}/README.md +26 -1
- frequenz_gridpool-0.7.1/RELEASE_NOTES.md +42 -0
- {frequenz_gridpool-0.7.0 → frequenz_gridpool-0.7.1}/src/frequenz/gridpool/cli/__main__.py +126 -30
- {frequenz_gridpool-0.7.0 → frequenz_gridpool-0.7.1}/src/frequenz/gridpool/cli/_dump_config.py +11 -6
- {frequenz_gridpool-0.7.0 → frequenz_gridpool-0.7.1}/src/frequenz/gridpool/cli/_patch_config.py +63 -19
- {frequenz_gridpool-0.7.0 → frequenz_gridpool-0.7.1}/src/frequenz/gridpool/config/__init__.py +2 -0
- {frequenz_gridpool-0.7.0 → frequenz_gridpool-0.7.1}/src/frequenz/gridpool/config/_assets.py +80 -2
- frequenz_gridpool-0.7.1/src/frequenz/gridpool/config/_gridpool.py +17 -0
- {frequenz_gridpool-0.7.0 → frequenz_gridpool-0.7.1}/src/frequenz/gridpool/config/_load.py +3 -0
- {frequenz_gridpool-0.7.0 → frequenz_gridpool-0.7.1/src/frequenz_gridpool.egg-info}/PKG-INFO +27 -2
- {frequenz_gridpool-0.7.0 → frequenz_gridpool-0.7.1}/src/frequenz_gridpool.egg-info/SOURCES.txt +1 -0
- {frequenz_gridpool-0.7.0 → frequenz_gridpool-0.7.1}/src/frequenz_gridpool.egg-info/scm_file_list.json +1 -0
- frequenz_gridpool-0.7.1/src/frequenz_gridpool.egg-info/scm_version.json +8 -0
- frequenz_gridpool-0.7.0/RELEASE_NOTES.md +0 -119
- frequenz_gridpool-0.7.0/src/frequenz_gridpool.egg-info/scm_version.json +0 -8
- {frequenz_gridpool-0.7.0 → frequenz_gridpool-0.7.1}/LICENSE +0 -0
- {frequenz_gridpool-0.7.0 → frequenz_gridpool-0.7.1}/MANIFEST.in +0 -0
- {frequenz_gridpool-0.7.0 → frequenz_gridpool-0.7.1}/pyproject.toml +0 -0
- {frequenz_gridpool-0.7.0 → frequenz_gridpool-0.7.1}/setup.cfg +0 -0
- {frequenz_gridpool-0.7.0 → frequenz_gridpool-0.7.1}/src/frequenz/gridpool/__init__.py +0 -0
- {frequenz_gridpool-0.7.0 → frequenz_gridpool-0.7.1}/src/frequenz/gridpool/_graph_generator.py +0 -0
- {frequenz_gridpool-0.7.0 → frequenz_gridpool-0.7.1}/src/frequenz/gridpool/cli/__init__.py +0 -0
- {frequenz_gridpool-0.7.0 → frequenz_gridpool-0.7.1}/src/frequenz/gridpool/cli/_render_graph.py +0 -0
- {frequenz_gridpool-0.7.0 → frequenz_gridpool-0.7.1}/src/frequenz/gridpool/config/_microgrid.py +0 -0
- {frequenz_gridpool-0.7.0 → frequenz_gridpool-0.7.1}/src/frequenz/gridpool/config/_migrations.py +0 -0
- {frequenz_gridpool-0.7.0 → frequenz_gridpool-0.7.1}/src/frequenz/gridpool/config/_topology.py +0 -0
- {frequenz_gridpool-0.7.0 → frequenz_gridpool-0.7.1}/src/frequenz/gridpool/conftest.py +0 -0
- {frequenz_gridpool-0.7.0 → frequenz_gridpool-0.7.1}/src/frequenz/gridpool/py.typed +0 -0
- {frequenz_gridpool-0.7.0 → frequenz_gridpool-0.7.1}/src/frequenz_gridpool.egg-info/dependency_links.txt +0 -0
- {frequenz_gridpool-0.7.0 → frequenz_gridpool-0.7.1}/src/frequenz_gridpool.egg-info/entry_points.txt +0 -0
- {frequenz_gridpool-0.7.0 → frequenz_gridpool-0.7.1}/src/frequenz_gridpool.egg-info/requires.txt +0 -0
- {frequenz_gridpool-0.7.0 → frequenz_gridpool-0.7.1}/src/frequenz_gridpool.egg-info/top_level.txt +0 -0
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: frequenz-gridpool
|
|
3
|
-
Version: 0.7.
|
|
3
|
+
Version: 0.7.1
|
|
4
4
|
Summary: High-level interface to grid pools for the Frequenz platform.
|
|
5
5
|
Author-email: Frequenz Energy-as-a-Service GmbH <floss@frequenz.com>
|
|
6
6
|
License-Expression: MIT
|
|
@@ -150,7 +150,7 @@ The following platforms are officially supported (tested):
|
|
|
150
150
|
|
|
151
151
|
## CLI
|
|
152
152
|
|
|
153
|
-
This package ships the `gridpool-cli` command with
|
|
153
|
+
This package ships the `gridpool-cli` command with five subcommands.
|
|
154
154
|
|
|
155
155
|
### Setup
|
|
156
156
|
|
|
@@ -162,6 +162,9 @@ export ASSETS_API_AUTH_KEY="..."
|
|
|
162
162
|
export ASSETS_API_SIGN_SECRET="..."
|
|
163
163
|
```
|
|
164
164
|
|
|
165
|
+
`FREQUENZ_API_KEY` and `FREQUENZ_API_SECRET` are accepted as fallbacks for
|
|
166
|
+
`ASSETS_API_AUTH_KEY` and `ASSETS_API_SIGN_SECRET`.
|
|
167
|
+
|
|
165
168
|
### Print component formulas
|
|
166
169
|
|
|
167
170
|
```bash
|
|
@@ -246,6 +249,28 @@ gridpool-cli generate-config <microgrid_id> \
|
|
|
246
249
|
--prefer-meters-in-component-formulas > microgrid.toml
|
|
247
250
|
```
|
|
248
251
|
|
|
252
|
+
### Validate config files
|
|
253
|
+
|
|
254
|
+
Check config files offline, without contacting the Assets API, and exit
|
|
255
|
+
non-zero on the first error, to gate config-repo CI:
|
|
256
|
+
|
|
257
|
+
```bash
|
|
258
|
+
gridpool-cli validate microgrid.toml [more.toml ...]
|
|
259
|
+
```
|
|
260
|
+
|
|
261
|
+
Each file is validated on its own first, so every record names its own key and
|
|
262
|
+
required fields; the files are then validated merged, for the cross-record
|
|
263
|
+
checks.
|
|
264
|
+
|
|
265
|
+
### Look up a gridpool's enterprise
|
|
266
|
+
|
|
267
|
+
Print the enterprise ID that owns a gridpool, read from the config files
|
|
268
|
+
(merged as one stack); exits non-zero if the gridpool is not declared:
|
|
269
|
+
|
|
270
|
+
```bash
|
|
271
|
+
gridpool-cli find-enterprise <gridpool_id> config.toml [more.toml ...]
|
|
272
|
+
```
|
|
273
|
+
|
|
249
274
|
## Contributing
|
|
250
275
|
|
|
251
276
|
If you want to know how to build this project and contribute to it, please
|
|
@@ -75,7 +75,7 @@ The following platforms are officially supported (tested):
|
|
|
75
75
|
|
|
76
76
|
## CLI
|
|
77
77
|
|
|
78
|
-
This package ships the `gridpool-cli` command with
|
|
78
|
+
This package ships the `gridpool-cli` command with five subcommands.
|
|
79
79
|
|
|
80
80
|
### Setup
|
|
81
81
|
|
|
@@ -87,6 +87,9 @@ export ASSETS_API_AUTH_KEY="..."
|
|
|
87
87
|
export ASSETS_API_SIGN_SECRET="..."
|
|
88
88
|
```
|
|
89
89
|
|
|
90
|
+
`FREQUENZ_API_KEY` and `FREQUENZ_API_SECRET` are accepted as fallbacks for
|
|
91
|
+
`ASSETS_API_AUTH_KEY` and `ASSETS_API_SIGN_SECRET`.
|
|
92
|
+
|
|
90
93
|
### Print component formulas
|
|
91
94
|
|
|
92
95
|
```bash
|
|
@@ -171,6 +174,28 @@ gridpool-cli generate-config <microgrid_id> \
|
|
|
171
174
|
--prefer-meters-in-component-formulas > microgrid.toml
|
|
172
175
|
```
|
|
173
176
|
|
|
177
|
+
### Validate config files
|
|
178
|
+
|
|
179
|
+
Check config files offline, without contacting the Assets API, and exit
|
|
180
|
+
non-zero on the first error, to gate config-repo CI:
|
|
181
|
+
|
|
182
|
+
```bash
|
|
183
|
+
gridpool-cli validate microgrid.toml [more.toml ...]
|
|
184
|
+
```
|
|
185
|
+
|
|
186
|
+
Each file is validated on its own first, so every record names its own key and
|
|
187
|
+
required fields; the files are then validated merged, for the cross-record
|
|
188
|
+
checks.
|
|
189
|
+
|
|
190
|
+
### Look up a gridpool's enterprise
|
|
191
|
+
|
|
192
|
+
Print the enterprise ID that owns a gridpool, read from the config files
|
|
193
|
+
(merged as one stack); exits non-zero if the gridpool is not declared:
|
|
194
|
+
|
|
195
|
+
```bash
|
|
196
|
+
gridpool-cli find-enterprise <gridpool_id> config.toml [more.toml ...]
|
|
197
|
+
```
|
|
198
|
+
|
|
174
199
|
## Contributing
|
|
175
200
|
|
|
176
201
|
If you want to know how to build this project and contribute to it, please
|
|
@@ -0,0 +1,42 @@
|
|
|
1
|
+
# Frequenz Gridpool Library Release Notes
|
|
2
|
+
|
|
3
|
+
## Summary
|
|
4
|
+
|
|
5
|
+
This release rounds out `gridpool-cli` for the `assets` config layout and adds
|
|
6
|
+
gridpool enterprise ownership to the config model. The CLI can now generate,
|
|
7
|
+
validate, patch, and query merged config files.
|
|
8
|
+
|
|
9
|
+
## Upgrading
|
|
10
|
+
|
|
11
|
+
- `generate-config` now writes the `assets.microgrids` layout and stamps
|
|
12
|
+
`assets.version`, instead of the legacy layout. Regenerate configs to get the
|
|
13
|
+
current format; generated files then load back without a migration.
|
|
14
|
+
|
|
15
|
+
- `generate-config --inplace` refuses legacy-layout files (top-level or
|
|
16
|
+
`meta`-nested), which it would duplicate rather than edit. Rebuild those from
|
|
17
|
+
scratch.
|
|
18
|
+
|
|
19
|
+
## New Features
|
|
20
|
+
|
|
21
|
+
- `gridpool-cli validate <files>` checks config files offline and exits
|
|
22
|
+
non-zero on the first error, to gate config-repo CI. Each file must be valid
|
|
23
|
+
on its own, so a record names its own key and required fields; the files are
|
|
24
|
+
then checked merged, for the cross-record checks.
|
|
25
|
+
|
|
26
|
+
- `gridpool-cli` accepts `FREQUENZ_API_KEY` and `FREQUENZ_API_SECRET` as a
|
|
27
|
+
fallback pair for `ASSETS_API_AUTH_KEY` and `ASSETS_API_SIGN_SECRET`.
|
|
28
|
+
|
|
29
|
+
- Gridpools are described under `assets.gridpools`, each entry naming the
|
|
30
|
+
enterprise that owns the gridpool. `AssetsConfig.find_enterprise(gridpool_id)`
|
|
31
|
+
returns the configured owner.
|
|
32
|
+
`gridpool-cli find-enterprise <gridpool_id> <files>` prints it from the config.
|
|
33
|
+
`AssetsConfig.check` enforces the one-enterprise-per-gridpool invariant: a
|
|
34
|
+
gridpool's microgrids may not disagree on it, and a declared enterprise must
|
|
35
|
+
match the inferred one.
|
|
36
|
+
|
|
37
|
+
- A config derived from the Assets API now carries each microgrid's
|
|
38
|
+
`enterprise_id`.
|
|
39
|
+
|
|
40
|
+
- `generate-config --inplace` refreshes managed values while preserving
|
|
41
|
+
formatting; `--fill-missing` only adds absent values. Generated patches are
|
|
42
|
+
validated before writing.
|
|
@@ -5,11 +5,13 @@
|
|
|
5
5
|
|
|
6
6
|
import os
|
|
7
7
|
import tempfile
|
|
8
|
+
import tomllib
|
|
8
9
|
from pathlib import Path
|
|
9
10
|
|
|
10
11
|
import asyncclick as click
|
|
11
12
|
from frequenz.client.assets import AssetsApiClient
|
|
12
13
|
from frequenz.client.common.microgrid import MicrogridId
|
|
14
|
+
from marshmallow import ValidationError
|
|
13
15
|
|
|
14
16
|
from frequenz.gridpool import ComponentGraphConfig, ComponentGraphGenerator
|
|
15
17
|
from frequenz.gridpool.cli._dump_config import dump_map
|
|
@@ -17,12 +19,37 @@ from frequenz.gridpool.cli._patch_config import patch_file
|
|
|
17
19
|
from frequenz.gridpool.cli._render_graph import ComponentGraphRenderer, RenderOptions
|
|
18
20
|
from frequenz.gridpool.config import AssetsConfig, load_configs
|
|
19
21
|
|
|
22
|
+
_LOAD_ERRORS = (ValueError, TypeError, tomllib.TOMLDecodeError, ValidationError)
|
|
23
|
+
"""Exceptions raised when a config file fails to load or validate."""
|
|
24
|
+
|
|
20
25
|
|
|
21
26
|
@click.group()
|
|
22
27
|
async def cli() -> None:
|
|
23
28
|
"""CLI tool for gridpool functionality."""
|
|
24
29
|
|
|
25
30
|
|
|
31
|
+
def _assets_credentials() -> tuple[str, str, str]:
|
|
32
|
+
"""Resolve Assets API URL, auth key and sign secret from the environment.
|
|
33
|
+
|
|
34
|
+
`FREQUENZ_API_KEY` and `FREQUENZ_API_SECRET` are accepted as a fallback pair
|
|
35
|
+
for `ASSETS_API_AUTH_KEY` and `ASSETS_API_SIGN_SECRET`. The key and secret
|
|
36
|
+
are taken as a whole from one source, never mixed across the two.
|
|
37
|
+
"""
|
|
38
|
+
url = os.environ.get("ASSETS_API_URL")
|
|
39
|
+
key = os.environ.get("ASSETS_API_AUTH_KEY")
|
|
40
|
+
secret = os.environ.get("ASSETS_API_SIGN_SECRET")
|
|
41
|
+
if not key and not secret:
|
|
42
|
+
key = os.environ.get("FREQUENZ_API_KEY")
|
|
43
|
+
secret = os.environ.get("FREQUENZ_API_SECRET")
|
|
44
|
+
if not url or not key or not secret:
|
|
45
|
+
raise click.ClickException(
|
|
46
|
+
"ASSETS_API_URL and auth credentials must be set: "
|
|
47
|
+
"ASSETS_API_AUTH_KEY (or FREQUENZ_API_KEY) and "
|
|
48
|
+
"ASSETS_API_SIGN_SECRET (or FREQUENZ_API_SECRET)."
|
|
49
|
+
)
|
|
50
|
+
return url, key, secret
|
|
51
|
+
|
|
52
|
+
|
|
26
53
|
def _graph_config(prefer_meters: bool) -> ComponentGraphConfig | None:
|
|
27
54
|
"""Build the graph config for `--prefer-meters-in-component-formulas`.
|
|
28
55
|
|
|
@@ -55,13 +82,7 @@ async def print_formulas(
|
|
|
55
82
|
prefer_meters_in_component_formulas: bool,
|
|
56
83
|
) -> None:
|
|
57
84
|
"""Fetch and print component graph formulas for a microgrid."""
|
|
58
|
-
url =
|
|
59
|
-
key = os.environ.get("ASSETS_API_AUTH_KEY")
|
|
60
|
-
secret = os.environ.get("ASSETS_API_SIGN_SECRET")
|
|
61
|
-
if not url or not key or not secret:
|
|
62
|
-
raise click.ClickException(
|
|
63
|
-
"ASSETS_API_URL, ASSETS_API_AUTH_KEY, ASSETS_API_SIGN_SECRET must be set."
|
|
64
|
-
)
|
|
85
|
+
url, key, secret = _assets_credentials()
|
|
65
86
|
|
|
66
87
|
async with AssetsApiClient(
|
|
67
88
|
url,
|
|
@@ -90,6 +111,70 @@ async def print_formulas(
|
|
|
90
111
|
)
|
|
91
112
|
|
|
92
113
|
|
|
114
|
+
@cli.command()
|
|
115
|
+
@click.argument(
|
|
116
|
+
"config_files",
|
|
117
|
+
nargs=-1,
|
|
118
|
+
required=True,
|
|
119
|
+
type=click.Path(exists=True, dir_okay=False, path_type=Path),
|
|
120
|
+
)
|
|
121
|
+
async def validate(config_files: tuple[Path, ...]) -> None:
|
|
122
|
+
"""Validate each config file on its own, then the merged stack.
|
|
123
|
+
|
|
124
|
+
Each file must stand alone: every record names its own key and required
|
|
125
|
+
fields. The merged stack then adds the cross-record checks. Exits non-zero
|
|
126
|
+
on the first error, to gate CI.
|
|
127
|
+
"""
|
|
128
|
+
for config_file in config_files:
|
|
129
|
+
try:
|
|
130
|
+
AssetsConfig.load_from_files([config_file])
|
|
131
|
+
except _LOAD_ERRORS as exc:
|
|
132
|
+
raise click.ClickException(f"{config_file}: {exc}") from exc
|
|
133
|
+
|
|
134
|
+
try:
|
|
135
|
+
config = AssetsConfig.load_from_files(list(config_files))
|
|
136
|
+
except _LOAD_ERRORS as exc:
|
|
137
|
+
raise click.ClickException(str(exc)) from exc
|
|
138
|
+
|
|
139
|
+
click.echo(
|
|
140
|
+
f"OK: {len(config_files)} file(s), {len(config.microgrids)} microgrid(s), "
|
|
141
|
+
f"{len(config.relations)} relation(s), "
|
|
142
|
+
f"{len(config.market_locations)} market location(s).",
|
|
143
|
+
err=True,
|
|
144
|
+
)
|
|
145
|
+
|
|
146
|
+
|
|
147
|
+
@cli.command("find-enterprise")
|
|
148
|
+
@click.argument("gridpool_id", type=int)
|
|
149
|
+
@click.argument(
|
|
150
|
+
"config_files",
|
|
151
|
+
nargs=-1,
|
|
152
|
+
required=True,
|
|
153
|
+
type=click.Path(exists=True, dir_okay=False, path_type=Path),
|
|
154
|
+
)
|
|
155
|
+
async def find_enterprise(gridpool_id: int, config_files: tuple[Path, ...]) -> None:
|
|
156
|
+
"""Print the enterprise ID owning GRIDPOOL_ID, read from the config files.
|
|
157
|
+
|
|
158
|
+
The files are read as one merged stack. The owner is taken from the
|
|
159
|
+
`gridpools` entry without validating the full document. Exits non-zero if
|
|
160
|
+
the gridpool is not configured.
|
|
161
|
+
"""
|
|
162
|
+
try:
|
|
163
|
+
# The declared `gridpools` entry is the source of truth for ownership,
|
|
164
|
+
# so read it even from a document whole-document validation would reject.
|
|
165
|
+
config = AssetsConfig.load_from_files(list(config_files), check=False)
|
|
166
|
+
enterprise_id = config.find_enterprise(gridpool_id)
|
|
167
|
+
except _LOAD_ERRORS as exc:
|
|
168
|
+
raise click.ClickException(str(exc)) from exc
|
|
169
|
+
|
|
170
|
+
if enterprise_id is None:
|
|
171
|
+
raise click.ClickException(
|
|
172
|
+
f"Could not determine the enterprise for gridpool {gridpool_id} "
|
|
173
|
+
"from the given config(s)."
|
|
174
|
+
)
|
|
175
|
+
click.echo(enterprise_id)
|
|
176
|
+
|
|
177
|
+
|
|
93
178
|
@cli.command("render-graph")
|
|
94
179
|
@click.argument("microgrid_id", type=int)
|
|
95
180
|
@click.option(
|
|
@@ -107,13 +192,7 @@ async def print_formulas(
|
|
|
107
192
|
)
|
|
108
193
|
async def render_graph(microgrid_id: int, output: str, show: bool) -> None:
|
|
109
194
|
"""Render and save a component graph visualization for a microgrid."""
|
|
110
|
-
url =
|
|
111
|
-
key = os.environ.get("ASSETS_API_AUTH_KEY")
|
|
112
|
-
secret = os.environ.get("ASSETS_API_SIGN_SECRET")
|
|
113
|
-
if not url or not key or not secret:
|
|
114
|
-
raise click.ClickException(
|
|
115
|
-
"ASSETS_API_URL, ASSETS_API_AUTH_KEY, ASSETS_API_SIGN_SECRET must be set."
|
|
116
|
-
)
|
|
195
|
+
url, key, secret = _assets_credentials()
|
|
117
196
|
|
|
118
197
|
try:
|
|
119
198
|
async with AssetsApiClient(
|
|
@@ -151,9 +230,16 @@ async def render_graph(microgrid_id: int, output: str, show: bool) -> None:
|
|
|
151
230
|
"--inplace",
|
|
152
231
|
is_flag=True,
|
|
153
232
|
default=False,
|
|
154
|
-
help="Patch --default in place instead of printing to stdout
|
|
155
|
-
"
|
|
156
|
-
"
|
|
233
|
+
help="Patch --default in place instead of printing to stdout, refreshing "
|
|
234
|
+
"the managed values while keeping its comments, ordering and formatting. "
|
|
235
|
+
"Requires --default.",
|
|
236
|
+
)
|
|
237
|
+
@click.option(
|
|
238
|
+
"--fill-missing",
|
|
239
|
+
is_flag=True,
|
|
240
|
+
default=False,
|
|
241
|
+
help="With --inplace, only add values --default is missing, leaving the "
|
|
242
|
+
"values already in it untouched.",
|
|
157
243
|
)
|
|
158
244
|
@click.option(
|
|
159
245
|
"--prefer-meters-in-component-formulas",
|
|
@@ -162,11 +248,13 @@ async def render_graph(microgrid_id: int, output: str, show: bool) -> None:
|
|
|
162
248
|
help="Read the meter before the component in the per-category formulas. "
|
|
163
249
|
"This is the order used before component graph v0.5.0.",
|
|
164
250
|
)
|
|
165
|
-
async def generate_config(
|
|
251
|
+
async def generate_config( # pylint: disable=too-many-locals
|
|
166
252
|
microgrid_ids: tuple[int, ...],
|
|
253
|
+
*,
|
|
167
254
|
default_file: Path | None,
|
|
168
255
|
override_file: Path | None,
|
|
169
256
|
inplace: bool,
|
|
257
|
+
fill_missing: bool,
|
|
170
258
|
prefer_meters_in_component_formulas: bool,
|
|
171
259
|
) -> None:
|
|
172
260
|
"""Generate microgrid config from the Assets API as TOML.
|
|
@@ -184,21 +272,17 @@ async def generate_config(
|
|
|
184
272
|
component graph v0.5.0.
|
|
185
273
|
|
|
186
274
|
With `--inplace`, `--default` is patched directly instead: candidate values
|
|
187
|
-
come from the Assets API (with `--override` layered on top)
|
|
188
|
-
|
|
189
|
-
|
|
190
|
-
already in `--default` is processed.
|
|
275
|
+
come from the Assets API (with `--override` layered on top) and refresh the
|
|
276
|
+
managed values, keeping the file's comments, field order and formatting.
|
|
277
|
+
With `--fill-missing`, only values `--default` lacks are added. If no
|
|
278
|
+
microgrid IDs are given, every microgrid already in `--default` is processed.
|
|
191
279
|
"""
|
|
192
280
|
if inplace and default_file is None:
|
|
193
281
|
raise click.ClickException("--inplace requires --default.")
|
|
282
|
+
if fill_missing and not inplace:
|
|
283
|
+
raise click.ClickException("--fill-missing requires --inplace.")
|
|
194
284
|
|
|
195
|
-
url =
|
|
196
|
-
key = os.environ.get("ASSETS_API_AUTH_KEY")
|
|
197
|
-
secret = os.environ.get("ASSETS_API_SIGN_SECRET")
|
|
198
|
-
if not url or not key or not secret:
|
|
199
|
-
raise click.ClickException(
|
|
200
|
-
"ASSETS_API_URL, ASSETS_API_AUTH_KEY, ASSETS_API_SIGN_SECRET must be set."
|
|
201
|
-
)
|
|
285
|
+
url, key, secret = _assets_credentials()
|
|
202
286
|
|
|
203
287
|
ids = list(dict.fromkeys(microgrid_ids)) or None
|
|
204
288
|
if inplace and ids is None:
|
|
@@ -236,13 +320,25 @@ async def generate_config(
|
|
|
236
320
|
|
|
237
321
|
if inplace:
|
|
238
322
|
assert default_file is not None
|
|
239
|
-
|
|
323
|
+
try:
|
|
324
|
+
patched = patch_file(default_file, configs, fill_missing=fill_missing)
|
|
325
|
+
except ValueError as exc:
|
|
326
|
+
raise click.ClickException(str(exc)) from exc
|
|
240
327
|
fd, tmp_name = tempfile.mkstemp(
|
|
241
328
|
dir=default_file.parent, prefix=f".{default_file.name}."
|
|
242
329
|
)
|
|
243
330
|
try:
|
|
244
331
|
with os.fdopen(fd, "w") as tmp_file:
|
|
245
332
|
tmp_file.write(patched)
|
|
333
|
+
# Guard against a patch that corrupts the file (e.g. bad splicing)
|
|
334
|
+
# before it overwrites the user's config.
|
|
335
|
+
try:
|
|
336
|
+
AssetsConfig.load_from_files(Path(tmp_name))
|
|
337
|
+
except _LOAD_ERRORS as exc:
|
|
338
|
+
raise click.ClickException(
|
|
339
|
+
f"Refusing to write {default_file}: the patched result is "
|
|
340
|
+
f"invalid: {exc}"
|
|
341
|
+
) from exc
|
|
246
342
|
os.replace(tmp_name, default_file)
|
|
247
343
|
except BaseException:
|
|
248
344
|
os.remove(tmp_name)
|
{frequenz_gridpool-0.7.0 → frequenz_gridpool-0.7.1}/src/frequenz/gridpool/cli/_dump_config.py
RENAMED
|
@@ -5,9 +5,11 @@
|
|
|
5
5
|
|
|
6
6
|
Renders a `{microgrid_id: MicrogridConfig}` mapping as dotted-key TOML, e.g.:
|
|
7
7
|
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
115.
|
|
8
|
+
assets.version = 1
|
|
9
|
+
|
|
10
|
+
assets.microgrids.115.microgrid_id = 115
|
|
11
|
+
assets.microgrids.115.name = "Demo Grid"
|
|
12
|
+
assets.microgrids.115.latitude = 52.52
|
|
11
13
|
|
|
12
14
|
This is the inverse of `AssetsConfig.load_from_files`. Value rendering (quoting,
|
|
13
15
|
escaping, key-quoting, list/number/datetime formatting) is delegated to
|
|
@@ -21,6 +23,9 @@ import tomlkit
|
|
|
21
23
|
from tomlkit.items import Integer, Trivia
|
|
22
24
|
|
|
23
25
|
from frequenz.gridpool.config import MicrogridConfig
|
|
26
|
+
from frequenz.gridpool.config._migrations import _CURRENT_VERSION
|
|
27
|
+
|
|
28
|
+
_MICROGRID_PREFIX = ["assets", "microgrids"]
|
|
24
29
|
|
|
25
30
|
|
|
26
31
|
def _is_empty(value: Any) -> bool:
|
|
@@ -83,14 +88,14 @@ def dump_map(configs: dict[int, MicrogridConfig]) -> str:
|
|
|
83
88
|
"""
|
|
84
89
|
schema = MicrogridConfig.Schema()
|
|
85
90
|
doc = tomlkit.document()
|
|
91
|
+
doc.append(tomlkit.key(["assets", "version"]), _CURRENT_VERSION)
|
|
86
92
|
for mid in sorted(configs):
|
|
87
93
|
dumped = schema.dump(configs[mid])
|
|
88
94
|
assert isinstance(dumped, dict)
|
|
89
|
-
leaves = _iter_leaves([str(mid)], dumped)
|
|
95
|
+
leaves = _iter_leaves([*_MICROGRID_PREFIX, str(mid)], dumped)
|
|
90
96
|
if not leaves:
|
|
91
97
|
continue
|
|
92
|
-
|
|
93
|
-
doc.add(tomlkit.nl())
|
|
98
|
+
doc.add(tomlkit.nl())
|
|
94
99
|
for path, value in leaves:
|
|
95
100
|
doc.append(tomlkit.key(path), _format_value(value))
|
|
96
101
|
return tomlkit.dumps(doc)
|
{frequenz_gridpool-0.7.0 → frequenz_gridpool-0.7.1}/src/frequenz/gridpool/cli/_patch_config.py
RENAMED
|
@@ -4,8 +4,9 @@
|
|
|
4
4
|
"""Patch existing dotted-key TOML files with new microgrid config values.
|
|
5
5
|
|
|
6
6
|
Unlike `dump_map`, which rebuilds a TOML document from scratch, this module
|
|
7
|
-
|
|
8
|
-
|
|
7
|
+
edits the document in place, so its comments, field order and number formatting
|
|
8
|
+
survive. By default it refreshes the managed leaves, overwriting them; with
|
|
9
|
+
`fill_missing` it only adds the leaves the document lacks.
|
|
9
10
|
"""
|
|
10
11
|
|
|
11
12
|
from collections.abc import Mapping
|
|
@@ -17,33 +18,42 @@ from tomlkit import TOMLDocument
|
|
|
17
18
|
|
|
18
19
|
from frequenz.gridpool.config import MicrogridConfig
|
|
19
20
|
|
|
20
|
-
from ._dump_config import _format_value, _iter_leaves
|
|
21
|
+
from ._dump_config import _MICROGRID_PREFIX, _format_value, _iter_leaves
|
|
21
22
|
|
|
22
23
|
|
|
23
|
-
def patch_file(
|
|
24
|
-
|
|
24
|
+
def patch_file(
|
|
25
|
+
path: Path, configs: dict[int, MicrogridConfig], *, fill_missing: bool = False
|
|
26
|
+
) -> str:
|
|
27
|
+
"""Patch the TOML file at `path` with the values in `configs`.
|
|
25
28
|
|
|
26
29
|
Args:
|
|
27
30
|
path: Path to the existing TOML file to patch.
|
|
28
31
|
configs: Mapping from microgrid ID to `MicrogridConfig`.
|
|
32
|
+
fill_missing: Only add leaves the file lacks, leaving existing values
|
|
33
|
+
untouched; by default the managed leaves are overwritten.
|
|
29
34
|
|
|
30
35
|
Returns:
|
|
31
36
|
The patched TOML text; the caller is responsible for writing it back.
|
|
32
37
|
"""
|
|
33
|
-
return patch_text(path.read_text(), configs)
|
|
38
|
+
return patch_text(path.read_text(), configs, fill_missing=fill_missing)
|
|
34
39
|
|
|
35
40
|
|
|
36
|
-
def patch_text(
|
|
37
|
-
|
|
41
|
+
def patch_text(
|
|
42
|
+
original: str, configs: dict[int, MicrogridConfig], *, fill_missing: bool = False
|
|
43
|
+
) -> str:
|
|
44
|
+
"""Patch dotted-key TOML text with the values in `configs`.
|
|
38
45
|
|
|
39
46
|
Args:
|
|
40
47
|
original: The existing TOML text to patch.
|
|
41
48
|
configs: Mapping from microgrid ID to `MicrogridConfig`.
|
|
49
|
+
fill_missing: Only add leaves the text lacks, leaving existing values
|
|
50
|
+
untouched; by default the managed leaves are overwritten.
|
|
42
51
|
|
|
43
52
|
Returns:
|
|
44
53
|
The patched TOML text.
|
|
45
54
|
"""
|
|
46
55
|
doc = tomlkit.parse(original)
|
|
56
|
+
_reject_legacy_layout(doc)
|
|
47
57
|
schema = MicrogridConfig.Schema()
|
|
48
58
|
|
|
49
59
|
# Leaves needing a whole new sub-table can't be inserted via item assignment
|
|
@@ -59,12 +69,12 @@ def patch_text(original: str, configs: dict[int, MicrogridConfig]) -> str:
|
|
|
59
69
|
if not leaves:
|
|
60
70
|
continue
|
|
61
71
|
|
|
62
|
-
if
|
|
72
|
+
if not _leaf_exists(doc, mid, []):
|
|
63
73
|
_append_new_entry(doc, mid, leaves)
|
|
64
74
|
continue
|
|
65
75
|
|
|
66
76
|
for path, value in leaves:
|
|
67
|
-
if _leaf_exists(doc, mid, path):
|
|
77
|
+
if fill_missing and _leaf_exists(doc, mid, path):
|
|
68
78
|
continue
|
|
69
79
|
if not _insert_leaf(doc, mid, path, value):
|
|
70
80
|
orphans.setdefault(mid, []).append((path, value))
|
|
@@ -75,10 +85,44 @@ def patch_text(original: str, configs: dict[int, MicrogridConfig]) -> str:
|
|
|
75
85
|
return text
|
|
76
86
|
|
|
77
87
|
|
|
88
|
+
def _reject_legacy_layout(doc: TOMLDocument) -> None:
|
|
89
|
+
"""Refuse a document that is not in the current `assets.microgrids` layout.
|
|
90
|
+
|
|
91
|
+
Patching navigates by `assets.microgrids.<id>`, so a top-level or
|
|
92
|
+
`meta`-nested entry would be duplicated rather than edited. Such a file
|
|
93
|
+
must be rebuilt with `generate-config` (without `--inplace`) first.
|
|
94
|
+
|
|
95
|
+
Args:
|
|
96
|
+
doc: The parsed document to check.
|
|
97
|
+
|
|
98
|
+
Raises:
|
|
99
|
+
ValueError: If the document uses a deprecated layout.
|
|
100
|
+
"""
|
|
101
|
+
hint = "rebuild it with generate-config (without --inplace) first"
|
|
102
|
+
if stray := sorted(k for k in doc if k != "assets"):
|
|
103
|
+
raise ValueError(
|
|
104
|
+
f"Cannot patch in place: deprecated top-level keys {stray}; {hint}."
|
|
105
|
+
)
|
|
106
|
+
assets = doc.get("assets", {})
|
|
107
|
+
microgrids = assets.get("microgrids", {}) if isinstance(assets, Mapping) else {}
|
|
108
|
+
if not isinstance(microgrids, Mapping):
|
|
109
|
+
microgrids = {}
|
|
110
|
+
metas = sorted(
|
|
111
|
+
mid
|
|
112
|
+
for mid, entry in microgrids.items()
|
|
113
|
+
if isinstance(entry, Mapping) and "meta" in entry
|
|
114
|
+
)
|
|
115
|
+
if metas:
|
|
116
|
+
raise ValueError(
|
|
117
|
+
f"Cannot patch in place: microgrids {metas} nest fields under the "
|
|
118
|
+
f"deprecated `meta` table; {hint}."
|
|
119
|
+
)
|
|
120
|
+
|
|
121
|
+
|
|
78
122
|
def _leaf_exists(doc: TOMLDocument, mid: str, path: list[str]) -> bool:
|
|
79
|
-
"""Whether the dotted key `mid.path...`
|
|
123
|
+
"""Whether the dotted key `assets.microgrids.mid.path...` has a value in `doc`."""
|
|
80
124
|
node: Any = doc
|
|
81
|
-
for key in (mid, *path):
|
|
125
|
+
for key in (*_MICROGRID_PREFIX, mid, *path):
|
|
82
126
|
if not isinstance(node, Mapping) or key not in node:
|
|
83
127
|
return False
|
|
84
128
|
node = node[key]
|
|
@@ -100,14 +144,14 @@ def _insert_leaf(doc: TOMLDocument, mid: str, path: list[str], value: Any) -> bo
|
|
|
100
144
|
"""
|
|
101
145
|
node: Any = doc
|
|
102
146
|
matched = 0
|
|
103
|
-
|
|
147
|
+
full_path = [*_MICROGRID_PREFIX, mid, *path]
|
|
148
|
+
for key in full_path[:-1]:
|
|
104
149
|
nxt = node[key] if isinstance(node, Mapping) and key in node else None
|
|
105
150
|
if not isinstance(nxt, Mapping):
|
|
106
151
|
break
|
|
107
152
|
node = nxt
|
|
108
153
|
matched += 1
|
|
109
154
|
|
|
110
|
-
full_path = [mid, *path]
|
|
111
155
|
if matched != len(full_path) - 1:
|
|
112
156
|
return False
|
|
113
157
|
node[full_path[-1]] = _format_value(value)
|
|
@@ -128,14 +172,14 @@ def _append_new_entry(
|
|
|
128
172
|
if doc.body:
|
|
129
173
|
doc.add(tomlkit.nl())
|
|
130
174
|
for path, value in leaves:
|
|
131
|
-
doc.append(tomlkit.key([mid, *path]), _format_value(value))
|
|
175
|
+
doc.append(tomlkit.key([*_MICROGRID_PREFIX, mid, *path]), _format_value(value))
|
|
132
176
|
|
|
133
177
|
|
|
134
178
|
def _render_lines(mid: str, leaves: list[tuple[list[str], Any]]) -> list[str]:
|
|
135
|
-
"""Render
|
|
179
|
+
"""Render leaves as standalone `assets.microgrids.mid.path = value` lines."""
|
|
136
180
|
tmp = tomlkit.document()
|
|
137
181
|
for path, value in leaves:
|
|
138
|
-
tmp.append(tomlkit.key([mid, *path]), _format_value(value))
|
|
182
|
+
tmp.append(tomlkit.key([*_MICROGRID_PREFIX, mid, *path]), _format_value(value))
|
|
139
183
|
return tomlkit.dumps(tmp).splitlines(keepends=True)
|
|
140
184
|
|
|
141
185
|
|
|
@@ -159,8 +203,8 @@ def _splice_orphans(text: str, orphans: dict[str, list[tuple[list[str], Any]]])
|
|
|
159
203
|
|
|
160
204
|
|
|
161
205
|
def _last_line_index_for_mid(lines: list[str], mid: str) -> int:
|
|
162
|
-
"""Index of the last line belonging to `mid` (its key starts with
|
|
163
|
-
prefix = f"{mid}."
|
|
206
|
+
"""Index of the last line belonging to `mid` (its key starts with the prefix)."""
|
|
207
|
+
prefix = f"{'.'.join(_MICROGRID_PREFIX)}.{mid}."
|
|
164
208
|
for idx in range(len(lines) - 1, -1, -1):
|
|
165
209
|
if lines[idx].lstrip().startswith(prefix):
|
|
166
210
|
return idx
|
{frequenz_gridpool-0.7.0 → frequenz_gridpool-0.7.1}/src/frequenz/gridpool/config/__init__.py
RENAMED
|
@@ -6,6 +6,7 @@
|
|
|
6
6
|
from frequenz.microgrid_component_graph import ComponentGraphConfig, FormulaOverrides
|
|
7
7
|
|
|
8
8
|
from ._assets import AssetsConfig
|
|
9
|
+
from ._gridpool import GridpoolConfig
|
|
9
10
|
from ._load import load_configs
|
|
10
11
|
from ._microgrid import (
|
|
11
12
|
BatteryConfig,
|
|
@@ -32,6 +33,7 @@ __all__ = [
|
|
|
32
33
|
"ComponentTypeConfig",
|
|
33
34
|
"DeliveryAreaConfig",
|
|
34
35
|
"FormulaOverrides",
|
|
36
|
+
"GridpoolConfig",
|
|
35
37
|
"MarketLocationConfig",
|
|
36
38
|
"MicrogridConfig",
|
|
37
39
|
"PVConfig",
|
|
@@ -15,6 +15,7 @@ from frequenz.client.assets import MarketParticipationType
|
|
|
15
15
|
from marshmallow import Schema
|
|
16
16
|
from marshmallow_dataclass import dataclass
|
|
17
17
|
|
|
18
|
+
from ._gridpool import GridpoolConfig
|
|
18
19
|
from ._microgrid import MicrogridConfig
|
|
19
20
|
from ._migrations import _CURRENT_VERSION, migrate
|
|
20
21
|
from ._topology import DeliveryAreaConfig, MarketLocationConfig, RelationConfig
|
|
@@ -89,6 +90,9 @@ class AssetsConfig:
|
|
|
89
90
|
microgrids: dict[int, MicrogridConfig] = field(default_factory=dict)
|
|
90
91
|
"""Microgrids, keyed by microgrid ID."""
|
|
91
92
|
|
|
93
|
+
gridpools: dict[int, GridpoolConfig] = field(default_factory=dict)
|
|
94
|
+
"""Gridpools, keyed by gridpool ID."""
|
|
95
|
+
|
|
92
96
|
market_locations: dict[str, MarketLocationConfig] = field(default_factory=dict)
|
|
93
97
|
"""Market locations, keyed by their identifier."""
|
|
94
98
|
|
|
@@ -123,6 +127,11 @@ class AssetsConfig:
|
|
|
123
127
|
raise ValueError(
|
|
124
128
|
f"Microgrid ID mismatch: key {mid} != {cfg.microgrid_id}"
|
|
125
129
|
)
|
|
130
|
+
for gpid, gridpool in self.gridpools.items():
|
|
131
|
+
if int(gridpool.gridpool_id) != gpid:
|
|
132
|
+
raise ValueError(
|
|
133
|
+
f"Gridpool ID mismatch: key {gpid} != {gridpool.gridpool_id}"
|
|
134
|
+
)
|
|
126
135
|
|
|
127
136
|
def check(self) -> None:
|
|
128
137
|
"""Check the document as a whole, once every layer has been merged.
|
|
@@ -131,8 +140,9 @@ class AssetsConfig:
|
|
|
131
140
|
ValueError: If an entry's identifier disagrees with the key it is
|
|
132
141
|
filed under, a relation names fewer than two sides, its key
|
|
133
142
|
disagrees with its fields, a gridpool relation names no delivery
|
|
134
|
-
area, one market location is placed in two of them,
|
|
135
|
-
microgrid gridpool ID disagrees with its relations
|
|
143
|
+
area, one market location is placed in two of them, a legacy
|
|
144
|
+
microgrid gridpool ID disagrees with its relations, or a
|
|
145
|
+
gridpool's declared and inferred enterprise disagree.
|
|
136
146
|
"""
|
|
137
147
|
for key, location in self.market_locations.items():
|
|
138
148
|
if location.id is None:
|
|
@@ -143,6 +153,7 @@ class AssetsConfig:
|
|
|
143
153
|
)
|
|
144
154
|
self._check_relations()
|
|
145
155
|
self._check_legacy_gridpool_ids()
|
|
156
|
+
self._check_gridpool_enterprises()
|
|
146
157
|
|
|
147
158
|
def _check_relations(self) -> None:
|
|
148
159
|
"""Check the relations are complete, well-keyed and area-consistent.
|
|
@@ -213,6 +224,61 @@ class AssetsConfig:
|
|
|
213
224
|
f"{sorted(relation_gids)}; remove gid when several apply"
|
|
214
225
|
)
|
|
215
226
|
|
|
227
|
+
def _derive_enterprise(self, gridpool_id: int) -> int | None:
|
|
228
|
+
"""Infer a gridpool's enterprise from the microgrids its relations name.
|
|
229
|
+
|
|
230
|
+
Args:
|
|
231
|
+
gridpool_id: The gridpool whose enterprise to infer.
|
|
232
|
+
|
|
233
|
+
Returns:
|
|
234
|
+
The inferred enterprise ID, or `None` if none can be inferred.
|
|
235
|
+
|
|
236
|
+
Raises:
|
|
237
|
+
ValueError: If the related microgrids disagree on the enterprise.
|
|
238
|
+
"""
|
|
239
|
+
enterprises: set[int] = set()
|
|
240
|
+
for mid in self.find_microgrids(gridpool_id=gridpool_id):
|
|
241
|
+
microgrid = self.microgrids.get(mid)
|
|
242
|
+
if microgrid is not None and microgrid.enterprise_id is not None:
|
|
243
|
+
enterprises.add(microgrid.enterprise_id)
|
|
244
|
+
if not enterprises:
|
|
245
|
+
return None
|
|
246
|
+
if len(enterprises) > 1:
|
|
247
|
+
raise ValueError(
|
|
248
|
+
f"Gridpool {gridpool_id}: its microgrids disagree on the owning "
|
|
249
|
+
f"enterprise: {sorted(enterprises)}"
|
|
250
|
+
)
|
|
251
|
+
return enterprises.pop()
|
|
252
|
+
|
|
253
|
+
def _check_gridpool_enterprises(self) -> None:
|
|
254
|
+
"""Check declared and inferable gridpool enterprises agree.
|
|
255
|
+
|
|
256
|
+
A gridpool owns one enterprise, so its microgrids must not disagree on
|
|
257
|
+
it, and a declared `gridpools` entry must match what they imply.
|
|
258
|
+
|
|
259
|
+
Raises:
|
|
260
|
+
ValueError: If a gridpool's microgrids disagree on the enterprise,
|
|
261
|
+
or a declared enterprise differs from the inferred one.
|
|
262
|
+
"""
|
|
263
|
+
gridpool_ids = set(self.gridpools) | {
|
|
264
|
+
relation.gridpool_id
|
|
265
|
+
for relation in self.relations.values()
|
|
266
|
+
if relation.gridpool_id is not None
|
|
267
|
+
}
|
|
268
|
+
for gpid in gridpool_ids:
|
|
269
|
+
inferred = self._derive_enterprise(gpid)
|
|
270
|
+
declared = self.gridpools.get(gpid)
|
|
271
|
+
if (
|
|
272
|
+
declared is not None
|
|
273
|
+
and inferred is not None
|
|
274
|
+
and declared.enterprise_id != inferred
|
|
275
|
+
):
|
|
276
|
+
raise ValueError(
|
|
277
|
+
f"Gridpool {gpid}: declared enterprise "
|
|
278
|
+
f"{declared.enterprise_id} disagrees with its microgrids' "
|
|
279
|
+
f"enterprise {inferred}"
|
|
280
|
+
)
|
|
281
|
+
|
|
216
282
|
def find_relations(
|
|
217
283
|
self,
|
|
218
284
|
*,
|
|
@@ -350,6 +416,18 @@ class AssetsConfig:
|
|
|
350
416
|
)
|
|
351
417
|
)
|
|
352
418
|
|
|
419
|
+
def find_enterprise(self, gridpool_id: int) -> int | None:
|
|
420
|
+
"""Find the configured enterprise owning `gridpool_id`.
|
|
421
|
+
|
|
422
|
+
Args:
|
|
423
|
+
gridpool_id: The gridpool to look up.
|
|
424
|
+
|
|
425
|
+
Returns:
|
|
426
|
+
The owning enterprise ID, or `None` when the gridpool is not configured.
|
|
427
|
+
"""
|
|
428
|
+
gridpool = self.gridpools.get(gridpool_id)
|
|
429
|
+
return gridpool.enterprise_id if gridpool is not None else None
|
|
430
|
+
|
|
353
431
|
@classmethod
|
|
354
432
|
def _warn_unknown_entities(cls, assets: dict[str, Any], source: Path) -> None:
|
|
355
433
|
"""Warn about entity tables that this version drops on load."""
|
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
# License: MIT
|
|
2
|
+
# Copyright © 2026 Frequenz Energy-as-a-Service GmbH
|
|
3
|
+
|
|
4
|
+
"""Data model for a gridpool."""
|
|
5
|
+
|
|
6
|
+
from marshmallow_dataclass import dataclass
|
|
7
|
+
|
|
8
|
+
|
|
9
|
+
@dataclass
|
|
10
|
+
class GridpoolConfig:
|
|
11
|
+
"""Configuration of a gridpool."""
|
|
12
|
+
|
|
13
|
+
gridpool_id: int
|
|
14
|
+
"""ID of the gridpool."""
|
|
15
|
+
|
|
16
|
+
enterprise_id: int
|
|
17
|
+
"""Enterprise that owns the gridpool."""
|
|
@@ -219,6 +219,9 @@ async def _build_config_from_metadata(
|
|
|
219
219
|
location = mgrid.location if mgrid.location else None
|
|
220
220
|
return MicrogridConfig(
|
|
221
221
|
microgrid_id=microgrid_id,
|
|
222
|
+
# The Assets API always sets an EnterpriseId; an unassigned enterprise
|
|
223
|
+
# comes back as the proto default 0, which we treat as absent.
|
|
224
|
+
enterprise_id=int(mgrid.enterprise_id) or None,
|
|
222
225
|
latitude=location.latitude if location else None,
|
|
223
226
|
longitude=location.longitude if location else None,
|
|
224
227
|
)
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: frequenz-gridpool
|
|
3
|
-
Version: 0.7.
|
|
3
|
+
Version: 0.7.1
|
|
4
4
|
Summary: High-level interface to grid pools for the Frequenz platform.
|
|
5
5
|
Author-email: Frequenz Energy-as-a-Service GmbH <floss@frequenz.com>
|
|
6
6
|
License-Expression: MIT
|
|
@@ -150,7 +150,7 @@ The following platforms are officially supported (tested):
|
|
|
150
150
|
|
|
151
151
|
## CLI
|
|
152
152
|
|
|
153
|
-
This package ships the `gridpool-cli` command with
|
|
153
|
+
This package ships the `gridpool-cli` command with five subcommands.
|
|
154
154
|
|
|
155
155
|
### Setup
|
|
156
156
|
|
|
@@ -162,6 +162,9 @@ export ASSETS_API_AUTH_KEY="..."
|
|
|
162
162
|
export ASSETS_API_SIGN_SECRET="..."
|
|
163
163
|
```
|
|
164
164
|
|
|
165
|
+
`FREQUENZ_API_KEY` and `FREQUENZ_API_SECRET` are accepted as fallbacks for
|
|
166
|
+
`ASSETS_API_AUTH_KEY` and `ASSETS_API_SIGN_SECRET`.
|
|
167
|
+
|
|
165
168
|
### Print component formulas
|
|
166
169
|
|
|
167
170
|
```bash
|
|
@@ -246,6 +249,28 @@ gridpool-cli generate-config <microgrid_id> \
|
|
|
246
249
|
--prefer-meters-in-component-formulas > microgrid.toml
|
|
247
250
|
```
|
|
248
251
|
|
|
252
|
+
### Validate config files
|
|
253
|
+
|
|
254
|
+
Check config files offline, without contacting the Assets API, and exit
|
|
255
|
+
non-zero on the first error, to gate config-repo CI:
|
|
256
|
+
|
|
257
|
+
```bash
|
|
258
|
+
gridpool-cli validate microgrid.toml [more.toml ...]
|
|
259
|
+
```
|
|
260
|
+
|
|
261
|
+
Each file is validated on its own first, so every record names its own key and
|
|
262
|
+
required fields; the files are then validated merged, for the cross-record
|
|
263
|
+
checks.
|
|
264
|
+
|
|
265
|
+
### Look up a gridpool's enterprise
|
|
266
|
+
|
|
267
|
+
Print the enterprise ID that owns a gridpool, read from the config files
|
|
268
|
+
(merged as one stack); exits non-zero if the gridpool is not declared:
|
|
269
|
+
|
|
270
|
+
```bash
|
|
271
|
+
gridpool-cli find-enterprise <gridpool_id> config.toml [more.toml ...]
|
|
272
|
+
```
|
|
273
|
+
|
|
249
274
|
## Contributing
|
|
250
275
|
|
|
251
276
|
If you want to know how to build this project and contribute to it, please
|
{frequenz_gridpool-0.7.0 → frequenz_gridpool-0.7.1}/src/frequenz_gridpool.egg-info/SOURCES.txt
RENAMED
|
@@ -14,6 +14,7 @@ src/frequenz/gridpool/cli/_patch_config.py
|
|
|
14
14
|
src/frequenz/gridpool/cli/_render_graph.py
|
|
15
15
|
src/frequenz/gridpool/config/__init__.py
|
|
16
16
|
src/frequenz/gridpool/config/_assets.py
|
|
17
|
+
src/frequenz/gridpool/config/_gridpool.py
|
|
17
18
|
src/frequenz/gridpool/config/_load.py
|
|
18
19
|
src/frequenz/gridpool/config/_microgrid.py
|
|
19
20
|
src/frequenz/gridpool/config/_migrations.py
|
|
@@ -45,6 +45,7 @@
|
|
|
45
45
|
"src/frequenz/gridpool/cli/_render_graph.py",
|
|
46
46
|
"src/frequenz/gridpool/config/__init__.py",
|
|
47
47
|
"src/frequenz/gridpool/config/_assets.py",
|
|
48
|
+
"src/frequenz/gridpool/config/_gridpool.py",
|
|
48
49
|
"src/frequenz/gridpool/config/_load.py",
|
|
49
50
|
"src/frequenz/gridpool/config/_microgrid.py",
|
|
50
51
|
"src/frequenz/gridpool/config/_migrations.py",
|
|
@@ -1,119 +0,0 @@
|
|
|
1
|
-
# Frequenz Gridpool Library Release Notes
|
|
2
|
-
|
|
3
|
-
## Summary
|
|
4
|
-
|
|
5
|
-
<!-- Here goes a general summary of what this release is about -->
|
|
6
|
-
|
|
7
|
-
## Upgrading
|
|
8
|
-
|
|
9
|
-
- `MicrogridConfig.load_from_file` is replaced by `AssetsConfig.load_from_files`,
|
|
10
|
-
which loads one or more files, merged into one document, and returns the whole
|
|
11
|
-
document rather than just its microgrids:
|
|
12
|
-
|
|
13
|
-
```python
|
|
14
|
-
configs = AssetsConfig.load_from_files(path).microgrids
|
|
15
|
-
```
|
|
16
|
-
|
|
17
|
-
`load_configs` now returns the whole `AssetsConfig` rather than just its
|
|
18
|
-
microgrids, so the file layers' `relations` and `market_locations` survive the
|
|
19
|
-
merge; replace `load_configs(...)` with `load_configs(...).microgrids` where
|
|
20
|
-
only the microgrid map is needed. `load_from_files` layers files field by field
|
|
21
|
-
instead of replacing a complete microgrid entry; fields omitted by a later file
|
|
22
|
-
retain the value from the earlier layer and cannot be removed by omission.
|
|
23
|
-
|
|
24
|
-
- `Metadata` is removed; its fields (`microgrid_id`, `name`, `gid`, coordinates,
|
|
25
|
-
times) now sit directly on `MicrogridConfig`:
|
|
26
|
-
|
|
27
|
-
```python
|
|
28
|
-
MicrogridConfig(microgrid_id=1, name="Grid") # was meta=Metadata(...)
|
|
29
|
-
```
|
|
30
|
-
|
|
31
|
-
In TOML they move up one level, `assets.microgrids.1.name` rather than
|
|
32
|
-
`assets.microgrids.1.meta.name`. A file still nesting a microgrid's fields
|
|
33
|
-
under `meta` loads, lifted with a deprecation warning.
|
|
34
|
-
|
|
35
|
-
- A microgrid's `delivery_area` is removed. Move its value to a relation's
|
|
36
|
-
`delivery_area.code`, and set `delivery_area.code_type` when the code is not
|
|
37
|
-
EIC. When relations are present, the legacy `gid` must be their sole
|
|
38
|
-
gridpool ID; remove it for a microgrid that participates in several gridpools.
|
|
39
|
-
|
|
40
|
-
- `load_configs_from_files` is removed. Use `AssetsConfig.load_from_files`,
|
|
41
|
-
which returns the whole document rather than just its microgrids: replace
|
|
42
|
-
`load_configs_from_files(files)` with
|
|
43
|
-
`AssetsConfig.load_from_files(files).microgrids`, or use the returned
|
|
44
|
-
`AssetsConfig` directly to keep relations and market locations.
|
|
45
|
-
|
|
46
|
-
- `load_configs_from_api` is now private. For an API-only load call
|
|
47
|
-
`load_configs(assets_client=..., microgrid_ids=...)` and read its `.microgrids`.
|
|
48
|
-
|
|
49
|
-
- `merge_config_maps` and `merge_microgrid_configs` are removed. Layering is now
|
|
50
|
-
done on the raw tables before loading, inside `load_configs` and
|
|
51
|
-
`AssetsConfig.load_from_files`; pass all the layers to one of those instead of
|
|
52
|
-
merging loaded objects.
|
|
53
|
-
|
|
54
|
-
- Relation validity bounds and `at` query instants must include a UTC offset.
|
|
55
|
-
|
|
56
|
-
- The implementation modules `config.load` and `config.microgrid` are now
|
|
57
|
-
private. Import their public names from `frequenz.gridpool.config` instead.
|
|
58
|
-
|
|
59
|
-
- Microgrids are now keyed by `int` microgrid ID, not `str`. This covers
|
|
60
|
-
`AssetsConfig.microgrids`, including documents returned by `load_configs`.
|
|
61
|
-
Index the mapping by integer ID:
|
|
62
|
-
|
|
63
|
-
```python
|
|
64
|
-
configs[1] # was configs["1"]
|
|
65
|
-
```
|
|
66
|
-
|
|
67
|
-
- The `AC_ACTIVE_POWER` deprecation warning is dropped. Use `AC_POWER_ACTIVE`
|
|
68
|
-
as the formula metric key; the old name is no longer flagged on load.
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
## New Features
|
|
72
|
-
|
|
73
|
-
- `AssetsConfig` gives the `assets` namespace a type, so the entities still to
|
|
74
|
-
come are added as fields rather than as more dict lookups. Microgrid IDs are
|
|
75
|
-
checked during construction. `AssetsConfig.check()` performs the topology-wide
|
|
76
|
-
checks after all layers have been merged; the file loaders call it unless
|
|
77
|
-
`AssetsConfig.load_from_files` is passed `check=False`.
|
|
78
|
-
|
|
79
|
-
File loaders ignore unknown entity tables with a warning, so a reader keeps
|
|
80
|
-
working against files that already carry newer entities.
|
|
81
|
-
|
|
82
|
-
The `assets` table may carry a `version`; on load a document is run through a
|
|
83
|
-
migration pipeline that brings older layouts up to the current format, so
|
|
84
|
-
legacy files keep working. The version tracks the assets format alone, not
|
|
85
|
-
the whole document.
|
|
86
|
-
|
|
87
|
-
- Market topology is described under `assets.relations`, based on the Assets
|
|
88
|
-
API `MarketTopologyRelation`: each record links at least two of a gridpool, a
|
|
89
|
-
microgrid and a market location, filed under a
|
|
90
|
-
`G<gridpool_id>M<microgrid_id>L<market_location_id>` key derived from its own
|
|
91
|
-
sides. A relation naming a gridpool sits in a `delivery_area` that rides on
|
|
92
|
-
the relation, so a gridpool-to-microgrid relation with no market location still
|
|
93
|
-
carries one. A relation's validity lives in `validity`, each entry a half-open
|
|
94
|
-
`[start, end)` datetime period it applies over. Use-case-specific periods
|
|
95
|
-
qualify a relation; separate relations let one microgrid participate in
|
|
96
|
-
several gridpools. The config extends the API with plain periods for relations
|
|
97
|
-
that do not distinguish use cases. A gridpool-free microgrid-to-market-location
|
|
98
|
-
relation may also carry a delivery area for a direct mapping. Market locations
|
|
99
|
-
live under `assets.market_locations` as self-describing entries carrying their
|
|
100
|
-
own identifier, how to read it (`MALO_ID` by default), and the Assets API market
|
|
101
|
-
area (`EU_DE` by default). A relation's `delivery_area` is a `code` plus a
|
|
102
|
-
`code_type` that defaults to EIC, so an EIC area is just
|
|
103
|
-
`delivery_area.code = "..."`; EIC codes are check-character-validated. Raw
|
|
104
|
-
market-location IDs must be unique within a document, including across market
|
|
105
|
-
areas.
|
|
106
|
-
|
|
107
|
-
`AssetsConfig` answers the common lookups with `find_relations` and the
|
|
108
|
-
projections `find_delivery_areas`, `find_market_locations` and
|
|
109
|
-
`find_microgrids`, each filtered by the other sides and an instant.
|
|
110
|
-
|
|
111
|
-
Time-varying enterprise ownership is outside this change;
|
|
112
|
-
`MicrogridConfig.enterprise_id` remains as a scalar field.
|
|
113
|
-
|
|
114
|
-
## Bug Fixes
|
|
115
|
-
|
|
116
|
-
- Layering config files no longer resets a field a later file leaves unset back
|
|
117
|
-
to its default. The raw tables are merged before they are loaded.
|
|
118
|
-
|
|
119
|
-
<!-- Here goes notable bug fixes that are worth a special mention or explanation -->
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
{frequenz_gridpool-0.7.0 → frequenz_gridpool-0.7.1}/src/frequenz/gridpool/_graph_generator.py
RENAMED
|
File without changes
|
|
File without changes
|
{frequenz_gridpool-0.7.0 → frequenz_gridpool-0.7.1}/src/frequenz/gridpool/cli/_render_graph.py
RENAMED
|
File without changes
|
{frequenz_gridpool-0.7.0 → frequenz_gridpool-0.7.1}/src/frequenz/gridpool/config/_microgrid.py
RENAMED
|
File without changes
|
{frequenz_gridpool-0.7.0 → frequenz_gridpool-0.7.1}/src/frequenz/gridpool/config/_migrations.py
RENAMED
|
File without changes
|
{frequenz_gridpool-0.7.0 → frequenz_gridpool-0.7.1}/src/frequenz/gridpool/config/_topology.py
RENAMED
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
{frequenz_gridpool-0.7.0 → frequenz_gridpool-0.7.1}/src/frequenz_gridpool.egg-info/entry_points.txt
RENAMED
|
File without changes
|
{frequenz_gridpool-0.7.0 → frequenz_gridpool-0.7.1}/src/frequenz_gridpool.egg-info/requires.txt
RENAMED
|
File without changes
|
{frequenz_gridpool-0.7.0 → frequenz_gridpool-0.7.1}/src/frequenz_gridpool.egg-info/top_level.txt
RENAMED
|
File without changes
|