""" Configuration loading utilities. Provides unified utilities for loading configuration from files, environment variables, and other sources. """ import os import json from pathlib import Path from typing import Dict, Any, Optional, Union from .exceptions import ConfigFileError, EnvironmentVariableError def load_env_file(file_path: Union[str, Path], required: bool = False) -> Dict[str, str]: """Load environment variables from a file. Consolidates the duplicate load_dotenv_file() functions found in tddai/config.py and gitea/config.py. Args: file_path: Path to the environment file required: Whether the file must exist Returns: Dictionary of environment variables loaded from file Raises: ConfigFileError: If required file is missing or cannot be parsed """ file_path = Path(file_path) if not file_path.exists(): if required: raise ConfigFileError( f"Required configuration file not found: {file_path}", suggestion=f"Create {file_path} or use environment variables" ) return {} try: env_vars = {} with file_path.open('r') as f: for line_num, line in enumerate(f, 1): line = line.strip() # Skip empty lines and comments if not line or line.startswith('#'): continue # Parse KEY=VALUE format if '=' not in line: continue key, value = line.split('=', 1) key = key.strip() value = value.strip() # Remove quotes if present if value.startswith('"') and value.endswith('"'): value = value[1:-1] elif value.startswith("'") and value.endswith("'"): value = value[1:-1] env_vars[key] = value return env_vars except (OSError, UnicodeDecodeError) as e: raise ConfigFileError( f"Failed to read configuration file: {file_path}", context={'error': str(e), 'line': line_num if 'line_num' in locals() else None} ) from e def resolve_path(path: Union[str, Path], base_dir: Optional[Union[str, Path]] = None, create_parents: bool = False) -> Path: """Resolve and normalize a file path. Handles tilde expansion, relative paths, and optionally creates parent directories. Args: path: Path to resolve base_dir: Base directory for relative paths (defaults to current working directory) create_parents: Whether to create parent directories if they don't exist Returns: Resolved absolute Path object Raises: ConfigFileError: If path resolution fails or parent creation fails """ try: # Convert to Path object path = Path(path) # Expand user home directory (~) if str(path).startswith('~'): path = path.expanduser() # Handle relative paths if not path.is_absolute(): if base_dir: base_dir = Path(base_dir).expanduser().resolve() path = base_dir / path else: path = Path.cwd() / path # Resolve to absolute path path = path.resolve() # Create parent directories if requested if create_parents and not path.parent.exists(): path.parent.mkdir(parents=True, exist_ok=True) return path except (OSError, RuntimeError) as e: raise ConfigFileError( f"Failed to resolve path: {path}", context={'error': str(e), 'base_dir': base_dir} ) from e def get_env_var(key: str, default: Optional[str] = None, required: bool = False, var_type: type = str) -> Any: """Get environment variable with type conversion and validation. Args: key: Environment variable name default: Default value if not found required: Whether the variable is required var_type: Type to convert the value to (str, int, bool, Path) Returns: Environment variable value converted to specified type Raises: EnvironmentVariableError: If required variable is missing or conversion fails """ value = os.getenv(key) if value is None: if required: raise EnvironmentVariableError( f"Required environment variable not set: {key}", field=key, suggestion=f"Set {key} environment variable or provide default in config file" ) return default # Type conversion try: if var_type == bool: return value.lower() in ('true', '1', 'yes', 'on') elif var_type == int: return int(value) elif var_type == float: return float(value) elif var_type == Path: return resolve_path(value) else: return var_type(value) except (ValueError, TypeError) as e: raise EnvironmentVariableError( f"Invalid value for environment variable {key}: {value}", field=key, value=value, suggestion=f"Provide a valid {var_type.__name__} value for {key}" ) from e def load_json_config(file_path: Union[str, Path], required: bool = False) -> Dict[str, Any]: """Load configuration from JSON file. Args: file_path: Path to JSON configuration file required: Whether the file must exist Returns: Configuration dictionary loaded from JSON Raises: ConfigFileError: If file cannot be read or parsed """ file_path = Path(file_path) if not file_path.exists(): if required: raise ConfigFileError( f"Required JSON configuration file not found: {file_path}", suggestion=f"Create {file_path} with valid JSON configuration" ) return {} try: with file_path.open('r') as f: return json.load(f) except json.JSONDecodeError as e: raise ConfigFileError( f"Invalid JSON in configuration file: {file_path}", context={'error': str(e), 'line': e.lineno, 'column': e.colno} ) from e except (OSError, UnicodeDecodeError) as e: raise ConfigFileError( f"Failed to read JSON configuration file: {file_path}", context={'error': str(e)} ) from e