frequenz-gridpool 0.7.1__tar.gz → 0.9.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 (35) hide show
  1. {frequenz_gridpool-0.7.1/src/frequenz_gridpool.egg-info → frequenz_gridpool-0.9.0}/PKG-INFO +15 -14
  2. frequenz_gridpool-0.9.0/RELEASE_NOTES.md +20 -0
  3. {frequenz_gridpool-0.7.1 → frequenz_gridpool-0.9.0}/pyproject.toml +23 -17
  4. {frequenz_gridpool-0.7.1 → frequenz_gridpool-0.9.0}/src/frequenz/gridpool/cli/_dump_config.py +2 -4
  5. {frequenz_gridpool-0.7.1 → frequenz_gridpool-0.9.0}/src/frequenz/gridpool/cli/_patch_config.py +2 -4
  6. {frequenz_gridpool-0.7.1 → frequenz_gridpool-0.9.0}/src/frequenz/gridpool/config/_assets.py +9 -4
  7. frequenz_gridpool-0.9.0/src/frequenz/gridpool/config/_fields.py +29 -0
  8. {frequenz_gridpool-0.7.1 → frequenz_gridpool-0.9.0}/src/frequenz/gridpool/config/_microgrid.py +10 -8
  9. frequenz_gridpool-0.9.0/src/frequenz/gridpool/config/_migrations.py +58 -0
  10. {frequenz_gridpool-0.7.1 → frequenz_gridpool-0.9.0}/src/frequenz/gridpool/config/_topology.py +4 -2
  11. {frequenz_gridpool-0.7.1 → frequenz_gridpool-0.9.0/src/frequenz_gridpool.egg-info}/PKG-INFO +15 -14
  12. {frequenz_gridpool-0.7.1 → frequenz_gridpool-0.9.0}/src/frequenz_gridpool.egg-info/SOURCES.txt +1 -0
  13. {frequenz_gridpool-0.7.1 → frequenz_gridpool-0.9.0}/src/frequenz_gridpool.egg-info/requires.txt +14 -13
  14. {frequenz_gridpool-0.7.1 → frequenz_gridpool-0.9.0}/src/frequenz_gridpool.egg-info/scm_file_list.json +1 -1
  15. frequenz_gridpool-0.9.0/src/frequenz_gridpool.egg-info/scm_version.json +8 -0
  16. frequenz_gridpool-0.7.1/RELEASE_NOTES.md +0 -42
  17. frequenz_gridpool-0.7.1/src/frequenz/gridpool/config/_migrations.py +0 -139
  18. frequenz_gridpool-0.7.1/src/frequenz_gridpool.egg-info/scm_version.json +0 -8
  19. {frequenz_gridpool-0.7.1 → frequenz_gridpool-0.9.0}/LICENSE +0 -0
  20. {frequenz_gridpool-0.7.1 → frequenz_gridpool-0.9.0}/MANIFEST.in +0 -0
  21. {frequenz_gridpool-0.7.1 → frequenz_gridpool-0.9.0}/README.md +0 -0
  22. {frequenz_gridpool-0.7.1 → frequenz_gridpool-0.9.0}/setup.cfg +0 -0
  23. {frequenz_gridpool-0.7.1 → frequenz_gridpool-0.9.0}/src/frequenz/gridpool/__init__.py +0 -0
  24. {frequenz_gridpool-0.7.1 → frequenz_gridpool-0.9.0}/src/frequenz/gridpool/_graph_generator.py +0 -0
  25. {frequenz_gridpool-0.7.1 → frequenz_gridpool-0.9.0}/src/frequenz/gridpool/cli/__init__.py +0 -0
  26. {frequenz_gridpool-0.7.1 → frequenz_gridpool-0.9.0}/src/frequenz/gridpool/cli/__main__.py +0 -0
  27. {frequenz_gridpool-0.7.1 → frequenz_gridpool-0.9.0}/src/frequenz/gridpool/cli/_render_graph.py +0 -0
  28. {frequenz_gridpool-0.7.1 → frequenz_gridpool-0.9.0}/src/frequenz/gridpool/config/__init__.py +0 -0
  29. {frequenz_gridpool-0.7.1 → frequenz_gridpool-0.9.0}/src/frequenz/gridpool/config/_gridpool.py +0 -0
  30. {frequenz_gridpool-0.7.1 → frequenz_gridpool-0.9.0}/src/frequenz/gridpool/config/_load.py +0 -0
  31. {frequenz_gridpool-0.7.1 → frequenz_gridpool-0.9.0}/src/frequenz/gridpool/conftest.py +0 -0
  32. {frequenz_gridpool-0.7.1 → frequenz_gridpool-0.9.0}/src/frequenz/gridpool/py.typed +0 -0
  33. {frequenz_gridpool-0.7.1 → frequenz_gridpool-0.9.0}/src/frequenz_gridpool.egg-info/dependency_links.txt +0 -0
  34. {frequenz_gridpool-0.7.1 → frequenz_gridpool-0.9.0}/src/frequenz_gridpool.egg-info/entry_points.txt +0 -0
  35. {frequenz_gridpool-0.7.1 → frequenz_gridpool-0.9.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.7.1
3
+ Version: 0.9.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,9 +19,10 @@ 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
- Requires-Dist: frequenz-microgrid-component-graph<0.6,>=0.5.0
25
+ Requires-Dist: frequenz-microgrid-component-graph<0.7,>=0.6.0
25
26
  Requires-Dist: frequenz-client-assets<0.6,>=0.5.0
26
27
  Provides-Extra: cli
27
28
  Requires-Dist: asyncclick<9,>=8.3.0.4; extra == "cli"
@@ -31,8 +32,8 @@ Requires-Dist: frequenz-gridpool[cli]; extra == "render-graph"
31
32
  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
- Requires-Dist: flake8==7.3.0; extra == "dev-flake8"
35
- Requires-Dist: flake8-datetimez==20.10.0; extra == "dev-flake8"
35
+ Requires-Dist: flake8==7.4.1; extra == "dev-flake8"
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,31 +42,31 @@ 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.2; extra == "dev-mkdocs"
45
+ Requires-Dist: Markdown==3.11; 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"
48
49
  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
- Requires-Dist: mkdocstrings[python]==1.0.6; extra == "dev-mkdocs"
52
- Requires-Dist: mkdocstrings-python==2.0.5; extra == "dev-mkdocs"
53
- Requires-Dist: frequenz-repo-config[lib]==0.18.0; extra == "dev-mkdocs"
52
+ Requires-Dist: mkdocstrings[python]==1.0.7; extra == "dev-mkdocs"
53
+ Requires-Dist: mkdocstrings-python==2.0.9; extra == "dev-mkdocs"
54
+ Requires-Dist: frequenz-repo-config[lib]==0.19.0; extra == "dev-mkdocs"
54
55
  Provides-Extra: dev-mypy
55
- Requires-Dist: mypy==2.3.0; extra == "dev-mypy"
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.7.11; extra == "dev-noxfile"
61
- Requires-Dist: frequenz-repo-config[lib]==0.18.0; extra == "dev-noxfile"
61
+ Requires-Dist: nox==2026.8.17; extra == "dev-noxfile"
62
+ Requires-Dist: frequenz-repo-config[lib]==0.19.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.6; extra == "dev-pytest"
67
- Requires-Dist: frequenz-repo-config[extra-lint-examples]==0.18.0; extra == "dev-pytest"
68
- Requires-Dist: pytest-mock==3.15.1; extra == "dev-pytest"
67
+ Requires-Dist: pylint==4.0.9; extra == "dev-pytest"
68
+ Requires-Dist: frequenz-repo-config[extra-lint-examples]==0.19.0; extra == "dev-pytest"
69
+ Requires-Dist: pytest-mock==3.16.0; extra == "dev-pytest"
69
70
  Requires-Dist: pytest-asyncio==1.4.0; extra == "dev-pytest"
70
71
  Requires-Dist: async-solipsism==0.9; extra == "dev-pytest"
71
72
  Requires-Dist: frequenz-gridpool[render-graph]; extra == "dev-pytest"
@@ -0,0 +1,20 @@
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
+ - Requires `frequenz-microgrid-component-graph` 0.6, which stops clamping the consumer
10
+ and producer formulas. `ComponentGraph.consumer_formula()` and
11
+ `ComponentGraph.producer_formula()` can now return values previously pinned
12
+ to zero: a negative consumer total, or a positive producer total.
13
+
14
+ ## New Features
15
+
16
+ <!-- Here goes the main new features and examples or instructions on how to use them -->
17
+
18
+ ## Bug Fixes
19
+
20
+ <!-- Here goes notable bug fixes that are worth a special mention or explanation -->
@@ -3,9 +3,9 @@
3
3
 
4
4
  [build-system]
5
5
  requires = [
6
- "setuptools == 83.0.0",
7
- "setuptools_scm[toml] == 10.2.1",
8
- "frequenz-repo-config[lib] == 0.18.0",
6
+ "setuptools == 84.0.0",
7
+ "setuptools_scm[toml] == 10.3.4",
8
+ "frequenz-repo-config[lib] == 0.19.0",
9
9
  ]
10
10
  build-backend = "setuptools.build_meta"
11
11
 
@@ -26,9 +26,13 @@ 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
- "frequenz-microgrid-component-graph >= 0.5.0, < 0.6",
35
+ "frequenz-microgrid-component-graph >= 0.6.0, < 0.7",
32
36
  "frequenz-client-assets >= 0.5.0, < 0.6",
33
37
  ]
