Spaces:
Sleeping
Sleeping
Adzacam
feat: implement upsert logic for fact tables, update CORS settings, and extend estudiante models with enrollment fields
d7b4e65 | 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. | |