614 lines
22 KiB
Python
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
|