File size: 7,943 Bytes
461adca
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
"""
Configuration loader for YAML files.
"""
import os
import yaml
from pathlib import Path
from typing import Dict, Any, Optional
from config.settings import Settings


class ConfigLoader:
    """Loads and merges YAML configuration files."""
    
    def __init__(self, config_dir: Optional[Path] = None):
        """
        Initialize the configuration loader.
        
        Args:
            config_dir: Path to configuration directory. Defaults to config/environments/
        """
        if config_dir is None:
            # Get the directory where this file is located
            current_dir = Path(__file__).parent
            config_dir = current_dir / "environments"
        
        self.config_dir = Path(config_dir)
        if not self.config_dir.exists():
            raise FileNotFoundError(f"Configuration directory not found: {self.config_dir}")
    
    def load_yaml(self, filename: str) -> Dict[str, Any]:
        """
        Load a YAML file and return its contents.
        
        Args:
            filename: Name of the YAML file to load
            
        Returns:
            Dictionary containing the YAML contents
        """
        filepath = self.config_dir / filename
        if not filepath.exists():
            raise FileNotFoundError(f"Configuration file not found: {filepath}")
        
        with open(filepath, 'r', encoding='utf-8') as f:
            content = f.read()
            # Replace environment variables in the format ${VAR_NAME}
            content = self._replace_env_vars(content)
            return yaml.safe_load(content)
    
    def _replace_env_vars(self, content: str) -> str:
        """
        Replace environment variables in the format ${VAR_NAME} or ${VAR_NAME:default}.
        
        Args:
            content: String content with potential environment variables
            
        Returns:
            Content with environment variables replaced
        """
        import re
        
        def replace_var(match):
            var_expr = match.group(1)
            if ':' in var_expr:
                var_name, default_value = var_expr.split(':', 1)
                return os.getenv(var_name.strip(), default_value.strip())
            else:
                return os.getenv(var_expr.strip(), match.group(0))
        
        # Pattern to match ${VAR_NAME} or ${VAR_NAME:default}
        pattern = r'\$\{([^}]+)\}'
        return re.sub(pattern, replace_var, content)
    
    def merge_configs(self, base_config: Dict[str, Any], override_config: Dict[str, Any]) -> Dict[str, Any]:
        """
        Deep merge two configuration dictionaries.
        
        Args:
            base_config: Base configuration dictionary
            override_config: Configuration to override base with
            
        Returns:
            Merged configuration dictionary
        """
        result = base_config.copy()
        
        for key, value in override_config.items():
            if key in result and isinstance(result[key], dict) and isinstance(value, dict):
                result[key] = self.merge_configs(result[key], value)
            else:
                result[key] = value
        
        return result
    
    def load_config(self, environment: Optional[str] = None, validate_api_keys: bool = True) -> Settings:
        """
        Load configuration for the specified environment.
        
        Args:
            environment: Environment name (development, production, etc.)
                        If None, uses ENVIRONMENT env var or defaults to production
            validate_api_keys: Whether to validate API keys during loading
            
        Returns:
            Settings object with loaded configuration
        """
        # Determine environment
        if environment is None:
            environment = os.getenv('ENVIRONMENT', 'production')
        
        # Load default configuration
        default_config = self.load_yaml('default.yaml')
        
        # Load environment-specific configuration if it exists
        env_file = f'{environment}.yaml'
        env_config = {}
        
        env_filepath = self.config_dir / env_file
        if env_filepath.exists():
            env_config = self.load_yaml(env_file)
        else:
            print(f"Warning: Environment config file not found: {env_filepath}")
            print(f"Using default configuration only")
        
        # Merge configurations
        merged_config = self.merge_configs(default_config, env_config)
        
        # Create and validate Settings object
        try:
            settings = Settings(**merged_config)
            
            # Optionally validate API keys
            if validate_api_keys:
                self.validate_config(settings)
            
            return settings
        except Exception as e:
            raise ValueError(f"Failed to validate configuration: {str(e)}")
    
    def validate_config(self, settings: Settings) -> bool:
        """
        Validate the loaded configuration.
        
        Args:
            settings: Settings object to validate
            
        Returns:
            True if configuration is valid
            
        Raises:
            ValueError: If configuration is invalid
        """
        # Check required environment variables for API keys
        required_env_vars = []
        
        if 'openai' in settings.models.providers:
            required_env_vars.append('OPENAI_API_KEY')
        
        if 'anthropic' in settings.models.providers:
            required_env_vars.append('ANTHROPIC_API_KEY')
        
        if 'gemini' in settings.models.providers:
            required_env_vars.append('GEMINI_API_KEY')
        
        if 'deepseek' in settings.models.providers:
            required_env_vars.append('DEEPSEEK_API_KEY')
        
        missing_vars = [var for var in required_env_vars if not os.getenv(var)]
        
        if missing_vars:
            raise ValueError(
                f"Missing required environment variables: {', '.join(missing_vars)}"
            )
        
        # Validate local directory exists or can be created
        local_dir = Path(settings.aws.local_dir)
        if not local_dir.exists():
            try:
                local_dir.mkdir(parents=True, exist_ok=True)
                print(f"Created local directory: {local_dir}")
            except Exception as e:
                raise ValueError(f"Cannot create local directory {local_dir}: {str(e)}")
        
        # Validate logging directory
        if settings.logging.file:
            log_dir = Path(settings.logging.file).parent
            if not log_dir.exists():
                try:
                    log_dir.mkdir(parents=True, exist_ok=True)
                    print(f"Created log directory: {log_dir}")
                except Exception as e:
                    raise ValueError(f"Cannot create log directory {log_dir}: {str(e)}")
        
        return True


# Global configuration loader instance
_config_loader: Optional[ConfigLoader] = None
_settings: Optional[Settings] = None


def get_config_loader() -> ConfigLoader:
    """Get or create the global configuration loader instance."""
    global _config_loader
    if _config_loader is None:
        _config_loader = ConfigLoader()
    return _config_loader


def get_settings(environment: Optional[str] = None, reload: bool = False, validate_api_keys: bool = True) -> Settings:
    """
    Get the application settings.
    
    Args:
        environment: Environment name (development, production, etc.)
        reload: Force reload of configuration
        validate_api_keys: Whether to validate API keys
        
    Returns:
        Settings object
    """
    global _settings
    
    if _settings is None or reload:
        loader = get_config_loader()
        _settings = loader.load_config(environment, validate_api_keys=validate_api_keys)
    
    return _settings