EU-Utility/core/data/sqlite_store.py

614 lines
22 KiB
Python

"""
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