MogensR commited on
Commit
4d1eba1
·
1 Parent(s): 71bb9ba

Create config/base.py

Browse files
Files changed (1) hide show
  1. config/base.py +385 -0
config/base.py ADDED
@@ -0,0 +1,385 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ """
2
+ Base configuration class for BackgroundFX Pro.
3
+
4
+ Provides a flexible, environment-aware configuration system with validation,
5
+ type checking, and automatic loading from multiple sources.
6
+ """
7
+
8
+ import os
9
+ import json
10
+ import yaml
11
+ from pathlib import Path
12
+ from typing import Any, Dict, Optional, List, Union, Type, TypeVar
13
+ from dataclasses import dataclass, field, asdict
14
+ from abc import ABC, abstractmethod
15
+ import logging
16
+ from dotenv import load_dotenv
17
+ from pydantic import BaseModel, ValidationError, Field
18
+
19
+ logger = logging.getLogger(__name__)
20
+
21
+ T = TypeVar('T', bound='BaseConfig')
22
+
23
+
24
+ class ConfigurationError(Exception):
25
+ """Raised when configuration is invalid or missing."""
26
+ pass
27
+
28
+
29
+ class ConfigSource(ABC):
30
+ """Abstract base class for configuration sources."""
31
+
32
+ @abstractmethod
33
+ def load(self) -> Dict[str, Any]:
34
+ """Load configuration from source."""
35
+ pass
36
+
37
+ @abstractmethod
38
+ def exists(self) -> bool:
39
+ """Check if configuration source exists."""
40
+ pass
41
+
42
+
43
+ class EnvFileSource(ConfigSource):
44
+ """Load configuration from .env file."""
45
+
46
+ def __init__(self, path: str = ".env"):
47
+ self.path = Path(path)
48
+
49
+ def exists(self) -> bool:
50
+ return self.path.exists()
51
+
52
+ def load(self) -> Dict[str, Any]:
53
+ if not self.exists():
54
+ return {}
55
+
56
+ load_dotenv(self.path)
57
+ return dict(os.environ)
58
+
59
+
60
+ class JSONFileSource(ConfigSource):
61
+ """Load configuration from JSON file."""
62
+
63
+ def __init__(self, path: str):
64
+ self.path = Path(path)
65
+
66
+ def exists(self) -> bool:
67
+ return self.path.exists()
68
+
69
+ def load(self) -> Dict[str, Any]:
70
+ if not self.exists():
71
+ return {}
72
+
73
+ with open(self.path, 'r') as f:
74
+ return json.load(f)
75
+
76
+
77
+ class YAMLFileSource(ConfigSource):
78
+ """Load configuration from YAML file."""
79
+
80
+ def __init__(self, path: str):
81
+ self.path = Path(path)
82
+
83
+ def exists(self) -> bool:
84
+ return self.path.exists()
85
+
86
+ def load(self) -> Dict[str, Any]:
87
+ if not self.exists():
88
+ return {}
89
+
90
+ with open(self.path, 'r') as f:
91
+ return yaml.safe_load(f)
92
+
93
+
94
+ class EnvironmentSource(ConfigSource):
95
+ """Load configuration from environment variables."""
96
+
97
+ def __init__(self, prefix: str = "BACKGROUNDFX_"):
98
+ self.prefix = prefix
99
+
100
+ def exists(self) -> bool:
101
+ return True
102
+
103
+ def load(self) -> Dict[str, Any]:
104
+ config = {}
105
+ for key, value in os.environ.items():
106
+ if key.startswith(self.prefix):
107
+ # Remove prefix and convert to lowercase
108
+ clean_key = key[len(self.prefix):].lower()
109
+ config[clean_key] = value
110
+ return config
111
+
112
+
113
+ @dataclass
114
+ class BaseConfig:
115
+ """
116
+ Base configuration class with common settings.
117
+
118
+ Provides methods for loading, validating, and merging configurations
119
+ from multiple sources.
120
+ """
121
+
122
+ # Application settings
123
+ app_name: str = "BackgroundFX Pro"
124
+ app_version: str = "1.0.0"
125
+ environment: str = field(default_factory=lambda: os.getenv("ENVIRONMENT", "development"))
126
+ debug: bool = field(default_factory=lambda: os.getenv("DEBUG", "false").lower() == "true")
127
+
128
+ # Server settings
129
+ host: str = "0.0.0.0"
130
+ port: int = 8000
131
+ workers: int = 4
132
+ reload: bool = False
133
+
134
+ # Paths
135
+ base_dir: Path = field(default_factory=lambda: Path(__file__).parent.parent)
136
+ data_dir: Path = field(default_factory=lambda: Path("data"))
137
+ temp_dir: Path = field(default_factory=lambda: Path("/tmp/backgroundfx"))
138
+ log_dir: Path = field(default_factory=lambda: Path("logs"))
139
+
140
+ # Logging
141
+ log_level: str = "INFO"
142
+ log_format: str = "%(asctime)s - %(name)s - %(levelname)s - %(message)s"
143
+ log_file: Optional[str] = None
144
+
145
+ # Security
146
+ secret_key: str = field(default_factory=lambda: os.getenv("SECRET_KEY", "change-me-in-production"))
147
+ api_key_header: str = "X-API-Key"
148
+ cors_origins: List[str] = field(default_factory=lambda: ["*"])
149
+
150
+ # Rate limiting
151
+ rate_limit_enabled: bool = True
152
+ rate_limit_requests: int = 100
153
+ rate_limit_window: int = 3600 # seconds
154
+
155
+ # Feature flags
156
+ enable_video_processing: bool = True
157
+ enable_batch_processing: bool = True
158
+ enable_ai_backgrounds: bool = True
159
+ enable_webhooks: bool = True
160
+
161
+ _sources: List[ConfigSource] = field(default_factory=list, init=False, repr=False)
162
+ _validators: List[callable] = field(default_factory=list, init=False, repr=False)
163
+
164
+ def __post_init__(self):
165
+ """Initialize configuration after dataclass creation."""
166
+ self.setup_directories()
167
+ self.validate()
168
+
169
+ def setup_directories(self):
170
+ """Create necessary directories if they don't exist."""
171
+ for dir_attr in ['data_dir', 'temp_dir', 'log_dir']:
172
+ dir_path = getattr(self, dir_attr)
173
+ if isinstance(dir_path, str):
174
+ dir_path = Path(dir_path)
175
+ setattr(self, dir_attr, dir_path)
176
+ dir_path.mkdir(parents=True, exist_ok=True)
177
+
178
+ def add_source(self, source: ConfigSource):
179
+ """Add a configuration source."""
180
+ self._sources.append(source)
181
+
182
+ def add_validator(self, validator: callable):
183
+ """Add a configuration validator."""
184
+ self._validators.append(validator)
185
+
186
+ def load_from_sources(self):
187
+ """Load configuration from all registered sources."""
188
+ merged_config = {}
189
+
190
+ for source in self._sources:
191
+ if source.exists():
192
+ try:
193
+ config = source.load()
194
+ merged_config.update(config)
195
+ logger.debug(f"Loaded configuration from {source.__class__.__name__}")
196
+ except Exception as e:
197
+ logger.warning(f"Failed to load from {source.__class__.__name__}: {e}")
198
+
199
+ # Update instance attributes
200
+ for key, value in merged_config.items():
201
+ if hasattr(self, key):
202
+ # Type conversion
203
+ attr_type = type(getattr(self, key))
204
+ try:
205
+ if attr_type == bool:
206
+ value = str(value).lower() in ('true', '1', 'yes', 'on')
207
+ elif attr_type == Path:
208
+ value = Path(value)
209
+ else:
210
+ value = attr_type(value)
211
+ setattr(self, key, value)
212
+ except (ValueError, TypeError) as e:
213
+ logger.warning(f"Failed to set {key}: {e}")
214
+
215
+ def validate(self):
216
+ """Validate configuration."""
217
+ errors = []
218
+
219
+ # Run custom validators
220
+ for validator in self._validators:
221
+ try:
222
+ validator(self)
223
+ except Exception as e:
224
+ errors.append(str(e))
225
+
226
+ # Basic validation
227
+ if not self.secret_key or self.secret_key == "change-me-in-production":
228
+ if self.environment == "production":
229
+ errors.append("SECRET_KEY must be set in production")
230
+
231
+ if self.port < 1 or self.port > 65535:
232
+ errors.append(f"Invalid port: {self.port}")
233
+
234
+ if self.workers < 1:
235
+ errors.append(f"Invalid workers count: {self.workers}")
236
+
237
+ if self.rate_limit_requests < 1:
238
+ errors.append(f"Invalid rate limit: {self.rate_limit_requests}")
239
+
240
+ if errors:
241
+ raise ConfigurationError("\n".join(errors))
242
+
243
+ def to_dict(self) -> Dict[str, Any]:
244
+ """Convert configuration to dictionary."""
245
+ return {
246
+ k: str(v) if isinstance(v, Path) else v
247
+ for k, v in asdict(self).items()
248
+ if not k.startswith('_')
249
+ }
250
+
251
+ def to_json(self, indent: int = 2) -> str:
252
+ """Convert configuration to JSON string."""
253
+ return json.dumps(self.to_dict(), indent=indent, default=str)
254
+
255
+ def to_yaml(self) -> str:
256
+ """Convert configuration to YAML string."""
257
+ return yaml.dump(self.to_dict(), default_flow_style=False)
258
+
259
+ def save(self, path: Union[str, Path], format: str = "json"):
260
+ """Save configuration to file."""
261
+ path = Path(path)
262
+
263
+ if format == "json":
264
+ content = self.to_json()
265
+ elif format == "yaml":
266
+ content = self.to_yaml()
267
+ elif format == "env":
268
+ content = self.to_env()
269
+ else:
270
+ raise ValueError(f"Unsupported format: {format}")
271
+
272
+ with open(path, 'w') as f:
273
+ f.write(content)
274
+
275
+ def to_env(self) -> str:
276
+ """Convert configuration to .env format."""
277
+ lines = []
278
+ for key, value in self.to_dict().items():
279
+ if value is not None:
280
+ env_key = f"BACKGROUNDFX_{key.upper()}"
281
+ if isinstance(value, (list, dict)):
282
+ value = json.dumps(value)
283
+ lines.append(f"{env_key}={value}")
284
+ return "\n".join(lines)
285
+
286
+ @classmethod
287
+ def from_file(cls: Type[T], path: Union[str, Path]) -> T:
288
+ """Load configuration from file."""
289
+ path = Path(path)
290
+
291
+ if not path.exists():
292
+ raise ConfigurationError(f"Configuration file not found: {path}")
293
+
294
+ config = cls()
295
+
296
+ if path.suffix == ".json":
297
+ config.add_source(JSONFileSource(str(path)))
298
+ elif path.suffix in (".yaml", ".yml"):
299
+ config.add_source(YAMLFileSource(str(path)))
300
+ elif path.suffix == ".env":
301
+ config.add_source(EnvFileSource(str(path)))
302
+ else:
303
+ raise ConfigurationError(f"Unsupported file format: {path.suffix}")
304
+
305
+ config.load_from_sources()
306
+ return config
307
+
308
+ @classmethod
309
+ def from_env(cls: Type[T], prefix: str = "BACKGROUNDFX_") -> T:
310
+ """Load configuration from environment variables."""
311
+ config = cls()
312
+ config.add_source(EnvironmentSource(prefix))
313
+ config.load_from_sources()
314
+ return config
315
+
316
+ def merge(self, other: 'BaseConfig'):
317
+ """Merge another configuration into this one."""
318
+ for key, value in other.to_dict().items():
319
+ if hasattr(self, key):
320
+ setattr(self, key, value)
321
+
322
+ def get_section(self, section: str) -> Dict[str, Any]:
323
+ """Get configuration section as dictionary."""
324
+ result = {}
325
+ prefix = f"{section}_"
326
+
327
+ for key, value in self.to_dict().items():
328
+ if key.startswith(prefix):
329
+ clean_key = key[len(prefix):]
330
+ result[clean_key] = value
331
+
332
+ return result
333
+
334
+ def __repr__(self) -> str:
335
+ """String representation of configuration."""
336
+ items = []
337
+ for key, value in self.to_dict().items():
338
+ if 'secret' in key.lower() or 'password' in key.lower() or 'key' in key.lower():
339
+ value = "***"
340
+ items.append(f"{key}={value}")
341
+ return f"{self.__class__.__name__}({', '.join(items[:5])}...)"
342
+
343
+
344
+ class ConfigManager:
345
+ """
346
+ Manages multiple configuration instances and environments.
347
+ """
348
+
349
+ def __init__(self):
350
+ self._configs: Dict[str, BaseConfig] = {}
351
+ self._active: Optional[str] = None
352
+
353
+ def register(self, name: str, config: BaseConfig):
354
+ """Register a configuration."""
355
+ self._configs[name] = config
356
+ if self._active is None:
357
+ self._active = name
358
+
359
+ def get(self, name: Optional[str] = None) -> BaseConfig:
360
+ """Get configuration by name or active configuration."""
361
+ name = name or self._active
362
+ if name not in self._configs:
363
+ raise ConfigurationError(f"Configuration not found: {name}")
364
+ return self._configs[name]
365
+
366
+ def set_active(self, name: str):
367
+ """Set active configuration."""
368
+ if name not in self._configs:
369
+ raise ConfigurationError(f"Configuration not found: {name}")
370
+ self._active = name
371
+
372
+ def reload(self, name: Optional[str] = None):
373
+ """Reload configuration from sources."""
374
+ config = self.get(name)
375
+ config.load_from_sources()
376
+ config.validate()
377
+
378
+ @property
379
+ def active(self) -> BaseConfig:
380
+ """Get active configuration."""
381
+ return self.get()
382
+
383
+
384
+ # Global configuration manager
385
+ config_manager = ConfigManager()