Skip to content

Commit 6b6341d

Browse files
committed
refactor(config): key microgrids by int
Normalize microgrid map keys to plain integers, matching metadata and relation IDs. Keep string keys only at TOML and serialization boundaries. Signed-off-by: cwasicki <126617870+cwasicki@users.noreply.github.com>
1 parent 2c6262a commit 6b6341d

12 files changed

Lines changed: 66 additions & 58 deletions

File tree

RELEASE_NOTES.md

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -45,6 +45,14 @@
4545
- The implementation modules `config.load` and `config.microgrid` are now
4646
private. Import their public names from `frequenz.gridpool.config` instead.
4747

48+
- Microgrids are now keyed by `int` microgrid ID, not `str`. This covers
49+
`AssetsConfig.microgrids`, including documents returned by `load_configs`.
50+
Index the mapping by integer ID:
51+
52+
```python
53+
configs[1] # was configs["1"]
54+
```
55+
4856
## New Features
4957

5058
- `AssetsConfig` gives the `assets` namespace a type, so the entities still to

src/frequenz/gridpool/cli/__main__.py

Lines changed: 1 addition & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -203,9 +203,7 @@ async def generate_config(
203203
ids = list(dict.fromkeys(microgrid_ids)) or None
204204
if inplace and ids is None:
205205
assert default_file is not None
206-
ids = sorted(
207-
int(mid) for mid in AssetsConfig.load_from_files(default_file).microgrids
208-
)
206+
ids = sorted(AssetsConfig.load_from_files(default_file).microgrids)
209207

210208
async with AssetsApiClient(url, auth_key=key, sign_secret=secret) as client:
211209
if inplace:

src/frequenz/gridpool/cli/_dump_config.py

Lines changed: 4 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -71,22 +71,22 @@ def _iter_leaves(
7171
return leaves
7272

7373

74-
def dump_map(configs: dict[str, MicrogridConfig]) -> str:
74+
def dump_map(configs: dict[int, MicrogridConfig]) -> str:
7575
"""Serialize a mapping of microgrid configs to dotted-key TOML.
7676
7777
Args:
78-
configs: Mapping from microgrid ID (as string) to `MicrogridConfig`.
78+
configs: Mapping from microgrid ID to `MicrogridConfig`.
7979
8080
Returns:
8181
The TOML representation as a string, with one blank line between
8282
microgrids and entries sorted by numeric microgrid ID.
8383
"""
8484
schema = MicrogridConfig.Schema()
8585
doc = tomlkit.document()
86-
for mid in sorted(configs, key=int):
86+
for mid in sorted(configs):
8787
dumped = schema.dump(configs[mid])
8888
assert isinstance(dumped, dict)
89-
leaves = _iter_leaves([mid], dumped)
89+
leaves = _iter_leaves([str(mid)], dumped)
9090
if not leaves:
9191
continue
9292
if doc.body:

src/frequenz/gridpool/cli/_patch_config.py

Lines changed: 6 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -20,25 +20,25 @@
2020
from ._dump_config import _format_value, _iter_leaves
2121

2222

23-
def patch_file(path: Path, configs: dict[str, MicrogridConfig]) -> str:
23+
def patch_file(path: Path, configs: dict[int, MicrogridConfig]) -> str:
2424
"""Patch the TOML file at `path` with any leaves missing from `configs`.
2525
2626
Args:
2727
path: Path to the existing TOML file to patch.
28-
configs: Mapping from microgrid ID (as string) to `MicrogridConfig`.
28+
configs: Mapping from microgrid ID to `MicrogridConfig`.
2929
3030
Returns:
3131
The patched TOML text; the caller is responsible for writing it back.
3232
"""
3333
return patch_text(path.read_text(), configs)
3434

3535

36-
def patch_text(original: str, configs: dict[str, MicrogridConfig]) -> str:
36+
def patch_text(original: str, configs: dict[int, MicrogridConfig]) -> str:
3737
"""Patch dotted-key TOML text with any leaves missing from `configs`.
3838
3939
Args:
4040
original: The existing TOML text to patch.
41-
configs: Mapping from microgrid ID (as string) to `MicrogridConfig`.
41+
configs: Mapping from microgrid ID to `MicrogridConfig`.
4242
4343
Returns:
4444
The patched TOML text.
@@ -51,7 +51,8 @@ def patch_text(original: str, configs: dict[str, MicrogridConfig]) -> str:
5151
# into the rendered text instead.
5252
orphans: dict[str, list[tuple[list[str], Any]]] = {}
5353

54-
for mid, cfg in configs.items():
54+
for microgrid_id, cfg in configs.items():
55+
mid = str(microgrid_id)
5556
dumped = schema.dump(cfg)
5657
assert isinstance(dumped, dict)
5758
leaves = _iter_leaves([], dumped)

src/frequenz/gridpool/config/_assets.py

Lines changed: 2 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -82,7 +82,7 @@ def _merge_file_tables(
8282
class AssetsConfig:
8383
"""Entities described by a config document, keyed by their ID."""
8484

85-
microgrids: dict[str, MicrogridConfig] = field(default_factory=dict)
85+
microgrids: dict[int, MicrogridConfig] = field(default_factory=dict)
8686
"""Microgrids, keyed by microgrid ID."""
8787

8888
market_locations: dict[str, MarketLocationConfig] = field(default_factory=dict)
@@ -115,9 +115,7 @@ def __post_init__(self) -> None:
115115
ValueError: If a key is not the ID of the entry it holds.
116116
"""
117117
for mid, cfg in self.microgrids.items():
118-
if not mid.isdigit():
119-
raise ValueError(f"Microgrid ID key must be numeric, got {mid}")
120-
if int(cfg.meta.microgrid_id) != int(mid):
118+
if int(cfg.meta.microgrid_id) != mid:
121119
raise ValueError(
122120
f"Microgrid ID mismatch: key {mid} != {cfg.meta.microgrid_id}"
123121
)

src/frequenz/gridpool/config/_load.py

Lines changed: 10 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -126,7 +126,9 @@ async def load_configs(
126126
)
127127
schema = MicrogridConfig.Schema()
128128
api_table: dict[str, Any] = {
129-
"microgrids": {mid: schema.dump(cfg) for mid, cfg in assets_configs.items()}
129+
"microgrids": {
130+
str(mid): schema.dump(cfg) for mid, cfg in assets_configs.items()
131+
}
130132
}
131133
merged = _deep_merge(merged, api_table)
132134

@@ -142,7 +144,7 @@ async def _load_microgrids_from_api(
142144
assets_client: AssetsApiClient,
143145
microgrid_ids: list[int],
144146
component_graph_config: ComponentGraphConfig | None = None,
145-
) -> dict[str, "MicrogridConfig"]:
147+
) -> dict[int, "MicrogridConfig"]:
146148
"""Load microgrid configs from the Assets API.
147149
148150
For each microgrid, fetches its location metadata (latitude, longitude) and
@@ -165,14 +167,13 @@ async def _load_microgrids_from_api(
165167
`ComponentGraphConfig`. Defaults to that class's own defaults.
166168
167169
Returns:
168-
dict[str, MicrogridConfig]:
169-
Mapping from microgrid ID (as string) to the loaded
170-
`MicrogridConfig` instance. Microgrids whose metadata could not be
171-
loaded are omitted, so the returned mapping may cover fewer
172-
microgrids than were requested.
170+
dict[int, MicrogridConfig]:
171+
Mapping from microgrid ID to the loaded `MicrogridConfig` instance.
172+
Microgrids whose metadata could not be loaded are omitted, so the
173+
returned mapping may cover fewer microgrids than were requested.
173174
"""
174175
generator = ComponentGraphGenerator(assets_client, config=component_graph_config)
175-
configs: dict[str, MicrogridConfig] = {}
176+
configs: dict[int, MicrogridConfig] = {}
176177
for microgrid_id in microgrid_ids:
177178
try:
178179
cfg = await _build_config_from_metadata(assets_client, microgrid_id)
@@ -195,7 +196,7 @@ async def _load_microgrids_from_api(
195196
exc,
196197
)
197198

198-
configs[str(microgrid_id)] = cfg
199+
configs[microgrid_id] = cfg
199200

200201
return configs
201202

src/frequenz/gridpool/config/_microgrid.py

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -291,7 +291,7 @@ def formula(self, component_type: str, metric: str) -> str:
291291
Schema: ClassVar[Type[Schema]] = Schema
292292

293293
@classmethod
294-
def _load_table_entries(cls, data: dict[str, Any]) -> dict[str, Self]:
294+
def _load_table_entries(cls, data: dict[str, Any]) -> dict[int, Self]:
295295
"""Load microgrid configurations from table entries.
296296
297297
Args:
@@ -327,6 +327,6 @@ def _load_table_entries(cls, data: dict[str, Any]) -> dict[str, Self]:
327327
f"Table reader: Microgrid ID mismatch: key {mid} != {mgrid.meta.microgrid_id}"
328328
)
329329

330-
mgrids[mid] = mgrid
330+
mgrids[int(mid)] = mgrid
331331

332332
return mgrids

tests/test_config.py

Lines changed: 14 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -48,7 +48,7 @@
4848
def valid_microgrid_config() -> MicrogridConfig:
4949
"""Fixture to provide a valid MicrogridConfig instance."""
5050
# pylint: disable=protected-access
51-
return MicrogridConfig._load_table_entries(VALID_CONFIG)["1"]
51+
return MicrogridConfig._load_table_entries(VALID_CONFIG)[1]
5252

5353

5454
def test_is_valid_type() -> None:
@@ -136,17 +136,17 @@ def test_load_configs(mocker: MockerFixture) -> None:
136136
mocker.patch("pathlib.Path.is_file", mocker.Mock(return_value=True))
137137
configs = AssetsConfig.load_from_files(Path("mock_path.toml")).microgrids
138138

139-
assert "1" in configs
140-
assert configs["1"].meta is not None
141-
assert configs["1"].meta.name == "Test Grid"
139+
assert 1 in configs
140+
assert configs[1].meta is not None
141+
assert configs[1].meta.name == "Test Grid"
142142

143-
pv_config = configs["1"].pv
143+
pv_config = configs[1].pv
144144
assert pv_config is not None
145145
pv_system = pv_config.get("PV1")
146146
assert pv_system is not None
147147
assert pv_system.peak_power == 5000
148148

149-
battery_config = configs["1"].battery
149+
battery_config = configs[1].battery
150150
assert battery_config is not None
151151
battery_system = battery_config.get("BAT1")
152152
assert battery_system is not None
@@ -194,8 +194,8 @@ def test_load_prefixed(tmp_path: Path, caplog: pytest.LogCaptureFixture) -> None
194194
_write(tmp_path, "prefixed.toml", _PREFIXED_TOML)
195195
).microgrids
196196

197-
assert configs["1"].meta.name == "Test Grid"
198-
assert configs["1"].component_type_ids("pv") == [101, 102]
197+
assert configs[1].meta.name == "Test Grid"
198+
assert configs[1].component_type_ids("pv") == [101, 102]
199199
assert "deprecated" not in caplog.text
200200

201201

@@ -206,7 +206,7 @@ def test_load_legacy_warns(tmp_path: Path, caplog: pytest.LogCaptureFixture) ->
206206
with caplog.at_level(logging.WARNING):
207207
configs = AssetsConfig.load_from_files(path).microgrids
208208

209-
assert configs["1"].meta.name == "Test Grid"
209+
assert configs[1].meta.name == "Test Grid"
210210
assert "deprecated" in caplog.text
211211
assert str(path) in caplog.text
212212

@@ -241,8 +241,8 @@ async def test_merge_prefixed_base_with_legacy_override(tmp_path: Path) -> None:
241241
await load_configs(default_files=base, override_files=override)
242242
).microgrids
243243

244-
assert configs["1"].meta.name == "Renamed"
245-
assert configs["1"].component_type_ids("pv") == [101, 102]
244+
assert configs[1].meta.name == "Renamed"
245+
assert configs[1].component_type_ids("pv") == [101, 102]
246246

247247

248248
def test_load_from_files_layers_fields(tmp_path: Path) -> None:
@@ -257,8 +257,8 @@ def test_load_from_files_layers_fields(tmp_path: Path) -> None:
257257

258258
configs = AssetsConfig.load_from_files([base, override]).microgrids
259259

260-
assert configs["1"].meta.name == "Renamed"
261-
assert configs["1"].component_type_ids("pv") == [101, 102]
260+
assert configs[1].meta.name == "Renamed"
261+
assert configs[1].component_type_ids("pv") == [101, 102]
262262

263263

264264
def test_assets_config_rejects_mismatched_id() -> None:
@@ -292,5 +292,5 @@ def test_assets_config_warns_on_unknown_entities(
292292
with caplog.at_level(logging.WARNING):
293293
config = AssetsConfig.load_from_files(path)
294294

295-
assert sorted(config.microgrids) == ["1"]
295+
assert sorted(config.microgrids) == [1]
296296
assert "gridpool" in caplog.text

tests/test_dump_config.py

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -17,7 +17,7 @@
1717
def test_dump_map_round_trips() -> None:
1818
"""A serialized config parses back to the same dotted-key structure."""
1919
configs = {
20-
"10": MicrogridConfig(
20+
10: MicrogridConfig(
2121
meta=Metadata(microgrid_id=10, name="Demo", latitude=52.5),
2222
ctype={
2323
"pv": ComponentTypeConfig(meter=[2], formula={"AC_POWER_ACTIVE": "#2"}),
@@ -36,7 +36,7 @@ def test_dump_map_round_trips() -> None:
3636

3737
def test_dump_map_omits_empty_and_none() -> None:
3838
"""Empty and None fields are dropped from the output."""
39-
configs = {"7": MicrogridConfig(meta=Metadata(microgrid_id=7))}
39+
configs = {7: MicrogridConfig(meta=Metadata(microgrid_id=7))}
4040

4141
text = dump_map(configs)
4242

@@ -51,7 +51,7 @@ def test_dump_map_empty() -> None:
5151
def test_dump_map_renders_whole_floats_as_underscored_ints() -> None:
5252
"""Whole-number float fields (e.g. peak/rated power) render as `1_736_680`, not `1736680.0`."""
5353
configs = {
54-
"10": MicrogridConfig(
54+
10: MicrogridConfig(
5555
meta=Metadata(microgrid_id=10, latitude=52.5),
5656
pv={"1": PVConfig(peak_power=1_736_680.0, rated_power=1_400_000.0)},
5757
)

tests/test_load.py

Lines changed: 8 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -69,7 +69,7 @@ async def test_load_microgrids_from_api_derives_formulas_and_ids() -> None:
6969
"""A config loaded from the API gets both formulas and component IDs."""
7070
configs = await _load_microgrids_from_api(_mock_client(), [10])
7171

72-
cfg = configs["10"]
72+
cfg = configs[10]
7373
assert cfg.ctype["pv"].formula == {"AC_POWER_ACTIVE": "COALESCE(#4, #2, 0.0)"}
7474
assert cfg.ctype["pv"].inverter == [4]
7575
assert cfg.ctype["pv"].meter == [2]
@@ -89,7 +89,7 @@ async def test_load_microgrids_from_api_honours_the_component_graph_config() ->
8989
)
9090

9191
# Meter first, the opposite of the default order asserted above.
92-
ctype = configs["10"].ctype
92+
ctype = configs[10].ctype
9393
assert ctype["pv"].formula == {"AC_POWER_ACTIVE": "COALESCE(#2, #4, 0.0)"}
9494

9595

@@ -105,7 +105,7 @@ async def test_load_configs_forwards_the_component_graph_config() -> None:
105105
)
106106
).microgrids
107107

108-
assert configs["10"].ctype["pv"].formula == {
108+
assert configs[10].ctype["pv"].formula == {
109109
"AC_POWER_ACTIVE": "COALESCE(#2, #4, 0.0)"
110110
}
111111

@@ -128,7 +128,7 @@ async def test_load_microgrids_from_api_keeps_metadata_when_graph_fails() -> Non
128128

129129
configs = await _load_microgrids_from_api(client, [10])
130130

131-
cfg = configs["10"]
131+
cfg = configs[10]
132132
assert cfg.meta.microgrid_id == 10
133133
assert cfg.ctype == {}
134134

@@ -149,7 +149,7 @@ async def test_load_configs_validates_the_merged_whole(tmp_path: Path) -> None:
149149

150150
document = await load_configs(default_files=default, override_files=override)
151151

152-
assert document.microgrids["1"].meta.name == "Override"
152+
assert document.microgrids[1].meta.name == "Override"
153153

154154

155155
async def test_load_configs_returns_the_whole_document(tmp_path: Path) -> None:
@@ -161,6 +161,7 @@ async def test_load_configs_returns_the_whole_document(tmp_path: Path) -> None:
161161
default = tmp_path / "default.toml"
162162
default.write_text(
163163
"assets.microgrids.10.meta.microgrid_id = 10\n"
164+
'assets.microgrids.10.meta.name = "File name"\n'
164165
'assets.market_locations.51171875559.id = "51171875559"\n'
165166
"assets.relations.M10L51171875559.microgrid_id = 10\n"
166167
'assets.relations.M10L51171875559.market_location_id = "51171875559"\n'
@@ -169,7 +170,8 @@ async def test_load_configs_returns_the_whole_document(tmp_path: Path) -> None:
169170
document = await load_configs(default_files=default, assets_client=_mock_client())
170171

171172
# The API layer filled the microgrid's component config.
172-
assert document.microgrids["10"].ctype
173+
assert document.microgrids[10].ctype
174+
assert document.microgrids[10].meta.name == "File name"
173175
# The file's topology survived the merge with the API layer.
174176
assert "M10L51171875559" in document.relations
175177
assert "51171875559" in document.market_locations

0 commit comments

Comments
 (0)