Source code for xflow.utils.config
"""Config Manager Module"""
import copy
from pathlib import Path
from typing import Any, Dict, List, Optional, Tuple, Type, Union
# shim Self for Python <3.11
try:
from typing import Self
except ImportError:
from typing_extensions import (
Self,
) # ensure typing-extensions>=4.0.0 is in your deps
from .helper import deep_update
from .io import copy_file
from .parser import load_file, save_file
from .typing import PathLikeStr, Schema
[docs]
def load_validated_config(
file_path: PathLikeStr, schema: Optional[Schema] = None
) -> Dict[str, Any]:
"""Load and optionally validate config using any validation schema.
Args:
file_path: Path to config file
schema: Optional schema class for validation. If None, returns raw dict
Returns:
Dict containing configuration data (validated if schema provided)
"""
raw = load_file(file_path)
if schema is None:
return raw
validated = schema(**raw)
return validated.model_dump()
[docs]
class ConfigManager:
"""In-memory config manager.
Keeps an immutable “source of truth” (_original_config) and a mutable working copy (_config).
"""
[docs]
def __init__(self, initial_config: Dict[str, Any]):
if not isinstance(initial_config, dict):
raise TypeError("initial_config must be a dictionary")
self._original_config = copy.deepcopy(initial_config)
self._config = copy.deepcopy(initial_config)
self._files: List[PathLikeStr] = []
def __repr__(self) -> str:
return f"ConfigManager(keys={list(self._config.keys())})"
def __getitem__(self, key: str) -> Any:
"""Get config value by key."""
return self._config[key]
def __setitem__(self, key: str, value: Any) -> None:
"""Set config value by key."""
self._config[key] = value
def __delitem__(self, key: str) -> None:
"""Delete config key."""
del self._config[key]
def __contains__(self, key: str) -> bool:
"""Check if key exists in config."""
return key in self._config
def __iter__(self):
"""Iterate over config keys."""
return iter(self._config)
def __len__(self) -> int:
"""Return number of config items."""
return len(self._config)
[docs]
def keys(self):
"""Return config keys."""
return self._config.keys()
[docs]
def values(self):
"""Return config values."""
return self._config.values()
[docs]
def items(self):
"""Return config items."""
return self._config.items()
[docs]
def add_files(self, file_paths: Union[PathLikeStr, List[PathLikeStr], Tuple[PathLikeStr, ...]]) -> Self:
"""Add files that are part of this configuration.
Args:
file_paths: Single file path or iterable of file paths
"""
# Handle single file path
if isinstance(file_paths, (str, Path)):
if file_paths not in self._files:
self._files.append(file_paths)
# Handle iterable of file paths
else:
for file_path in file_paths:
if file_path not in self._files:
self._files.append(file_path)
return self
[docs]
def get(self) -> Dict[str, Any]:
"""Return a fully independent snapshot of the working config."""
return copy.deepcopy(self._config)
[docs]
def reset(self) -> None:
"""Revert working config back to original."""
self._config = copy.deepcopy(self._original_config)
self._files = []
[docs]
def update(self, updates: Dict[str, Any]) -> Self:
"""Recursively update in config, Nested dictionaries are merged, other values are replaced."""
deep_update(self._config, updates)
return self
[docs]
def validate(self, schema: Schema) -> Self:
"""Validate working config against provided schema. Raises Error if invalid."""
validated = schema(**self._config)
validated.model_dump() # Just to ensure it works, but we don't need the result
return self
[docs]
def save_config(self, file_path: PathLikeStr) -> None:
"""Save the internal config to a specific file path (must include filename and extension)."""
save_file(self._config, file_path)
[docs]
def copy_associated_files(self, target_dir: PathLikeStr) -> None:
"""Copy all associated files to the target directory."""
if self._files:
target_dir = Path(target_dir)
for file_path in self._files:
copy_file(file_path, target_dir)
[docs]
def save(self, output_dir: PathLikeStr, config_filename: Optional[str] = None) -> None:
"""Save config and copy associated files to target directory.
Args:
output_dir: Target directory path
config_filename: Config filename with extension (e.g., 'config.yaml').
If None or empty, only copies associated files.
"""
output_dir = Path(output_dir)
# Save config only if filename is provided
if config_filename:
config_path = output_dir / config_filename
self.save_config(config_path)
# Always copy associated files
self.copy_associated_files(output_dir)