""" Utility module for configuration management. This module provides a global APP_CONFIG object for accessing configuration from both config.ini files and environment variables stored in .env files, using a flat structure. """ import os import logging from typing import Any, Dict, Optional from pathlib import Path # Set up logging logger = logging.getLogger(__name__) class Configuration: """ Configuration class with attribute-style access to flattened configuration. """ def __init__(self): """Initialize the configuration object""" self._data = {} self._config_file_path = None self._env_file_path = None self._config_mtime = 0 self._env_mtime = 0 self.refresh() def refresh(self): """Reload configuration from files""" self._load_config() self._load_env() logger.info("Configuration refreshed") def _load_config(self): """Load configuration from config.ini file in flattened format""" # Find config.ini file (look in current directory and parent directory) config_path = Path('config.ini') if not config_path.exists(): # Try in parent directory config_path = Path('../config.ini') if not config_path.exists(): logger.warning(f"Configuration file not found at {config_path.absolute()}") return self._config_file_path = config_path current_mtime = os.path.getmtime(config_path) # Skip if file hasn't changed if current_mtime <= self._config_mtime: return self._config_mtime = current_mtime try: with open(config_path, 'r') as f: for line in f: line = line.strip() # Skip empty lines and comments if not line or line.startswith('#'): continue # Parse key-value pairs if '=' in line: key, value = line.split('=', 1) key = key.strip() value = value.strip() # Add directly to data dictionary self._data[key] = value except Exception as e: logger.error(f"Error loading configuration: {e}") def _load_env(self): """Load environment variables from .env file""" # Find .env file (look in current directory and parent directory) env_path = Path('.env') if not env_path.exists(): # Try in parent directory env_path = Path('../.env') if not env_path.exists(): logger.warning(f"Environment file not found at {env_path.absolute()}") return self._env_file_path = env_path current_mtime = os.path.getmtime(env_path) # Skip if file hasn't changed if current_mtime <= self._env_mtime: return self._env_mtime = current_mtime try: with open(env_path, 'r') as f: for line in f: line = line.strip() # Skip empty lines and comments if not line or line.startswith('#'): continue # Parse key-value pairs if '=' in line: key, value = line.split('=', 1) key = key.strip() value = value.strip() # Add directly to data dictionary self._data[key] = value logger.info(f"Loaded environment variables from {env_path.absolute()}") # Also load system environment variables (don't override existing) for key, value in os.environ.items(): if key not in self._data: self._data[key] = value except Exception as e: logger.error(f"Error loading environment variables: {e}") def check_for_updates(self): """Check if configuration files have changed and reload if necessary""" if self._config_file_path and os.path.exists(self._config_file_path): current_mtime = os.path.getmtime(self._config_file_path) if current_mtime > self._config_mtime: logger.info("Config file has changed, reloading...") self._load_config() if self._env_file_path and os.path.exists(self._env_file_path): current_mtime = os.path.getmtime(self._env_file_path) if current_mtime > self._env_mtime: logger.info("Environment file has changed, reloading...") self._load_env() def get(self, key: str, default: Any = None) -> Any: """Get configuration value with optional default""" self.check_for_updates() # Check for file changes if key in self._data: value = self._data[key] # Handle secrets (keys ending with _SECRET) if key.endswith("_SECRET"): return handle_secret(value) return value return default def __getattr__(self, name: str) -> Any: """Enable attribute-style access to configuration""" self.check_for_updates() # Check for file changes value = self.get(name) if value is None: raise AttributeError(f"Configuration key '{name}' not found") return value def __dir__(self) -> list: """Support auto-completion of attributes""" self.check_for_updates() # Check for file changes return list(self._data.keys()) + super().__dir__() def set(self, key: str, value: Any) -> None: """Set a configuration value (for testing/overrides)""" self._data[key] = value def handle_secret(value: str) -> str: """ Handle secret values. Currently just returns the plain text value, but can be enhanced to provide actual decryption in the future. Args: value: The secret value to handle Returns: str: Processed secret value """ # For now, just return the value as-is # In the future, this could be enhanced to decrypt values return value # Create the global APP_CONFIG instance APP_CONFIG = Configuration()