Files
IPSExtractor/application/database.py
T

232 lines
8.4 KiB
Python

import sqlite3
import json
import logging
from datetime import datetime
from typing import List, Dict, Any, Optional
import os
logger = logging.getLogger(__name__)
class DatabaseManager:
"""
Gestiona la persistencia de resultados de preclasificación en una base de datos SQLite.
Estructura de tablas:
- runs: Registra cada ejecución global (fecha, ruta raíz, configuración).
- results: Almacena el estado de procesamiento de cada archivo individual.
- matches: Detalle de campos específicos encontrados en cada archivo.
"""
def __init__(self, db_path: str = "preclassification.db"):
self.db_path = db_path
self._init_db()
def _init_db(self):
"""
Inicializa el esquema de la base de datos si no existe.
Crea las tablas 'runs', 'results' y 'matches' con sus respectivas llaves foráneas.
"""
try:
conn = sqlite3.connect(self.db_path)
c = conn.cursor()
# Tabla de ejecuciones
c.execute('''
CREATE TABLE IF NOT EXISTS runs (
id INTEGER PRIMARY KEY AUTOINCREMENT,
timestamp TEXT NOT NULL,
root_path TEXT NOT NULL,
config TEXT
)
''')
# Tabla de resultados por archivo/radicado
c.execute('''
CREATE TABLE IF NOT EXISTS results (
id INTEGER PRIMARY KEY AUTOINCREMENT,
run_id INTEGER NOT NULL,
radicado TEXT NOT NULL,
file_path TEXT,
status TEXT NOT NULL, -- 'MATCH', 'NO_MATCH', 'ERROR'
raw_response TEXT,
FOREIGN KEY(run_id) REFERENCES runs(id)
)
''')
# Tabla de coincidencias detalladas (campo por campo)
c.execute('''
CREATE TABLE IF NOT EXISTS matches (
id INTEGER PRIMARY KEY AUTOINCREMENT,
result_id INTEGER NOT NULL,
key TEXT NOT NULL,
expected_value TEXT,
found_value TEXT,
exists_bool BOOLEAN NOT NULL,
FOREIGN KEY(result_id) REFERENCES results(id)
)
''')
conn.commit()
conn.close()
except Exception as e:
logger.error(f"Error inicializando base de datos: {e}")
def create_run(self, root_path: str, config: Dict[str, Any]) -> int:
"""
Registra una nueva sesión de procesamiento.
Args:
root_path: Directorio raíz procesado.
config: Diccionario de configuración usado (se guarda como JSON).
Returns:
ID de la ejecución creada.
"""
conn = sqlite3.connect(self.db_path)
c = conn.cursor()
timestamp = datetime.now().isoformat()
c.execute('INSERT INTO runs (timestamp, root_path, config) VALUES (?, ?, ?)',
(timestamp, root_path, json.dumps(config)))
run_id = c.lastrowid
conn.commit()
conn.close()
return run_id
def add_result(self, run_id: int, radicado: str, file_path: str, status: str, raw_response: str) -> int:
"""
Añade el resultado del procesamiento de un archivo.
Args:
run_id: ID de la ejecución padre.
radicado: Identificador del radicado.
file_path: Ruta al archivo físico.
status: Estado final ('MATCH', 'NO_MATCH', 'ERROR').
raw_response: Respuesta completa del modelo.
Returns:
ID del resultado insertado.
"""
conn = sqlite3.connect(self.db_path)
c = conn.cursor()
c.execute('INSERT INTO results (run_id, radicado, file_path, status, raw_response) VALUES (?, ?, ?, ?, ?)',
(run_id, radicado, file_path, status, raw_response))
result_id = c.lastrowid
conn.commit()
conn.close()
return result_id
def add_match(self, result_id: int, key: str, expected_value: Optional[str], found_value: Optional[str], exists: bool):
"""
Registra el hallazgo (o ausencia) de un campo específico en un documento.
Args:
result_id: ID del resultado al que pertenece.
key: Nombre del campo/llave.
expected_value: Valor que se esperaba encontrar (si aplica).
found_value: Valor extraído por el modelo.
exists: Booleano que indica si el campo fue efectivamente localizado.
"""
conn = sqlite3.connect(self.db_path)
c = conn.cursor()
c.execute('INSERT INTO matches (result_id, key, expected_value, found_value, exists_bool) VALUES (?, ?, ?, ?, ?)',
(result_id, key, expected_value, found_value, exists))
conn.commit()
conn.close()
def get_run_results(self, run_id: int) -> List[Dict[str, Any]]:
"""Obtiene resultados completos de una ejecución."""
conn = sqlite3.connect(self.db_path)
conn.row_factory = sqlite3.Row
c = conn.cursor()
c.execute('''
SELECT r.radicado, r.file_path, r.status, m.key, m.expected_value, m.found_value, m.exists_bool
FROM results r
LEFT JOIN matches m ON r.id = m.result_id
WHERE r.run_id = ?
''', (run_id,))
rows = c.fetchall()
conn.close()
return [dict(row) for row in rows]
def get_all_runs(self) -> List[Dict[str, Any]]:
"""Obtiene lista de todos los runs ordenados por fecha descendente."""
conn = sqlite3.connect(self.db_path)
conn.row_factory = sqlite3.Row
c = conn.cursor()
c.execute("SELECT * FROM runs ORDER BY timestamp DESC")
rows = c.fetchall()
conn.close()
return [dict(row) for row in rows]
def search_results(self, run_id: Optional[int] = None, status: Optional[str] = None, text: Optional[str] = None) -> List[Dict[str, Any]]:
"""Busca resultados con filtros."""
conn = sqlite3.connect(self.db_path)
conn.row_factory = sqlite3.Row
c = conn.cursor()
query = '''
SELECT r.id, r.run_id, r.radicado, r.file_path, r.status, r.raw_response, runs.timestamp
FROM results r
JOIN runs ON r.run_id = runs.id
WHERE 1=1
'''
params = []
if run_id:
query += " AND r.run_id = ?"
params.append(run_id)
if status and status != "Todos":
query += " AND r.status = ?"
params.append(status)
if text:
# Busqueda simple en radicado o path
query += " AND (r.radicado LIKE ? OR r.file_path LIKE ?)"
wild = f"%{text}%"
params.extend([wild, wild])
query += " ORDER BY r.id DESC LIMIT 1000"
c.execute(query, params)
rows = c.fetchall()
conn.close()
return [dict(row) for row in rows]
def update_result_status(self, result_id: int, status: str) -> None:
"""Actualiza el status de un resultado existente."""
conn = sqlite3.connect(self.db_path)
c = conn.cursor()
c.execute("UPDATE results SET status = ? WHERE id = ?", (status, result_id))
conn.commit()
conn.close()
def get_matches_for_result(self, result_id: int) -> List[Dict[str, Any]]:
"""Obtiene el detalle de matches para un resultado específico."""
conn = sqlite3.connect(self.db_path)
conn.row_factory = sqlite3.Row
c = conn.cursor()
c.execute("SELECT * FROM matches WHERE result_id = ?", (result_id,))
rows = c.fetchall()
conn.close()
return [dict(row) for row in rows]
def get_run(self, run_id: int) -> Optional[Dict[str, Any]]:
"""Obtiene un run por ID (incluye config como dict si aplica)."""
conn = sqlite3.connect(self.db_path)
conn.row_factory = sqlite3.Row
c = conn.cursor()
c.execute("SELECT * FROM runs WHERE id = ?", (run_id,))
row = c.fetchone()
conn.close()
if not row:
return None
d = dict(row)
try:
cfg_raw = d.get("config")
d["config"] = json.loads(cfg_raw) if cfg_raw else {}
except Exception:
d["config"] = {}
return d