""" EU-Utility - SQLite Data Layer Persistent data storage using SQLite for settings, plugin state, user preferences, and session data. """ import json import sqlite3 import threading import platform from pathlib import Path from typing import Any, Dict, List, Optional, Union from datetime import datetime from dataclasses import dataclass, asdict from contextlib import contextmanager @dataclass class PluginState: """Plugin state record.""" plugin_id: str enabled: bool = False version: str = "" settings: Dict[str, Any] = None last_loaded: Optional[str] = None load_count: int = 0 error_count: int = 0 def __post_init__(self): if self.settings is None: self.settings = {} @dataclass class UserPreference: """User preference record.""" key: str value: Any category: str = "general" updated_at: Optional[str] = None @dataclass class SessionData: """Session data record.""" session_id: str started_at: str ended_at: Optional[str] = None plugin_stats: Dict[str, Any] = None system_info: Dict[str, Any] = None def __post_init__(self): if self.plugin_stats is None: self.plugin_stats = {} if self.system_info is None: self.system_info = {} class SQLiteDataStore: """ SQLite-based persistent data store for EU-Utility. Features: - Thread-safe database access - Connection pooling - Automatic migrations - JSON support for complex data """ _instance = None _lock = threading.Lock() def __new__(cls): if cls._instance is None: with cls._lock: if cls._instance is None: cls._instance = super().__new__(cls) cls._instance._initialized = False return cls._instance def __init__(self, db_path: str = "data/eu_utility.db"): if self._initialized: return self.db_path = Path(db_path) self.db_path.parent.mkdir(parents=True, exist_ok=True) # Thread-local connections self._local = threading.local() self._init_lock = threading.Lock() # Initialize database self._init_database() self._initialized = True def _get_connection(self) -> sqlite3.Connection: """Get thread-local database connection.""" if not hasattr(self._local, 'connection') or self._local.connection is None: self._local.connection = sqlite3.connect( self.db_path, check_same_thread=False, detect_types=sqlite3.PARSE_DECLTYPES ) self._local.connection.row_factory = sqlite3.Row # Enable foreign keys self._local.connection.execute("PRAGMA foreign_keys = ON") return self._local.connection @contextmanager def _transaction(self): """Context manager for database transactions.""" conn = self._get_connection() try: yield conn conn.commit() except Exception: conn.rollback() raise def _init_database(self): """Initialize database schema.""" with self._transaction() as conn: # Plugin states table conn.execute(""" CREATE TABLE IF NOT EXISTS plugin_states ( plugin_id TEXT PRIMARY KEY, enabled INTEGER DEFAULT 0, version TEXT DEFAULT '', settings TEXT DEFAULT '{}', last_loaded TEXT, load_count INTEGER DEFAULT 0, error_count INTEGER DEFAULT 0, created_at TEXT DEFAULT CURRENT_TIMESTAMP, updated_at TEXT DEFAULT CURRENT_TIMESTAMP ) """) # User preferences table conn.execute(""" CREATE TABLE IF NOT EXISTS user_preferences ( key TEXT PRIMARY KEY, value TEXT, category TEXT DEFAULT 'general', updated_at TEXT DEFAULT CURRENT_TIMESTAMP ) """) # Session data table conn.execute(""" CREATE TABLE IF NOT EXISTS sessions ( session_id TEXT PRIMARY KEY, started_at TEXT DEFAULT CURRENT_TIMESTAMP, ended_at TEXT, plugin_stats TEXT DEFAULT '{}', system_info TEXT DEFAULT '{}' ) """) # Activity log table conn.execute(""" CREATE TABLE IF NOT EXISTS activity_log ( id INTEGER PRIMARY KEY AUTOINCREMENT, timestamp TEXT DEFAULT CURRENT_TIMESTAMP, category TEXT, action TEXT, details TEXT, plugin_id TEXT ) """) # Dashboard widgets table conn.execute(""" CREATE TABLE IF NOT EXISTS dashboard_widgets ( widget_id TEXT PRIMARY KEY, widget_type TEXT, position_row INTEGER DEFAULT 0, position_col INTEGER DEFAULT 0, size_width INTEGER DEFAULT 1, size_height INTEGER DEFAULT 1, config TEXT DEFAULT '{}', enabled INTEGER DEFAULT 1 ) """) # Hotkeys table conn.execute(""" CREATE TABLE IF NOT EXISTS hotkeys ( action TEXT PRIMARY KEY, key_combo TEXT, enabled INTEGER DEFAULT 1, plugin_id TEXT ) """) # Create indexes conn.execute(""" CREATE INDEX IF NOT EXISTS idx_plugin_states_enabled ON plugin_states(enabled) """) conn.execute(""" CREATE INDEX IF NOT EXISTS idx_activity_category ON activity_log(category) """) conn.execute(""" CREATE INDEX IF NOT EXISTS idx_activity_timestamp ON activity_log(timestamp) """) # === Plugin State Management === def save_plugin_state(self, state: PluginState) -> bool: """Save plugin state to database.""" try: with self._transaction() as conn: conn.execute(""" INSERT INTO plugin_states (plugin_id, enabled, version, settings, last_loaded, load_count, error_count, updated_at) VALUES (?, ?, ?, ?, ?, ?, ?, ?) ON CONFLICT(plugin_id) DO UPDATE SET enabled = excluded.enabled, version = excluded.version, settings = excluded.settings, last_loaded = excluded.last_loaded, load_count = excluded.load_count, error_count = excluded.error_count, updated_at = excluded.updated_at """, ( state.plugin_id, int(state.enabled), state.version, json.dumps(state.settings), state.last_loaded, state.load_count, state.error_count, datetime.now().isoformat() )) return True except Exception as e: print(f"[SQLite] Error saving plugin state: {e}") return False def load_plugin_state(self, plugin_id: str) -> Optional[PluginState]: """Load plugin state from database.""" try: conn = self._get_connection() row = conn.execute( "SELECT * FROM plugin_states WHERE plugin_id = ?", (plugin_id,) ).fetchone() if row: return PluginState( plugin_id=row['plugin_id'], enabled=bool(row['enabled']), version=row['version'], settings=json.loads(row['settings']), last_loaded=row['last_loaded'], load_count=row['load_count'], error_count=row['error_count'] ) return None except Exception as e: print(f"[SQLite] Error loading plugin state: {e}") return None def get_all_plugin_states(self) -> Dict[str, PluginState]: """Get all plugin states.""" try: conn = self._get_connection() rows = conn.execute("SELECT * FROM plugin_states").fetchall() states = {} for row in rows: states[row['plugin_id']] = PluginState( plugin_id=row['plugin_id'], enabled=bool(row['enabled']), version=row['version'], settings=json.loads(row['settings']), last_loaded=row['last_loaded'], load_count=row['load_count'], error_count=row['error_count'] ) return states except Exception as e: print(f"[SQLite] Error loading plugin states: {e}") return {} # === User Preferences === def set_preference(self, key: str, value: Any, category: str = "general") -> bool: """Set a user preference.""" try: with self._transaction() as conn: conn.execute(""" INSERT INTO user_preferences (key, value, category, updated_at) VALUES (?, ?, ?, ?) ON CONFLICT(key) DO UPDATE SET value = excluded.value, category = excluded.category, updated_at = excluded.updated_at """, (key, json.dumps(value), category, datetime.now().isoformat())) return True except Exception as e: print(f"[SQLite] Error setting preference: {e}") return False def get_preference(self, key: str, default: Any = None) -> Any: """Get a user preference.""" try: conn = self._get_connection() row = conn.execute( "SELECT value FROM user_preferences WHERE key = ?", (key,) ).fetchone() if row: return json.loads(row['value']) return default except Exception as e: print(f"[SQLite] Error getting preference: {e}") return default def get_preferences_by_category(self, category: str) -> Dict[str, Any]: """Get all preferences in a category.""" try: conn = self._get_connection() rows = conn.execute( "SELECT key, value FROM user_preferences WHERE category = ?", (category,) ).fetchall() return {row['key']: json.loads(row['value']) for row in rows} except Exception as e: print(f"[SQLite] Error getting preferences: {e}") return {} def delete_preference(self, key: str) -> bool: """Delete a user preference.""" try: with self._transaction() as conn: conn.execute("DELETE FROM user_preferences WHERE key = ?", (key,)) return True except Exception as e: print(f"[SQLite] Error deleting preference: {e}") return False # === Session Management === def start_session(self) -> str: """Start a new session and return session ID.""" session_id = datetime.now().strftime("%Y%m%d_%H%M%S_%f") system_info = { 'platform': platform.system(), 'version': platform.version(), 'machine': platform.machine(), 'processor': platform.processor() } try: with self._transaction() as conn: conn.execute(""" INSERT INTO sessions (session_id, system_info) VALUES (?, ?) """, (session_id, json.dumps(system_info))) return session_id except Exception as e: print(f"[SQLite] Error starting session: {e}") return session_id def end_session(self, session_id: str, plugin_stats: Dict = None) -> bool: """End a session.""" try: with self._transaction() as conn: conn.execute(""" UPDATE sessions SET ended_at = ?, plugin_stats = ? WHERE session_id = ? """, ( datetime.now().isoformat(), json.dumps(plugin_stats or {}), session_id )) return True except Exception as e: print(f"[SQLite] Error ending session: {e}") return False # === Activity Logging === def log_activity(self, category: str, action: str, details: str = "", plugin_id: str = None) -> bool: """Log an activity.""" try: with self._transaction() as conn: conn.execute(""" INSERT INTO activity_log (category, action, details, plugin_id) VALUES (?, ?, ?, ?) """, (category, action, details, plugin_id)) return True except Exception as e: print(f"[SQLite] Error logging activity: {e}") return False def get_recent_activity(self, limit: int = 50, category: str = None) -> List[Dict]: """Get recent activity log entries.""" try: conn = self._get_connection() if category: rows = conn.execute(""" SELECT * FROM activity_log WHERE category = ? ORDER BY timestamp DESC LIMIT ? """, (category, limit)).fetchall() else: rows = conn.execute(""" SELECT * FROM activity_log ORDER BY timestamp DESC LIMIT ? """, (limit,)).fetchall() return [dict(row) for row in rows] except Exception as e: print(f"[SQLite] Error getting activity: {e}") return [] def clear_old_activity(self, days: int = 30) -> int: """Clear activity logs older than specified days.""" try: with self._transaction() as conn: result = conn.execute(""" DELETE FROM activity_log WHERE timestamp < datetime('now', '-{} days') """.format(days)) return result.rowcount except Exception as e: print(f"[SQLite] Error clearing activity: {e}") return 0 # === Dashboard Widgets === def save_widget_config(self, widget_id: str, widget_type: str, row: int, col: int, width: int, height: int, config: Dict = None, enabled: bool = True) -> bool: """Save dashboard widget configuration.""" try: with self._transaction() as conn: conn.execute(""" INSERT INTO dashboard_widgets (widget_id, widget_type, position_row, position_col, size_width, size_height, config, enabled) VALUES (?, ?, ?, ?, ?, ?, ?, ?) ON CONFLICT(widget_id) DO UPDATE SET widget_type = excluded.widget_type, position_row = excluded.position_row, position_col = excluded.position_col, size_width = excluded.size_width, size_height = excluded.size_height, config = excluded.config, enabled = excluded.enabled """, ( widget_id, widget_type, row, col, width, height, json.dumps(config or {}), int(enabled) )) return True except Exception as e: print(f"[SQLite] Error saving widget config: {e}") return False def load_widget_configs(self) -> List[Dict]: """Load all widget configurations.""" try: conn = self._get_connection() rows = conn.execute(""" SELECT * FROM dashboard_widgets WHERE enabled = 1 ORDER BY position_row, position_col """).fetchall() widgets = [] for row in rows: widgets.append({ 'widget_id': row['widget_id'], 'widget_type': row['widget_type'], 'position': {'row': row['position_row'], 'col': row['position_col']}, 'size': {'width': row['size_width'], 'height': row['size_height']}, 'config': json.loads(row['config']) }) return widgets except Exception as e: print(f"[SQLite] Error loading widget configs: {e}") return [] def delete_widget(self, widget_id: str) -> bool: """Delete a widget configuration.""" try: with self._transaction() as conn: conn.execute("DELETE FROM dashboard_widgets WHERE widget_id = ?", (widget_id,)) return True except Exception as e: print(f"[SQLite] Error deleting widget: {e}") return False # === Hotkeys === def save_hotkey(self, action: str, key_combo: str, enabled: bool = True, plugin_id: str = None) -> bool: """Save a hotkey configuration.""" try: with self._transaction() as conn: conn.execute(""" INSERT INTO hotkeys (action, key_combo, enabled, plugin_id) VALUES (?, ?, ?, ?) ON CONFLICT(action) DO UPDATE SET key_combo = excluded.key_combo, enabled = excluded.enabled, plugin_id = excluded.plugin_id """, (action, key_combo, int(enabled), plugin_id)) return True except Exception as e: print(f"[SQLite] Error saving hotkey: {e}") return False def get_hotkeys(self, plugin_id: str = None) -> Dict[str, Dict]: """Get all hotkey configurations.""" try: conn = self._get_connection() if plugin_id: rows = conn.execute( "SELECT * FROM hotkeys WHERE plugin_id = ?", (plugin_id,) ).fetchall() else: rows = conn.execute("SELECT * FROM hotkeys").fetchall() return { row['action']: { 'key_combo': row['key_combo'], 'enabled': bool(row['enabled']), 'plugin_id': row['plugin_id'] } for row in rows } except Exception as e: print(f"[SQLite] Error getting hotkeys: {e}") return {} def delete_hotkey(self, action: str) -> bool: """Delete a hotkey configuration.""" try: with self._transaction() as conn: conn.execute("DELETE FROM hotkeys WHERE action = ?", (action,)) return True except Exception as e: print(f"[SQLite] Error deleting hotkey: {e}") return False # === Utility Methods === def get_stats(self) -> Dict: """Get database statistics.""" try: conn = self._get_connection() stats = {} tables = ['plugin_states', 'user_preferences', 'sessions', 'activity_log', 'dashboard_widgets', 'hotkeys'] for table in tables: count = conn.execute(f"SELECT COUNT(*) FROM {table}").fetchone()[0] stats[table] = count # Database size db_size = self.db_path.stat().st_size if self.db_path.exists() else 0 stats['db_size_bytes'] = db_size stats['db_size_mb'] = round(db_size / (1024 * 1024), 2) return stats except Exception as e: print(f"[SQLite] Error getting stats: {e}") return {} def vacuum(self) -> bool: """Optimize database.""" try: conn = self._get_connection() conn.execute("VACUUM") return True except Exception as e: print(f"[SQLite] Error vacuuming database: {e}") return False def close(self): """Close database connection.""" if hasattr(self._local, 'connection') and self._local.connection: self._local.connection.close() self._local.connection = None # Singleton instance _sqlite_store = None _sqlite_lock = threading.Lock() def get_sqlite_store() -> SQLiteDataStore: """Get the global SQLiteDataStore instance.""" global _sqlite_store if _sqlite_store is None: with _sqlite_lock: if _sqlite_store is None: _sqlite_store = SQLiteDataStore() return _sqlite_store