"""Base configuration utilities."""
from __future__ import annotations
import typing
if typing.TYPE_CHECKING:
from typing import Any
from devops.enums import StrEnum
# TODO(97gamjak): centralize exception handling
# https://github.com/97gamjak/devops/issues/24
[docs]
class ConfigError(Exception):
"""Custom exception for configuration-related errors."""
def __init__(self, message: str) -> None:
"""Initialize the exception with a message."""
super().__init__(f"ConfigError: {message}")
self.message = message
[docs]
def get_table(mapping: dict[str, Any], key: str) -> dict[str, Any]:
"""Get a sub-table from a mapping.
Parameters
----------
mapping: dict[str, Any]
The mapping to extract the sub-table from.
key: str
The key of the sub-table.
Returns
-------
dict[str, Any]
The extracted sub-table or an empty dictionary if the key is not found.
Raises
------
ConfigError
If the value associated with the key is not a dictionary.
"""
value = mapping.get(key)
if value is None:
return {}
if not isinstance(value, dict):
msg = f"Expected dict for key '{key}', got {type(value).__name__}"
raise ConfigError(msg)
return value
def _get_type(mapping: dict[str, Any], key: str, expected_type: type) -> Any:
"""Get a value of expected type from a mapping.
Parameters
----------
mapping: dict[str, Any]
The mapping to extract the value from.
key: str
The key of the value.
expected_type: type
The expected type of the value.
Returns
-------
Any
The extracted value or the default value if the key is not found.
Raises
------
ConfigError
If the value associated with the key is not of the expected type.
"""
value = mapping.get(key)
if value is None:
return None
if not isinstance(value, expected_type):
msg = (
f"Expected {expected_type.__name__} for "
f"key '{key}', got {type(value).__name__}"
)
raise ConfigError(msg)
return value
[docs]
def get_bool(
mapping: dict[str, Any], key: str, *, default: bool | None = None
) -> bool | None:
"""Get a boolean value from a mapping.
Parameters
----------
mapping: dict[str, Any]
The mapping to extract the boolean from.
key: str
The key of the boolean.
default: bool | None
The default value to return if the key is not found.
Returns
-------
bool | None
The extracted boolean value or None if the key is not found.
"""
value = _get_type(mapping, key, bool)
if value is None:
return default
return value
[docs]
def get_str(
mapping: dict[str, Any], key: str, default: str | None = None
) -> str | None:
"""Get a string value from a mapping.
Parameters
----------
mapping: dict[str, Any]
The mapping to extract the string from.
key: str
The key of the string.
default: str | None
The default value to return if the key is not found.
Returns
-------
str | None
The extracted string value or None if the key is not found.
"""
value = _get_type(mapping, key, str)
if value is None:
return default
return value
[docs]
def get_str_or_str_list(
mapping: dict[str, Any], key: str, default: str | list[str] | None = None
) -> str | list[str] | None:
"""Get a string or list of strings from a mapping.
Parameters
----------
mapping: dict[str, Any]
The mapping to extract the value from.
key: str
The key of the value.
default: str | list[str] | None
The default value to return if the key is not found.
Returns
-------
str | list[str] | None
The extracted string or list of strings value or None if the key is not found.
"""
value = mapping.get(key, default)
if value is None:
return None
if isinstance(value, str):
return value
if isinstance(value, list):
if all(isinstance(item, str) for item in value):
return value
msg = (
f"Expected str or list of str for key '{key}', "
"got list with non-string items"
)
raise ConfigError(msg)
msg = f"Expected str or list of str for key '{key}', got {type(value).__name__}"
raise ConfigError(msg)
[docs]
def get_str_enum(
mapping: dict[str, Any], key: str, enum_type: type, default: str | None = None
) -> StrEnum | None:
"""Get a string enum value from a mapping.
Parameters
----------
mapping: dict[str, Any]
The mapping to extract the enum value from.
key: str
The key of the enum value.
enum_type: type
The enum type to validate against.
default: str
The default value to return if the key is not found.
Returns
-------
StrEnum | None
The extracted enum value or None if the key is not found.
Raises
------
ConfigError
If the value associated with the key is not a valid enum value.
"""
value = _get_type(mapping, key, str)
if value is None:
value = default
if value is None:
return None
if enum_type.is_valid(value):
return enum_type(value)
msg = (
f"Invalid value for key '{key}': {value},"
f" expected one of {enum_type.list_values()}"
)
raise ConfigError(msg)
[docs]
def get_str_list(
mapping: dict[str, Any], key: str, default: list[str] | None = None
) -> list[str]:
"""Get a list of strings from a mapping.
Parameters
----------
mapping: dict[str, Any]
The mapping to extract the list from.
key: str
The key of the list.
default: list[str] | None
The default value to return if the key is not found. Defaults to None.
Returns
-------
list[str]
The extracted list of strings or an empty list if the key is not found.
Raises
------
ConfigError
If the value associated with the key is not a list of strings.
"""
value = _get_type(mapping, key, list)
if value is None:
value = default
if value is None:
return []
if not isinstance(value, list) or not all(isinstance(item, str) for item in value):
msg = f"Expected list of strings for key '{key}', got {type(value).__name__}"
raise ConfigError(msg)
return value