config_manager.py 30 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653654655656657658659660661662663664665666667668669670671672673674675676677678679680681682683684685686687688689690691692693694695696697698699700701702703704705706707708709710711712713714715716717718719720721722723724725726727728729730731732733734735736737738739740741742743744745746747748749750751752753754755756757758759760761762763764765766767768769770771772773774775776777778779780781782783784785786787788789790791792793794795796797798799800801802803804805806807808809810811812813814815816817818819820821822823824825826827828829830831832833834835836837
  1. """
  2. Configuration Manager for Trixy Application
  3. This module implements the main ConfigManager class that provides:
  4. - Configuration loading and saving with validation
  5. - Hot-reloading of configuration files during runtime
  6. - Thread-safe configuration access for multi-satellite support
  7. - Command line argument override support (higher priority than config files)
  8. - Environment variable integration
  9. - Configuration backup and restore functionality
  10. - Comprehensive error handling with detailed error messages
  11. The ConfigManager is the central point for all configuration operations in the
  12. Trixy application and ensures consistent, safe access to configuration data.
  13. """
  14. import json
  15. import os
  16. import threading
  17. import time
  18. import shutil
  19. from pathlib import Path
  20. from typing import Dict, Any, Optional, Union, List, Callable
  21. from datetime import datetime, timezone
  22. from copy import deepcopy
  23. from dataclasses import asdict
  24. import argparse
  25. from .config_models import (
  26. ServerConfig, ClientConfig, StandaloneConfig, ConfigType,
  27. DeploymentMode, get_config_class, create_default_config, merge_configs
  28. )
  29. from .config_defaults import (
  30. create_default_config_for_mode, apply_environment_overrides,
  31. get_environment_overrides
  32. )
  33. from .config_validator import ConfigValidator, ValidationResult
  34. class ConfigurationError(Exception):
  35. """Base exception for configuration-related errors."""
  36. pass
  37. class ConfigurationValidationError(ConfigurationError):
  38. """Exception raised when configuration validation fails."""
  39. def __init__(self, message: str, validation_result: ValidationResult = None):
  40. super().__init__(message)
  41. self.validation_result = validation_result
  42. class ConfigurationLoadError(ConfigurationError):
  43. """Exception raised when configuration loading fails."""
  44. pass
  45. class ConfigurationSaveError(ConfigurationError):
  46. """Exception raised when configuration saving fails."""
  47. pass
  48. class ConfigManager:
  49. """
  50. Main configuration manager for the Trixy application.
  51. Provides thread-safe configuration management with hot-reloading,
  52. validation, backup/restore, and override support.
  53. """
  54. def __init__(
  55. self,
  56. mode: DeploymentMode,
  57. config_file: Optional[str] = None,
  58. debug_mode: bool = False,
  59. auto_create_missing: bool = True,
  60. enable_validation: bool = True,
  61. strict_validation: bool = False
  62. ):
  63. """
  64. Initialize the configuration manager.
  65. Args:
  66. mode: Deployment mode (server, client, standalone)
  67. config_file: Path to configuration file (optional)
  68. debug_mode: Enable debug mode
  69. auto_create_missing: Create default config if file doesn't exist
  70. enable_validation: Enable configuration validation
  71. strict_validation: Treat warnings as errors
  72. """
  73. self.mode = mode
  74. self.debug_mode = debug_mode
  75. self.auto_create_missing = auto_create_missing
  76. self.enable_validation = enable_validation
  77. self.strict_validation = strict_validation
  78. # Configuration data and state
  79. self._config: Optional[ConfigType] = None
  80. self._config_file = config_file
  81. self._config_lock = threading.RLock()
  82. self._last_modified: Optional[float] = None
  83. self._backup_configs: List[Dict[str, Any]] = []
  84. self._max_backups = 10
  85. # Hot-reloading support
  86. self._hot_reload_enabled = False
  87. self._hot_reload_thread: Optional[threading.Thread] = None
  88. self._hot_reload_stop_event = threading.Event()
  89. self._hot_reload_interval = 1.0 # seconds
  90. self._reload_callbacks: List[Callable[[ConfigType, ConfigType], None]] = []
  91. # Override support
  92. self._cli_overrides: Dict[str, Any] = {}
  93. self._runtime_overrides: Dict[str, Any] = {}
  94. # Validation
  95. self._validator = ConfigValidator(strict_mode=strict_validation)
  96. # Determine default config file if not provided
  97. if not self._config_file:
  98. self._config_file = self._get_default_config_file()
  99. self._config_file = str(Path(self._config_file).resolve())
  100. def _get_default_config_file(self) -> str:
  101. """Get default configuration file path for the deployment mode."""
  102. config_files = {
  103. DeploymentMode.SERVER: "config/server_config.json",
  104. DeploymentMode.CLIENT: "config/client_config.json",
  105. DeploymentMode.STANDALONE: "config/standalone_config.json",
  106. }
  107. return config_files[self.mode]
  108. @classmethod
  109. def from_file(
  110. cls,
  111. config_file: str,
  112. debug_mode: bool = False,
  113. **kwargs
  114. ) -> 'ConfigManager':
  115. """
  116. Create ConfigManager from a configuration file.
  117. Automatically detects deployment mode from filename or file contents.
  118. Args:
  119. config_file: Path to configuration file
  120. debug_mode: Enable debug mode
  121. **kwargs: Additional arguments passed to ConfigManager
  122. Returns:
  123. ConfigManager instance
  124. Raises:
  125. ConfigurationError: If mode cannot be determined
  126. """
  127. # Try to determine mode from filename
  128. filename = Path(config_file).name.lower()
  129. mode = None
  130. if 'server' in filename:
  131. mode = DeploymentMode.SERVER
  132. elif 'client' in filename:
  133. mode = DeploymentMode.CLIENT
  134. elif 'standalone' in filename:
  135. mode = DeploymentMode.STANDALONE
  136. # If mode not determined from filename, try to read it from file
  137. if mode is None and Path(config_file).exists():
  138. try:
  139. with open(config_file, 'r', encoding='utf-8') as f:
  140. config_data = json.load(f)
  141. if 'mode' in config_data:
  142. mode = DeploymentMode(config_data['mode'])
  143. except (json.JSONDecodeError, ValueError, KeyError):
  144. pass
  145. if mode is None:
  146. raise ConfigurationError(
  147. f"Cannot determine deployment mode from config file: {config_file}. "
  148. "Ensure filename contains 'server', 'client', or 'standalone', "
  149. "or that the config file contains a 'mode' field."
  150. )
  151. return cls(
  152. mode=mode,
  153. config_file=config_file,
  154. debug_mode=debug_mode,
  155. **kwargs
  156. )
  157. @classmethod
  158. def from_args(
  159. cls,
  160. args: argparse.Namespace,
  161. **kwargs
  162. ) -> 'ConfigManager':
  163. """
  164. Create ConfigManager from command line arguments.
  165. Expected arguments:
  166. - mode: Deployment mode (server/client/standalone)
  167. - config: Configuration file path (optional)
  168. - debug: Debug mode flag (optional)
  169. Args:
  170. args: Parsed command line arguments
  171. **kwargs: Additional arguments passed to ConfigManager
  172. Returns:
  173. ConfigManager instance
  174. """
  175. mode = DeploymentMode(args.mode.lower())
  176. config_file = getattr(args, 'config', None)
  177. debug_mode = getattr(args, 'debug', False)
  178. # Create manager
  179. manager = cls(
  180. mode=mode,
  181. config_file=config_file,
  182. debug_mode=debug_mode,
  183. **kwargs
  184. )
  185. # Extract configuration overrides from args
  186. cli_overrides = {}
  187. # Common override patterns
  188. if hasattr(args, 'port') and args.port:
  189. cli_overrides.setdefault('network', {})['command_port'] = args.port
  190. if hasattr(args, 'host') and args.host:
  191. if mode == DeploymentMode.CLIENT:
  192. cli_overrides.setdefault('satellite', {})['server_host'] = args.host
  193. else:
  194. cli_overrides.setdefault('network', {})['command_bind_address'] = args.host
  195. if hasattr(args, 'log_level') and args.log_level:
  196. cli_overrides.setdefault('logging', {})['log_level'] = args.log_level
  197. if hasattr(args, 'data_dir') and args.data_dir:
  198. cli_overrides['data_directory'] = args.data_dir
  199. # Set CLI overrides
  200. manager.set_cli_overrides(cli_overrides)
  201. return manager
  202. def load_config(
  203. self,
  204. validate: bool = None,
  205. strict_validation: bool = None
  206. ) -> ConfigType:
  207. """
  208. Load configuration from file with validation.
  209. Args:
  210. validate: Whether to validate configuration (uses instance default if None)
  211. strict_validation: Whether to treat warnings as errors (uses instance default if None)
  212. Returns:
  213. Loaded and validated configuration object
  214. Raises:
  215. ConfigurationLoadError: If loading fails
  216. ConfigurationValidationError: If validation fails
  217. """
  218. validate = validate if validate is not None else self.enable_validation
  219. strict_validation = strict_validation if strict_validation is not None else self.strict_validation
  220. with self._config_lock:
  221. try:
  222. # Load configuration data
  223. config_data = self._load_config_data()
  224. # Apply overrides
  225. config_data = self._apply_overrides(config_data)
  226. # Create configuration object
  227. config_class = get_config_class(self.mode)
  228. config = config_class.from_dict(config_data)
  229. # Validate if requested
  230. if validate:
  231. self._validate_config(config, strict_validation)
  232. # Update internal state
  233. self._config = config
  234. self._update_last_modified()
  235. # Create backup
  236. self._create_backup(config_data)
  237. return config
  238. except Exception as e:
  239. if isinstance(e, (ConfigurationValidationError, ConfigurationLoadError)):
  240. raise
  241. else:
  242. raise ConfigurationLoadError(f"Failed to load configuration: {str(e)}") from e
  243. def _load_config_data(self) -> Dict[str, Any]:
  244. """Load raw configuration data from file."""
  245. config_path = Path(self._config_file)
  246. # Create default config if file doesn't exist and auto-create is enabled
  247. if not config_path.exists():
  248. if self.auto_create_missing:
  249. self._create_default_config_file()
  250. else:
  251. raise ConfigurationLoadError(f"Configuration file not found: {self._config_file}")
  252. # Load configuration from file
  253. try:
  254. with open(config_path, 'r', encoding='utf-8') as f:
  255. config_data = json.load(f)
  256. except json.JSONDecodeError as e:
  257. raise ConfigurationLoadError(f"Invalid JSON in configuration file: {str(e)}") from e
  258. except Exception as e:
  259. raise ConfigurationLoadError(f"Error reading configuration file: {str(e)}") from e
  260. return config_data
  261. def _create_default_config_file(self) -> None:
  262. """Create default configuration file."""
  263. config_path = Path(self._config_file)
  264. config_path.parent.mkdir(parents=True, exist_ok=True)
  265. # Create default configuration
  266. default_config = create_default_config_for_mode(self.mode, self.debug_mode)
  267. # Save to file
  268. try:
  269. with open(config_path, 'w', encoding='utf-8') as f:
  270. json.dump(default_config.to_dict(), f, indent=2, ensure_ascii=False)
  271. if self.debug_mode:
  272. from . import pprint
  273. pprint(f"Created default configuration file: {self._config_file}")
  274. except Exception as e:
  275. raise ConfigurationLoadError(f"Failed to create default configuration file: {str(e)}") from e
  276. def _apply_overrides(self, config_data: Dict[str, Any]) -> Dict[str, Any]:
  277. """Apply environment variables and runtime overrides to configuration data."""
  278. # Apply environment variables first
  279. config_data = apply_environment_overrides(config_data.copy())
  280. # Apply CLI overrides
  281. if self._cli_overrides:
  282. config_data = self._deep_merge_dict(config_data, self._cli_overrides)
  283. # Apply runtime overrides
  284. if self._runtime_overrides:
  285. config_data = self._deep_merge_dict(config_data, self._runtime_overrides)
  286. return config_data
  287. def _deep_merge_dict(self, base: Dict[str, Any], override: Dict[str, Any]) -> Dict[str, Any]:
  288. """Deep merge two dictionaries."""
  289. result = base.copy()
  290. for key, value in override.items():
  291. if key in result and isinstance(result[key], dict) and isinstance(value, dict):
  292. result[key] = self._deep_merge_dict(result[key], value)
  293. else:
  294. result[key] = value
  295. return result
  296. def _validate_config(self, config: ConfigType, strict: bool) -> None:
  297. """Validate configuration and raise exception if invalid."""
  298. result = self._validator.validate_config(config)
  299. if not result.is_valid or (strict and result.warnings):
  300. error_msg = f"Configuration validation failed:\n"
  301. if result.errors:
  302. error_msg += "Errors:\n"
  303. for error in result.errors:
  304. error_msg += f" - {error}\n"
  305. if strict and result.warnings:
  306. error_msg += "Warnings (treated as errors):\n"
  307. for warning in result.warnings:
  308. error_msg += f" - {warning}\n"
  309. raise ConfigurationValidationError(error_msg, result)
  310. # Log warnings in debug mode
  311. if self.debug_mode and result.warnings:
  312. from . import pprint
  313. pprint("Configuration warnings:")
  314. for warning in result.warnings:
  315. pprint(f" - {warning}")
  316. def _update_last_modified(self) -> None:
  317. """Update last modified timestamp."""
  318. try:
  319. config_path = Path(self._config_file)
  320. if config_path.exists():
  321. self._last_modified = config_path.stat().st_mtime
  322. except Exception:
  323. self._last_modified = None
  324. def _create_backup(self, config_data: Dict[str, Any]) -> None:
  325. """Create a backup of the current configuration."""
  326. backup = {
  327. 'timestamp': datetime.now(timezone.utc).isoformat(),
  328. 'config_file': self._config_file,
  329. 'data': deepcopy(config_data)
  330. }
  331. self._backup_configs.append(backup)
  332. # Limit number of backups
  333. if len(self._backup_configs) > self._max_backups:
  334. self._backup_configs.pop(0)
  335. def save_config(
  336. self,
  337. config: ConfigType = None,
  338. backup: bool = True,
  339. validate: bool = None
  340. ) -> None:
  341. """
  342. Save configuration to file.
  343. Args:
  344. config: Configuration to save (uses current if None)
  345. backup: Whether to create backup before saving
  346. validate: Whether to validate before saving
  347. Raises:
  348. ConfigurationSaveError: If saving fails
  349. ConfigurationValidationError: If validation fails
  350. """
  351. validate = validate if validate is not None else self.enable_validation
  352. with self._config_lock:
  353. if config is None:
  354. if self._config is None:
  355. raise ConfigurationSaveError("No configuration loaded to save")
  356. config = self._config
  357. try:
  358. # Validate if requested
  359. if validate:
  360. self._validate_config(config, self.strict_validation)
  361. # Create backup if requested
  362. if backup:
  363. self._backup_current_file()
  364. # Save to file
  365. self.save_config_to_file(config, self._config_file)
  366. # Update internal state
  367. self._config = config
  368. self._update_last_modified()
  369. if self.debug_mode:
  370. from . import pprint
  371. pprint(f"Saved configuration to: {self._config_file}")
  372. except Exception as e:
  373. if isinstance(e, ConfigurationValidationError):
  374. raise
  375. else:
  376. raise ConfigurationSaveError(f"Failed to save configuration: {str(e)}") from e
  377. def save_config_to_file(self, config: ConfigType, file_path: str) -> None:
  378. """
  379. Save configuration to a specific file.
  380. Args:
  381. config: Configuration to save
  382. file_path: Path where to save the configuration
  383. Raises:
  384. ConfigurationSaveError: If saving fails
  385. """
  386. try:
  387. config_path = Path(file_path)
  388. config_path.parent.mkdir(parents=True, exist_ok=True)
  389. # Convert to dictionary
  390. config_data = config.to_dict()
  391. # Save to file with pretty formatting
  392. with open(config_path, 'w', encoding='utf-8') as f:
  393. json.dump(config_data, f, indent=2, ensure_ascii=False)
  394. except Exception as e:
  395. raise ConfigurationSaveError(f"Failed to save configuration to {file_path}: {str(e)}") from e
  396. def _backup_current_file(self) -> None:
  397. """Create a backup of the current configuration file."""
  398. config_path = Path(self._config_file)
  399. if not config_path.exists():
  400. return
  401. timestamp = datetime.now().strftime("%Y%m%d_%H%M%S")
  402. backup_path = config_path.with_suffix(f".backup_{timestamp}.json")
  403. try:
  404. shutil.copy2(config_path, backup_path)
  405. if self.debug_mode:
  406. from . import pprint
  407. pprint(f"Created configuration backup: {backup_path}")
  408. except Exception as e:
  409. if self.debug_mode:
  410. from . import pprint
  411. pprint(f"Failed to create configuration backup: {str(e)}")
  412. def get_config(self) -> ConfigType:
  413. """
  414. Get current configuration.
  415. Returns:
  416. Current configuration object
  417. Raises:
  418. ConfigurationError: If no configuration is loaded
  419. """
  420. with self._config_lock:
  421. if self._config is None:
  422. raise ConfigurationError("No configuration loaded. Call load_config() first.")
  423. return self._config
  424. def get_default_config(self) -> ConfigType:
  425. """
  426. Get default configuration for the deployment mode.
  427. Returns:
  428. Default configuration object
  429. """
  430. return create_default_config_for_mode(self.mode, self.debug_mode)
  431. def set_cli_overrides(self, overrides: Dict[str, Any]) -> None:
  432. """
  433. Set command line argument overrides.
  434. Args:
  435. overrides: Dictionary of configuration overrides
  436. """
  437. with self._config_lock:
  438. self._cli_overrides = deepcopy(overrides)
  439. def override_config(self, overrides: Dict[str, Any], reload: bool = True) -> None:
  440. """
  441. Apply runtime configuration overrides.
  442. Args:
  443. overrides: Dictionary of configuration overrides
  444. reload: Whether to reload configuration after applying overrides
  445. """
  446. with self._config_lock:
  447. self._runtime_overrides.update(overrides)
  448. if reload and self._config is not None:
  449. # Reload configuration with new overrides
  450. self.load_config()
  451. def clear_overrides(self, reload: bool = True) -> None:
  452. """
  453. Clear all runtime overrides.
  454. Args:
  455. reload: Whether to reload configuration after clearing overrides
  456. """
  457. with self._config_lock:
  458. self._runtime_overrides.clear()
  459. if reload and self._config is not None:
  460. self.load_config()
  461. def validate_current_config(self, strict: bool = None) -> ValidationResult:
  462. """
  463. Validate current configuration.
  464. Args:
  465. strict: Whether to treat warnings as errors (uses instance default if None)
  466. Returns:
  467. ValidationResult with detailed feedback
  468. Raises:
  469. ConfigurationError: If no configuration is loaded
  470. """
  471. strict = strict if strict is not None else self.strict_validation
  472. with self._config_lock:
  473. if self._config is None:
  474. raise ConfigurationError("No configuration loaded. Call load_config() first.")
  475. return self._validator.validate_config(self._config)
  476. def enable_hot_reload(
  477. self,
  478. interval: float = 1.0,
  479. callback: Callable[[ConfigType, ConfigType], None] = None
  480. ) -> None:
  481. """
  482. Enable hot-reloading of configuration files.
  483. Args:
  484. interval: Check interval in seconds
  485. callback: Optional callback function called when config changes
  486. """
  487. with self._config_lock:
  488. if self._hot_reload_enabled:
  489. return
  490. self._hot_reload_interval = interval
  491. if callback:
  492. self._reload_callbacks.append(callback)
  493. self._hot_reload_enabled = True
  494. self._hot_reload_stop_event.clear()
  495. self._hot_reload_thread = threading.Thread(
  496. target=self._hot_reload_worker,
  497. name="ConfigHotReload",
  498. daemon=True
  499. )
  500. self._hot_reload_thread.start()
  501. if self.debug_mode:
  502. from . import pprint
  503. pprint(f"Enabled configuration hot-reloading (interval: {interval}s)")
  504. def disable_hot_reload(self) -> None:
  505. """Disable hot-reloading of configuration files."""
  506. with self._config_lock:
  507. if not self._hot_reload_enabled:
  508. return
  509. self._hot_reload_enabled = False
  510. self._hot_reload_stop_event.set()
  511. if self._hot_reload_thread and self._hot_reload_thread.is_alive():
  512. self._hot_reload_thread.join(timeout=5.0)
  513. self._hot_reload_thread = None
  514. if self.debug_mode:
  515. from . import pprint
  516. pprint("Disabled configuration hot-reloading")
  517. def add_reload_callback(self, callback: Callable[[ConfigType, ConfigType], None]) -> None:
  518. """
  519. Add a callback function to be called when configuration reloads.
  520. Args:
  521. callback: Function that takes (old_config, new_config) as arguments
  522. """
  523. with self._config_lock:
  524. self._reload_callbacks.append(callback)
  525. def remove_reload_callback(self, callback: Callable[[ConfigType, ConfigType], None]) -> None:
  526. """
  527. Remove a reload callback function.
  528. Args:
  529. callback: Function to remove
  530. """
  531. with self._config_lock:
  532. if callback in self._reload_callbacks:
  533. self._reload_callbacks.remove(callback)
  534. def _hot_reload_worker(self) -> None:
  535. """Worker thread for hot-reloading configuration files."""
  536. while not self._hot_reload_stop_event.wait(self._hot_reload_interval):
  537. try:
  538. self._check_and_reload()
  539. except Exception as e:
  540. if self.debug_mode:
  541. from . import pprint
  542. pprint(f"Error during hot-reload check: {str(e)}")
  543. def _check_and_reload(self) -> None:
  544. """Check if configuration file has changed and reload if necessary."""
  545. config_path = Path(self._config_file)
  546. if not config_path.exists():
  547. return
  548. try:
  549. current_mtime = config_path.stat().st_mtime
  550. # Check if file has been modified
  551. if self._last_modified is None or current_mtime > self._last_modified:
  552. with self._config_lock:
  553. old_config = deepcopy(self._config) if self._config else None
  554. # Reload configuration
  555. new_config = self.load_config()
  556. # Call callbacks
  557. for callback in self._reload_callbacks:
  558. try:
  559. callback(old_config, new_config)
  560. except Exception as e:
  561. if self.debug_mode:
  562. from . import pprint
  563. pprint(f"Error in reload callback: {str(e)}")
  564. if self.debug_mode:
  565. from . import pprint
  566. pprint("Configuration reloaded due to file change")
  567. except Exception as e:
  568. if self.debug_mode:
  569. from . import pprint
  570. pprint(f"Error checking configuration file modification: {str(e)}")
  571. def get_backup_count(self) -> int:
  572. """Get the number of configuration backups."""
  573. return len(self._backup_configs)
  574. def list_backups(self) -> List[Dict[str, Any]]:
  575. """
  576. List available configuration backups.
  577. Returns:
  578. List of backup metadata
  579. """
  580. return [
  581. {
  582. 'index': i,
  583. 'timestamp': backup['timestamp'],
  584. 'config_file': backup['config_file']
  585. }
  586. for i, backup in enumerate(self._backup_configs)
  587. ]
  588. def restore_backup(self, index: int, save_to_file: bool = True) -> ConfigType:
  589. """
  590. Restore configuration from a backup.
  591. Args:
  592. index: Backup index (0 = oldest, -1 = newest)
  593. save_to_file: Whether to save restored config to file
  594. Returns:
  595. Restored configuration object
  596. Raises:
  597. ConfigurationError: If backup index is invalid
  598. """
  599. with self._config_lock:
  600. if not self._backup_configs:
  601. raise ConfigurationError("No configuration backups available")
  602. try:
  603. backup = self._backup_configs[index]
  604. except IndexError:
  605. raise ConfigurationError(f"Invalid backup index: {index}")
  606. # Create configuration from backup data
  607. config_class = get_config_class(self.mode)
  608. config = config_class.from_dict(backup['data'])
  609. # Save to file if requested
  610. if save_to_file:
  611. self.save_config(config, backup=False)
  612. # Update internal state
  613. self._config = config
  614. if self.debug_mode:
  615. from . import pprint
  616. pprint(f"Restored configuration from backup {index} ({backup['timestamp']})")
  617. return config
  618. def clear_backups(self) -> None:
  619. """Clear all configuration backups."""
  620. with self._config_lock:
  621. self._backup_configs.clear()
  622. if self.debug_mode:
  623. from . import pprint
  624. pprint("Cleared all configuration backups")
  625. def get_config_info(self) -> Dict[str, Any]:
  626. """
  627. Get information about the current configuration.
  628. Returns:
  629. Dictionary with configuration metadata
  630. """
  631. with self._config_lock:
  632. config_path = Path(self._config_file)
  633. info = {
  634. 'mode': self.mode.value,
  635. 'config_file': str(config_path.absolute()),
  636. 'config_exists': config_path.exists(),
  637. 'debug_mode': self.debug_mode,
  638. 'hot_reload_enabled': self._hot_reload_enabled,
  639. 'validation_enabled': self.enable_validation,
  640. 'strict_validation': self.strict_validation,
  641. 'backup_count': len(self._backup_configs),
  642. 'cli_overrides_count': len(self._cli_overrides),
  643. 'runtime_overrides_count': len(self._runtime_overrides),
  644. 'config_loaded': self._config is not None,
  645. }
  646. if config_path.exists():
  647. stat = config_path.stat()
  648. info.update({
  649. 'file_size': stat.st_size,
  650. 'last_modified': datetime.fromtimestamp(stat.st_mtime).isoformat(),
  651. })
  652. env_overrides = get_environment_overrides()
  653. info['env_overrides_count'] = len(env_overrides)
  654. return info
  655. def __enter__(self):
  656. """Context manager entry."""
  657. return self
  658. def __exit__(self, exc_type, exc_val, exc_tb):
  659. """Context manager exit."""
  660. self.disable_hot_reload()
  661. return False
  662. def __repr__(self) -> str:
  663. """String representation."""
  664. return (
  665. f"ConfigManager(mode={self.mode.value}, "
  666. f"config_file={self._config_file}, "
  667. f"debug_mode={self.debug_mode})"
  668. )