giragroup-bi-backend / database.py
Adzacam
feat: implement upsert logic for fact tables, update CORS settings, and extend estudiante models with enrollment fields
d7b4e65
Raw
History Blame
9.36 kB
import os
import datetime
from dotenv import load_dotenv
from sqlalchemy import create_engine, Column, Integer, String, Numeric, Boolean, DateTime, ForeignKey, CheckConstraint
from sqlalchemy.orm import declarative_base, sessionmaker
# Load environment variables from .env file
load_dotenv()
DATABASE_URL = os.getenv("DATABASE_URL")
if not DATABASE_URL:
# En producción (Hugging Face), DATABASE_URL DEBE estar en los Secrets del Space.
# Nunca hardcodear credenciales aquí — este archivo está en el repositorio público.
raise RuntimeError(
"DATABASE_URL no configurada. "
"Añádela en Hugging Face Space → Settings → Variables and secrets."
)
# Supabase Transaction Pooler (pgBouncer, puerto 6543):
# - pool_pre_ping: detecta conexiones muertas antes de usarlas
# - pool_size / max_overflow: límites conservadores para entorno serverless
# - connect_args: deshabilita prepared statements (requerido por pgBouncer Transaction mode)
engine = create_engine(
DATABASE_URL,
pool_pre_ping=True,
pool_size=5,
max_overflow=10,
pool_recycle=300, # recicla más rápido en pooler
connect_args={
"connect_timeout": 10,
"options": "-c statement_timeout=30000" # 30s max por query
},
)
SessionLocal = sessionmaker(autocommit=False, autoflush=False, bind=engine)
Base = declarative_base()
# =============================================================================
# DIMENSIONES — mapeadas exactamente al DDL de Supabase
# =============================================================================
class DimTiempo(Base):
__tablename__ = "dim_tiempo"
id_tiempo = Column(Integer, primary_key=True)
gestion = Column(Integer, nullable=False, default=2026)
semestre = Column(Integer, default=1)
mes = Column(String(20), default="Mayo")
class DimEstudiante(Base):
__tablename__ = "dim_estudiante"
id_estudiante = Column(Integer, primary_key=True, index=True)
nombre_completo = Column(String(200), nullable=False)
codigo_estudiante = Column(String(50))
genero = Column(String(20))
ciudad = Column(String(100))
nivel_academico = Column(String(100))
edad = Column(Integer)
ocupacion = Column(String(100))
estado_civil = Column(String(50))
class DimDocente(Base):
__tablename__ = "dim_docente"
id_docente = Column(Integer, primary_key=True)
nombre_completo = Column(String(200), nullable=False, default="Docente Generico")
area_especialidad = Column(String(200), default="Generico")
class DimModulo(Base):
__tablename__ = "dim_modulo"
id_modulo = Column(Integer, primary_key=True)
nombre_modulo = Column(String(200), nullable=False, default="Modulo Generico")
nombre_institucion = Column(String(200), nullable=False, default="GiraGroup")
programa = Column(String(200), default="General")
pos_code = Column(String(50))
class DimOrigenDocumental(Base):
"""Mapeada a la tabla dim_origen_documental en Supabase."""
__tablename__ = "dim_origen_documental"
id_documento = Column(Integer, primary_key=True)
tipo_documento = Column(String(10), default="SHEET")
nombre_archivo = Column(String(500), default="archivo_generico")
fecha_procesamiento = Column(DateTime, default=datetime.datetime.utcnow)
class Users(Base):
"""Tabla de usuarios/autenticación en Supabase."""
__tablename__ = "users"
id = Column(Integer, primary_key=True)
username = Column(String(100), unique=True, nullable=False, default="sistema")
hashed_password = Column(String(255), nullable=False, default="$placeholder$")
role = Column(String(20), default="admin")
created_at = Column(DateTime, default=datetime.datetime.utcnow)
class LogAuditoriaNlp(Base):
"""Tabla para registrar retroalimentación de correcciones MLOps."""
__tablename__ = "log_auditoria_nlp"
id_log = Column(Integer, primary_key=True, index=True)
texto_original = Column(String(500), nullable=False)
prediccion_beto = Column(String(200))
confianza_ia = Column(Numeric(5, 4))
correccion_humana = Column(String(200), nullable=False)
usuario_auditor = Column(Integer, ForeignKey("users.id"))
created_at = Column(DateTime, default=datetime.datetime.utcnow)
class DimCategoriaFinanciera(Base):
"""Nueva dimension para Egresos, Ingresos y OKRs Financieros"""
__tablename__ = "dim_categoria_financiera"
id_categoria = Column(Integer, primary_key=True)
nombre_categoria = Column(String(200), nullable=False)
tipo = Column(String(50)) # 'INGRESO', 'EGRESO', 'RENTABILIDAD', 'EBITDA'
# =============================================================================
# TABLAS DE HECHOS
# =============================================================================
class FactRendimientoAcademico(Base):
__tablename__ = "fact_rendimiento_academico"
id_hecho_aca = Column(Integer, primary_key=True, index=True)
id_estudiante = Column(Integer, ForeignKey("dim_estudiante.id_estudiante"))
id_docente = Column(Integer, ForeignKey("dim_docente.id_docente"))
id_modulo = Column(Integer, ForeignKey("dim_modulo.id_modulo"))
id_tiempo = Column(Integer, ForeignKey("dim_tiempo.id_tiempo"))
id_documento = Column(Integer, ForeignKey("dim_origen_documental.id_documento"))
id_usuario_carga = Column(Integer, ForeignKey("users.id"))
nota_final = Column(Numeric(5, 2))
asistencia_pct = Column(Numeric(5, 2))
incumplimiento_actividades_pct = Column(Numeric(5, 2), default=0.00)
nivel_confianza_ia = Column(Numeric(5, 4))
requiere_revision = Column(Boolean, default=False)
estado_academico = Column(String(50))
created_at = Column(DateTime, default=datetime.datetime.utcnow)
class FactSituacionFinanciera(Base):
__tablename__ = "fact_situacion_financiera"
id_hecho_fin = Column(Integer, primary_key=True)
id_estudiante = Column(Integer, ForeignKey("dim_estudiante.id_estudiante"))
id_tiempo = Column(Integer, ForeignKey("dim_tiempo.id_tiempo"))
monto_deuda = Column(Numeric(10, 2))
cuotas_impagas = Column(Integer)
estado_cartera = Column(String(20))
tipo_alerta = Column(String(20))
fecha_registro = Column(DateTime, default=datetime.datetime.utcnow)
class FactCobranzasProyectadas(Base):
"""Almacena montos despivotados de ingresos mensuales proyectados."""
__tablename__ = "fact_cobranzas_proyectadas"
id_hecho_cobro = Column(Integer, primary_key=True, index=True)
id_estudiante = Column(Integer, ForeignKey("dim_estudiante.id_estudiante"))
id_tiempo = Column(Integer, ForeignKey("dim_tiempo.id_tiempo")) # Referencia al mes proyectado
monto_esperado = Column(Numeric(10, 2))
estado_pago = Column(String(50)) # Ej. PAGADO, PENDIENTE
created_at = Column(DateTime, default=datetime.datetime.utcnow)
class FactEvaluacionDocente(Base):
"""Respuestas a encuestas de satisfacción docente y NPS."""
__tablename__ = "fact_evaluacion_docente"
id_hecho_eval = Column(Integer, primary_key=True)
id_docente = Column(Integer, ForeignKey("dim_docente.id_docente"))
id_modulo = Column(Integer, ForeignKey("dim_modulo.id_modulo"))
id_estudiante = Column(Integer, ForeignKey("dim_estudiante.id_estudiante"), nullable=True) # A veces anonimo
id_tiempo = Column(Integer, ForeignKey("dim_tiempo.id_tiempo"))
pregunta_bloque = Column(String(500))
puntuacion = Column(Numeric(4, 2)) # Ej. 1 al 5
comentario = Column(String(1000))
class FactMarketingInscripciones(Base):
"""Métricas de OKRs de Marketing y ventas."""
__tablename__ = "fact_marketing"
id_hecho_mkt = Column(Integer, primary_key=True)
id_modulo = Column(Integer, ForeignKey("dim_modulo.id_modulo"))
id_tiempo = Column(Integer, ForeignKey("dim_tiempo.id_tiempo"))
leads = Column(Integer, default=0)
reservas = Column(Integer, default=0)
inscritos = Column(Integer, default=0)
costo_programa= Column(Numeric(12, 2))
class FactRentabilidadPresupuesto(Base):
"""Indicadores de Rentabilidad, Egresos y EBITDA (Ejecutado vs Meta)."""
__tablename__ = "fact_rentabilidad"
id_hecho_rent = Column(Integer, primary_key=True)
id_modulo = Column(Integer, ForeignKey("dim_modulo.id_modulo"))
id_tiempo = Column(Integer, ForeignKey("dim_tiempo.id_tiempo"))
id_categoria = Column(Integer, ForeignKey("dim_categoria_financiera.id_categoria"))
monto_ejecutado = Column(Numeric(14, 2))
monto_meta = Column(Numeric(14, 2))
def get_db():
"""
Dependency injection helper to yield a database session.
Guarantees that the session is closed after the request completes.
"""
db = SessionLocal()
try:
yield db
finally:
db.close()
# Database tables creation is handled manually in Supabase.
# Base.metadata.create_all(bind=engine) has been removed.