Initial commit — Pedrito Hechakuaa v0
Bot de Telegram para monitoreo de notificaciones judiciales del PJ Paraguay. Multi-tenant con onboarding conversacional, audit log, y polling automático. Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
140
database.py
Normal file
140
database.py
Normal file
@@ -0,0 +1,140 @@
|
||||
"""
|
||||
database.py — Persistencia de tenants y preferencias.
|
||||
|
||||
SQLite vía aiosqlite. En v1 esto se reemplaza por Postgres con el mismo interface público.
|
||||
|
||||
Schema:
|
||||
- tenants: un registro por abogado registrado.
|
||||
Las credenciales van cifradas con vault.py (Fernet derivada por chat_id).
|
||||
"""
|
||||
from __future__ import annotations
|
||||
|
||||
from datetime import datetime, timezone
|
||||
from pathlib import Path
|
||||
|
||||
import aiosqlite
|
||||
import structlog
|
||||
|
||||
logger = structlog.get_logger(__name__)
|
||||
|
||||
_conn: aiosqlite.Connection | None = None
|
||||
|
||||
_CREATE_AUDIT_LOG = """
|
||||
CREATE TABLE IF NOT EXISTS audit_log (
|
||||
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
||||
timestamp TEXT NOT NULL,
|
||||
timestamp_local TEXT,
|
||||
chat_id INTEGER,
|
||||
evento TEXT NOT NULL,
|
||||
detalle TEXT,
|
||||
resultado TEXT,
|
||||
duracion_ms INTEGER
|
||||
);
|
||||
CREATE INDEX IF NOT EXISTS idx_audit_chat_id ON audit_log(chat_id);
|
||||
CREATE INDEX IF NOT EXISTS idx_audit_timestamp ON audit_log(timestamp);
|
||||
CREATE INDEX IF NOT EXISTS idx_audit_evento ON audit_log(evento);
|
||||
"""
|
||||
|
||||
_CREATE_TENANTS = """
|
||||
CREATE TABLE IF NOT EXISTS tenants (
|
||||
chat_id INTEGER PRIMARY KEY,
|
||||
nombre_preferido TEXT,
|
||||
tono TEXT DEFAULT 'cotidiano',
|
||||
timezone TEXT DEFAULT 'America/Asuncion',
|
||||
credentials_enc TEXT,
|
||||
estado TEXT DEFAULT 'pendiente',
|
||||
consentimiento_aceptado INTEGER DEFAULT 0,
|
||||
created_at TEXT,
|
||||
last_active TEXT
|
||||
);
|
||||
"""
|
||||
|
||||
|
||||
def get_conn() -> aiosqlite.Connection:
|
||||
"""Retorna la conexión activa. Llamar solo después de init_db()."""
|
||||
return _assert_conn()
|
||||
|
||||
|
||||
def _now_iso() -> str:
|
||||
return datetime.now(timezone.utc).isoformat()
|
||||
|
||||
|
||||
async def init_db(db_path: Path) -> None:
|
||||
"""Crear tablas si no existen. Llamar una sola vez al arrancar."""
|
||||
global _conn
|
||||
db_path.parent.mkdir(parents=True, exist_ok=True)
|
||||
_conn = await aiosqlite.connect(str(db_path))
|
||||
_conn.row_factory = aiosqlite.Row
|
||||
await _conn.execute(_CREATE_TENANTS)
|
||||
# CREATE INDEX no soporta executescript — ejecutar sentencias separadas
|
||||
for stmt in _CREATE_AUDIT_LOG.strip().split(";"):
|
||||
stmt = stmt.strip()
|
||||
if stmt:
|
||||
await _conn.execute(stmt)
|
||||
await _conn.commit()
|
||||
logger.info("database_iniciada", path=str(db_path))
|
||||
|
||||
|
||||
def _assert_conn() -> aiosqlite.Connection:
|
||||
if _conn is None:
|
||||
raise RuntimeError("DB no inicializada — llamar a init_db() antes de usar database.")
|
||||
return _conn
|
||||
|
||||
|
||||
async def get_tenant(chat_id: int) -> dict | None:
|
||||
"""Retorna el tenant o None si no existe."""
|
||||
conn = _assert_conn()
|
||||
async with conn.execute(
|
||||
"SELECT * FROM tenants WHERE chat_id = ?", (chat_id,)
|
||||
) as cur:
|
||||
row = await cur.fetchone()
|
||||
return dict(row) if row else None
|
||||
|
||||
|
||||
async def save_tenant(chat_id: int, **fields: object) -> None:
|
||||
"""
|
||||
Crear o actualizar un tenant. Los kwargs son los campos de la tabla.
|
||||
Hace upsert: inserta si no existe, actualiza si ya existe.
|
||||
"""
|
||||
conn = _assert_conn()
|
||||
fields["last_active"] = _now_iso()
|
||||
existing = await get_tenant(chat_id)
|
||||
|
||||
if existing is None:
|
||||
fields.setdefault("created_at", _now_iso())
|
||||
columns = ["chat_id", *fields.keys()]
|
||||
placeholders = ", ".join("?" * len(columns))
|
||||
values = [chat_id, *fields.values()]
|
||||
await conn.execute(
|
||||
f"INSERT INTO tenants ({', '.join(columns)}) VALUES ({placeholders})",
|
||||
values,
|
||||
)
|
||||
logger.info("tenant_creado", chat_id=chat_id)
|
||||
else:
|
||||
set_clause = ", ".join(f"{k} = ?" for k in fields)
|
||||
values_upd = [*fields.values(), chat_id]
|
||||
await conn.execute(
|
||||
f"UPDATE tenants SET {set_clause} WHERE chat_id = ?",
|
||||
values_upd,
|
||||
)
|
||||
logger.info("tenant_actualizado", chat_id=chat_id, campos=list(fields.keys()))
|
||||
|
||||
await conn.commit()
|
||||
|
||||
|
||||
async def list_active_tenants() -> list[dict]:
|
||||
"""Retorna todos los tenants con estado='activo'."""
|
||||
conn = _assert_conn()
|
||||
async with conn.execute(
|
||||
"SELECT * FROM tenants WHERE estado = 'activo'"
|
||||
) as cur:
|
||||
rows = await cur.fetchall()
|
||||
return [dict(r) for r in rows]
|
||||
|
||||
|
||||
async def delete_tenant(chat_id: int) -> None:
|
||||
"""Elimina el tenant y sus datos de la DB."""
|
||||
conn = _assert_conn()
|
||||
await conn.execute("DELETE FROM tenants WHERE chat_id = ?", (chat_id,))
|
||||
await conn.commit()
|
||||
logger.info("tenant_eliminado", chat_id=chat_id)
|
||||
Reference in New Issue
Block a user