geronimo.config.loader
Configuration loader for Geronimo.
Handles loading, parsing, and environment variable interpolation for geronimo.yaml configuration files.
1"""Configuration loader for Geronimo. 2 3Handles loading, parsing, and environment variable interpolation 4for geronimo.yaml configuration files. 5""" 6 7import os 8import re 9from pathlib import Path 10 11import yaml 12from pydantic import ValidationError 13 14from geronimo.config.schema import GeronimoConfig 15 16 17class ConfigurationError(Exception): 18 """Raised when configuration is invalid or cannot be loaded.""" 19 20 pass 21 22 23def interpolate_env_vars(value: str) -> str: 24 """Interpolate environment variables in a string. 25 26 Supports ${VAR_NAME} and ${VAR_NAME:-default} syntax. 27 28 Args: 29 value: String potentially containing environment variable references. 30 31 Returns: 32 String with environment variables replaced with their values. 33 """ 34 # Pattern matches ${VAR_NAME} or ${VAR_NAME:-default} 35 pattern = r"\$\{([A-Z_][A-Z0-9_]*)(?::-([^}]*))?\}" 36 37 def replace(match: re.Match) -> str: 38 var_name = match.group(1) 39 default = match.group(2) 40 env_value = os.environ.get(var_name) 41 42 if env_value is not None: 43 return env_value 44 if default is not None: 45 return default 46 # Return original if no value and no default 47 return match.group(0) 48 49 return re.sub(pattern, replace, value) 50 51 52def interpolate_dict(data: dict) -> dict: 53 """Recursively interpolate environment variables in a dictionary. 54 55 Args: 56 data: Dictionary potentially containing string values with env vars. 57 58 Returns: 59 Dictionary with all string values interpolated. 60 """ 61 result = {} 62 for key, value in data.items(): 63 if isinstance(value, str): 64 result[key] = interpolate_env_vars(value) 65 elif isinstance(value, dict): 66 result[key] = interpolate_dict(value) 67 elif isinstance(value, list): 68 result[key] = [ 69 interpolate_env_vars(item) if isinstance(item, str) else item 70 for item in value 71 ] 72 else: 73 result[key] = value 74 return result 75 76 77def load_config(config_path: str | Path) -> GeronimoConfig: 78 """Load and validate a geronimo.yaml configuration file. 79 80 Args: 81 config_path: Path to the geronimo.yaml file. 82 83 Returns: 84 Validated GeronimoConfig instance. 85 86 Raises: 87 FileNotFoundError: If the config file doesn't exist. 88 ConfigurationError: If the config is invalid. 89 """ 90 path = Path(config_path) 91 92 if not path.exists(): 93 raise FileNotFoundError(f"Configuration file not found: {path}") 94 95 if not path.is_file(): 96 raise ConfigurationError(f"Path is not a file: {path}") 97 98 try: 99 with open(path) as f: 100 raw_config = yaml.safe_load(f) 101 except yaml.YAMLError as e: 102 raise ConfigurationError(f"Invalid YAML syntax: {e}") 103 104 if raw_config is None: 105 raise ConfigurationError("Configuration file is empty") 106 107 # Interpolate environment variables 108 config_data = interpolate_dict(raw_config) 109 110 # Validate and parse with Pydantic 111 try: 112 return GeronimoConfig(**config_data) 113 except ValidationError as e: 114 errors = [] 115 for error in e.errors(): 116 location = ".".join(str(loc) for loc in error["loc"]) 117 errors.append(f" {location}: {error['msg']}") 118 raise ConfigurationError( 119 f"Configuration validation failed:\n" + "\n".join(errors) 120 ) 121 122 123def save_config(config: GeronimoConfig, config_path: str | Path) -> None: 124 """Save a GeronimoConfig to a YAML file. 125 126 Args: 127 config: The configuration to save. 128 config_path: Path to write the YAML file. 129 """ 130 path = Path(config_path) 131 path.parent.mkdir(parents=True, exist_ok=True) 132 133 # Convert to dict, excluding None values for cleaner output 134 config_dict = config.model_dump(exclude_none=True, mode="json") 135 136 with open(path, "w") as f: 137 yaml.dump(config_dict, f, default_flow_style=False, sort_keys=False)
18class ConfigurationError(Exception): 19 """Raised when configuration is invalid or cannot be loaded.""" 20 21 pass
Raised when configuration is invalid or cannot be loaded.
24def interpolate_env_vars(value: str) -> str: 25 """Interpolate environment variables in a string. 26 27 Supports ${VAR_NAME} and ${VAR_NAME:-default} syntax. 28 29 Args: 30 value: String potentially containing environment variable references. 31 32 Returns: 33 String with environment variables replaced with their values. 34 """ 35 # Pattern matches ${VAR_NAME} or ${VAR_NAME:-default} 36 pattern = r"\$\{([A-Z_][A-Z0-9_]*)(?::-([^}]*))?\}" 37 38 def replace(match: re.Match) -> str: 39 var_name = match.group(1) 40 default = match.group(2) 41 env_value = os.environ.get(var_name) 42 43 if env_value is not None: 44 return env_value 45 if default is not None: 46 return default 47 # Return original if no value and no default 48 return match.group(0) 49 50 return re.sub(pattern, replace, value)
Interpolate environment variables in a string.
Supports ${VAR_NAME} and ${VAR_NAME:-default} syntax.
Args: value: String potentially containing environment variable references.
Returns: String with environment variables replaced with their values.
53def interpolate_dict(data: dict) -> dict: 54 """Recursively interpolate environment variables in a dictionary. 55 56 Args: 57 data: Dictionary potentially containing string values with env vars. 58 59 Returns: 60 Dictionary with all string values interpolated. 61 """ 62 result = {} 63 for key, value in data.items(): 64 if isinstance(value, str): 65 result[key] = interpolate_env_vars(value) 66 elif isinstance(value, dict): 67 result[key] = interpolate_dict(value) 68 elif isinstance(value, list): 69 result[key] = [ 70 interpolate_env_vars(item) if isinstance(item, str) else item 71 for item in value 72 ] 73 else: 74 result[key] = value 75 return result
Recursively interpolate environment variables in a dictionary.
Args: data: Dictionary potentially containing string values with env vars.
Returns: Dictionary with all string values interpolated.
78def load_config(config_path: str | Path) -> GeronimoConfig: 79 """Load and validate a geronimo.yaml configuration file. 80 81 Args: 82 config_path: Path to the geronimo.yaml file. 83 84 Returns: 85 Validated GeronimoConfig instance. 86 87 Raises: 88 FileNotFoundError: If the config file doesn't exist. 89 ConfigurationError: If the config is invalid. 90 """ 91 path = Path(config_path) 92 93 if not path.exists(): 94 raise FileNotFoundError(f"Configuration file not found: {path}") 95 96 if not path.is_file(): 97 raise ConfigurationError(f"Path is not a file: {path}") 98 99 try: 100 with open(path) as f: 101 raw_config = yaml.safe_load(f) 102 except yaml.YAMLError as e: 103 raise ConfigurationError(f"Invalid YAML syntax: {e}") 104 105 if raw_config is None: 106 raise ConfigurationError("Configuration file is empty") 107 108 # Interpolate environment variables 109 config_data = interpolate_dict(raw_config) 110 111 # Validate and parse with Pydantic 112 try: 113 return GeronimoConfig(**config_data) 114 except ValidationError as e: 115 errors = [] 116 for error in e.errors(): 117 location = ".".join(str(loc) for loc in error["loc"]) 118 errors.append(f" {location}: {error['msg']}") 119 raise ConfigurationError( 120 f"Configuration validation failed:\n" + "\n".join(errors) 121 )
Load and validate a geronimo.yaml configuration file.
Args: config_path: Path to the geronimo.yaml file.
Returns: Validated GeronimoConfig instance.
Raises: FileNotFoundError: If the config file doesn't exist. ConfigurationError: If the config is invalid.
124def save_config(config: GeronimoConfig, config_path: str | Path) -> None: 125 """Save a GeronimoConfig to a YAML file. 126 127 Args: 128 config: The configuration to save. 129 config_path: Path to write the YAML file. 130 """ 131 path = Path(config_path) 132 path.parent.mkdir(parents=True, exist_ok=True) 133 134 # Convert to dict, excluding None values for cleaner output 135 config_dict = config.model_dump(exclude_none=True, mode="json") 136 137 with open(path, "w") as f: 138 yaml.dump(config_dict, f, default_flow_style=False, sort_keys=False)
Save a GeronimoConfig to a YAML file.
Args: config: The configuration to save. config_path: Path to write the YAML file.