-
Notifications
You must be signed in to change notification settings - Fork 0
Expand file tree
/
Copy pathconfig_loader.py
More file actions
237 lines (190 loc) · 8.54 KB
/
config_loader.py
File metadata and controls
237 lines (190 loc) · 8.54 KB
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
230
231
232
233
234
235
236
237
"""
Secure Configuration Loader with Environment Variable Substitution
This module provides secure configuration loading with environment variable
substitution, validation, and proper error handling for sensitive values.
"""
import os
import re
import yaml
import logging
from typing import Dict, Any, Optional
from pathlib import Path
from dotenv import load_dotenv
logger = logging.getLogger(__name__)
class ConfigValidationError(Exception):
"""Raised when configuration validation fails."""
pass
class EnvironmentVariableError(Exception):
"""Raised when required environment variables are missing."""
pass
class SecureConfigLoader:
"""
Secure configuration loader with environment variable substitution.
Features:
- Environment variable substitution using ${VAR_NAME} syntax
- Secure handling of sensitive configuration values
- Configuration validation
- Proper error handling and logging
- Support for .env files
"""
def __init__(self, env_file: Optional[str] = None):
"""
Initialize the secure configuration loader.
Args:
env_file: Path to .env file (optional)
"""
self.env_file = env_file
self._load_environment_variables()
def _load_environment_variables(self):
"""Load environment variables from .env file if specified."""
if self.env_file and Path(self.env_file).exists():
load_dotenv(self.env_file)
logger.info(f"Loaded environment variables from {self.env_file}")
elif self.env_file:
logger.warning(f"Environment file {self.env_file} not found, using system environment")
def _substitute_environment_variables(self, value: str) -> str:
"""
Substitute environment variables in a string value.
Args:
value: String value that may contain environment variable references
Returns:
String with environment variables substituted
Raises:
EnvironmentVariableError: If a required environment variable is missing
"""
if not isinstance(value, str):
return value
# Pattern to match ${VAR_NAME} or ${VAR_NAME:default}
pattern = r'\$\{([^:}]+)(?::([^}]*))?\}'
def replace_var(match):
var_name = match.group(1)
default_value = match.group(2)
# Get environment variable value
env_value = os.getenv(var_name)
if env_value is not None:
return env_value
elif default_value is not None:
logger.debug(f"Environment variable {var_name} not found, using default value")
return default_value
else:
raise EnvironmentVariableError(
f"Required environment variable '{var_name}' is not set"
)
return re.sub(pattern, replace_var, value)
def _recursive_substitution(self, obj: Any) -> Any:
"""
Recursively substitute environment variables in configuration objects.
Args:
obj: Configuration object (dict, list, or primitive type)
Returns:
Object with environment variables substituted
"""
if isinstance(obj, dict):
return {key: self._recursive_substitution(value) for key, value in obj.items()}
elif isinstance(obj, list):
return [self._recursive_substitution(item) for item in obj]
elif isinstance(obj, str):
return self._substitute_environment_variables(obj)
else:
return obj
def _validate_configuration(self, config: Dict[str, Any]) -> None:
"""
Validate configuration structure and required values.
Args:
config: Configuration dictionary to validate
Raises:
ConfigValidationError: If validation fails
"""
required_sections = ['app', 'data_pipeline', 'smc_detector', 'decision_engine']
for section in required_sections:
if section not in config:
raise ConfigValidationError(f"Required configuration section '{section}' is missing")
# Validate execution engine API keys
execution_engine = config.get('execution_engine', {})
api_keys = execution_engine.get('api_keys', {})
for exchange, keys in api_keys.items():
if not keys.get('key') or keys['key'] == 'YOUR_API_KEY':
raise ConfigValidationError(
f"API key for {exchange} is not properly configured. "
"Use environment variables like ${{BINANCE_API_KEY}}"
)
if not keys.get('secret') or keys['secret'] == 'YOUR_API_SECRET':
raise ConfigValidationError(
f"API secret for {exchange} is not properly configured. "
"Use environment variables like ${{BINANCE_API_SECRET}}"
)
def _sanitize_config_for_logging(self, config: Dict[str, Any]) -> Dict[str, Any]:
"""
Create a sanitized version of config for logging (removes sensitive data).
Args:
config: Original configuration dictionary
Returns:
Sanitized configuration dictionary safe for logging
"""
sanitized = config.copy()
# Remove sensitive API keys from logging
if 'execution_engine' in sanitized:
if 'api_keys' in sanitized['execution_engine']:
sanitized['execution_engine']['api_keys'] = {
exchange: {
'key': '[REDACTED]' if keys.get('key') else None,
'secret': '[REDACTED]' if keys.get('secret') else None
}
for exchange, keys in sanitized['execution_engine']['api_keys'].items()
}
return sanitized
def load_config(self, config_path: str) -> Dict[str, Any]:
"""
Load and process configuration file with environment variable substitution.
Args:
config_path: Path to the configuration file
Returns:
Processed configuration dictionary
Raises:
FileNotFoundError: If configuration file is not found
yaml.YAMLError: If YAML parsing fails
EnvironmentVariableError: If required environment variables are missing
ConfigValidationError: If configuration validation fails
"""
try:
# Load YAML configuration
with open(config_path, 'r', encoding='utf-8') as file:
config = yaml.safe_load(file)
if config is None:
raise ConfigValidationError("Configuration file is empty or invalid")
logger.info(f"Loaded configuration from {config_path}")
# Substitute environment variables
config = self._recursive_substitution(config)
# Validate configuration
self._validate_configuration(config)
# Log sanitized configuration
sanitized_config = self._sanitize_config_for_logging(config)
logger.info("Configuration loaded successfully", extra={'config': sanitized_config})
return config
except FileNotFoundError:
logger.error(f"Configuration file not found: {config_path}")
raise
except yaml.YAMLError as e:
logger.error(f"Error parsing YAML configuration: {e}")
raise
except EnvironmentVariableError as e:
logger.error(f"Environment variable error: {e}")
raise
except ConfigValidationError as e:
logger.error(f"Configuration validation error: {e}")
raise
except Exception as e:
logger.error(f"Unexpected error loading configuration: {e}")
raise
def load_secure_config(config_path: str = "config.yaml",
env_file: Optional[str] = None) -> Dict[str, Any]:
"""
Convenience function to load secure configuration.
Args:
config_path: Path to the configuration file
env_file: Path to .env file (optional)
Returns:
Processed configuration dictionary
"""
loader = SecureConfigLoader(env_file)
return loader.load_config(config_path)