Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
8 changes: 8 additions & 0 deletions RELEASE_NOTES.md
Original file line number Diff line number Diff line change
Expand Up @@ -45,6 +45,14 @@
- The implementation modules `config.load` and `config.microgrid` are now
private. Import their public names from `frequenz.gridpool.config` instead.

- Microgrids are now keyed by `int` microgrid ID, not `str`. This covers
`AssetsConfig.microgrids`, including documents returned by `load_configs`.
Index the mapping by integer ID:

```python
configs[1] # was configs["1"]
```

## New Features

- `AssetsConfig` gives the `assets` namespace a type, so the entities still to
Expand Down
4 changes: 1 addition & 3 deletions src/frequenz/gridpool/cli/__main__.py
Original file line number Diff line number Diff line change
Expand Up @@ -203,9 +203,7 @@ async def generate_config(
ids = list(dict.fromkeys(microgrid_ids)) or None
if inplace and ids is None:
assert default_file is not None
ids = sorted(
int(mid) for mid in AssetsConfig.load_from_files(default_file).microgrids
)
ids = sorted(AssetsConfig.load_from_files(default_file).microgrids)

async with AssetsApiClient(url, auth_key=key, sign_secret=secret) as client:
if inplace:
Expand Down
8 changes: 4 additions & 4 deletions src/frequenz/gridpool/cli/_dump_config.py
Original file line number Diff line number Diff line change
Expand Up @@ -71,22 +71,22 @@ def _iter_leaves(
return leaves


def dump_map(configs: dict[str, MicrogridConfig]) -> str:
def dump_map(configs: dict[int, MicrogridConfig]) -> str:
"""Serialize a mapping of microgrid configs to dotted-key TOML.

Args:
configs: Mapping from microgrid ID (as string) to `MicrogridConfig`.
configs: Mapping from microgrid ID to `MicrogridConfig`.

Returns:
The TOML representation as a string, with one blank line between
microgrids and entries sorted by numeric microgrid ID.
"""
schema = MicrogridConfig.Schema()
doc = tomlkit.document()
for mid in sorted(configs, key=int):
for mid in sorted(configs):
dumped = schema.dump(configs[mid])
assert isinstance(dumped, dict)
leaves = _iter_leaves([mid], dumped)
leaves = _iter_leaves([str(mid)], dumped)
if not leaves:
continue
if doc.body:
Expand Down
11 changes: 6 additions & 5 deletions src/frequenz/gridpool/cli/_patch_config.py
Original file line number Diff line number Diff line change
Expand Up @@ -20,25 +20,25 @@
from ._dump_config import _format_value, _iter_leaves


def patch_file(path: Path, configs: dict[str, MicrogridConfig]) -> str:
def patch_file(path: Path, configs: dict[int, MicrogridConfig]) -> str:
"""Patch the TOML file at `path` with any leaves missing from `configs`.

Args:
path: Path to the existing TOML file to patch.
configs: Mapping from microgrid ID (as string) to `MicrogridConfig`.
configs: Mapping from microgrid ID to `MicrogridConfig`.

Returns:
The patched TOML text; the caller is responsible for writing it back.
"""
return patch_text(path.read_text(), configs)


def patch_text(original: str, configs: dict[str, MicrogridConfig]) -> str:
def patch_text(original: str, configs: dict[int, MicrogridConfig]) -> str:
"""Patch dotted-key TOML text with any leaves missing from `configs`.

Args:
original: The existing TOML text to patch.
configs: Mapping from microgrid ID (as string) to `MicrogridConfig`.
configs: Mapping from microgrid ID to `MicrogridConfig`.

Returns:
The patched TOML text.
Expand All @@ -51,7 +51,8 @@ def patch_text(original: str, configs: dict[str, MicrogridConfig]) -> str:
# into the rendered text instead.
orphans: dict[str, list[tuple[list[str], Any]]] = {}

for mid, cfg in configs.items():
for microgrid_id, cfg in configs.items():
mid = str(microgrid_id)
dumped = schema.dump(cfg)
assert isinstance(dumped, dict)
leaves = _iter_leaves([], dumped)
Expand Down
6 changes: 2 additions & 4 deletions src/frequenz/gridpool/config/_assets.py
Original file line number Diff line number Diff line change
Expand Up @@ -82,7 +82,7 @@ def _merge_file_tables(
class AssetsConfig:
"""Entities described by a config document, keyed by their ID."""

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

market_locations: dict[str, MarketLocationConfig] = field(default_factory=dict)
Expand Down Expand Up @@ -115,9 +115,7 @@ def __post_init__(self) -> None:
ValueError: If a key is not the ID of the entry it holds.
"""
for mid, cfg in self.microgrids.items():
if not mid.isdigit():
raise ValueError(f"Microgrid ID key must be numeric, got {mid}")
if int(cfg.meta.microgrid_id) != int(mid):
if int(cfg.meta.microgrid_id) != mid:
raise ValueError(
f"Microgrid ID mismatch: key {mid} != {cfg.meta.microgrid_id}"
)
Expand Down
19 changes: 10 additions & 9 deletions src/frequenz/gridpool/config/_load.py
Original file line number Diff line number Diff line change
Expand Up @@ -126,7 +126,9 @@ async def load_configs(
)
schema = MicrogridConfig.Schema()
api_table: dict[str, Any] = {
"microgrids": {mid: schema.dump(cfg) for mid, cfg in assets_configs.items()}
"microgrids": {
str(mid): schema.dump(cfg) for mid, cfg in assets_configs.items()
}
}
merged = _deep_merge(merged, api_table)

Expand All @@ -142,7 +144,7 @@ async def _load_microgrids_from_api(
assets_client: AssetsApiClient,
microgrid_ids: list[int],
component_graph_config: ComponentGraphConfig | None = None,
) -> dict[str, "MicrogridConfig"]:
) -> dict[int, "MicrogridConfig"]:
"""Load microgrid configs from the Assets API.

For each microgrid, fetches its location metadata (latitude, longitude) and
Expand All @@ -165,14 +167,13 @@ async def _load_microgrids_from_api(
`ComponentGraphConfig`. Defaults to that class's own defaults.

Returns:
dict[str, MicrogridConfig]:
Mapping from microgrid ID (as string) to the loaded
`MicrogridConfig` instance. Microgrids whose metadata could not be
loaded are omitted, so the returned mapping may cover fewer
microgrids than were requested.
dict[int, MicrogridConfig]:
Mapping from microgrid ID to the loaded `MicrogridConfig` instance.
Microgrids whose metadata could not be loaded are omitted, so the
returned mapping may cover fewer microgrids than were requested.
"""
generator = ComponentGraphGenerator(assets_client, config=component_graph_config)
configs: dict[str, MicrogridConfig] = {}
configs: dict[int, MicrogridConfig] = {}
for microgrid_id in microgrid_ids:
try:
cfg = await _build_config_from_metadata(assets_client, microgrid_id)
Expand All @@ -195,7 +196,7 @@ async def _load_microgrids_from_api(
exc,
)

configs[str(microgrid_id)] = cfg
configs[microgrid_id] = cfg

return configs

Expand Down
4 changes: 2 additions & 2 deletions src/frequenz/gridpool/config/_microgrid.py
Original file line number Diff line number Diff line change
Expand Up @@ -291,7 +291,7 @@ def formula(self, component_type: str, metric: str) -> str:
Schema: ClassVar[Type[Schema]] = Schema

@classmethod
def _load_table_entries(cls, data: dict[str, Any]) -> dict[str, Self]:
def _load_table_entries(cls, data: dict[str, Any]) -> dict[int, Self]:
"""Load microgrid configurations from table entries.

Args:
Expand Down Expand Up @@ -327,6 +327,6 @@ def _load_table_entries(cls, data: dict[str, Any]) -> dict[str, Self]:
f"Table reader: Microgrid ID mismatch: key {mid} != {mgrid.meta.microgrid_id}"
)

mgrids[mid] = mgrid
mgrids[int(mid)] = mgrid

return mgrids
28 changes: 14 additions & 14 deletions tests/test_config.py
Original file line number Diff line number Diff line change
Expand Up @@ -48,7 +48,7 @@
def valid_microgrid_config() -> MicrogridConfig:
"""Fixture to provide a valid MicrogridConfig instance."""
# pylint: disable=protected-access
return MicrogridConfig._load_table_entries(VALID_CONFIG)["1"]
return MicrogridConfig._load_table_entries(VALID_CONFIG)[1]


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

assert "1" in configs
assert configs["1"].meta is not None
assert configs["1"].meta.name == "Test Grid"
assert 1 in configs
assert configs[1].meta is not None
assert configs[1].meta.name == "Test Grid"

pv_config = configs["1"].pv
pv_config = configs[1].pv
assert pv_config is not None
pv_system = pv_config.get("PV1")
assert pv_system is not None
assert pv_system.peak_power == 5000

battery_config = configs["1"].battery
battery_config = configs[1].battery
assert battery_config is not None
battery_system = battery_config.get("BAT1")
assert battery_system is not None
Expand Down Expand Up @@ -194,8 +194,8 @@ def test_load_prefixed(tmp_path: Path, caplog: pytest.LogCaptureFixture) -> None
_write(tmp_path, "prefixed.toml", _PREFIXED_TOML)
).microgrids

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


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

assert configs["1"].meta.name == "Test Grid"
assert configs[1].meta.name == "Test Grid"
assert "deprecated" in caplog.text
assert str(path) in caplog.text

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

assert configs["1"].meta.name == "Renamed"
assert configs["1"].component_type_ids("pv") == [101, 102]
assert configs[1].meta.name == "Renamed"
assert configs[1].component_type_ids("pv") == [101, 102]


def test_load_from_files_layers_fields(tmp_path: Path) -> None:
Expand All @@ -257,8 +257,8 @@ def test_load_from_files_layers_fields(tmp_path: Path) -> None:

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

assert configs["1"].meta.name == "Renamed"
assert configs["1"].component_type_ids("pv") == [101, 102]
assert configs[1].meta.name == "Renamed"
assert configs[1].component_type_ids("pv") == [101, 102]


def test_assets_config_rejects_mismatched_id() -> None:
Expand Down Expand Up @@ -292,5 +292,5 @@ def test_assets_config_warns_on_unknown_entities(
with caplog.at_level(logging.WARNING):
config = AssetsConfig.load_from_files(path)

assert sorted(config.microgrids) == ["1"]
assert sorted(config.microgrids) == [1]
assert "gridpool" in caplog.text
6 changes: 3 additions & 3 deletions tests/test_dump_config.py
Original file line number Diff line number Diff line change
Expand Up @@ -17,7 +17,7 @@
def test_dump_map_round_trips() -> None:
"""A serialized config parses back to the same dotted-key structure."""
configs = {
"10": MicrogridConfig(
10: MicrogridConfig(
meta=Metadata(microgrid_id=10, name="Demo", latitude=52.5),
ctype={
"pv": ComponentTypeConfig(meter=[2], formula={"AC_POWER_ACTIVE": "#2"}),
Expand All @@ -36,7 +36,7 @@ def test_dump_map_round_trips() -> None:

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

text = dump_map(configs)

Expand All @@ -51,7 +51,7 @@ def test_dump_map_empty() -> None:
def test_dump_map_renders_whole_floats_as_underscored_ints() -> None:
"""Whole-number float fields (e.g. peak/rated power) render as `1_736_680`, not `1736680.0`."""
configs = {
"10": MicrogridConfig(
10: MicrogridConfig(
meta=Metadata(microgrid_id=10, latitude=52.5),
pv={"1": PVConfig(peak_power=1_736_680.0, rated_power=1_400_000.0)},
)
Expand Down
14 changes: 8 additions & 6 deletions tests/test_load.py
Original file line number Diff line number Diff line change
Expand Up @@ -69,7 +69,7 @@ async def test_load_microgrids_from_api_derives_formulas_and_ids() -> None:
"""A config loaded from the API gets both formulas and component IDs."""
configs = await _load_microgrids_from_api(_mock_client(), [10])

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

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


Expand All @@ -105,7 +105,7 @@ async def test_load_configs_forwards_the_component_graph_config() -> None:
)
).microgrids

assert configs["10"].ctype["pv"].formula == {
assert configs[10].ctype["pv"].formula == {
"AC_POWER_ACTIVE": "COALESCE(#2, #4, 0.0)"
}

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

configs = await _load_microgrids_from_api(client, [10])

cfg = configs["10"]
cfg = configs[10]
assert cfg.meta.microgrid_id == 10
assert cfg.ctype == {}

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

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

assert document.microgrids["1"].meta.name == "Override"
assert document.microgrids[1].meta.name == "Override"


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

# The API layer filled the microgrid's component config.
assert document.microgrids["10"].ctype
assert document.microgrids[10].ctype
assert document.microgrids[10].meta.name == "File name"
# The file's topology survived the merge with the API layer.
assert "M10L51171875559" in document.relations
assert "51171875559" in document.market_locations
Expand Down
Loading