mirror of
https://github.com/marcogll/telegram_new_socias.git
synced 2026-01-13 21:25:16 +00:00
feat: Implement multi-database architecture
This commit introduces a three-database architecture to the application, as specified in the `db_logic.md` file. The changes include: - A SQL initialization script (`db/init/init.sql`) to create the `USERS_ALMA`, `vanity_hr`, and `vanity_attendance` databases and their respective tables. - SQLAlchemy models for all tables, organized into separate files within the `models` directory. - Refactoring of the database connection logic in `modules/database.py` to support connections to all three databases. - Creation of a `modules/logger.py` to handle request logging to the `USERS_ALMA` database. - Updates to `docker-compose.yml` to mount the initialization script and build the bot image locally. - Updates to `.env.example` to include the new database environment variables. - Restoration of the data dictionary to `db_tasks.md`.
This commit is contained in:
@@ -1,14 +1,40 @@
|
||||
import logging
|
||||
import os
|
||||
from datetime import datetime
|
||||
from sqlalchemy import Column, DateTime, Integer, MetaData, String, create_engine
|
||||
from sqlalchemy.ext.declarative import declarative_base
|
||||
from sqlalchemy import create_engine
|
||||
from sqlalchemy.orm import sessionmaker
|
||||
from models.users_alma_models import Base as BaseUsersAlma, User
|
||||
from models.vanity_hr_models import Base as BaseVanityHr, DataEmpleadas, Vacaciones, Permisos
|
||||
from models.vanity_attendance_models import Base as BaseVanityAttendance, AsistenciaRegistros, HorarioEmpleadas
|
||||
import gspread
|
||||
from google.oauth2.service_account import Credentials
|
||||
|
||||
# Configuración de logging
|
||||
logging.basicConfig(level=logging.INFO, format="%(asctime)s - %(name)s - %(levelname)s - %(message)s")
|
||||
# --- DATABASE (MySQL) SETUP ---
|
||||
def _build_engine(db_name_env_var):
|
||||
user = os.getenv("MYSQL_USER")
|
||||
password = os.getenv("MYSQL_PASSWORD")
|
||||
db_name = os.getenv(db_name_env_var)
|
||||
host = os.getenv("MYSQL_HOST", "db")
|
||||
|
||||
if not all([user, password, db_name]):
|
||||
logging.warning(f"Database connection disabled: missing environment variables for {db_name_env_var}.")
|
||||
return None
|
||||
|
||||
try:
|
||||
db_url = f"mysql+mysqlconnector://{user}:{password}@{host}:3306/{db_name}"
|
||||
return create_engine(db_url, pool_pre_ping=True)
|
||||
except Exception as exc:
|
||||
logging.error(f"Could not create database engine for {db_name}: {exc}")
|
||||
return None
|
||||
|
||||
# Create engines for each database
|
||||
engine_users_alma = _build_engine("MYSQL_DATABASE_USERS_ALMA")
|
||||
engine_vanity_hr = _build_engine("MYSQL_DATABASE_VANITY_HR")
|
||||
engine_vanity_attendance = _build_engine("MYSQL_DATABASE_VANITY_ATTENDANCE")
|
||||
|
||||
# Create sessions for each database
|
||||
SessionUsersAlma = sessionmaker(autocommit=False, autoflush=False, bind=engine_users_alma) if engine_users_alma else None
|
||||
SessionVanityHr = sessionmaker(autocommit=False, autoflush=False, bind=engine_vanity_hr) if engine_vanity_hr else None
|
||||
SessionVanityAttendance = sessionmaker(autocommit=False, autoflush=False, bind=engine_vanity_attendance) if engine_vanity_attendance else None
|
||||
|
||||
# --- GOOGLE SHEETS SETUP ---
|
||||
GSHEET_URL = os.getenv("GOOGLE_SHEET_URL")
|
||||
@@ -16,15 +42,14 @@ GOOGLE_CREDENTIALS_FILE = os.getenv("GOOGLE_CREDENTIALS_FILE", "google_credentia
|
||||
SHEET_COLUMN_INDEX = 40 # AN is the 40th column
|
||||
|
||||
def get_gsheet_client():
|
||||
"""Retorna un cliente de gspread autenticado o None si falla."""
|
||||
"""Returns an authenticated gspread client or None if it fails."""
|
||||
if not GSHEET_URL:
|
||||
logging.warning("GOOGLE_SHEET_URL no está configurada. La verificación de duplicados está deshabilitada.")
|
||||
logging.warning("GOOGLE_SHEET_URL is not configured. Duplicate checking is disabled.")
|
||||
return None
|
||||
|
||||
creds = None
|
||||
scopes = ["https://www.googleapis.com/auth/spreadsheets.readonly"]
|
||||
|
||||
# Prioridad 1: Cargar desde variables de entorno
|
||||
gsa_creds_dict = {
|
||||
"type": os.getenv("GSA_TYPE"),
|
||||
"project_id": os.getenv("GSA_PROJECT_ID"),
|
||||
@@ -41,139 +66,39 @@ def get_gsheet_client():
|
||||
if all(gsa_creds_dict.values()):
|
||||
try:
|
||||
creds = Credentials.from_service_account_info(gsa_creds_dict, scopes=scopes)
|
||||
logging.info("Autenticando con Google Sheets usando variables de entorno.")
|
||||
except Exception as e:
|
||||
logging.error(f"Error al procesar credenciales de entorno de Google: {e}")
|
||||
logging.error(f"Error processing Google credentials from environment: {e}")
|
||||
return None
|
||||
# Prioridad 2: Cargar desde archivo JSON
|
||||
elif os.path.exists(GOOGLE_CREDENTIALS_FILE):
|
||||
try:
|
||||
creds = Credentials.from_service_account_file(GOOGLE_CREDENTIALS_FILE, scopes=scopes)
|
||||
logging.info(f"Autenticando con Google Sheets usando el archivo '{GOOGLE_CREDENTIALS_FILE}'.")
|
||||
except Exception as e:
|
||||
logging.error(f"Error al procesar el archivo de credenciales '{GOOGLE_CREDENTIALS_FILE}': {e}")
|
||||
logging.error(f"Error processing credentials file '{GOOGLE_CREDENTIALS_FILE}': {e}")
|
||||
return None
|
||||
else:
|
||||
logging.warning("No se encontraron credenciales de Google (ni por variables de entorno ni por archivo). La verificación de duplicados está deshabilitada.")
|
||||
logging.warning("Google credentials not found (neither environment variables nor file). Duplicate checking is disabled.")
|
||||
return None
|
||||
|
||||
try:
|
||||
client = gspread.authorize(creds)
|
||||
return client
|
||||
return gspread.authorize(creds)
|
||||
except Exception as e:
|
||||
logging.error(f"Error al autorizar cliente de gspread: {e}")
|
||||
logging.error(f"Error authorizing gspread client: {e}")
|
||||
return None
|
||||
|
||||
def chat_id_exists(chat_id: int) -> bool:
|
||||
"""Verifica si un chat_id de Telegram ya existe en la columna AN de la hoja de cálculo."""
|
||||
"""Checks if a Telegram chat_id already exists in the Google Sheet."""
|
||||
client = get_gsheet_client()
|
||||
if not client:
|
||||
return False # Si no hay cliente, no podemos verificar, así que asumimos que no existe.
|
||||
return False
|
||||
|
||||
try:
|
||||
spreadsheet = client.open_by_url(GSHEET_URL)
|
||||
worksheet = spreadsheet.get_worksheet(0) # Primera hoja
|
||||
|
||||
# Obtener todos los valores de la columna AN (índice 40)
|
||||
worksheet = spreadsheet.get_worksheet(0)
|
||||
chat_ids_in_sheet = worksheet.col_values(SHEET_COLUMN_INDEX)
|
||||
|
||||
# El ID de chat puede venir como número o texto, así que comparamos como string
|
||||
return str(chat_id) in chat_ids_in_sheet
|
||||
|
||||
except gspread.exceptions.SpreadsheetNotFound:
|
||||
logging.error(f"No se pudo encontrar la hoja de cálculo en la URL proporcionada.")
|
||||
logging.error("Could not find the spreadsheet at the provided URL.")
|
||||
return False
|
||||
except Exception as e:
|
||||
logging.error(f"Error al leer la hoja de cálculo: {e}")
|
||||
logging.error(f"Error reading the spreadsheet: {e}")
|
||||
return False
|
||||
|
||||
|
||||
# --- DATABASE (MySQL) SETUP ---
|
||||
|
||||
# Base para los modelos declarativos
|
||||
Base = declarative_base()
|
||||
|
||||
# Clase que mapea a la tabla de logs
|
||||
class RequestLog(Base):
|
||||
__tablename__ = 'request_logs'
|
||||
id = Column(Integer, primary_key=True)
|
||||
telegram_id = Column(String(50))
|
||||
username = Column(String(100))
|
||||
command = Column(String(100))
|
||||
message = Column(String(500))
|
||||
created_at = Column(DateTime, default=datetime.utcnow)
|
||||
|
||||
def _build_engine():
|
||||
"""Crea un engine de SQLAlchemy si hay variables de entorno suficientes."""
|
||||
user = os.getenv("MYSQL_USER")
|
||||
password = os.getenv("MYSQL_PASSWORD")
|
||||
database = os.getenv("MYSQL_DATABASE")
|
||||
host = os.getenv("MYSQL_HOST") or "db" # Permitimos override para uso local
|
||||
|
||||
if not all([user, password, database]):
|
||||
logging.warning("DB logging deshabilitado: faltan MYSQL_USER/MYSQL_PASSWORD/MYSQL_DATABASE.")
|
||||
return None
|
||||
|
||||
try:
|
||||
db_url = f"mysql+mysqlconnector://{user}:{password}@{host}:3306/{database}"
|
||||
return create_engine(db_url, pool_pre_ping=True)
|
||||
except Exception as exc:
|
||||
logging.error(f"No se pudo crear el engine de base de datos: {exc}")
|
||||
return None
|
||||
|
||||
def _disable_db_logging(reason: str):
|
||||
"""Deshabilita el logging a DB después de un error para evitar spam."""
|
||||
global engine, SessionLocal
|
||||
engine = None
|
||||
SessionLocal = None
|
||||
logging.warning(f"DB logging deshabilitado: {reason}")
|
||||
|
||||
# Crear el engine y sesión si es posible
|
||||
engine = _build_engine()
|
||||
metadata = MetaData() if engine else None
|
||||
SessionLocal = sessionmaker(autocommit=False, autoflush=False, bind=engine) if engine else None
|
||||
|
||||
# Función para inicializar la base de datos
|
||||
def init_db():
|
||||
global engine, SessionLocal
|
||||
if not engine:
|
||||
return
|
||||
try:
|
||||
logging.info("Inicializando la base de datos y creando tablas si no existen...")
|
||||
Base.metadata.create_all(bind=engine)
|
||||
logging.info("Tablas verificadas/creadas correctamente.")
|
||||
except Exception as e:
|
||||
logging.error(f"Error al inicializar la base de datos: {e}")
|
||||
_disable_db_logging("no se pudo inicializar la base de datos (se omitirán logs).")
|
||||
# No propagamos para que el bot pueda seguir levantando aunque no haya DB
|
||||
|
||||
# Función para registrar una solicitud en la base de datos
|
||||
def log_request(telegram_id, username, command, message):
|
||||
if not SessionLocal:
|
||||
logging.debug("Log de DB omitido (DB no configurada).")
|
||||
return
|
||||
|
||||
try:
|
||||
db_session = SessionLocal()
|
||||
except Exception as exc:
|
||||
logging.error(f"No se pudo crear sesión DB, se deshabilita el log: {exc}")
|
||||
_disable_db_logging("no se pudo abrir sesión")
|
||||
return
|
||||
try:
|
||||
log_entry = RequestLog(
|
||||
telegram_id=str(telegram_id),
|
||||
username=username,
|
||||
command=command,
|
||||
message=message
|
||||
)
|
||||
db_session.add(log_entry)
|
||||
db_session.commit()
|
||||
logging.info(f"Log guardado: {command} de {username}")
|
||||
except Exception as e:
|
||||
logging.error(f"Error al guardar el log: {e}")
|
||||
db_session.rollback()
|
||||
finally:
|
||||
db_session.close()
|
||||
|
||||
# Inicializar la base de datos al arrancar el módulo
|
||||
init_db()
|
||||
|
||||
29
modules/logger.py
Normal file
29
modules/logger.py
Normal file
@@ -0,0 +1,29 @@
|
||||
import logging
|
||||
from modules.database import SessionUsersAlma
|
||||
from models.users_alma_models import RequestLog
|
||||
|
||||
def log_request(telegram_id, username, command, message):
|
||||
if not SessionUsersAlma:
|
||||
logging.debug("DB log omitted (DB not configured).")
|
||||
return
|
||||
|
||||
try:
|
||||
db_session = SessionUsersAlma()
|
||||
except Exception as exc:
|
||||
logging.error(f"Could not create DB session, logging is disabled: {exc}")
|
||||
return
|
||||
try:
|
||||
log_entry = RequestLog(
|
||||
telegram_id=str(telegram_id),
|
||||
username=username,
|
||||
command=command,
|
||||
message=message
|
||||
)
|
||||
db_session.add(log_entry)
|
||||
db_session.commit()
|
||||
logging.info(f"Log saved: {command} from {username}")
|
||||
except Exception as e:
|
||||
logging.error(f"Error saving log: {e}")
|
||||
db_session.rollback()
|
||||
finally:
|
||||
db_session.close()
|
||||
@@ -17,7 +17,8 @@ from telegram.ext import (
|
||||
Defaults,
|
||||
)
|
||||
|
||||
from modules.database import log_request, chat_id_exists
|
||||
from modules.logger import log_request
|
||||
from modules.database import chat_id_exists
|
||||
from modules.ui import main_actions_keyboard
|
||||
|
||||
# --- 1. CARGA DE ENTORNO ---
|
||||
|
||||
@@ -5,7 +5,7 @@ import string
|
||||
from datetime import datetime, date
|
||||
from telegram import ReplyKeyboardMarkup, ReplyKeyboardRemove, Update
|
||||
from telegram.ext import CommandHandler, ContextTypes, ConversationHandler, MessageHandler, filters
|
||||
from modules.database import log_request
|
||||
from modules.logger import log_request
|
||||
from modules.ui import main_actions_keyboard
|
||||
from modules.ai import classify_reason
|
||||
|
||||
|
||||
Reference in New Issue
Block a user