| 123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653654655656657658659660661662663664665666667668669670671672673674675676677678679680681682683684685686687688689690691692693694695696697698699700701702703704705706707708709710711712713714715716717718719720721722723724725726727728729730731732733734735736737738739740741742743744745746747748749750751752753754755756757758759760761762763764765766767768769770771772773774775776777778779780781782783784785786787788789790791792793794795796797798799800801802803804805806807808809810811812813814815816817818819820821822823824825826827828829830831832833834835836837 |
- """
- Configuration Manager for Trixy Application
- This module implements the main ConfigManager class that provides:
- - Configuration loading and saving with validation
- - Hot-reloading of configuration files during runtime
- - Thread-safe configuration access for multi-satellite support
- - Command line argument override support (higher priority than config files)
- - Environment variable integration
- - Configuration backup and restore functionality
- - Comprehensive error handling with detailed error messages
- The ConfigManager is the central point for all configuration operations in the
- Trixy application and ensures consistent, safe access to configuration data.
- """
- import json
- import os
- import threading
- import time
- import shutil
- from pathlib import Path
- from typing import Dict, Any, Optional, Union, List, Callable
- from datetime import datetime, timezone
- from copy import deepcopy
- from dataclasses import asdict
- import argparse
- from .config_models import (
- ServerConfig, ClientConfig, StandaloneConfig, ConfigType,
- DeploymentMode, get_config_class, create_default_config, merge_configs
- )
- from .config_defaults import (
- create_default_config_for_mode, apply_environment_overrides,
- get_environment_overrides
- )
- from .config_validator import ConfigValidator, ValidationResult
- class ConfigurationError(Exception):
- """Base exception for configuration-related errors."""
- pass
- class ConfigurationValidationError(ConfigurationError):
- """Exception raised when configuration validation fails."""
-
- def __init__(self, message: str, validation_result: ValidationResult = None):
- super().__init__(message)
- self.validation_result = validation_result
- class ConfigurationLoadError(ConfigurationError):
- """Exception raised when configuration loading fails."""
- pass
- class ConfigurationSaveError(ConfigurationError):
- """Exception raised when configuration saving fails."""
- pass
- class ConfigManager:
- """
- Main configuration manager for the Trixy application.
-
- Provides thread-safe configuration management with hot-reloading,
- validation, backup/restore, and override support.
- """
-
- def __init__(
- self,
- mode: DeploymentMode,
- config_file: Optional[str] = None,
- debug_mode: bool = False,
- auto_create_missing: bool = True,
- enable_validation: bool = True,
- strict_validation: bool = False
- ):
- """
- Initialize the configuration manager.
-
- Args:
- mode: Deployment mode (server, client, standalone)
- config_file: Path to configuration file (optional)
- debug_mode: Enable debug mode
- auto_create_missing: Create default config if file doesn't exist
- enable_validation: Enable configuration validation
- strict_validation: Treat warnings as errors
- """
- self.mode = mode
- self.debug_mode = debug_mode
- self.auto_create_missing = auto_create_missing
- self.enable_validation = enable_validation
- self.strict_validation = strict_validation
-
- # Configuration data and state
- self._config: Optional[ConfigType] = None
- self._config_file = config_file
- self._config_lock = threading.RLock()
- self._last_modified: Optional[float] = None
- self._backup_configs: List[Dict[str, Any]] = []
- self._max_backups = 10
-
- # Hot-reloading support
- self._hot_reload_enabled = False
- self._hot_reload_thread: Optional[threading.Thread] = None
- self._hot_reload_stop_event = threading.Event()
- self._hot_reload_interval = 1.0 # seconds
- self._reload_callbacks: List[Callable[[ConfigType, ConfigType], None]] = []
-
- # Override support
- self._cli_overrides: Dict[str, Any] = {}
- self._runtime_overrides: Dict[str, Any] = {}
-
- # Validation
- self._validator = ConfigValidator(strict_mode=strict_validation)
-
- # Determine default config file if not provided
- if not self._config_file:
- self._config_file = self._get_default_config_file()
-
- self._config_file = str(Path(self._config_file).resolve())
-
- def _get_default_config_file(self) -> str:
- """Get default configuration file path for the deployment mode."""
- config_files = {
- DeploymentMode.SERVER: "config/server_config.json",
- DeploymentMode.CLIENT: "config/client_config.json",
- DeploymentMode.STANDALONE: "config/standalone_config.json",
- }
- return config_files[self.mode]
-
- @classmethod
- def from_file(
- cls,
- config_file: str,
- debug_mode: bool = False,
- **kwargs
- ) -> 'ConfigManager':
- """
- Create ConfigManager from a configuration file.
-
- Automatically detects deployment mode from filename or file contents.
-
- Args:
- config_file: Path to configuration file
- debug_mode: Enable debug mode
- **kwargs: Additional arguments passed to ConfigManager
-
- Returns:
- ConfigManager instance
-
- Raises:
- ConfigurationError: If mode cannot be determined
- """
- # Try to determine mode from filename
- filename = Path(config_file).name.lower()
- mode = None
-
- if 'server' in filename:
- mode = DeploymentMode.SERVER
- elif 'client' in filename:
- mode = DeploymentMode.CLIENT
- elif 'standalone' in filename:
- mode = DeploymentMode.STANDALONE
-
- # If mode not determined from filename, try to read it from file
- if mode is None and Path(config_file).exists():
- try:
- with open(config_file, 'r', encoding='utf-8') as f:
- config_data = json.load(f)
- if 'mode' in config_data:
- mode = DeploymentMode(config_data['mode'])
- except (json.JSONDecodeError, ValueError, KeyError):
- pass
-
- if mode is None:
- raise ConfigurationError(
- f"Cannot determine deployment mode from config file: {config_file}. "
- "Ensure filename contains 'server', 'client', or 'standalone', "
- "or that the config file contains a 'mode' field."
- )
-
- return cls(
- mode=mode,
- config_file=config_file,
- debug_mode=debug_mode,
- **kwargs
- )
-
- @classmethod
- def from_args(
- cls,
- args: argparse.Namespace,
- **kwargs
- ) -> 'ConfigManager':
- """
- Create ConfigManager from command line arguments.
-
- Expected arguments:
- - mode: Deployment mode (server/client/standalone)
- - config: Configuration file path (optional)
- - debug: Debug mode flag (optional)
-
- Args:
- args: Parsed command line arguments
- **kwargs: Additional arguments passed to ConfigManager
-
- Returns:
- ConfigManager instance
- """
- mode = DeploymentMode(args.mode.lower())
- config_file = getattr(args, 'config', None)
- debug_mode = getattr(args, 'debug', False)
-
- # Create manager
- manager = cls(
- mode=mode,
- config_file=config_file,
- debug_mode=debug_mode,
- **kwargs
- )
-
- # Extract configuration overrides from args
- cli_overrides = {}
-
- # Common override patterns
- if hasattr(args, 'port') and args.port:
- cli_overrides.setdefault('network', {})['command_port'] = args.port
-
- if hasattr(args, 'host') and args.host:
- if mode == DeploymentMode.CLIENT:
- cli_overrides.setdefault('satellite', {})['server_host'] = args.host
- else:
- cli_overrides.setdefault('network', {})['command_bind_address'] = args.host
-
- if hasattr(args, 'log_level') and args.log_level:
- cli_overrides.setdefault('logging', {})['log_level'] = args.log_level
-
- if hasattr(args, 'data_dir') and args.data_dir:
- cli_overrides['data_directory'] = args.data_dir
-
- # Set CLI overrides
- manager.set_cli_overrides(cli_overrides)
-
- return manager
-
- def load_config(
- self,
- validate: bool = None,
- strict_validation: bool = None
- ) -> ConfigType:
- """
- Load configuration from file with validation.
-
- Args:
- validate: Whether to validate configuration (uses instance default if None)
- strict_validation: Whether to treat warnings as errors (uses instance default if None)
-
- Returns:
- Loaded and validated configuration object
-
- Raises:
- ConfigurationLoadError: If loading fails
- ConfigurationValidationError: If validation fails
- """
- validate = validate if validate is not None else self.enable_validation
- strict_validation = strict_validation if strict_validation is not None else self.strict_validation
-
- with self._config_lock:
- try:
- # Load configuration data
- config_data = self._load_config_data()
-
- # Apply overrides
- config_data = self._apply_overrides(config_data)
-
- # Create configuration object
- config_class = get_config_class(self.mode)
- config = config_class.from_dict(config_data)
-
- # Validate if requested
- if validate:
- self._validate_config(config, strict_validation)
-
- # Update internal state
- self._config = config
- self._update_last_modified()
-
- # Create backup
- self._create_backup(config_data)
-
- return config
-
- except Exception as e:
- if isinstance(e, (ConfigurationValidationError, ConfigurationLoadError)):
- raise
- else:
- raise ConfigurationLoadError(f"Failed to load configuration: {str(e)}") from e
-
- def _load_config_data(self) -> Dict[str, Any]:
- """Load raw configuration data from file."""
- config_path = Path(self._config_file)
-
- # Create default config if file doesn't exist and auto-create is enabled
- if not config_path.exists():
- if self.auto_create_missing:
- self._create_default_config_file()
- else:
- raise ConfigurationLoadError(f"Configuration file not found: {self._config_file}")
-
- # Load configuration from file
- try:
- with open(config_path, 'r', encoding='utf-8') as f:
- config_data = json.load(f)
- except json.JSONDecodeError as e:
- raise ConfigurationLoadError(f"Invalid JSON in configuration file: {str(e)}") from e
- except Exception as e:
- raise ConfigurationLoadError(f"Error reading configuration file: {str(e)}") from e
-
- return config_data
-
- def _create_default_config_file(self) -> None:
- """Create default configuration file."""
- config_path = Path(self._config_file)
- config_path.parent.mkdir(parents=True, exist_ok=True)
-
- # Create default configuration
- default_config = create_default_config_for_mode(self.mode, self.debug_mode)
-
- # Save to file
- try:
- with open(config_path, 'w', encoding='utf-8') as f:
- json.dump(default_config.to_dict(), f, indent=2, ensure_ascii=False)
-
- if self.debug_mode:
- from . import pprint
- pprint(f"Created default configuration file: {self._config_file}")
-
- except Exception as e:
- raise ConfigurationLoadError(f"Failed to create default configuration file: {str(e)}") from e
-
- def _apply_overrides(self, config_data: Dict[str, Any]) -> Dict[str, Any]:
- """Apply environment variables and runtime overrides to configuration data."""
- # Apply environment variables first
- config_data = apply_environment_overrides(config_data.copy())
-
- # Apply CLI overrides
- if self._cli_overrides:
- config_data = self._deep_merge_dict(config_data, self._cli_overrides)
-
- # Apply runtime overrides
- if self._runtime_overrides:
- config_data = self._deep_merge_dict(config_data, self._runtime_overrides)
-
- return config_data
-
- def _deep_merge_dict(self, base: Dict[str, Any], override: Dict[str, Any]) -> Dict[str, Any]:
- """Deep merge two dictionaries."""
- result = base.copy()
-
- for key, value in override.items():
- if key in result and isinstance(result[key], dict) and isinstance(value, dict):
- result[key] = self._deep_merge_dict(result[key], value)
- else:
- result[key] = value
-
- return result
-
- def _validate_config(self, config: ConfigType, strict: bool) -> None:
- """Validate configuration and raise exception if invalid."""
- result = self._validator.validate_config(config)
-
- if not result.is_valid or (strict and result.warnings):
- error_msg = f"Configuration validation failed:\n"
-
- if result.errors:
- error_msg += "Errors:\n"
- for error in result.errors:
- error_msg += f" - {error}\n"
-
- if strict and result.warnings:
- error_msg += "Warnings (treated as errors):\n"
- for warning in result.warnings:
- error_msg += f" - {warning}\n"
-
- raise ConfigurationValidationError(error_msg, result)
-
- # Log warnings in debug mode
- if self.debug_mode and result.warnings:
- from . import pprint
- pprint("Configuration warnings:")
- for warning in result.warnings:
- pprint(f" - {warning}")
-
- def _update_last_modified(self) -> None:
- """Update last modified timestamp."""
- try:
- config_path = Path(self._config_file)
- if config_path.exists():
- self._last_modified = config_path.stat().st_mtime
- except Exception:
- self._last_modified = None
-
- def _create_backup(self, config_data: Dict[str, Any]) -> None:
- """Create a backup of the current configuration."""
- backup = {
- 'timestamp': datetime.now(timezone.utc).isoformat(),
- 'config_file': self._config_file,
- 'data': deepcopy(config_data)
- }
-
- self._backup_configs.append(backup)
-
- # Limit number of backups
- if len(self._backup_configs) > self._max_backups:
- self._backup_configs.pop(0)
-
- def save_config(
- self,
- config: ConfigType = None,
- backup: bool = True,
- validate: bool = None
- ) -> None:
- """
- Save configuration to file.
-
- Args:
- config: Configuration to save (uses current if None)
- backup: Whether to create backup before saving
- validate: Whether to validate before saving
-
- Raises:
- ConfigurationSaveError: If saving fails
- ConfigurationValidationError: If validation fails
- """
- validate = validate if validate is not None else self.enable_validation
-
- with self._config_lock:
- if config is None:
- if self._config is None:
- raise ConfigurationSaveError("No configuration loaded to save")
- config = self._config
-
- try:
- # Validate if requested
- if validate:
- self._validate_config(config, self.strict_validation)
-
- # Create backup if requested
- if backup:
- self._backup_current_file()
-
- # Save to file
- self.save_config_to_file(config, self._config_file)
-
- # Update internal state
- self._config = config
- self._update_last_modified()
-
- if self.debug_mode:
- from . import pprint
- pprint(f"Saved configuration to: {self._config_file}")
-
- except Exception as e:
- if isinstance(e, ConfigurationValidationError):
- raise
- else:
- raise ConfigurationSaveError(f"Failed to save configuration: {str(e)}") from e
-
- def save_config_to_file(self, config: ConfigType, file_path: str) -> None:
- """
- Save configuration to a specific file.
-
- Args:
- config: Configuration to save
- file_path: Path where to save the configuration
-
- Raises:
- ConfigurationSaveError: If saving fails
- """
- try:
- config_path = Path(file_path)
- config_path.parent.mkdir(parents=True, exist_ok=True)
-
- # Convert to dictionary
- config_data = config.to_dict()
-
- # Save to file with pretty formatting
- with open(config_path, 'w', encoding='utf-8') as f:
- json.dump(config_data, f, indent=2, ensure_ascii=False)
-
- except Exception as e:
- raise ConfigurationSaveError(f"Failed to save configuration to {file_path}: {str(e)}") from e
-
- def _backup_current_file(self) -> None:
- """Create a backup of the current configuration file."""
- config_path = Path(self._config_file)
- if not config_path.exists():
- return
-
- timestamp = datetime.now().strftime("%Y%m%d_%H%M%S")
- backup_path = config_path.with_suffix(f".backup_{timestamp}.json")
-
- try:
- shutil.copy2(config_path, backup_path)
- if self.debug_mode:
- from . import pprint
- pprint(f"Created configuration backup: {backup_path}")
- except Exception as e:
- if self.debug_mode:
- from . import pprint
- pprint(f"Failed to create configuration backup: {str(e)}")
-
- def get_config(self) -> ConfigType:
- """
- Get current configuration.
-
- Returns:
- Current configuration object
-
- Raises:
- ConfigurationError: If no configuration is loaded
- """
- with self._config_lock:
- if self._config is None:
- raise ConfigurationError("No configuration loaded. Call load_config() first.")
- return self._config
-
- def get_default_config(self) -> ConfigType:
- """
- Get default configuration for the deployment mode.
-
- Returns:
- Default configuration object
- """
- return create_default_config_for_mode(self.mode, self.debug_mode)
-
- def set_cli_overrides(self, overrides: Dict[str, Any]) -> None:
- """
- Set command line argument overrides.
-
- Args:
- overrides: Dictionary of configuration overrides
- """
- with self._config_lock:
- self._cli_overrides = deepcopy(overrides)
-
- def override_config(self, overrides: Dict[str, Any], reload: bool = True) -> None:
- """
- Apply runtime configuration overrides.
-
- Args:
- overrides: Dictionary of configuration overrides
- reload: Whether to reload configuration after applying overrides
- """
- with self._config_lock:
- self._runtime_overrides.update(overrides)
-
- if reload and self._config is not None:
- # Reload configuration with new overrides
- self.load_config()
-
- def clear_overrides(self, reload: bool = True) -> None:
- """
- Clear all runtime overrides.
-
- Args:
- reload: Whether to reload configuration after clearing overrides
- """
- with self._config_lock:
- self._runtime_overrides.clear()
-
- if reload and self._config is not None:
- self.load_config()
-
- def validate_current_config(self, strict: bool = None) -> ValidationResult:
- """
- Validate current configuration.
-
- Args:
- strict: Whether to treat warnings as errors (uses instance default if None)
-
- Returns:
- ValidationResult with detailed feedback
-
- Raises:
- ConfigurationError: If no configuration is loaded
- """
- strict = strict if strict is not None else self.strict_validation
-
- with self._config_lock:
- if self._config is None:
- raise ConfigurationError("No configuration loaded. Call load_config() first.")
-
- return self._validator.validate_config(self._config)
-
- def enable_hot_reload(
- self,
- interval: float = 1.0,
- callback: Callable[[ConfigType, ConfigType], None] = None
- ) -> None:
- """
- Enable hot-reloading of configuration files.
-
- Args:
- interval: Check interval in seconds
- callback: Optional callback function called when config changes
- """
- with self._config_lock:
- if self._hot_reload_enabled:
- return
-
- self._hot_reload_interval = interval
- if callback:
- self._reload_callbacks.append(callback)
-
- self._hot_reload_enabled = True
- self._hot_reload_stop_event.clear()
-
- self._hot_reload_thread = threading.Thread(
- target=self._hot_reload_worker,
- name="ConfigHotReload",
- daemon=True
- )
- self._hot_reload_thread.start()
-
- if self.debug_mode:
- from . import pprint
- pprint(f"Enabled configuration hot-reloading (interval: {interval}s)")
-
- def disable_hot_reload(self) -> None:
- """Disable hot-reloading of configuration files."""
- with self._config_lock:
- if not self._hot_reload_enabled:
- return
-
- self._hot_reload_enabled = False
- self._hot_reload_stop_event.set()
-
- if self._hot_reload_thread and self._hot_reload_thread.is_alive():
- self._hot_reload_thread.join(timeout=5.0)
-
- self._hot_reload_thread = None
-
- if self.debug_mode:
- from . import pprint
- pprint("Disabled configuration hot-reloading")
-
- def add_reload_callback(self, callback: Callable[[ConfigType, ConfigType], None]) -> None:
- """
- Add a callback function to be called when configuration reloads.
-
- Args:
- callback: Function that takes (old_config, new_config) as arguments
- """
- with self._config_lock:
- self._reload_callbacks.append(callback)
-
- def remove_reload_callback(self, callback: Callable[[ConfigType, ConfigType], None]) -> None:
- """
- Remove a reload callback function.
-
- Args:
- callback: Function to remove
- """
- with self._config_lock:
- if callback in self._reload_callbacks:
- self._reload_callbacks.remove(callback)
-
- def _hot_reload_worker(self) -> None:
- """Worker thread for hot-reloading configuration files."""
- while not self._hot_reload_stop_event.wait(self._hot_reload_interval):
- try:
- self._check_and_reload()
- except Exception as e:
- if self.debug_mode:
- from . import pprint
- pprint(f"Error during hot-reload check: {str(e)}")
-
- def _check_and_reload(self) -> None:
- """Check if configuration file has changed and reload if necessary."""
- config_path = Path(self._config_file)
-
- if not config_path.exists():
- return
-
- try:
- current_mtime = config_path.stat().st_mtime
-
- # Check if file has been modified
- if self._last_modified is None or current_mtime > self._last_modified:
- with self._config_lock:
- old_config = deepcopy(self._config) if self._config else None
-
- # Reload configuration
- new_config = self.load_config()
-
- # Call callbacks
- for callback in self._reload_callbacks:
- try:
- callback(old_config, new_config)
- except Exception as e:
- if self.debug_mode:
- from . import pprint
- pprint(f"Error in reload callback: {str(e)}")
-
- if self.debug_mode:
- from . import pprint
- pprint("Configuration reloaded due to file change")
-
- except Exception as e:
- if self.debug_mode:
- from . import pprint
- pprint(f"Error checking configuration file modification: {str(e)}")
-
- def get_backup_count(self) -> int:
- """Get the number of configuration backups."""
- return len(self._backup_configs)
-
- def list_backups(self) -> List[Dict[str, Any]]:
- """
- List available configuration backups.
-
- Returns:
- List of backup metadata
- """
- return [
- {
- 'index': i,
- 'timestamp': backup['timestamp'],
- 'config_file': backup['config_file']
- }
- for i, backup in enumerate(self._backup_configs)
- ]
-
- def restore_backup(self, index: int, save_to_file: bool = True) -> ConfigType:
- """
- Restore configuration from a backup.
-
- Args:
- index: Backup index (0 = oldest, -1 = newest)
- save_to_file: Whether to save restored config to file
-
- Returns:
- Restored configuration object
-
- Raises:
- ConfigurationError: If backup index is invalid
- """
- with self._config_lock:
- if not self._backup_configs:
- raise ConfigurationError("No configuration backups available")
-
- try:
- backup = self._backup_configs[index]
- except IndexError:
- raise ConfigurationError(f"Invalid backup index: {index}")
-
- # Create configuration from backup data
- config_class = get_config_class(self.mode)
- config = config_class.from_dict(backup['data'])
-
- # Save to file if requested
- if save_to_file:
- self.save_config(config, backup=False)
-
- # Update internal state
- self._config = config
-
- if self.debug_mode:
- from . import pprint
- pprint(f"Restored configuration from backup {index} ({backup['timestamp']})")
-
- return config
-
- def clear_backups(self) -> None:
- """Clear all configuration backups."""
- with self._config_lock:
- self._backup_configs.clear()
-
- if self.debug_mode:
- from . import pprint
- pprint("Cleared all configuration backups")
-
- def get_config_info(self) -> Dict[str, Any]:
- """
- Get information about the current configuration.
-
- Returns:
- Dictionary with configuration metadata
- """
- with self._config_lock:
- config_path = Path(self._config_file)
- info = {
- 'mode': self.mode.value,
- 'config_file': str(config_path.absolute()),
- 'config_exists': config_path.exists(),
- 'debug_mode': self.debug_mode,
- 'hot_reload_enabled': self._hot_reload_enabled,
- 'validation_enabled': self.enable_validation,
- 'strict_validation': self.strict_validation,
- 'backup_count': len(self._backup_configs),
- 'cli_overrides_count': len(self._cli_overrides),
- 'runtime_overrides_count': len(self._runtime_overrides),
- 'config_loaded': self._config is not None,
- }
-
- if config_path.exists():
- stat = config_path.stat()
- info.update({
- 'file_size': stat.st_size,
- 'last_modified': datetime.fromtimestamp(stat.st_mtime).isoformat(),
- })
-
- env_overrides = get_environment_overrides()
- info['env_overrides_count'] = len(env_overrides)
-
- return info
-
- def __enter__(self):
- """Context manager entry."""
- return self
-
- def __exit__(self, exc_type, exc_val, exc_tb):
- """Context manager exit."""
- self.disable_hot_reload()
- return False
-
- def __repr__(self) -> str:
- """String representation."""
- return (
- f"ConfigManager(mode={self.mode.value}, "
- f"config_file={self._config_file}, "
- f"debug_mode={self.debug_mode})"
- )
|