34
38
  dynamic = ["version"]
@@ -52,8 +56,8 @@ render-graph = [
52
56
  ]
53
57
 
54
58
  dev-flake8 = [
55
- "flake8 == 7.3.0",
56
- "flake8-datetimez == 20.10.0",
59
+ "flake8 == 7.4.1",
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,27 +65,27 @@ dev-flake8 = [
61
65
  ]
62
66
  dev-formatting = ["black == 26.5.1", "isort == 8.0.1"]
63
67
  dev-mkdocs = [
64
- "Markdown == 3.10.2",
68
+ "Markdown == 3.11",
65
69
  "black == 26.5.1",
66
70
  "mike == 2.2.0",
67
71
  "mkdocs-gen-files == 0.6.1",
68
72
  "mkdocs-literate-nav == 0.6.3",
69
73
  "mkdocs-macros-plugin == 1.5.0",
70
74
  "mkdocs-material == 9.7.7",
71
- "mkdocstrings[python] == 1.0.6",
72
- "mkdocstrings-python == 2.0.5",
73
- "frequenz-repo-config[lib] == 0.18.0",
75
+ "mkdocstrings[python] == 1.0.7",
76
+ "mkdocstrings-python == 2.0.9",
77
+ "frequenz-repo-config[lib] == 0.19.0",
74
78
  ]
75
79
  dev-mypy = [
76
- "mypy == 2.3.0",
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.7.11",
84
- "frequenz-repo-config[lib] == 0.18.0",
87
+ "nox == 2026.8.17",
88
+ "frequenz-repo-config[lib] == 0.19.0",
85
89
  ]
86
90
  dev-pylint = [
87
91
  # dev-pytest already defines a dependency to pylint because of the examples
@@ -90,9 +94,9 @@ dev-pylint = [
90
94
  ]
91
95
  dev-pytest = [
92
96
  "pytest == 9.1.1",
93
- "pylint == 4.0.6", # We need this to check for the examples
94
- "frequenz-repo-config[extra-lint-examples] == 0.18.0",
95
- "pytest-mock == 3.15.1",
97
+ "pylint == 4.0.9", # We need this to check for the examples
98
+ "frequenz-repo-config[extra-lint-examples] == 0.19.0",
99
+ "pytest-mock == 3.16.0",
96
100
  "pytest-asyncio == 1.4.0",
97
101
  "async-solipsism == 0.9",
98
102
  # The tests cover the cli package and the graph renderer. render-graph
@@ -168,13 +172,15 @@ disable = [
168
172
  ]
169
173
 
170
174
  [tool.pytest.ini_options]
171
- addopts = "-W=all -Werror -Wdefault::DeprecationWarning -Wdefault::PendingDeprecationWarning -vv"
175
+ addopts = "-W=all -Werror -Wdefault::DeprecationWarning -Wdefault::PendingDeprecationWarning"
172
176
  testpaths = ["tests", "src"]
177
+ asyncio_debug = true
173
178
  asyncio_mode = "auto"
174
179
  asyncio_default_fixture_loop_scope = "function"
175
180
  required_plugins = ["pytest-asyncio", "pytest-mock"]
176
181
 
177
182
  [tool.mypy]
183
+ enable_error_code = ["exhaustive-match"]
178
184
  explicit_package_bases = true
179
185
  namespace_packages = true
180
186
  # This option disables mypy cache, and it is sometimes useful to enable it if
@@ -17,6 +17,7 @@ escaping, key-quoting, list/number/datetime formatting) is delegated to
17
17
  fields are done here, since TOML has no null and `tomlkit` rejects it.
18
18
  """
19
19
 
20
+ from dataclasses import asdict
20
21
  from typing import Any
21
22
 
22
23
  import tomlkit
@@ -86,13 +87,10 @@ def dump_map(configs: dict[int, MicrogridConfig]) -> str:
86
87
  The TOML representation as a string, with one blank line between
87
88
  microgrids and entries sorted by numeric microgrid ID.
88
89
  """
89
- schema = MicrogridConfig.Schema()
90
90
  doc = tomlkit.document()
91
91
  doc.append(tomlkit.key(["assets", "version"]), _CURRENT_VERSION)
92
92
  for mid in sorted(configs):
93
- dumped = schema.dump(configs[mid])
94
- assert isinstance(dumped, dict)
95
- leaves = _iter_leaves([*_MICROGRID_PREFIX, str(mid)], dumped)
93
+ leaves = _iter_leaves([*_MICROGRID_PREFIX, str(mid)], asdict(configs[mid]))
96
94
  if not leaves:
97
95
  continue
98
96
  doc.add(tomlkit.nl())
@@ -10,6 +10,7 @@ survive. By default it refreshes the managed leaves, overwriting them; with
10
10
  """
11
11
 
12
12
  from collections.abc import Mapping
13
+ from dataclasses import asdict
13
14
  from pathlib import Path
14
15
  from typing import Any
15
16
 
@@ -54,7 +55,6 @@ def patch_text(
54
55
  """
55
56
  doc = tomlkit.parse(original)
56
57
  _reject_legacy_layout(doc)
57
- schema = MicrogridConfig.Schema()
58
58
 
59
59
  # Leaves needing a whole new sub-table can't be inserted via item assignment
60
60
  # without tomlkit falling back to bracket-header syntax, so they are spliced
@@ -63,9 +63,7 @@ def patch_text(
63
63
 
64
64
  for microgrid_id, cfg in configs.items():
65
65
  mid = str(microgrid_id)
66
- dumped = schema.dump(cfg)
67
- assert isinstance(dumped, dict)
68
- leaves = _iter_leaves([], dumped)
66
+ leaves = _iter_leaves([], asdict(cfg))
69
67
  if not leaves:
70
68
  continue
71
69
 
@@ -443,23 +443,28 @@ class AssetsConfig:
443
443
  """Read the raw `assets` table from a TOML file.
444
444
 
445
445
  The document is migrated to the current format before its `assets`
446
- table is returned for merging.
446
+ table is returned for merging. A file with no `assets` table contributes
447
+ nothing to the merge, so consumers can pass a mixed list of files and
448
+ gridpool reads only the `assets`-bearing ones.
447
449
 
448
450
  Args:
449
451
  config_path: The path to the TOML configuration file.
450
452
 
451
453
  Returns:
452
- The raw `assets` table, unvalidated, for merging before it is loaded.
454
+ The raw `assets` table, unvalidated, for merging before it is loaded,
455
+ or an empty table if the file has none.
453
456
 
454
457
  Raises:
455
- TypeError: If `assets` is not a table.
458
+ TypeError: If `assets` is present but not a table.
456
459
  """
457
460
  with config_path.open("rb") as f:
458
461
  data: dict[str, Any] = tomllib.load(f)
459
462
 
460
463
  data = migrate(data, config_path)
461
464
 
462
- assets = data["assets"]
465
+ assets = data.get("assets")
466
+ if assets is None:
467
+ return {}
463
468
  if not isinstance(assets, dict):
464
469
  raise TypeError(
465
470
  f"{config_path}: `assets` must be a table, got {type(assets)}"
@@ -0,0 +1,29 @@
1
+ # License: MIT
2
+ # Copyright © 2026 Frequenz Energy-as-a-Service GmbH
3
+
4
+ """Reusable marshmallow fields for config data models."""
5
+
6
+ from datetime import datetime
7
+ from typing import Any
8
+
9
+ import marshmallow
10
+
11
+
12
+ class TomlAwareDateTime(marshmallow.fields.DateTime):
13
+ """A `DateTime` that also accepts an already-parsed `datetime`.
14
+
15
+ marshmallow 3's `DateTime` deserialises only ISO strings, but `tomllib`
16
+ yields native `datetime` objects before the value reaches marshmallow, so
17
+ those inputs must pass through unchanged. marshmallow 4 accepts them itself;
18
+ this keeps both behaving the same.
19
+ """
20
+
21
+ def _deserialize(self, value: Any, *args: Any, **kwargs: Any) -> datetime:
22
+ if isinstance(value, datetime):
23
+ return value
24
+ return super()._deserialize(value, *args, **kwargs)
25
+
26
+
27
+ def toml_datetime_metadata() -> dict[str, Any]:
28
+ """Field metadata mapping a TOML-sourced `datetime` to `TomlAwareDateTime`."""
29
+ return {"marshmallow_field": TomlAwareDateTime(allow_none=True)}
@@ -11,6 +11,8 @@ from typing import Any, ClassVar, Literal, Self, Type, cast, get_args
11
11
  from marshmallow import Schema
12
12
  from marshmallow_dataclass import dataclass
13
13
 
14
+ from ._fields import toml_datetime_metadata
15
+
14
16
  ComponentType = Literal["grid", "pv", "battery", "consumption", "chp", "ev"]
15
17
  """Valid component types."""
16
18
 
@@ -98,10 +100,10 @@ class ComponentTypeConfig:
98
100
  class PVConfig:
99
101
  """Configuration of a PV system in a microgrid."""
100
102
 
101
- start_time: datetime | None = None
103
+ start_time: datetime | None = field(default=None, metadata=toml_datetime_metadata())
102
104
  """Start time of the PV system installation."""
103
105
 
104
- end_time: datetime | None = None
106
+ end_time: datetime | None = field(default=None, metadata=toml_datetime_metadata())
105
107
  """End time of the PV system installation."""
106
108
 
107
109
  peak_power: float | None = None
@@ -119,10 +121,10 @@ class WindConfig:
119
121
  # pylint: disable=too-many-instance-attributes
120
122
  """Configuration of a wind turbine in a microgrid."""
121
123
 
122
- start_time: datetime | None = None
124
+ start_time: datetime | None = field(default=None, metadata=toml_datetime_metadata())
123
125
  """Start time of the wind turbine installation."""
124
126
 
125
- end_time: datetime | None = None
127
+ end_time: datetime | None = field(default=None, metadata=toml_datetime_metadata())
126
128
  """End time of the wind turbine installation."""
127
129
 
128
130
  turbine_model: str | None = None
@@ -151,10 +153,10 @@ class WindConfig:
151
153
  class BatteryConfig:
152
154
  """Configuration of a battery in a microgrid."""
153
155
 
154
- start_time: datetime | None = None
156
+ start_time: datetime | None = field(default=None, metadata=toml_datetime_metadata())
155
157
  """Start time of the battery installation."""
156
158
 
157
- end_time: datetime | None = None
159
+ end_time: datetime | None = field(default=None, metadata=toml_datetime_metadata())
158
160
  """End time of the battery installation."""
159
161
 
160
162
  capacity: float | None = None
@@ -187,10 +189,10 @@ class MicrogridConfig:
187
189
  altitude: float | None = None
188
190
  """Geographic altitude of the microgrid."""
189
191
 
190
- start_time: datetime | None = None
192
+ start_time: datetime | None = field(default=None, metadata=toml_datetime_metadata())
191
193
  """Start time of the microgrid operation."""
192
194
 
193
- end_time: datetime | None = None
195
+ end_time: datetime | None = field(default=None, metadata=toml_datetime_metadata())
194
196
  """End time of the microgrid operation."""
195
197
 
196
198
  pv: dict[str, PVConfig] | None = None
@@ -0,0 +1,58 @@
1
+ # License: MIT
2
+ # Copyright © 2026 Frequenz Energy-as-a-Service GmbH
3
+
4
+ """Format migrations for raw config documents.
5
+
6
+ Loading validates `assets.version` and stamps the current version, so a file
7
+ written for a newer reader is rejected rather than silently misread. Each
8
+ migration step rewrites the raw `dict` before it is loaded into the typed
9
+ model; steps are self-detecting and idempotent, so the `version` field does not
10
+ gate whether a step runs. No legacy layouts remain, so `_STEPS` is empty.
11
+ """
12
+
13
+ from pathlib import Path
14
+ from typing import Any, Callable
15
+
16
+ _CURRENT_VERSION = 1
17
+
18
+
19
+ def migrate(raw: dict[str, Any], source: Path) -> dict[str, Any]:
20
+ """Migrate a raw config document to the current format.
21
+
22
+ Reads and validates `assets.version` (absent means the oldest, `0`), applies
23
+ every self-detecting step, and stamps the current version. The version sits
24
+ inside `assets` so it tracks the assets format alone, not the whole document.
25
+
26
+ Args:
27
+ raw: The raw document as parsed from a file.
28
+ source: The file it was parsed from, named in error messages.
29
+
30
+ Returns:
31
+ The document in the current format.
32
+
33
+ Raises:
34
+ TypeError: If `assets.version` is not an integer.
35
+ ValueError: If `assets.version` is outside the supported range.
36
+ """
37
+ assets = raw.get("assets")
38
+ version = assets.get("version", 0) if isinstance(assets, dict) else 0
39
+ if not isinstance(version, int) or isinstance(version, bool):
40
+ raise TypeError(f"{source}: `assets.version` must be an integer")
41
+ if version < 0:
42
+ raise ValueError(f"{source}: `assets.version` must not be negative")
43
+ if version > _CURRENT_VERSION:
44
+ raise ValueError(
45
+ f"{source}: assets version {version} is newer than supported "
46
+ f"version {_CURRENT_VERSION}"
47
+ )
48
+
49
+ for _, step in _STEPS:
50
+ raw = step(raw, source)
51
+ assets = raw.get("assets")
52
+ if isinstance(assets, dict):
53
+ assets["version"] = _CURRENT_VERSION
54
+ return raw
55
+
56
+
57
+ _STEPS: list[tuple[int, Callable[[dict[str, Any], Path], dict[str, Any]]]] = []
58
+ """Migration steps as `(from_version, step)`, oldest first."""
@@ -26,6 +26,8 @@ from frequenz.client.assets import (
26
26
  from marshmallow import Schema
27
27
  from marshmallow_dataclass import dataclass
28
28
 
29
+ from ._fields import toml_datetime_metadata
30
+
29
31
  _EIC_ALPHABET = "0123456789ABCDEFGHIJKLMNOPQRSTUVWXYZ-"
30
32
  _MARKET_AREA_EU_DE = 101
31
33
 
@@ -165,10 +167,10 @@ class ValidityConfig:
165
167
  participation: MarketParticipationType | None = None
166
168
  """The market use case, unset for a plain validity window."""
167
169
 
168
- start: datetime | None = None
170
+ start: datetime | None = field(default=None, metadata=toml_datetime_metadata())
169
171
  """Inclusive start of the period."""
170
172
 
171
- end: datetime | None = None
173
+ end: datetime | None = field(default=None, metadata=toml_datetime_metadata())
172
174
  """Exclusive end of the period."""
173
175
 
174
176
  Schema: ClassVar[Type[Schema]] = Schema
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: frequenz-gridpool
3
- Version: 0.7.1
3
+ Version: 0.9.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,9 +19,10 @@ 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
- Requires-Dist: frequenz-microgrid-component-graph<0.6,>=0.5.0
25
+ Requires-Dist: frequenz-microgrid-component-graph<0.7,>=0.6.0
25
26
  Requires-Dist: frequenz-client-assets<0.6,>=0.5.0
26
27
  Provides-Extra: cli
27
28
  Requires-Dist: asyncclick<9,>=8.3.0.4; extra == "cli"
@@ -31,8 +32,8 @@ Requires-Dist: frequenz-gridpool[cli]; extra == "render-graph"
31
32
  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
- Requires-Dist: flake8==7.3.0; extra == "dev-flake8"
35
- Requires-Dist: flake8-datetimez==20.10.0; extra == "dev-flake8"
35
+ Requires-Dist: flake8==7.4.1; extra == "dev-flake8"
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,31 +42,31 @@ 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.2; extra == "dev-mkdocs"
45
+ Requires-Dist: Markdown==3.11; 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"
48
49
  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
- Requires-Dist: mkdocstrings[python]==1.0.6; extra == "dev-mkdocs"
52
- Requires-Dist: mkdocstrings-python==2.0.5; extra == "dev-mkdocs"
53
- Requires-Dist: frequenz-repo-config[lib]==0.18.0; extra == "dev-mkdocs"
52
+ Requires-Dist: mkdocstrings[python]==1.0.7; extra == "dev-mkdocs"
53
+ Requires-Dist: mkdocstrings-python==2.0.9; extra == "dev-mkdocs"
54
+ Requires-Dist: frequenz-repo-config[lib]==0.19.0; extra == "dev-mkdocs"
54
55
  Provides-Extra: dev-mypy
55
- Requires-Dist: mypy==2.3.0; extra == "dev-mypy"
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.7.11; extra == "dev-noxfile"
61
- Requires-Dist: frequenz-repo-config[lib]==0.18.0; extra == "dev-noxfile"
61
+ Requires-Dist: nox==2026.8.17; extra == "dev-noxfile"
62
+ Requires-Dist: frequenz-repo-config[lib]==0.19.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.6; extra == "dev-pytest"
67
- Requires-Dist: frequenz-repo-config[extra-lint-examples]==0.18.0; extra == "dev-pytest"
68
- Requires-Dist: pytest-mock==3.15.1; extra == "dev-pytest"
67
+ Requires-Dist: pylint==4.0.9; extra == "dev-pytest"
68
+ Requires-Dist: frequenz-repo-config[extra-lint-examples]==0.19.0; extra == "dev-pytest"
69
+ Requires-Dist: pytest-mock==3.16.0; extra == "dev-pytest"
69
70
  Requires-Dist: pytest-asyncio==1.4.0; extra == "dev-pytest"
70
71
  Requires-Dist: async-solipsism==0.9; extra == "dev-pytest"
71
72
  Requires-Dist: frequenz-gridpool[render-graph]; extra == "dev-pytest"
@@ -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/_fields.py
17
18
  src/frequenz/gridpool/config/_gridpool.py
18
19
  src/frequenz/gridpool/config/_load.py
19
20
  src/frequenz/gridpool/config/_microgrid.py
@@ -1,6 +1,7 @@
1
+ marshmallow<5,>=3.18.0
1
2
  marshmallow-dataclass<9,>=8.7.1
2
3
  typing-extensions<5,>=4.14.1
3
- frequenz-microgrid-component-graph<0.6,>=0.5.0
4
+ frequenz-microgrid-component-graph<0.7,>=0.6.0
4
5
  frequenz-client-assets<0.6,>=0.5.0
5
6
 
6
7
  [cli]
@@ -11,8 +12,8 @@ tomlkit<1,>=0.13.0
11
12
  frequenz-gridpool[dev-flake8,dev-formatting,dev-mkdocs,dev-mypy,dev-noxfile,dev-pylint,dev-pytest,render-graph]
12
13
 
13
14
  [dev-flake8]
14
- flake8==7.3.0
15
- flake8-datetimez==20.10.0
15
+ flake8==7.4.1
16
+ flake8-datetimez==26.8.1
16
17
  flake8-docstrings==1.7.0
17
18
  flake8-pyproject==1.2.4
18
19
  pydoclint==0.9.1
@@ -23,35 +24,35 @@ black==26.5.1
23
24
  isort==8.0.1
24
25
 
25
26
  [dev-mkdocs]
26
- Markdown==3.10.2
27
+ Markdown==3.11
27
28
  black==26.5.1
28
29
  mike==2.2.0
29
30
  mkdocs-gen-files==0.6.1
30
31
  mkdocs-literate-nav==0.6.3
31
32
  mkdocs-macros-plugin==1.5.0
32
33
  mkdocs-material==9.7.7
33
- mkdocstrings[python]==1.0.6
34
- mkdocstrings-python==2.0.5
35
- frequenz-repo-config[lib]==0.18.0
34
+ mkdocstrings[python]==1.0.7
35
+ mkdocstrings-python==2.0.9
36
+ frequenz-repo-config[lib]==0.19.0
36
37
 
37
38
  [dev-mypy]
38
- mypy==2.3.0
39
+ mypy==2.3.1
39
40
  types-networkx>=3.6.1.20251220
40
41
  types-Markdown==3.10.2.20260712
41
42
  frequenz-gridpool[dev-mkdocs,dev-noxfile,dev-pytest]
42
43
 
43
44
  [dev-noxfile]
44
- nox==2026.7.11
45
- frequenz-repo-config[lib]==0.18.0
45
+ nox==2026.8.17
46
+ frequenz-repo-config[lib]==0.19.0
46
47
 
47
48
  [dev-pylint]
48
49
  frequenz-gridpool[dev-mkdocs,dev-noxfile,dev-pytest]
49
50
 
50
51
  [dev-pytest]
51
52
  pytest==9.1.1
52
- pylint==4.0.6
53
- frequenz-repo-config[extra-lint-examples]==0.18.0
54
- pytest-mock==3.15.1
53
+ pylint==4.0.9
54
+ frequenz-repo-config[extra-lint-examples]==0.19.0
55
+ pytest-mock==3.16.0
55
56
  pytest-asyncio==1.4.0
56
57
  async-solipsism==0.9
57
58
  frequenz-gridpool[render-graph]
@@ -13,7 +13,6 @@
13
13
  ".github/workflows/black-migration.yaml",
14
14
  ".github/workflows/ci-pr.yaml",
15
15
  ".github/workflows/ci.yaml",
16
- ".github/workflows/dco-merge-queue.yml",
17
16
  ".github/workflows/isort-migration.yaml",
18
17
  ".github/workflows/labeler.yml",
19
18
  ".github/workflows/release-notes-check.yml",
@@ -45,6 +44,7 @@
45
44
  "src/frequenz/gridpool/cli/_render_graph.py",
46
45
  "src/frequenz/gridpool/config/__init__.py",
47
46
  "src/frequenz/gridpool/config/_assets.py",
47
+ "src/frequenz/gridpool/config/_fields.py",
48
48
  "src/frequenz/gridpool/config/_gridpool.py",
49
49
  "src/frequenz/gridpool/config/_load.py",
50
50
  "src/frequenz/gridpool/config/_microgrid.py",
@@ -0,0 +1,8 @@
1
+ {
2
+ "tag": "0.9.0",
3
+ "distance": 0,
4
+ "node": "g89c4f0c9d5205d548fb84419c3e63e6a4bd9608b",
5
+ "dirty": false,
6
+ "branch": "HEAD",
7
+ "node_date": "2026-10-14"
8
+ }
@@ -1,42 +0,0 @@
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.
@@ -1,139 +0,0 @@
1
- # License: MIT
2
- # Copyright © 2026 Frequenz Energy-as-a-Service GmbH
3
-
4
- """Format migrations for raw config documents.
5
-
6
- Each step rewrites the raw `dict` a file parses to, before it is loaded into the
7
- typed model. Steps are self-detecting and idempotent, so the `version` field
8
- does not gate whether a step runs; it only records how old a file may be, which
9
- bounds when a step may be dropped.
10
- """
11
-
12
- import logging
13
- from pathlib import Path
14
- from typing import Any, Callable
15
-
16
- _logger = logging.getLogger(__name__)
17
-
18
- _CURRENT_VERSION = 1
19
-
20
-
21
- def migrate(raw: dict[str, Any], source: Path) -> dict[str, Any]:
22
- """Migrate a raw config document to the current format.
23
-
24
- Reads and validates `assets.version` (absent means the oldest, `0`), applies
25
- every self-detecting step, and stamps the current version. The version sits
26
- inside `assets` so it tracks the assets format alone, not the whole document.
27
-
28
- Args:
29
- raw: The raw document as parsed from a file.
30
- source: The file it was parsed from, for warnings and errors.
31
-
32
- Returns:
33
- The document in the current format.
34
-
35
- Raises:
36
- TypeError: If `assets.version` is not an integer.
37
- ValueError: If `assets.version` is outside the supported range.
38
- """
39
- assets = raw.get("assets")
40
- version = assets.get("version", 0) if isinstance(assets, dict) else 0
41
- if not isinstance(version, int) or isinstance(version, bool):
42
- raise TypeError(f"{source}: `assets.version` must be an integer")
43
- if version < 0:
44
- raise ValueError(f"{source}: `assets.version` must not be negative")
45
- if version > _CURRENT_VERSION:
46
- raise ValueError(
47
- f"{source}: assets version {version} is newer than supported "
48
- f"version {_CURRENT_VERSION}"
49
- )
50
-
51
- for _, step in _STEPS:
52
- raw = step(raw, source)
53
- assets = raw.get("assets")
54
- if isinstance(assets, dict):
55
- assets["version"] = _CURRENT_VERSION
56
- return raw
57
-
58
-
59
- def _nest_top_level_microgrids(raw: dict[str, Any], source: Path) -> dict[str, Any]:
60
- """Wrap deprecated top-level microgrid entries under `assets.microgrids`.
61
-
62
- Args:
63
- raw: The raw document.
64
- source: The file it was parsed from.
65
-
66
- Returns:
67
- The document with its entries under `assets`.
68
-
69
- Raises:
70
- ValueError: If both layouts are present, a half-migrated file rather
71
- than a merge.
72
- """
73
- if "assets" in raw:
74
- stray = sorted(k for k in raw if k != "assets")
75
- if stray:
76
- raise ValueError(
77
- f"{source}: keys {stray} sit outside `assets` while the file "
78
- "already has an `assets` table; move them under `assets.microgrids`."
79
- )
80
- return raw
81
-
82
- _logger.warning(
83
- "%s: top-level microgrid IDs are deprecated, "
84
- "nest the entries under `assets.microgrids` instead.",
85
- source,
86
- )
87
- return {"assets": {"microgrids": dict(raw)}}
88
-
89
-
90
- def _lift_microgrid_meta(raw: dict[str, Any], source: Path) -> dict[str, Any]:
91
- """Merge a deprecated nested `meta` table onto its microgrid entry.
92
-
93
- Earlier files nested a microgrid's fields under `meta`; they now sit
94
- directly on the entry.
95
-
96
- Args:
97
- raw: The raw document.
98
- source: The file it was parsed from.
99
-
100
- Returns:
101
- The document with microgrid fields directly on each entry.
102
-
103
- Raises:
104
- TypeError: If a `meta` value is not a table.
105
- ValueError: If a field is set both under `meta` and on the entry.
106
- """
107
- assets = raw.get("assets")
108
- if not isinstance(assets, dict):
109
- return raw
110
- microgrids = assets.get("microgrids")
111
- if not isinstance(microgrids, dict):
112
- return raw
113
- for mid, entry in microgrids.items():
114
- if not isinstance(entry, dict) or "meta" not in entry:
115
- continue
116
- meta = entry.pop("meta")
117
- if not isinstance(meta, dict):
118
- raise TypeError(f"{source}: microgrid {mid} `meta` must be a table")
119
- _logger.warning(
120
- "%s: microgrid %s nests fields under `meta`; that is deprecated, "
121
- "set them directly on the entry instead.",
122
- source,
123
- mid,
124
- )
125
- for key, value in meta.items():
126
- if key in entry:
127
- raise ValueError(
128
- f"{source}: microgrid {mid} sets `{key}` both under `meta` "
129
- "and directly; keep only the direct one"
130
- )
131
- entry[key] = value
132
- return raw
133
-
134
-
135
- _STEPS: list[tuple[int, Callable[[dict[str, Any], Path], dict[str, Any]]]] = [
136
- (0, _nest_top_level_microgrids),
137
- (0, _lift_microgrid_meta),
138
- ]
139
- """Migration steps as `(from_version, step)`, oldest first."""
@@ -1,8 +0,0 @@
1
- {
2
- "tag": "0.7.1",
3
- "distance": 0,
4
- "node": "gb1e6ec5ad7f5fe27fd1db03ffd398de766bc012c",
5
- "dirty": false,
6
- "branch": "HEAD",
7
- "node_date": "2026-08-31"
8
- }