v0.3: SQLite persistence + ProjectService + dependency resolution
- New: services/sensu_db.py (SQLite, 5 tables) - New: services/project_service.py (project registry, port allocation, dep resolution) - Integrated into main.py (step 11.6) - Tests: 23/23 passing (15 original + 8 new)
This commit is contained in:
@@ -0,0 +1,119 @@
|
||||
#!/usr/bin/env python3
|
||||
"""SenSu SQLite 持久化层 — 替代零散 JSON 文件"""
|
||||
import sqlite3, os, json, logging, threading
|
||||
from typing import Optional, Dict, List, Any
|
||||
|
||||
logger = logging.getLogger(__name__)
|
||||
|
||||
DEFAULT_DB_PATH = "data/sensu.db"
|
||||
|
||||
class SenSuDB:
|
||||
def __init__(self, db_path: str = DEFAULT_DB_PATH):
|
||||
os.makedirs(os.path.dirname(db_path) or ".", exist_ok=True)
|
||||
self.db_path = db_path
|
||||
self._local = threading.local()
|
||||
self._init_schema()
|
||||
|
||||
@property
|
||||
def conn(self):
|
||||
if not hasattr(self._local, "conn") or self._local.conn is None:
|
||||
self._local.conn = sqlite3.connect(self.db_path)
|
||||
self._local.conn.row_factory = sqlite3.Row
|
||||
self._local.conn.execute("PRAGMA journal_mode=WAL")
|
||||
self._local.conn.execute("PRAGMA foreign_keys=ON")
|
||||
return self._local.conn
|
||||
|
||||
def _init_schema(self):
|
||||
c = self.conn
|
||||
c.executescript("""
|
||||
CREATE TABLE IF NOT EXISTS plugins (
|
||||
name TEXT PRIMARY KEY,
|
||||
version TEXT DEFAULT '0.1.0',
|
||||
status TEXT DEFAULT 'unloaded',
|
||||
config_json TEXT DEFAULT '{}',
|
||||
permissions_json TEXT DEFAULT '[]',
|
||||
depends_on TEXT DEFAULT '[]',
|
||||
project_path TEXT,
|
||||
project_port INTEGER,
|
||||
created_at TEXT DEFAULT (datetime('now')),
|
||||
updated_at TEXT DEFAULT (datetime('now'))
|
||||
);
|
||||
CREATE TABLE IF NOT EXISTS permissions (
|
||||
plugin_name TEXT,
|
||||
permission TEXT,
|
||||
granted INTEGER DEFAULT 0,
|
||||
granted_at TEXT,
|
||||
PRIMARY KEY (plugin_name, permission)
|
||||
);
|
||||
CREATE TABLE IF NOT EXISTS config_kv (
|
||||
key TEXT PRIMARY KEY,
|
||||
value TEXT,
|
||||
updated_at TEXT DEFAULT (datetime('now'))
|
||||
);
|
||||
CREATE TABLE IF NOT EXISTS audit_log (
|
||||
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
||||
plugin_name TEXT,
|
||||
action TEXT,
|
||||
detail TEXT,
|
||||
created_at TEXT DEFAULT (datetime('now'))
|
||||
);
|
||||
""")
|
||||
c.commit()
|
||||
|
||||
# -- Plugin CRUD --
|
||||
def save_plugin(self, name: str, **kwargs):
|
||||
fields = ["name"] + list(kwargs.keys())
|
||||
placeholders = ["?"] * len(fields)
|
||||
values = [name] + list(kwargs.values())
|
||||
for k in ["config", "permissions", "depends_on"]:
|
||||
if k in kwargs and not isinstance(kwargs[k], str):
|
||||
kwargs[k] = json.dumps(kwargs[k])
|
||||
idx = fields.index(k)
|
||||
values[idx] = kwargs[k]
|
||||
sql = f"INSERT OR REPLACE INTO plugins ({','.join(fields)}) VALUES ({','.join(placeholders)})"
|
||||
self.conn.execute("UPDATE plugins SET updated_at=datetime('now') WHERE name=?", [name])
|
||||
self.conn.execute(sql, values)
|
||||
self.conn.commit()
|
||||
|
||||
def get_plugin(self, name: str) -> Optional[Dict]:
|
||||
row = self.conn.execute("SELECT * FROM plugins WHERE name=?", [name]).fetchone()
|
||||
if not row: return None
|
||||
d = dict(row)
|
||||
for f in ["config_json", "permissions_json", "depends_on"]:
|
||||
if d.get(f):
|
||||
try: d[f.replace("_json","")] = json.loads(d.pop(f))
|
||||
except: pass
|
||||
return d
|
||||
|
||||
def list_plugins(self) -> List[Dict]:
|
||||
return [dict(r) for r in self.conn.execute("SELECT name,version,status,project_path,project_port FROM plugins").fetchall()]
|
||||
|
||||
# -- Permissions --
|
||||
def grant_permission(self, plugin: str, perm: str):
|
||||
self.conn.execute(
|
||||
"INSERT OR REPLACE INTO permissions(plugin_name,permission,granted,granted_at) VALUES(?,?,1,datetime('now'))",
|
||||
[plugin, perm])
|
||||
self.conn.commit()
|
||||
|
||||
def check_permission(self, plugin: str, perm: str) -> bool:
|
||||
r = self.conn.execute("SELECT granted FROM permissions WHERE plugin_name=? AND permission=?", [plugin, perm]).fetchone()
|
||||
return bool(r and r[0])
|
||||
|
||||
# -- Config --
|
||||
def set_config(self, key: str, value: str):
|
||||
self.conn.execute("INSERT OR REPLACE INTO config_kv(key,value,updated_at) VALUES(?,?,datetime('now'))", [key, value])
|
||||
self.conn.commit()
|
||||
|
||||
def get_config(self, key: str, default=None) -> Optional[str]:
|
||||
r = self.conn.execute("SELECT value FROM config_kv WHERE key=?", [key]).fetchone()
|
||||
return r[0] if r else default
|
||||
|
||||
# -- Audit --
|
||||
def log_audit(self, plugin: str, action: str, detail: str = ""):
|
||||
self.conn.execute("INSERT INTO audit_log(plugin_name,action,detail) VALUES(?,?,?)", [plugin, action, detail])
|
||||
self.conn.commit()
|
||||
|
||||
def close(self):
|
||||
if hasattr(self._local, "conn") and self._local.conn:
|
||||
self._local.conn.close()
|
||||
self._local.conn = None
|
||||
Reference in New Issue
Block a user