diff --git a/RELEASE_NOTES.md b/RELEASE_NOTES.md index d466455..6aa031a 100644 --- a/RELEASE_NOTES.md +++ b/RELEASE_NOTES.md @@ -14,4 +14,6 @@ ## Bug Fixes - +- `load_configs()` and `load_configs_from_files()` now accept directory paths in + addition to individual TOML files. When a directory is provided, all + `*.toml` files in that directory are loaded in sorted order. diff --git a/src/frequenz/gridpool/config/__init__.py b/src/frequenz/gridpool/config/__init__.py index 7be40f1..d2998eb 100644 --- a/src/frequenz/gridpool/config/__init__.py +++ b/src/frequenz/gridpool/config/__init__.py @@ -5,11 +5,7 @@ from frequenz.microgrid_component_graph import ComponentGraphConfig, FormulaOverrides -from .load import ( - load_configs, - load_configs_from_api, - load_configs_from_files, -) +from .load import load_configs, load_configs_from_api, load_configs_from_files from .microgrid import ( BatteryConfig, ComponentCategory, diff --git a/src/frequenz/gridpool/config/load.py b/src/frequenz/gridpool/config/load.py index 5aaf170..1e90b6b 100644 --- a/src/frequenz/gridpool/config/load.py +++ b/src/frequenz/gridpool/config/load.py @@ -24,12 +24,7 @@ pv_inverter_ids, pv_meter_ids, ) -from .microgrid import ( - ComponentTypeConfig, - Metadata, - MicrogridConfig, - merge_config_maps, -) +from .microgrid import ComponentTypeConfig, Metadata, MicrogridConfig, merge_config_maps _logger = logging.getLogger(__name__) @@ -123,16 +118,20 @@ async def load_configs( return merge_config_maps(base=configs, override=override_configs) +# pylint: disable=too-many-branches def load_configs_from_files( microgrid_config_files: str | Path | list[str | Path] | None = None, ) -> dict[str, "MicrogridConfig"]: - """Load multiple microgrid configurations from one or more files. + """Load multiple microgrid configurations from one or more paths/files. Configs for a single microgrid are expected to be in a single file. Later files with the same microgrid ID will overwrite the previous configs. + Directory paths are expanded to all `.toml` files they contain. Args: - microgrid_config_files: Path to a single microgrid config file or list of paths. + microgrid_config_files: + Path to a single microgrid config file or directory, or a list of + file and directory paths. Returns: Dictionary of single microgrid formula configs with microgrid IDs as keys. @@ -148,12 +147,18 @@ def load_configs_from_files( config_files: list[Path] = [] if microgrid_config_files: - if isinstance(microgrid_config_files, str): - config_files = [Path(microgrid_config_files)] - elif isinstance(microgrid_config_files, Path): - config_files = [microgrid_config_files] - elif isinstance(microgrid_config_files, list): - config_files = [Path(f) for f in microgrid_config_files] + # normalize to a list of items + if isinstance(microgrid_config_files, (str, Path)): + paths = [microgrid_config_files] + else: + paths = microgrid_config_files + + for item in paths: + config_path = Path(item) + if config_path.is_dir(): + config_files.extend(sorted(config_path.glob("*.toml"))) + else: + config_files.append(config_path) if len(config_files) == 0: raise ValueError( diff --git a/tests/test_config.py b/tests/test_config.py index 184c761..84aaa60 100644 --- a/tests/test_config.py +++ b/tests/test_config.py @@ -147,6 +147,37 @@ def test_load_configs(mocker: MockerFixture) -> None: assert battery_system.capacity == 10000 +def test_load_configs_from_directory(tmp_path: Path) -> None: + """Loading from a directory picks up its TOML files.""" + config_dir = tmp_path / "configs" + config_dir.mkdir() + (config_dir / "mg-1.toml").write_text( + """ + 1.meta.microgrid_id = 1 + 1.meta.name = "Test Grid 1" + 1.meta.gid = 1 + """, + encoding="utf-8", + ) + (config_dir / "mg-2.toml").write_text( + """ + 2.meta.microgrid_id = 2 + 2.meta.name = "Test Grid 2" + 2.meta.gid = 2 + """, + encoding="utf-8", + ) + (config_dir / "ignore.txt").write_text("not toml", encoding="utf-8") + + configs = load_configs_from_files(config_dir) + + assert set(configs) == {"1", "2"} + assert configs["1"].meta is not None + assert configs["1"].meta.name == "Test Grid 1" + assert configs["2"].meta is not None + assert configs["2"].meta.name == "Test Grid 2" + + def _assert_optional_field(value: float | None, expected: float) -> None: """Validate an optional field. diff --git a/tests/test_load.py b/tests/test_load.py index da71aa2..26c62f7 100644 --- a/tests/test_load.py +++ b/tests/test_load.py @@ -3,6 +3,7 @@ """Tests for loading microgrid configs from the Assets API.""" +from pathlib import Path from unittest.mock import AsyncMock, MagicMock import pytest @@ -107,6 +108,32 @@ async def test_load_configs_forwards_the_component_graph_config() -> None: } +async def test_load_configs_accepts_directories_for_file_layers( + tmp_path: Path, +) -> None: + """`load_configs` can derive API IDs from config files found in a directory.""" + config_dir = tmp_path / "configs" + config_dir.mkdir() + (config_dir / "mg-10.toml").write_text( + """ + 10.meta.microgrid_id = 10 + 10.meta.name = "Directory Grid" + 10.meta.gid = 10 + """, + encoding="utf-8", + ) + + configs = await load_configs( + default_files=config_dir, + assets_client=_mock_client(), + ) + + assert "10" in configs + assert configs["10"].ctype["pv"].formula == { + "AC_POWER_ACTIVE": "COALESCE(#4, #2, 0.0)" + } + + async def test_load_configs_rejects_a_component_graph_config_without_a_client() -> None: """A component graph config is only meaningful with an Assets API client.""" with pytest.raises(ValueError, match="requires an assets_client"):