REST: Allow setting single config value

* /v1/config/{path} supports setting single config value (post body). Lists are
   supported as well by using the index:
    - general/latitude (value: 55.55)
    - optimize/ev_available_charge_rates_percent/0 (value: 42)

   Whole tree can be overriden as well (no merge):
    - optimize/ev_available_charge_rates_percent (value: [42, 43, 44]

 * ConfigEOS: Add set_config_value, get_config_value
This commit is contained in:
Dominique Lasserre 2025-01-26 20:54:27 +01:00 committed by Bobby Noelte
parent 1bb74ed836
commit 94618f5f66
6 changed files with 709 additions and 242 deletions

View File

@ -257,6 +257,70 @@ Returns:
---
## GET /v1/config/{path}
**Links**: [local](http://localhost:8503/docs#/default/fastapi_config_get_key_v1_config__path__get), [eos](https://petstore3.swagger.io/?url=https://raw.githubusercontent.com/Akkudoktor-EOS/EOS/refs/heads/main/openapi.json#/default/fastapi_config_get_key_v1_config__path__get)
Fastapi Config Get Key
```
Get the value of a nested key or index in the config model.
Args:
path (str): The nested path to the key (e.g., "general/latitude" or "optimize/nested_list/0").
Returns:
value (Any): The value of the selected nested key.
```
**Parameters**:
- `path` (path, required): The nested path to the configuration key (e.g., general/latitude).
**Responses**:
- **200**: Successful Response
- **422**: Validation Error
---
## PUT /v1/config/{path}
**Links**: [local](http://localhost:8503/docs#/default/fastapi_config_put_key_v1_config__path__put), [eos](https://petstore3.swagger.io/?url=https://raw.githubusercontent.com/Akkudoktor-EOS/EOS/refs/heads/main/openapi.json#/default/fastapi_config_put_key_v1_config__path__put)
Fastapi Config Put Key
```
Update a nested key or index in the config model.
Args:
path (str): The nested path to the key (e.g., "general/latitude" or "optimize/nested_list/0").
value (Any): The new value to assign to the key or index at path.
Returns:
configuration (ConfigEOS): The current configuration after the update.
```
**Parameters**:
- `path` (path, required): The nested path to the configuration key (e.g., general/latitude).
**Request Body**:
- `application/json`: {
"description": "The value to assign to the specified configuration path.",
"title": "Value"
}
**Responses**:
- **200**: Successful Response
- **422**: Validation Error
---
## PUT /v1/measurement/data
**Links**: [local](http://localhost:8503/docs#/default/fastapi_measurement_data_put_v1_measurement_data_put), [eos](https://petstore3.swagger.io/?url=https://raw.githubusercontent.com/Akkudoktor-EOS/EOS/refs/heads/main/openapi.json#/default/fastapi_measurement_data_put_v1_measurement_data_put)

View File

@ -106,246 +106,6 @@
"title": "BaseBatteryParameters",
"type": "object"
},
"GeneralSettings-Input": {
"description": "Settings for common configuration.\n\nGeneral configuration to set directories of cache and output files and system location (latitude\nand longitude).\nValidators ensure each parameter is within a specified range. A computed property, `timezone`,\ndetermines the time zone based on latitude and longitude.\n\nAttributes:\n latitude (Optional[float]): Latitude in degrees, must be between -90 and 90.\n longitude (Optional[float]): Longitude in degrees, must be between -180 and 180.\n\nProperties:\n timezone (Optional[str]): Computed time zone string based on the specified latitude\n and longitude.\n\nValidators:\n validate_latitude (float): Ensures `latitude` is within the range -90 to 90.\n validate_longitude (float): Ensures `longitude` is within the range -180 to 180.",
"properties": {
"data_cache_subpath": {
"anyOf": [
{
"format": "path",
"type": "string"
},
{
"type": "null"
}
],
"default": "cache",
"description": "Sub-path for the EOS cache data directory.",
"title": "Data Cache Subpath"
},
"data_folder_path": {
"anyOf": [
{
"format": "path",
"type": "string"
},
{
"type": "null"
}
],
"description": "Path to EOS data directory.",
"examples": [
null,
"/home/eos/data"
],
"title": "Data Folder Path"
},
"data_output_subpath": {
"anyOf": [
{
"format": "path",
"type": "string"
},
{
"type": "null"
}
],
"default": "output",
"description": "Sub-path for the EOS output data directory.",
"title": "Data Output Subpath"
},
"latitude": {
"anyOf": [
{
"maximum": 90.0,
"minimum": -90.0,
"type": "number"
},
{
"type": "null"
}
],
"default": 52.52,
"description": "Latitude in decimal degrees, between -90 and 90, north is positive (ISO 19115) (\u00b0)",
"title": "Latitude"
},
"longitude": {
"anyOf": [
{
"maximum": 180.0,
"minimum": -180.0,
"type": "number"
},
{
"type": "null"
}
],
"default": 13.405,
"description": "Longitude in decimal degrees, within -180 to 180 (\u00b0)",
"title": "Longitude"
}
},
"title": "GeneralSettings",
"type": "object"
},
"GeneralSettings-Output": {
"description": "Settings for common configuration.\n\nGeneral configuration to set directories of cache and output files and system location (latitude\nand longitude).\nValidators ensure each parameter is within a specified range. A computed property, `timezone`,\ndetermines the time zone based on latitude and longitude.\n\nAttributes:\n latitude (Optional[float]): Latitude in degrees, must be between -90 and 90.\n longitude (Optional[float]): Longitude in degrees, must be between -180 and 180.\n\nProperties:\n timezone (Optional[str]): Computed time zone string based on the specified latitude\n and longitude.\n\nValidators:\n validate_latitude (float): Ensures `latitude` is within the range -90 to 90.\n validate_longitude (float): Ensures `longitude` is within the range -180 to 180.",
"properties": {
"config_file_path": {
"anyOf": [
{
"format": "path",
"type": "string"
},
{
"type": "null"
}
],
"description": "Path to EOS configuration file.",
"readOnly": true,
"title": "Config File Path"
},
"config_folder_path": {
"anyOf": [
{
"format": "path",
"type": "string"
},
{
"type": "null"
}
],
"description": "Path to EOS configuration directory.",
"readOnly": true,
"title": "Config Folder Path"
},
"data_cache_path": {
"anyOf": [
{
"format": "path",
"type": "string"
},
{
"type": "null"
}
],
"description": "Compute data_cache_path based on data_folder_path.",
"readOnly": true,
"title": "Data Cache Path"
},
"data_cache_subpath": {
"anyOf": [
{
"format": "path",
"type": "string"
},
{
"type": "null"
}
],
"default": "cache",
"description": "Sub-path for the EOS cache data directory.",
"title": "Data Cache Subpath"
},
"data_folder_path": {
"anyOf": [
{
"format": "path",
"type": "string"
},
{
"type": "null"
}
],
"description": "Path to EOS data directory.",
"examples": [
null,
"/home/eos/data"
],
"title": "Data Folder Path"
},
"data_output_path": {
"anyOf": [
{
"format": "path",
"type": "string"
},
{
"type": "null"
}
],
"description": "Compute data_output_path based on data_folder_path.",
"readOnly": true,
"title": "Data Output Path"
},
"data_output_subpath": {
"anyOf": [
{
"format": "path",
"type": "string"
},
{
"type": "null"
}
],
"default": "output",
"description": "Sub-path for the EOS output data directory.",
"title": "Data Output Subpath"
},
"latitude": {
"anyOf": [
{
"maximum": 90.0,
"minimum": -90.0,
"type": "number"
},
{
"type": "null"
}
],
"default": 52.52,
"description": "Latitude in decimal degrees, between -90 and 90, north is positive (ISO 19115) (\u00b0)",
"title": "Latitude"
},
"longitude": {
"anyOf": [
{
"maximum": 180.0,
"minimum": -180.0,
"type": "number"
},
{
"type": "null"
}
],
"default": 13.405,
"description": "Longitude in decimal degrees, within -180 to 180 (\u00b0)",
"title": "Longitude"
},
"timezone": {
"anyOf": [
{
"type": "string"
},
{
"type": "null"
}
],
"description": "Compute timezone based on latitude and longitude.",
"readOnly": true,
"title": "Timezone"
}
},
"required": [
"timezone",
"data_output_path",
"data_cache_path",
"config_folder_path",
"config_file_path"
],
"title": "GeneralSettings",
"type": "object"
},
"ConfigEOS": {
"additionalProperties": false,
"description": "Singleton configuration handler for the EOS application.\n\nConfigEOS extends `SettingsEOS` with support for default configuration paths and automatic\ninitialization.\n\n`ConfigEOS` ensures that only one instance of the class is created throughout the application,\nallowing consistent access to EOS configuration settings. This singleton instance loads\nconfiguration data from a predefined set of directories or creates a default configuration if\nnone is found.\n\nInitialization Process:\n - Upon instantiation, the singleton instance attempts to load a configuration file in this order:\n 1. The directory specified by the `EOS_CONFIG_DIR` environment variable\n 2. The directory specified by the `EOS_DIR` environment variable.\n 3. A platform specific default directory for EOS.\n 4. The current working directory.\n - The first available configuration file found in these directories is loaded.\n - If no configuration file is found, a default configuration file is created in the platform\n specific default directory, and default settings are loaded into it.\n\nAttributes from the loaded configuration are accessible directly as instance attributes of\n`ConfigEOS`, providing a centralized, shared configuration object for EOS.\n\nSingleton Behavior:\n - This class uses the `SingletonMixin` to ensure that all requests for `ConfigEOS` return\n the same instance, which contains the most up-to-date configuration. Modifying the configuration\n in one part of the application reflects across all references to this class.\n\nAttributes:\n config_folder_path (Optional[Path]): Path to the configuration directory.\n config_file_path (Optional[Path]): Path to the configuration file.\n\nRaises:\n FileNotFoundError: If no configuration file is found, and creating a default configuration fails.\n\nExample:\n To initialize and access configuration attributes (only one instance is created):\n ```python\n config_eos = ConfigEOS() # Always returns the same instance\n print(config_eos.prediction.hours) # Access a setting from the loaded configuration\n ```",
@ -877,6 +637,246 @@
"title": "ForecastResponse",
"type": "object"
},
"GeneralSettings-Input": {
"description": "Settings for common configuration.\n\nGeneral configuration to set directories of cache and output files and system location (latitude\nand longitude).\nValidators ensure each parameter is within a specified range. A computed property, `timezone`,\ndetermines the time zone based on latitude and longitude.\n\nAttributes:\n latitude (Optional[float]): Latitude in degrees, must be between -90 and 90.\n longitude (Optional[float]): Longitude in degrees, must be between -180 and 180.\n\nProperties:\n timezone (Optional[str]): Computed time zone string based on the specified latitude\n and longitude.\n\nValidators:\n validate_latitude (float): Ensures `latitude` is within the range -90 to 90.\n validate_longitude (float): Ensures `longitude` is within the range -180 to 180.",
"properties": {
"data_cache_subpath": {
"anyOf": [
{
"format": "path",
"type": "string"
},
{
"type": "null"
}
],
"default": "cache",
"description": "Sub-path for the EOS cache data directory.",
"title": "Data Cache Subpath"
},
"data_folder_path": {
"anyOf": [
{
"format": "path",
"type": "string"
},
{
"type": "null"
}
],
"description": "Path to EOS data directory.",
"examples": [
null,
"/home/eos/data"
],
"title": "Data Folder Path"
},
"data_output_subpath": {
"anyOf": [
{
"format": "path",
"type": "string"
},
{
"type": "null"
}
],
"default": "output",
"description": "Sub-path for the EOS output data directory.",
"title": "Data Output Subpath"
},
"latitude": {
"anyOf": [
{
"maximum": 90.0,
"minimum": -90.0,
"type": "number"
},
{
"type": "null"
}
],
"default": 52.52,
"description": "Latitude in decimal degrees, between -90 and 90, north is positive (ISO 19115) (\u00b0)",
"title": "Latitude"
},
"longitude": {
"anyOf": [
{
"maximum": 180.0,
"minimum": -180.0,
"type": "number"
},
{
"type": "null"
}
],
"default": 13.405,
"description": "Longitude in decimal degrees, within -180 to 180 (\u00b0)",
"title": "Longitude"
}
},
"title": "GeneralSettings",
"type": "object"
},
"GeneralSettings-Output": {
"description": "Settings for common configuration.\n\nGeneral configuration to set directories of cache and output files and system location (latitude\nand longitude).\nValidators ensure each parameter is within a specified range. A computed property, `timezone`,\ndetermines the time zone based on latitude and longitude.\n\nAttributes:\n latitude (Optional[float]): Latitude in degrees, must be between -90 and 90.\n longitude (Optional[float]): Longitude in degrees, must be between -180 and 180.\n\nProperties:\n timezone (Optional[str]): Computed time zone string based on the specified latitude\n and longitude.\n\nValidators:\n validate_latitude (float): Ensures `latitude` is within the range -90 to 90.\n validate_longitude (float): Ensures `longitude` is within the range -180 to 180.",
"properties": {
"config_file_path": {
"anyOf": [
{
"format": "path",
"type": "string"
},
{
"type": "null"
}
],
"description": "Path to EOS configuration file.",
"readOnly": true,
"title": "Config File Path"
},
"config_folder_path": {
"anyOf": [
{
"format": "path",
"type": "string"
},
{
"type": "null"
}
],
"description": "Path to EOS configuration directory.",
"readOnly": true,
"title": "Config Folder Path"
},
"data_cache_path": {
"anyOf": [
{
"format": "path",
"type": "string"
},
{
"type": "null"
}
],
"description": "Compute data_cache_path based on data_folder_path.",
"readOnly": true,
"title": "Data Cache Path"
},
"data_cache_subpath": {
"anyOf": [
{
"format": "path",
"type": "string"
},
{
"type": "null"
}
],
"default": "cache",
"description": "Sub-path for the EOS cache data directory.",
"title": "Data Cache Subpath"
},
"data_folder_path": {
"anyOf": [
{
"format": "path",
"type": "string"
},
{
"type": "null"
}
],
"description": "Path to EOS data directory.",
"examples": [
null,
"/home/eos/data"
],
"title": "Data Folder Path"
},
"data_output_path": {
"anyOf": [
{
"format": "path",
"type": "string"
},
{
"type": "null"
}
],
"description": "Compute data_output_path based on data_folder_path.",
"readOnly": true,
"title": "Data Output Path"
},
"data_output_subpath": {
"anyOf": [
{
"format": "path",
"type": "string"
},
{
"type": "null"
}
],
"default": "output",
"description": "Sub-path for the EOS output data directory.",
"title": "Data Output Subpath"
},
"latitude": {
"anyOf": [
{
"maximum": 90.0,
"minimum": -90.0,
"type": "number"
},
{
"type": "null"
}
],
"default": 52.52,
"description": "Latitude in decimal degrees, between -90 and 90, north is positive (ISO 19115) (\u00b0)",
"title": "Latitude"
},
"longitude": {
"anyOf": [
{
"maximum": 180.0,
"minimum": -180.0,
"type": "number"
},
{
"type": "null"
}
],
"default": 13.405,
"description": "Longitude in decimal degrees, within -180 to 180 (\u00b0)",
"title": "Longitude"
},
"timezone": {
"anyOf": [
{
"type": "string"
},
{
"type": "null"
}
],
"description": "Compute timezone based on latitude and longitude.",
"readOnly": true,
"title": "Timezone"
}
},
"required": [
"timezone",
"data_output_path",
"data_cache_path",
"config_folder_path",
"config_file_path"
],
"title": "GeneralSettings",
"type": "object"
},
"GesamtlastRequest": {
"properties": {
"hours": {
@ -3166,6 +3166,103 @@
]
}
},
"/v1/config/{path}": {
"get": {
"description": "Get the value of a nested key or index in the config model.\n\nArgs:\n path (str): The nested path to the key (e.g., \"general/latitude\" or \"optimize/nested_list/0\").\n\nReturns:\n value (Any): The value of the selected nested key.",
"operationId": "fastapi_config_get_key_v1_config__path__get",
"parameters": [
{
"description": "The nested path to the configuration key (e.g., general/latitude).",
"in": "path",
"name": "path",
"required": true,
"schema": {
"description": "The nested path to the configuration key (e.g., general/latitude).",
"title": "Path",
"type": "string"
}
}
],
"responses": {
"200": {
"content": {
"application/json": {
"schema": {}
}
},
"description": "Successful Response"
},
"422": {
"content": {
"application/json": {
"schema": {
"$ref": "#/components/schemas/HTTPValidationError"
}
}
},
"description": "Validation Error"
}
},
"summary": "Fastapi Config Get Key",
"tags": [
"config"
]
},
"put": {
"description": "Update a nested key or index in the config model.\n\nArgs:\n path (str): The nested path to the key (e.g., \"general/latitude\" or \"optimize/nested_list/0\").\n value (Any): The new value to assign to the key or index at path.\n\nReturns:\n configuration (ConfigEOS): The current configuration after the update.",
"operationId": "fastapi_config_put_key_v1_config__path__put",
"parameters": [
{
"description": "The nested path to the configuration key (e.g., general/latitude).",
"in": "path",
"name": "path",
"required": true,
"schema": {
"description": "The nested path to the configuration key (e.g., general/latitude).",
"title": "Path",
"type": "string"
}
}
],
"requestBody": {
"content": {
"application/json": {
"schema": {
"description": "The value to assign to the specified configuration path.",
"title": "Value"
}
}
},
"required": true
},
"responses": {
"200": {
"content": {
"application/json": {
"schema": {
"$ref": "#/components/schemas/ConfigEOS"
}
}
},
"description": "Successful Response"
},
"422": {
"content": {
"application/json": {
"schema": {
"$ref": "#/components/schemas/HTTPValidationError"
}
}
},
"description": "Validation Error"
}
},
"summary": "Fastapi Config Put Key",
"tags": [
"config"
]
}
},
"/v1/measurement/data": {
"put": {
"description": "Merge the measurement data given as datetime data into EOS measurements.",

View File

@ -30,7 +30,7 @@ from akkudoktoreos.core.coreabc import SingletonMixin
from akkudoktoreos.core.decorators import classproperty
from akkudoktoreos.core.logging import get_logger
from akkudoktoreos.core.logsettings import LoggingCommonSettings
from akkudoktoreos.core.pydantic import merge_models
from akkudoktoreos.core.pydantic import access_nested_value, merge_models
from akkudoktoreos.devices.settings import DevicesCommonSettings
from akkudoktoreos.measurement.measurement import MeasurementCommonSettings
from akkudoktoreos.optimization.optimization import OptimizationCommonSettings
@ -379,6 +379,32 @@ class ConfigEOS(SingletonMixin, SettingsEOSDefaults):
"""
self._setup()
def set_config_value(self, path: str, value: Any) -> None:
"""Set a configuration value based on the provided path.
Supports string paths (with '/' separators) or sequence paths (list/tuple).
Trims leading and trailing '/' from string paths.
Args:
path (str): The path to the configuration key (e.g., "key1/key2/key3" or key1/key2/0).
value (Any]): The value to set.
"""
access_nested_value(self, path, True, value)
def get_config_value(self, path: str) -> Any:
"""Get a configuration value based on the provided path.
Supports string paths (with '/' separators) or sequence paths (list/tuple).
Trims leading and trailing '/' from string paths.
Args:
path (str): The path to the configuration key (e.g., "key1/key2/key3" or key1/key2/0).
Returns:
Any: The retrieved value.
"""
return access_nested_value(self, path, False)
def _create_initial_config_file(self) -> None:
if self.general.config_file_path and not self.general.config_file_path.exists():
self.general.config_file_path.parent.mkdir(parents=True, exist_ok=True)

View File

@ -51,6 +51,70 @@ def merge_models(source: BaseModel, update_dict: dict[str, Any]) -> dict[str, An
return merged_dict
def access_nested_value(
model: BaseModel, path: str, setter: bool, value: Optional[Any] = None
) -> Any:
"""Get or set a nested model value based on the provided path.
Supports string paths (with '/' separators) or sequence paths (list/tuple).
Trims leading and trailing '/' from string paths.
Args:
model (BaseModel): The model object for partial assignment.
path (str): The path to the model key (e.g., "key1/key2/key3" or key1/key2/0).
setter (bool): True to set value at path, False to return value at path.
value (Optional[Any]): The value to set.
Returns:
Any: The retrieved value if acting as a getter, or None if setting a value.
"""
path_elements = path.strip("/").split("/")
cfg: Any = model
parent: BaseModel = model
model_key: str = ""
for i, key in enumerate(path_elements):
is_final_key = i == len(path_elements) - 1
if isinstance(cfg, list):
try:
idx = int(key)
if is_final_key:
if not setter: # Getter
return cfg[idx]
else: # Setter
new_list = list(cfg)
new_list[idx] = value
# Trigger validation
setattr(parent, model_key, new_list)
else:
cfg = cfg[idx]
except ValidationError as e:
raise ValueError(f"Error updating model: {e}") from e
except (ValueError, IndexError) as e:
raise IndexError(f"Invalid list index at {path}: {key}") from e
elif isinstance(cfg, BaseModel):
parent = cfg
model_key = key
if is_final_key:
if not setter: # Getter
return getattr(cfg, key)
else: # Setter
try:
# Verification also if nested value is provided opposed to just setattr
# Will merge partial assignment
cfg = cfg.__pydantic_validator__.validate_assignment(cfg, key, value)
except Exception as e:
raise ValueError(f"Error updating model: {e}") from e
else:
cfg = getattr(cfg, key)
else:
raise KeyError(f"Key '{key}' not found in model.")
class PydanticTypeAdapterDateTime(TypeAdapter[pendulum.DateTime]):
"""Custom type adapter for Pendulum DateTime fields."""

View File

@ -10,7 +10,9 @@ from typing import Annotated, Any, AsyncGenerator, Dict, List, Optional, Union
import httpx
import uvicorn
from fastapi import FastAPI, Query, Request
from fastapi import Body, FastAPI
from fastapi import Path as FastapiPath
from fastapi import Query, Request
from fastapi.exceptions import HTTPException
from fastapi.responses import FileResponse, HTMLResponse, RedirectResponse, Response
@ -302,6 +304,58 @@ def fastapi_config_put(settings: SettingsEOS) -> ConfigEOS:
return config_eos
@app.put("/v1/config/{path:path}", tags=["config"])
def fastapi_config_put_key(
path: str = FastapiPath(
..., description="The nested path to the configuration key (e.g., general/latitude)."
),
value: Any = Body(..., description="The value to assign to the specified configuration path."),
) -> ConfigEOS:
"""Update a nested key or index in the config model.
Args:
path (str): The nested path to the key (e.g., "general/latitude" or "optimize/nested_list/0").
value (Any): The new value to assign to the key or index at path.
Returns:
configuration (ConfigEOS): The current configuration after the update.
"""
try:
config_eos.set_config_value(path, value)
except IndexError as e:
raise HTTPException(status_code=400, detail=str(e))
except KeyError as e:
raise HTTPException(status_code=404, detail=str(e))
except Exception as e:
raise HTTPException(status_code=400, detail=str(e))
return config_eos
@app.get("/v1/config/{path:path}", tags=["config"])
def fastapi_config_get_key(
path: str = FastapiPath(
..., description="The nested path to the configuration key (e.g., general/latitude)."
),
) -> Response:
"""Get the value of a nested key or index in the config model.
Args:
path (str): The nested path to the key (e.g., "general/latitude" or "optimize/nested_list/0").
Returns:
value (Any): The value of the selected nested key.
"""
try:
return config_eos.get_config_value(path)
except IndexError as e:
raise HTTPException(status_code=400, detail=str(e))
except KeyError as e:
raise HTTPException(status_code=404, detail=str(e))
except Exception as e:
raise HTTPException(status_code=400, detail=str(e))
@app.get("/v1/measurement/keys", tags=["measurement"])
def fastapi_measurement_keys_get() -> list[str]:
"""Get a list of available measurement keys."""

View File

@ -212,3 +212,165 @@ def test_config_common_settings_timezone_none_when_coordinates_missing():
assert config_no_latitude.timezone is None
assert config_no_longitude.timezone is None
assert config_no_coords.timezone is None
# Test partial assignments and possible side effects
@pytest.mark.parametrize(
"path, value, expected, exception",
[
# Correct value assignment
(
"general/latitude",
42.0,
[("general.latitude", 42.0), ("general.longitude", 13.405)],
None,
),
# Correct value assignment (trailing /)
(
"general/latitude/",
41,
[("general.latitude", 41.0), ("general.longitude", 13.405)],
None,
),
# Correct value assignment (cast)
(
"general/latitude",
"43.0",
[("general.latitude", 43.0), ("general.longitude", 13.405)],
None,
),
# Invalid value assignment (constraint)
(
"general/latitude",
91.0,
[("general.latitude", 52.52), ("general.longitude", 13.405)],
ValueError,
),
# Invalid value assignment (type)
(
"general/latitude",
"test",
[("general.latitude", 52.52), ("general.longitude", 13.405)],
ValueError,
),
# Invalid path
(
"general/latitude/test",
"",
[("general.latitude", 52.52), ("general.longitude", 13.405)],
KeyError,
),
# Correct value nested assignment
(
"general",
{"latitude": 22},
[("general.latitude", 22.0), ("general.longitude", 13.405)],
None,
),
# Invalid value nested assignment
(
"general",
{"latitude": "test"},
[("general.latitude", 52.52), ("general.longitude", 13.405)],
ValueError,
),
# Correct value for list
(
"optimization/ev_available_charge_rates_percent/0",
0.1,
[
(
"optimization.ev_available_charge_rates_percent",
[0.1, 0.375, 0.5, 0.625, 0.75, 0.875, 1.0],
)
],
None,
),
# Invalid value for list
(
"optimization/ev_available_charge_rates_percent/0",
"invalid",
[
(
"optimization.ev_available_charge_rates_percent",
[0.0, 0.375, 0.5, 0.625, 0.75, 0.875, 1.0],
)
],
ValueError,
),
# Invalid index (out of bound)
(
"optimization/ev_available_charge_rates_percent/10",
0,
[
(
"optimization.ev_available_charge_rates_percent",
[0.0, 0.375, 0.5, 0.625, 0.75, 0.875, 1.0],
)
],
IndexError,
),
# Invalid index (no number)
(
"optimization/ev_available_charge_rates_percent/test",
0,
[
(
"optimization.ev_available_charge_rates_percent",
[0.0, 0.375, 0.5, 0.625, 0.75, 0.875, 1.0],
)
],
IndexError,
),
# Unset value (set None)
(
"optimization/ev_available_charge_rates_percent",
None,
[
(
"optimization.ev_available_charge_rates_percent",
None,
)
],
None,
),
],
)
def test_set_nested_key(path, value, expected, exception, config_eos):
if not exception:
config_eos.set_config_value(path, value)
for expected_path, expected_value in expected:
assert eval(f"config_eos.{expected_path}") == expected_value
else:
with pytest.raises(exception):
config_eos.set_config_value(path, value)
for expected_path, expected_value in expected:
assert eval(f"config_eos.{expected_path}") == expected_value
@pytest.mark.parametrize(
"path, expected_value, exception",
[
("general/latitude", 52.52, None),
("general/latitude/", 52.52, None),
("general/latitude/test", None, KeyError),
(
"optimization/ev_available_charge_rates_percent/1",
0.375,
None,
),
("optimization/ev_available_charge_rates_percent/10", 0, IndexError),
("optimization/ev_available_charge_rates_percent/test", 0, IndexError),
(
"optimization/ev_available_charge_rates_percent",
[0.0, 0.375, 0.5, 0.625, 0.75, 0.875, 1.0],
None,
),
],
)
def test_get_nested_key(path, expected_value, exception, config_eos):
if not exception:
assert config_eos.get_config_value(path) == expected_value
else:
with pytest.raises(exception):
config_eos.get_config_value(path)