""" Configuration loader for Frank Loads and validates configuration from JSON file """ import json from pathlib import Path from typing import Dict, Any from loguru import logger # Frank root dir (two levels up from this file: core/ -> Frank/) _FRANK_ROOT = Path(__file__).parent.parent class SchedulerConfig: """Configuration manager for Frank""" _instance = None _config = None def __new__(cls, config_path: str = None): if cls._instance is None: cls._instance = super().__new__(cls) return cls._instance def __init__(self, config_path: str = None): if self._config is not None: return if config_path is None: config_path = str(_FRANK_ROOT / 'config' / 'frank_config.json') self.config_path = config_path self._load_config() def _load_config(self): """Load configuration from JSON file""" config_file = Path(self.config_path) if not config_file.exists(): logger.warning(f"Config file not found at {self.config_path}, using defaults") self._config = self._get_default_config() self._save_config() else: with open(config_file, 'r', encoding='utf-8') as f: self._config = json.load(f) logger.info(f"Configuration loaded from {self.config_path}") def _save_config(self): """Save configuration to JSON file""" config_file = Path(self.config_path) config_file.parent.mkdir(parents=True, exist_ok=True) with open(config_file, 'w', encoding='utf-8') as f: json.dump(self._config, f, indent=2) logger.info(f"Configuration saved to {self.config_path}") def _get_default_config(self) -> Dict[str, Any]: """Get default configuration""" return { "version": "1.3.0", "environment": "production", "database": { "path": r"C:\PYTHON\Frank\data\frank.db", "wal_mode": True }, "daemon": { "check_interval_seconds": 30, "max_concurrent_jobs": 5, "job_timeout_default": 3600, "enable_recovery": True, "recovery_delay_seconds": 60 }, "logging": { "level": "INFO", "daemon_log_dir": str(_FRANK_ROOT / 'logs' / 'daemon'), "execution_log_dir": str(_FRANK_ROOT / 'logs' / 'executions'), "retention_days": 30, "max_file_size_mb": 100 }, "notifications": { "enable_popup": True, "enable_email": False, "popup_duration_seconds": 10, "notify_on_completion": False, "notify_on_failure": True, "notify_on_timeout": True }, "gui": { "theme": "light", "refresh_interval_ms": 2000, "dashboard_recent_jobs": 10, "history_page_size": 50 }, "projects": { "datahub": { "root": "C:/PYTHON/DataHub", "venv": "C:/PYTHON/DataHub/venv", "logs": "C:/PYTHON/DataHub/logs" } } } def get(self, key: str, default: Any = None) -> Any: """Get configuration value by dot-notation key""" keys = key.split('.') value = self._config for k in keys: if isinstance(value, dict) and k in value: value = value[k] else: return default return value def set(self, key: str, value: Any): """Set configuration value by dot-notation key""" keys = key.split('.') config = self._config for k in keys[:-1]: if k not in config: config[k] = {} config = config[k] config[keys[-1]] = value self._save_config() @property def database_path(self) -> str: return self.get('database.path') @property def local_database_path(self) -> str: val = self.get('database.local_path') if val: return val return str(_FRANK_ROOT / 'data' / 'frank_local.db') @property def log_level(self) -> str: return self.get('logging.level', 'INFO') @property def daemon_log_dir(self) -> str: val = self.get('logging.daemon_log_dir') if val: return val return str(_FRANK_ROOT / 'logs' / 'daemon') @property def execution_log_dir(self) -> str: val = self.get('logging.execution_log_dir') if val: return val return str(_FRANK_ROOT / 'logs' / 'executions') @property def max_concurrent_jobs(self) -> int: return self.get('daemon.max_concurrent_jobs', 5) @property def job_timeout_default(self) -> int: return self.get('daemon.job_timeout_default', 3600) @property def enable_popup_notifications(self) -> bool: return self.get('notifications.enable_popup', True) @property def notify_on_failure(self) -> bool: return self.get('notifications.notify_on_failure', True) def reload(self): """Reload configuration from file""" self._load_config()