121 lines
3.9 KiB
Python
121 lines
3.9 KiB
Python
from __future__ import annotations
|
|
|
|
from contextlib import contextmanager
|
|
from pathlib import Path
|
|
import json
|
|
import sqlite3
|
|
from typing import Any, Iterator
|
|
|
|
|
|
def connect(database_path: Path) -> sqlite3.Connection:
|
|
database_path.parent.mkdir(parents=True, exist_ok=True)
|
|
conn = sqlite3.connect(database_path)
|
|
conn.row_factory = sqlite3.Row
|
|
conn.execute("PRAGMA journal_mode=WAL")
|
|
conn.execute("PRAGMA foreign_keys=ON")
|
|
return conn
|
|
|
|
|
|
@contextmanager
|
|
def session(database_path: Path) -> Iterator[sqlite3.Connection]:
|
|
conn = connect(database_path)
|
|
try:
|
|
yield conn
|
|
conn.commit()
|
|
except Exception:
|
|
conn.rollback()
|
|
raise
|
|
finally:
|
|
conn.close()
|
|
|
|
|
|
def init_db(conn: sqlite3.Connection) -> None:
|
|
conn.executescript(
|
|
"""
|
|
CREATE TABLE IF NOT EXISTS raw_items (
|
|
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
|
source TEXT NOT NULL,
|
|
source_item_id TEXT NOT NULL,
|
|
source_url TEXT NOT NULL,
|
|
content_type TEXT NOT NULL,
|
|
author_id TEXT,
|
|
author_name TEXT,
|
|
title TEXT,
|
|
published_at INTEGER,
|
|
published_at_text TEXT,
|
|
collected_at INTEGER NOT NULL,
|
|
updated_at_source INTEGER,
|
|
content TEXT NOT NULL,
|
|
raw_json TEXT NOT NULL,
|
|
content_hash TEXT NOT NULL,
|
|
analysis_status TEXT NOT NULL DEFAULT 'pending',
|
|
UNIQUE(source, source_item_id)
|
|
);
|
|
|
|
CREATE TABLE IF NOT EXISTS analysis_results (
|
|
raw_item_id INTEGER PRIMARY KEY,
|
|
model TEXT NOT NULL,
|
|
sentiment TEXT NOT NULL,
|
|
is_positive INTEGER NOT NULL,
|
|
is_negative INTEGER NOT NULL,
|
|
has_actionable_feedback INTEGER NOT NULL,
|
|
feedback_types TEXT NOT NULL,
|
|
reply_recommended INTEGER NOT NULL,
|
|
reply_priority TEXT NOT NULL,
|
|
reply_suggestion TEXT NOT NULL,
|
|
summary TEXT NOT NULL,
|
|
priority TEXT NOT NULL,
|
|
confidence REAL NOT NULL,
|
|
reason TEXT NOT NULL,
|
|
model_json TEXT NOT NULL,
|
|
analyzed_at INTEGER NOT NULL,
|
|
FOREIGN KEY(raw_item_id) REFERENCES raw_items(id) ON DELETE CASCADE
|
|
);
|
|
|
|
CREATE TABLE IF NOT EXISTS work_items (
|
|
raw_item_id INTEGER PRIMARY KEY,
|
|
status TEXT NOT NULL DEFAULT 'new',
|
|
owner TEXT NOT NULL DEFAULT '',
|
|
notes TEXT NOT NULL DEFAULT '',
|
|
last_handled_at INTEGER,
|
|
created_at INTEGER NOT NULL,
|
|
updated_at INTEGER NOT NULL,
|
|
FOREIGN KEY(raw_item_id) REFERENCES raw_items(id) ON DELETE CASCADE
|
|
);
|
|
|
|
CREATE TABLE IF NOT EXISTS sync_state (
|
|
key TEXT PRIMARY KEY,
|
|
value TEXT NOT NULL,
|
|
updated_at INTEGER NOT NULL
|
|
);
|
|
|
|
CREATE TABLE IF NOT EXISTS sync_runs (
|
|
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
|
started_at INTEGER NOT NULL,
|
|
finished_at INTEGER,
|
|
mode TEXT NOT NULL,
|
|
status TEXT NOT NULL,
|
|
message TEXT NOT NULL DEFAULT '',
|
|
stats_json TEXT NOT NULL DEFAULT '{}'
|
|
);
|
|
|
|
CREATE INDEX IF NOT EXISTS idx_raw_items_collected_at ON raw_items(collected_at DESC);
|
|
CREATE INDEX IF NOT EXISTS idx_raw_items_content_type ON raw_items(content_type);
|
|
CREATE INDEX IF NOT EXISTS idx_raw_items_analysis_status ON raw_items(analysis_status);
|
|
CREATE INDEX IF NOT EXISTS idx_work_items_status ON work_items(status);
|
|
"""
|
|
)
|
|
|
|
|
|
def encode_json(value: Any) -> str:
|
|
return json.dumps(value, ensure_ascii=False, separators=(",", ":"))
|
|
|
|
|
|
def decode_json(value: str | None, default: Any = None) -> Any:
|
|
if value is None:
|
|
return default
|
|
try:
|
|
return json.loads(value)
|
|
except json.JSONDecodeError:
|
|
return default
|