frequenz-gridpool 0.7.0__tar.gz → 0.8.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.
- {frequenz_gridpool-0.7.0/src/frequenz_gridpool.egg-info → frequenz_gridpool-0.8.0}/PKG-INFO +34 -8
- {frequenz_gridpool-0.7.0 → frequenz_gridpool-0.8.0}/README.md +26 -1
- frequenz_gridpool-0.8.0/RELEASE_NOTES.md +57 -0
- {frequenz_gridpool-0.7.0 → frequenz_gridpool-0.8.0}/pyproject.toml +11 -7
- {frequenz_gridpool-0.7.0 → frequenz_gridpool-0.8.0}/src/frequenz/gridpool/cli/__main__.py +126 -30
- {frequenz_gridpool-0.7.0 → frequenz_gridpool-0.8.0}/src/frequenz/gridpool/cli/_dump_config.py +12 -9
- {frequenz_gridpool-0.7.0 → frequenz_gridpool-0.8.0}/src/frequenz/gridpool/cli/_patch_config.py +65 -23
- {frequenz_gridpool-0.7.0 → frequenz_gridpool-0.8.0}/src/frequenz/gridpool/config/__init__.py +2 -0
- {frequenz_gridpool-0.7.0 → frequenz_gridpool-0.8.0}/src/frequenz/gridpool/config/_assets.py +89 -6
- frequenz_gridpool-0.8.0/src/frequenz/gridpool/config/_fields.py +29 -0
- frequenz_gridpool-0.8.0/src/frequenz/gridpool/config/_gridpool.py +17 -0
- {frequenz_gridpool-0.7.0 → frequenz_gridpool-0.8.0}/src/frequenz/gridpool/config/_load.py +3 -0
- {frequenz_gridpool-0.7.0 → frequenz_gridpool-0.8.0}/src/frequenz/gridpool/config/_microgrid.py +10 -8
- frequenz_gridpool-0.8.0/src/frequenz/gridpool/config/_migrations.py +58 -0
- {frequenz_gridpool-0.7.0 → frequenz_gridpool-0.8.0}/src/frequenz/gridpool/config/_topology.py +4 -2
- {frequenz_gridpool-0.7.0 → frequenz_gridpool-0.8.0/src/frequenz_gridpool.egg-info}/PKG-INFO +34 -8
- {frequenz_gridpool-0.7.0 → frequenz_gridpool-0.8.0}/src/frequenz_gridpool.egg-info/SOURCES.txt +2 -0
- {frequenz_gridpool-0.7.0 → frequenz_gridpool-0.8.0}/src/frequenz_gridpool.egg-info/requires.txt +7 -6
- {frequenz_gridpool-0.7.0 → frequenz_gridpool-0.8.0}/src/frequenz_gridpool.egg-info/scm_file_list.json +2 -0
- frequenz_gridpool-0.8.0/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/config/_migrations.py +0 -139
- frequenz_gridpool-0.7.0/src/frequenz_gridpool.egg-info/scm_version.json +0 -8
- {frequenz_gridpool-0.7.0 → frequenz_gridpool-0.8.0}/LICENSE +0 -0
- {frequenz_gridpool-0.7.0 → frequenz_gridpool-0.8.0}/MANIFEST.in +0 -0
- {frequenz_gridpool-0.7.0 → frequenz_gridpool-0.8.0}/setup.cfg +0 -0
- {frequenz_gridpool-0.7.0 → frequenz_gridpool-0.8.0}/src/frequenz/gridpool/__init__.py +0 -0
- {frequenz_gridpool-0.7.0 → frequenz_gridpool-0.8.0}/src/frequenz/gridpool/_graph_generator.py +0 -0
- {frequenz_gridpool-0.7.0 → frequenz_gridpool-0.8.0}/src/frequenz/gridpool/cli/__init__.py +0 -0
- {frequenz_gridpool-0.7.0 → frequenz_gridpool-0.8.0}/src/frequenz/gridpool/cli/_render_graph.py +0 -0
- {frequenz_gridpool-0.7.0 → frequenz_gridpool-0.8.0}/src/frequenz/gridpool/conftest.py +0 -0
- {frequenz_gridpool-0.7.0 → frequenz_gridpool-0.8.0}/src/frequenz/gridpool/py.typed +0 -0
- {frequenz_gridpool-0.7.0 → frequenz_gridpool-0.8.0}/src/frequenz_gridpool.egg-info/dependency_links.txt +0 -0
- {frequenz_gridpool-0.7.0 → frequenz_gridpool-0.8.0}/src/frequenz_gridpool.egg-info/entry_points.txt +0 -0
- {frequenz_gridpool-0.7.0 → frequenz_gridpool-0.8.0}/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.
|
|
3
|
+
Version: 0.8.0
|
|
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
|
|
@@ -19,6 +19,7 @@ Classifier: Typing :: Typed
|
|
|
19
19
|
Requires-Python: <4,>=3.11
|
|
20
20
|
Description-Content-Type: text/markdown
|
|
21
21
|
License-File: LICENSE
|
|
22
|
+
Requires-Dist: marshmallow<5,>=3.18.0
|
|
22
23
|
Requires-Dist: marshmallow-dataclass<9,>=8.7.1
|
|
23
24
|
Requires-Dist: typing-extensions<5,>=4.14.1
|
|
24
25
|
Requires-Dist: frequenz-microgrid-component-graph<0.6,>=0.5.0
|
|
@@ -32,7 +33,7 @@ Requires-Dist: matplotlib<4,>=3.7.0; extra == "render-graph"
|
|
|
32
33
|
Requires-Dist: networkx<4,>=3.0; extra == "render-graph"
|
|
33
34
|
Provides-Extra: dev-flake8
|
|
34
35
|
Requires-Dist: flake8==7.3.0; extra == "dev-flake8"
|
|
35
|
-
Requires-Dist: flake8-datetimez==
|
|
36
|
+
Requires-Dist: flake8-datetimez==26.8.1; extra == "dev-flake8"
|
|
36
37
|
Requires-Dist: flake8-docstrings==1.7.0; extra == "dev-flake8"
|
|
37
38
|
Requires-Dist: flake8-pyproject==1.2.4; extra == "dev-flake8"
|
|
38
39
|
Requires-Dist: pydoclint==0.9.1; extra == "dev-flake8"
|
|
@@ -41,7 +42,7 @@ Provides-Extra: dev-formatting
|
|
|
41
42
|
Requires-Dist: black==26.5.1; extra == "dev-formatting"
|
|
42
43
|
Requires-Dist: isort==8.0.1; extra == "dev-formatting"
|
|
43
44
|
Provides-Extra: dev-mkdocs
|
|
44
|
-
Requires-Dist: Markdown==3.10.
|
|
45
|
+
Requires-Dist: Markdown==3.10.3; extra == "dev-mkdocs"
|
|
45
46
|
Requires-Dist: black==26.5.1; extra == "dev-mkdocs"
|
|
46
47
|
Requires-Dist: mike==2.2.0; extra == "dev-mkdocs"
|
|
47
48
|
Requires-Dist: mkdocs-gen-files==0.6.1; extra == "dev-mkdocs"
|
|
@@ -49,21 +50,21 @@ Requires-Dist: mkdocs-literate-nav==0.6.3; extra == "dev-mkdocs"
|
|
|
49
50
|
Requires-Dist: mkdocs-macros-plugin==1.5.0; extra == "dev-mkdocs"
|
|
50
51
|
Requires-Dist: mkdocs-material==9.7.7; extra == "dev-mkdocs"
|
|
51
52
|
Requires-Dist: mkdocstrings[python]==1.0.6; extra == "dev-mkdocs"
|
|
52
|
-
Requires-Dist: mkdocstrings-python==2.0.
|
|
53
|
+
Requires-Dist: mkdocstrings-python==2.0.7; extra == "dev-mkdocs"
|
|
53
54
|
Requires-Dist: frequenz-repo-config[lib]==0.18.0; extra == "dev-mkdocs"
|
|
54
55
|
Provides-Extra: dev-mypy
|
|
55
|
-
Requires-Dist: mypy==2.3.
|
|
56
|
+
Requires-Dist: mypy==2.3.1; extra == "dev-mypy"
|
|
56
57
|
Requires-Dist: types-networkx>=3.6.1.20251220; extra == "dev-mypy"
|
|
57
58
|
Requires-Dist: types-Markdown==3.10.2.20260712; extra == "dev-mypy"
|
|
58
59
|
Requires-Dist: frequenz-gridpool[dev-mkdocs,dev-noxfile,dev-pytest]; extra == "dev-mypy"
|
|
59
60
|
Provides-Extra: dev-noxfile
|
|
60
|
-
Requires-Dist: nox==2026.
|
|
61
|
+
Requires-Dist: nox==2026.8.17; extra == "dev-noxfile"
|
|
61
62
|
Requires-Dist: frequenz-repo-config[lib]==0.18.0; extra == "dev-noxfile"
|
|
62
63
|
Provides-Extra: dev-pylint
|
|
63
64
|
Requires-Dist: frequenz-gridpool[dev-mkdocs,dev-noxfile,dev-pytest]; extra == "dev-pylint"
|
|
64
65
|
Provides-Extra: dev-pytest
|
|
65
66
|
Requires-Dist: pytest==9.1.1; extra == "dev-pytest"
|
|
66
|
-
Requires-Dist: pylint==4.0.
|
|
67
|
+
Requires-Dist: pylint==4.0.7; extra == "dev-pytest"
|
|
67
68
|
Requires-Dist: frequenz-repo-config[extra-lint-examples]==0.18.0; extra == "dev-pytest"
|
|
68
69
|
Requires-Dist: pytest-mock==3.15.1; extra == "dev-pytest"
|
|
69
70
|
Requires-Dist: pytest-asyncio==1.4.0; extra == "dev-pytest"
|
|
@@ -150,7 +151,7 @@ The following platforms are officially supported (tested):
|
|
|
150
151
|
|
|
151
152
|
## CLI
|
|
152
153
|
|
|
153
|
-
This package ships the `gridpool-cli` command with
|
|
154
|
+
This package ships the `gridpool-cli` command with five subcommands.
|
|
154
155
|
|
|
155
156
|
### Setup
|
|
156
157
|
|
|
@@ -162,6 +163,9 @@ export ASSETS_API_AUTH_KEY="..."
|
|
|
162
163
|
export ASSETS_API_SIGN_SECRET="..."
|
|
163
164
|
```
|
|
164
165
|
|
|
166
|
+
`FREQUENZ_API_KEY` and `FREQUENZ_API_SECRET` are accepted as fallbacks for
|
|
167
|
+
`ASSETS_API_AUTH_KEY` and `ASSETS_API_SIGN_SECRET`.
|
|
168
|
+
|
|
165
169
|
### Print component formulas
|
|
166
170
|
|
|
167
171
|
```bash
|
|
@@ -246,6 +250,28 @@ gridpool-cli generate-config <microgrid_id> \
|
|
|
246
250
|
--prefer-meters-in-component-formulas > microgrid.toml
|
|
247
251
|
```
|
|
248
252
|
|
|
253
|
+
### Validate config files
|
|
254
|
+
|
|
255
|
+
Check config files offline, without contacting the Assets API, and exit
|
|
256
|
+
non-zero on the first error, to gate config-repo CI:
|
|
257
|
+
|
|
258
|
+
```bash
|
|
259
|
+
gridpool-cli validate microgrid.toml [more.toml ...]
|
|
260
|
+
```
|
|
261
|
+
|
|
262
|
+
Each file is validated on its own first, so every record names its own key and
|
|
263
|
+
required fields; the files are then validated merged, for the cross-record
|
|
264
|
+
checks.
|
|
265
|
+
|
|
266
|
+
### Look up a gridpool's enterprise
|
|
267
|
+
|
|
268
|
+
Print the enterprise ID that owns a gridpool, read from the config files
|
|
269
|
+
(merged as one stack); exits non-zero if the gridpool is not declared:
|
|
270
|
+
|
|
271
|
+
```bash
|
|
272
|
+
gridpool-cli find-enterprise <gridpool_id> config.toml [more.toml ...]
|
|
273
|
+
```
|
|
274
|
+
|
|
249
275
|
## Contributing
|
|
250
276
|
|
|
251
277
|
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,57 @@
|
|
|
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
|
+
- The legacy config layouts are no longer migrated on load: top-level microgrid
|
|
12
|
+
IDs and `meta`-nested microgrid fields are gone. A file with no `assets` table
|
|
13
|
+
now loads as empty and contributes nothing to a merge, rather than having its
|
|
14
|
+
top-level keys read as microgrid IDs. This lets a mixed list of files (some
|
|
15
|
+
carrying an `[app]` table for their own config, some an `assets` table) be
|
|
16
|
+
passed to `AssetsConfig.load_from_files` / `load_configs`, which reads only the
|
|
17
|
+
`assets`-bearing ones. Rebuild any legacy-layout files with `generate-config`.
|
|
18
|
+
|
|
19
|
+
- `generate-config` now writes the `assets.microgrids` layout and stamps
|
|
20
|
+
`assets.version`, instead of the legacy layout. Regenerate configs to get the
|
|
21
|
+
current format; generated files then load back without a migration.
|
|
22
|
+
|
|
23
|
+
- `generate-config --inplace` refuses legacy-layout files (top-level or
|
|
24
|
+
`meta`-nested), which it would duplicate rather than edit. Rebuild those from
|
|
25
|
+
scratch.
|
|
26
|
+
|
|
27
|
+
## New Features
|
|
28
|
+
|
|
29
|
+
- `gridpool-cli validate <files>` checks config files offline and exits
|
|
30
|
+
non-zero on the first error, to gate config-repo CI. Each file must be valid
|
|
31
|
+
on its own, so a record names its own key and required fields; the files are
|
|
32
|
+
then checked merged, for the cross-record checks.
|
|
33
|
+
|
|
34
|
+
- `gridpool-cli` accepts `FREQUENZ_API_KEY` and `FREQUENZ_API_SECRET` as a
|
|
35
|
+
fallback pair for `ASSETS_API_AUTH_KEY` and `ASSETS_API_SIGN_SECRET`.
|
|
36
|
+
|
|
37
|
+
- Gridpools are described under `assets.gridpools`, each entry naming the
|
|
38
|
+
enterprise that owns the gridpool. `AssetsConfig.find_enterprise(gridpool_id)`
|
|
39
|
+
returns the configured owner.
|
|
40
|
+
`gridpool-cli find-enterprise <gridpool_id> <files>` prints it from the config.
|
|
41
|
+
`AssetsConfig.check` enforces the one-enterprise-per-gridpool invariant: a
|
|
42
|
+
gridpool's microgrids may not disagree on it, and a declared enterprise must
|
|
43
|
+
match the inferred one.
|
|
44
|
+
|
|
45
|
+
- A config derived from the Assets API now carries each microgrid's
|
|
46
|
+
`enterprise_id`.
|
|
47
|
+
|
|
48
|
+
- `generate-config --inplace` refreshes managed values while preserving
|
|
49
|
+
formatting; `--fill-missing` only adds absent values. Generated patches are
|
|
50
|
+
validated before writing.
|
|
51
|
+
|
|
52
|
+
## Bug Fixes
|
|
53
|
+
|
|
54
|
+
- Config files with validity periods now load under marshmallow 3, not only
|
|
55
|
+
marshmallow 4. `tomllib` yields native `datetime` objects, which marshmallow
|
|
56
|
+
3's `DateTime` field rejected. `marshmallow` is now a direct dependency so the
|
|
57
|
+
minimum-version test exercises this path.
|
|
@@ -3,7 +3,7 @@
|
|
|
3
3
|
|
|
4
4
|
[build-system]
|
|
5
5
|
requires = [
|
|
6
|
-
"setuptools ==
|
|
6
|
+
"setuptools == 84.0.0",
|
|
7
7
|
"setuptools_scm[toml] == 10.2.1",
|
|
8
8
|
"frequenz-repo-config[lib] == 0.18.0",
|
|
9
9
|
]
|
|
@@ -26,6 +26,10 @@ classifiers = [
|
|
|
26
26
|
]
|
|
27
27
|
requires-python = ">= 3.11, < 4"
|
|
28
28
|
dependencies = [
|
|
29
|
+
# Used transitively via marshmallow-dataclass (every config schema is a
|
|
30
|
+
# `marshmallow.Schema`); declared directly so `pytest_min` pins its lower
|
|
31
|
+
# bound and the minimum-version path is actually tested.
|
|
32
|
+
"marshmallow >= 3.18.0, < 5",
|
|
29
33
|
"marshmallow-dataclass >= 8.7.1, < 9",
|
|
30
34
|
"typing-extensions >= 4.14.1, < 5",
|
|
31
35
|
"frequenz-microgrid-component-graph >= 0.5.0, < 0.6",
|
|
@@ -53,7 +57,7 @@ render-graph = [
|
|
|
53
57
|
|
|
54
58
|
dev-flake8 = [
|
|
55
59
|
"flake8 == 7.3.0",
|
|
56
|
-
"flake8-datetimez ==
|
|
60
|
+
"flake8-datetimez == 26.8.1",
|
|
57
61
|
"flake8-docstrings == 1.7.0",
|
|
58
62
|
"flake8-pyproject == 1.2.4", # For reading the flake8 config from pyproject.toml
|
|
59
63
|
"pydoclint == 0.9.1",
|
|
@@ -61,7 +65,7 @@ dev-flake8 = [
|
|
|
61
65
|
]
|
|
62
66
|
dev-formatting = ["black == 26.5.1", "isort == 8.0.1"]
|
|
63
67
|
dev-mkdocs = [
|
|
64
|
-
"Markdown == 3.10.
|
|
68
|
+
"Markdown == 3.10.3",
|
|
65
69
|
"black == 26.5.1",
|
|
66
70
|
"mike == 2.2.0",
|
|
67
71
|
"mkdocs-gen-files == 0.6.1",
|
|
@@ -69,18 +73,18 @@ dev-mkdocs = [
|
|
|
69
73
|
"mkdocs-macros-plugin == 1.5.0",
|
|
70
74
|
"mkdocs-material == 9.7.7",
|
|
71
75
|
"mkdocstrings[python] == 1.0.6",
|
|
72
|
-
"mkdocstrings-python == 2.0.
|
|
76
|
+
"mkdocstrings-python == 2.0.7",
|
|
73
77
|
"frequenz-repo-config[lib] == 0.18.0",
|
|
74
78
|
]
|
|
75
79
|
dev-mypy = [
|
|
76
|
-
"mypy == 2.3.
|
|
80
|
+
"mypy == 2.3.1",
|
|
77
81
|
"types-networkx>=3.6.1.20251220",
|
|
78
82
|
"types-Markdown == 3.10.2.20260712",
|
|
79
83
|
# For checking the noxfile, docs/ script, and tests
|
|
80
84
|
"frequenz-gridpool[dev-mkdocs,dev-noxfile,dev-pytest]",
|
|
81
85
|
]
|
|
82
86
|
dev-noxfile = [
|
|
83
|
-
"nox == 2026.
|
|
87
|
+
"nox == 2026.8.17",
|
|
84
88
|
"frequenz-repo-config[lib] == 0.18.0",
|
|
85
89
|
]
|
|
86
90
|
dev-pylint = [
|
|
@@ -90,7 +94,7 @@ dev-pylint = [
|
|
|
90
94
|
]
|
|
91
95
|
dev-pytest = [
|
|
92
96
|
"pytest == 9.1.1",
|
|
93
|
-
"pylint == 4.0.
|
|
97
|
+
"pylint == 4.0.7", # We need this to check for the examples
|
|
94
98
|
"frequenz-repo-config[extra-lint-examples] == 0.18.0",
|
|
95
99
|
"pytest-mock == 3.15.1",
|
|
96
100
|
"pytest-asyncio == 1.4.0",
|
|
@@ -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.8.0}/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
|
|
@@ -15,12 +17,16 @@ escaping, key-quoting, list/number/datetime formatting) is delegated to
|
|
|
15
17
|
fields are done here, since TOML has no null and `tomlkit` rejects it.
|
|
16
18
|
"""
|
|
17
19
|
|
|
20
|
+
from dataclasses import asdict
|
|
18
21
|
from typing import Any
|
|
19
22
|
|
|
20
23
|
import tomlkit
|
|
21
24
|
from tomlkit.items import Integer, Trivia
|
|
22
25
|
|
|
23
26
|
from frequenz.gridpool.config import MicrogridConfig
|
|
27
|
+
from frequenz.gridpool.config._migrations import _CURRENT_VERSION
|
|
28
|
+
|
|
29
|
+
_MICROGRID_PREFIX = ["assets", "microgrids"]
|
|
24
30
|
|
|
25
31
|
|
|
26
32
|
def _is_empty(value: Any) -> bool:
|
|
@@ -81,16 +87,13 @@ def dump_map(configs: dict[int, MicrogridConfig]) -> str:
|
|
|
81
87
|
The TOML representation as a string, with one blank line between
|
|
82
88
|
microgrids and entries sorted by numeric microgrid ID.
|
|
83
89
|
"""
|
|
84
|
-
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
|
-
|
|
88
|
-
assert isinstance(dumped, dict)
|
|
89
|
-
leaves = _iter_leaves([str(mid)], dumped)
|
|
93
|
+
leaves = _iter_leaves([*_MICROGRID_PREFIX, str(mid)], asdict(configs[mid]))
|
|
90
94
|
if not leaves:
|
|
91
95
|
continue
|
|
92
|
-
|
|
93
|
-
doc.add(tomlkit.nl())
|
|
96
|
+
doc.add(tomlkit.nl())
|
|
94
97
|
for path, value in leaves:
|
|
95
98
|
doc.append(tomlkit.key(path), _format_value(value))
|
|
96
99
|
return tomlkit.dumps(doc)
|