Team Ai
Apppublic

diegobeyl/backtesting

sourceHugging Faceupdated 9mo agoView on Hugging Face
2likes
config_manager.py187 linesDownload Raw Back to api
1"""
2Configuration Manager with Hot-Reload Support
3"""
4
5import json
6import os
7import sys
8from pathlib import Path
9from typing import Optional, Callable, List
10import threading
11import logging
12from watchdog.observers import Observer
13from watchdog.events import FileSystemEventHandler, FileModifiedEvent
14
15# Add parent directory for imports
16sys.path.insert(0, str(Path(__file__).parent.parent))
17
18from api.schemas import BacktestConfig
19
20logger = logging.getLogger(__name__)
21
22
23class ConfigFileHandler(FileSystemEventHandler):
24    """Handles file system events for config file changes"""
25    
26    def __init__(self, config_manager: 'ConfigManager'):
27        self.config_manager = config_manager
28        self._last_modified = 0
29        
30    def on_modified(self, event):
31        if isinstance(event, FileModifiedEvent):
32            if event.src_path.endswith('.json'):
33                # Debounce rapid changes
34                import time
35                current_time = time.time()
36                if current_time - self._last_modified > 1:  # 1 second debounce
37                    self._last_modified = current_time
38                    logger.info(f"Config file changed: {event.src_path}")
39                    self.config_manager.reload_config()
40
41
42class ConfigManager:
43    """
44    Manages configuration with hot-reload capability
45    
46    Features:
47    - Load/save configuration from JSON
48    - Watch for file changes and auto-reload
49    - Notify listeners on config changes
50    - Thread-safe operations
51    """
52    
53    def __init__(self, config_path: str = None):
54        # Determine config path
55        if config_path is None:
56            base_dir = Path(__file__).parent.parent
57            config_path = base_dir / "config" / "default_config.json"
58        
59        self.config_path = Path(config_path)
60        self._config: Optional[BacktestConfig] = None
61        self._lock = threading.RLock()
62        self._listeners: List[Callable[[BacktestConfig], None]] = []
63        self._observer: Optional[Observer] = None
64        
65        # Ensure config directory exists
66        self.config_path.parent.mkdir(parents=True, exist_ok=True)
67        
68        # Load initial config
69        self.reload_config()
70        
71    def reload_config(self):
72        """Reload configuration from file"""
73        with self._lock:
74            try:
75                if self.config_path.exists():
76                    with open(self.config_path, 'r', encoding='utf-8') as f:
77                        data = json.load(f)
78                    self._config = BacktestConfig(**data)
79                    logger.info(f"Config loaded from {self.config_path}")
80                else:
81                    # Create default config
82                    self._config = BacktestConfig()
83                    self.save_config()
84                    logger.info(f"Default config created at {self.config_path}")
85                
86                # Notify listeners
87                self._notify_listeners()
88                
89            except Exception as e:
90                logger.error(f"Error loading config: {e}")
91                if self._config is None:
92                    self._config = BacktestConfig()
93    
94    def save_config(self):
95        """Save current configuration to file"""
96        with self._lock:
97            try:
98                data = self._config.model_dump()
99                with open(self.config_path, 'w', encoding='utf-8') as f:
100                    json.dump(data, f, indent=4)
101                logger.info(f"Config saved to {self.config_path}")
102            except Exception as e:
103                logger.error(f"Error saving config: {e}")
104    
105    def get_config(self) -> BacktestConfig:
106        """Get current configuration"""
107        with self._lock:
108            if self._config is None:
109                self.reload_config()
110            return self._config
111    
112    def update_config(self, new_config: BacktestConfig):
113        """Update configuration"""
114        with self._lock:
115            self._config = new_config
116            self.save_config()
117            self._notify_listeners()
118    
119    def update_partial(self, updates: dict):
120        """Update specific config fields"""
121        with self._lock:
122            current = self._config.model_dump()
123            current.update(updates)
124            self._config = BacktestConfig(**current)
125            self.save_config()
126            self._notify_listeners()
127    
128    def add_listener(self, callback: Callable[[BacktestConfig], None]):
129        """Add a listener for config changes"""
130        with self._lock:
131            self._listeners.append(callback)
132    
133    def remove_listener(self, callback: Callable[[BacktestConfig], None]):
134        """Remove a config change listener"""
135        with self._lock:
136            if callback in self._listeners:
137                self._listeners.remove(callback)
138    
139    def _notify_listeners(self):
140        """Notify all listeners of config change"""
141        for listener in self._listeners:
142            try:
143                listener(self._config)
144            except Exception as e:
145                logger.error(f"Error in config listener: {e}")
146    
147    def start_watching(self):
148        """Start watching config file for changes"""
149        if self._observer is not None:
150            return
151            
152        try:
153            self._observer = Observer()
154            handler = ConfigFileHandler(self)
155            self._observer.schedule(handler, str(self.config_path.parent), recursive=False)
156            self._observer.start()
157            logger.info(f"Started watching config at {self.config_path.parent}")
158        except Exception as e:
159            logger.error(f"Error starting config watcher: {e}")
160    
161    def stop_watching(self):
162        """Stop watching config file"""
163        if self._observer is not None:
164            self._observer.stop()
165            self._observer.join()
166            self._observer = None
167            logger.info("Stopped watching config")
168    
169    def reset_to_defaults(self):
170        """Reset configuration to defaults"""
171        with self._lock:
172            self._config = BacktestConfig()
173            self.save_config()
174            self._notify_listeners()
175
176
177# Singleton instance
178_manager: Optional[ConfigManager] = None
179
180
181def get_config_manager() -> ConfigManager:
182    """Get singleton config manager instance"""
183    global _manager
184    if _manager is None:
185        _manager = ConfigManager()
186    return _manager
187