-
Notifications
You must be signed in to change notification settings - Fork 2
Expand file tree
/
Copy pathapp_config.py
More file actions
493 lines (390 loc) · 14 KB
/
Copy pathapp_config.py
File metadata and controls
493 lines (390 loc) · 14 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
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
433
434
435
436
437
438
439
440
441
442
443
444
445
446
447
448
449
450
451
452
453
454
455
456
457
458
459
460
461
462
463
464
465
466
467
468
469
470
471
472
473
474
475
476
477
478
479
480
481
482
483
484
485
486
487
488
489
490
491
492
493
"""
app_config.py
Centralized configuration management for LinuxReport.
This module provides a single source of truth for all configuration values,
eliminating duplicate configuration loading across the application.
Features:
- Cached configuration loading with validation
- Fail-fast error handling for missing configuration
- Type-safe configuration access
- Centralized configuration validation
- No local dependencies (foundational module)
Author: LinuxReport System
License: See LICENSE file
"""
# =============================================================================
# STANDARD LIBRARY IMPORTS
# =============================================================================
import os
import socket
from functools import lru_cache
from pathlib import Path
from dataclasses import dataclass
import yaml
from Logging import g_logger as logging
# =============================================================================
# GLOBAL CONSTANTS AND CONFIGURATION
# =============================================================================
PATH = Path(__file__).parent
DEBUG = False
USE_TOR = True
# =============================================================================
# SHARED CONFIGURATION DICTIONARIES
# =============================================================================
# --- Shared Reddit Fetch Config ---
# Import here to avoid circular imports
@dataclass(frozen=True)
class FetchConfig:
"""
Base class for fetch configurations.
This immutable configuration class provides type safety for all fetch-related
settings used across different sites and services.
"""
needs_selenium: bool = False
needs_tor: bool = False
post_container: str = ""
title_selector: str = ""
link_selector: str = ""
link_attr: str = "href"
filter_pattern: str = None
use_random_user_agent: bool = False
published_selector: str = None
@dataclass(frozen=True)
class RedditFetchConfig(FetchConfig):
"""
Reddit-specific fetch configuration.
Inherits from FetchConfig with Reddit-specific defaults.
"""
needs_selenium: bool = True
needs_tor: bool = True
post_container: str = "article"
title_selector: str = "a[id^='post-title-']"
link_selector: str = "a[id^='post-title-']"
link_attr: str = "href"
filter_pattern: str = None
use_random_user_agent: bool = True
REDDIT_FETCH_CONFIG = RedditFetchConfig()
# =============================================================================
# CONFIGURATION MANAGER CLASS
# =============================================================================
class ConfigManager:
"""
Centralized configuration manager with caching and validation.
This class provides a single source of truth for all configuration values,
eliminating duplicate configuration loading across the application.
"""
_instance = None
_config = None
_validated = False
def __new__(cls):
"""Singleton pattern to ensure only one configuration instance."""
if cls._instance is None:
cls._instance = super().__new__(cls)
return cls._instance
@classmethod
def get_instance(cls):
"""Get the singleton instance of ConfigManager."""
if cls._instance is None:
cls._instance = ConfigManager()
return cls._instance
@lru_cache(maxsize=1)
def load_config(self):
"""
Load and cache configuration from config.yaml file.
Returns:
Dict[str, Any]: Configuration dictionary
Raises:
FileNotFoundError: If config.yaml is not found
yaml.YAMLError: If config.yaml is malformed
ValueError: If required configuration sections are missing
"""
config_path = PATH / 'config.yaml'
if not config_path.exists():
raise FileNotFoundError(f"Configuration file not found: {config_path}")
try:
with open(config_path, 'r', encoding='utf-8') as f:
config = yaml.safe_load(f)
if not isinstance(config, dict):
raise ValueError("Configuration file must contain a valid YAML dictionary")
return config
except yaml.YAMLError as e:
raise ValueError(f"Malformed configuration file: {e}") from e
except (IOError, OSError) as e:
logging.error(f"Error reading config.yaml: {e}")
raise
def get_config(self):
"""
Get the configuration dictionary, loading it if necessary.
Returns:
Dict[str, Any]: Configuration dictionary
"""
if self._config is None:
self._config = self.load_config()
self._validate_config()
return self._config
def _validate_config(self):
"""
Validate configuration if present. This is now permissive to avoid exceptions.
"""
if self._validated:
return
# Just mark as validated without strict validation
# Let the calling code handle missing config naturally
self._validated = True
def get(self, key_path, default=None):
"""
Get a configuration value using dot notation.
Args:
key_path: Configuration key path (e.g., 'admin.password')
default: Default value if key is not found
Returns:
Configuration value or default
"""
config = self.get_config()
keys = key_path.split('.')
try:
value = config
for key in keys:
value = value[key]
return value
except (KeyError, TypeError):
return default
def require(self, key_path):
"""
Get a required configuration value, failing if not found.
Args:
key_path: Configuration key path (e.g., 'admin.password')
Returns:
Configuration value
Raises:
ValueError: If the configuration key is not found
"""
value = self.get(key_path)
if value is None:
raise ValueError(f"Required configuration key not found: {key_path}")
return value
def reload(self):
"""
Reload configuration from disk (useful for development).
This clears the cache and forces a fresh load of the configuration file.
"""
self._config = None
self._validated = False
self.load_config.cache_clear()
# =============================================================================
# GLOBAL CONFIGURATION INSTANCE
# =============================================================================
# Create global configuration manager instance
config_manager = ConfigManager.get_instance()
# =============================================================================
# UTILITY FUNCTIONS
# =============================================================================
def is_tor_running():
"""
Check if Tor is running by attempting to connect to the SOCKS proxy port.
Returns:
bool: True if Tor is running, False otherwise
"""
try:
sock = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
sock.settimeout(1) # 1 second timeout
result = sock.connect_ex(('127.0.0.1', 9050))
sock.close()
return result == 0
except socket.error:
return False
# Check if Tor is actually running and update USE_TOR accordingly
if USE_TOR and not is_tor_running():
print("Tor is enabled but not running. Falling back to direct connection.")
USE_TOR = False
# =============================================================================
# CONFIGURATION ACCESS FUNCTIONS
# =============================================================================
def load_config():
"""
Load configuration from config.yaml file.
This function is maintained for backward compatibility.
New code should use the ConfigManager directly.
Returns:
Dict[str, Any]: Configuration dictionary
"""
return config_manager.get_config()
def get_admin_password():
"""
Get the admin password from configuration.
Returns:
Optional[str]: Admin password or None if not configured
"""
return config_manager.get('admin.password')
def get_dashboard_credentials():
"""
Get the dashboard credentials from configuration.
Returns:
Dict[str, str]: A dictionary with 'username' and 'password' keys.
"""
return config_manager.get('admin.dashboard', {})
def get_secret_key():
"""
Get the secret key from configuration.
Returns:
Optional[str]: Secret key or None if not configured
"""
return config_manager.get('admin.secret_key')
def get_weather_api_key():
"""
Get the weather API key from configuration.
Returns:
Optional[str]: Weather API key or None if not configured
"""
return config_manager.get('admin.weather_api_key')
def get_storage_config():
"""
Get the storage configuration.
Returns:
Dict[str, Any]: Storage configuration dictionary
"""
return config_manager.get('storage', {})
def get_settings_config():
"""
Get the settings configuration.
Returns:
Dict[str, Any]: Settings configuration dictionary
"""
return config_manager.get('settings', {})
def get_allowed_domains():
"""
Get the list of allowed domains for CSP and CORS.
Returns:
List[str]: List of allowed domains
"""
return config_manager.get('settings.allowed_domains', [])
def get_allowed_requester_domains():
"""
Get the list of domains allowed to make API requests.
Returns:
List[str]: List of allowed requester domains
"""
return config_manager.get('settings.allowed_requester_domains', [])
def get_cdn_config():
"""
Get the CDN configuration.
Returns:
Dict[str, Any]: CDN configuration dictionary
"""
return config_manager.get('settings.cdn', {})
def get_object_store_config():
"""
Get the object store configuration.
Returns:
Dict[str, Any]: Object store configuration dictionary
"""
return config_manager.get('settings.object_store', {})
def get_welcome_html():
"""
Get the welcome HTML message.
Returns:
str: Welcome HTML message
"""
return config_manager.get('settings.welcome_html', '')
def get_reports_config():
"""
Get the reports configuration.
Returns:
Dict[str, Any]: Reports configuration dictionary
"""
return config_manager.get('reports', {})
def get_tor_password():
"""
Get the Tor control port password from configuration.
Returns:
Optional[str]: Tor password or None if not configured
"""
return config_manager.get('tor.password')
def is_storage_enabled():
"""
Check if object storage is enabled.
Returns:
bool: True if storage is enabled, False otherwise
"""
return config_manager.get('storage.enabled', False)
def is_cdn_enabled():
"""
Check if CDN is enabled.
Returns:
bool: True if CDN is enabled, False otherwise
"""
return config_manager.get('settings.cdn.enabled', False)
def is_object_store_enabled():
"""
Check if object store feeds are enabled.
Returns:
bool: True if object store feeds are enabled, False otherwise
"""
return config_manager.get('settings.object_store.enabled', False)
def get_proxy_config():
"""
Get the proxy server configuration.
Returns:
Dict[str, Any]: Proxy configuration dictionary
"""
return config_manager.get('proxy', {})
def get_proxy_server():
"""
Get the proxy server address and port.
Returns:
Optional[str]: Proxy server address:port or None if not configured
"""
return config_manager.get('proxy.server')
def get_proxy_username():
"""
Get the proxy server username.
Returns:
Optional[str]: Proxy username or None if not configured
"""
return config_manager.get('proxy.username')
def get_proxy_password():
"""
Get the proxy server password.
Returns:
Optional[str]: Proxy password or None if not configured
"""
return config_manager.get('proxy.password')
def get_reddit_username():
"""
Get the Reddit username from configuration for user agent construction.
Returns:
str: Reddit username, defaults to "keithcu" if not configured
"""
return config_manager.get('reddit.username', 'keithcu')
# =============================================================================
# CONFIGURATION VALIDATION
# =============================================================================
def validate_configuration():
"""
Validate configuration if present. Now permissive to avoid exceptions.
"""
try:
# Just try to load config, don't validate strictly
config_manager.get_config()
print("Configuration loaded")
except (FileNotFoundError, ValueError) as e:
print(f"Configuration loading failed: {e}")
# =============================================================================
# CONFIGURATION RELOADING (for development)
# =============================================================================
def reload_configuration():
"""
Reload configuration from disk (useful for development).
This clears the cache and forces a fresh load of the configuration file.
"""
config_manager.reload()
print("Configuration reloaded successfully")
# =============================================================================
# INITIALIZATION
# =============================================================================
# Validate configuration on module import
try:
validate_configuration()
except (FileNotFoundError, ValueError) as e:
print(f"Warning: Configuration validation failed during import: {e}")
# Don't raise here to allow the module to be imported for testing