Files
MT/database.py
T
2026-07-01 20:38:56 +00:00

168 lines
5.4 KiB
Python

# pyrefly: ignore [missing-import]
import pyodbc
from typing import Iterator, Any, Optional
from contextlib import contextmanager
import config
from logger import get_logger
logger = get_logger(__name__)
class DatabaseConnectionError(Exception):
"""Excepción personalizada para errores de conexión a la base de datos."""
pass
@contextmanager
def get_db_connection() -> Iterator[pyodbc.Connection]:
"""
Context Manager (Regla 3) para manejar de forma segura la conexión a SQL Server.
Garantiza que la conexión se cierre al salir del bloque.
Yields:
pyodbc.Connection: Objeto de conexión a la base de datos.
Raises:
DatabaseConnectionError: Si falla la conexión a la base de datos (Regla 7).
"""
conn_str = (
f"DRIVER={config.DB_DRIVER};"
f"SERVER={config.DB_SERVER};"
f"DATABASE={config.DB_NAME};"
f"Trusted_Connection=yes;"
)
conn = None
try:
conn = pyodbc.connect(conn_str)
logger.info("Conexión a base de datos establecida exitosamente.")
yield conn
except pyodbc.Error as e:
logger.exception("Error crítico al conectar a SQL Server.")
raise DatabaseConnectionError(f"No se pudo conectar a la base de datos: {e}")
finally:
if conn is not None:
conn.close()
logger.debug("Conexión a la base de datos cerrada.")
@contextmanager
def get_db_cursor(commit: bool = False) -> Iterator[pyodbc.Cursor]:
"""
Context Manager (Regla 3 y 6) para manejar un cursor de SQL Server,
con soporte para transacciones seguras (commit/rollback).
Args:
commit (bool): Indica si se debe ejecutar commit() al finalizar sin errores.
Yields:
pyodbc.Cursor: Cursor activo para ejecutar consultas.
"""
with get_db_connection() as conn:
cursor = conn.cursor()
try:
yield cursor
if commit:
conn.commit()
logger.info("Transacción confirmada (commit).")
except Exception as e:
conn.rollback()
logger.exception("Error en transacción SQL. Se ejecutó rollback().")
raise
finally:
cursor.close()
logger.debug("Cursor de base de datos cerrado.")
def ejecutar_backup(ruta_destino: str) -> bool:
"""
Ejecuta un backup nativo de SQL Server (BACKUP DATABASE) a la ruta indicada.
Utiliza una conexión independiente con autocommit=True, ya que el comando
BACKUP DATABASE de SQL Server no puede ejecutarse dentro de un bloque
de transacción explícito. Esta conexión se abre y cierra exclusivamente
para esta operación administrativa.
Args:
ruta_destino (str): Ruta absoluta del archivo .bak de destino en el
servidor de SQL Server. Ejemplo: ``'C:\\Backups\\mi_db.bak'``.
Returns:
bool: ``True`` si el backup se completó exitosamente,
``False`` si ocurrió cualquier error durante la operación.
Raises:
No levanta excepciones. Todos los errores se capturan, registran
en el logger y la función retorna ``False``.
Note:
El comando BACKUP DATABASE es una instrucción DDL administrativa que
no soporta consultas parametrizadas via ``cursor.execute(sql, params)``.
La interpolación directa de ``config.DB_NAME`` y ``ruta_destino`` en
el string SQL es una excepción técnica justificada a la Regla 6.
"""
conn_str: str = (
f"DRIVER={config.DB_DRIVER};"
f"SERVER={config.DB_SERVER};"
f"DATABASE={config.DB_NAME};"
f"Trusted_Connection=yes;"
)
conn: Optional[pyodbc.Connection] = None
cursor: Optional[pyodbc.Cursor] = None
try:
conn = pyodbc.connect(conn_str, autocommit=True)
cursor = conn.cursor()
logger.info(
"Conexión con autocommit establecida para backup de '%s'.",
config.DB_NAME,
)
sql_backup: str = (
f"BACKUP DATABASE [{config.DB_NAME}] "
f"TO DISK = '{ruta_destino}' "
f"WITH FORMAT, "
f"MEDIANAME = 'SQLServerBackups', "
f"NAME = 'Full Backup'"
)
cursor.execute(sql_backup)
# BACKUP DATABASE puede devolver múltiples result sets informativos.
# Consumirlos evita que la conexión quede en estado inconsistente.
while cursor.nextset():
pass
logger.info(
"Backup de la base de datos '%s' completado exitosamente en: %s",
config.DB_NAME,
ruta_destino,
)
return True
except pyodbc.Error as db_err:
logger.exception(
"Error de SQL Server al ejecutar backup de '%s': %s",
config.DB_NAME,
db_err,
)
return False
except OSError as os_err:
logger.exception(
"Error de sistema operativo durante el backup (posible ruta inválida '%s'): %s",
ruta_destino,
os_err,
)
return False
except Exception as unexpected_err:
logger.exception(
"Error inesperado durante el backup de '%s': %s",
config.DB_NAME,
unexpected_err,
)
return False
finally:
if cursor is not None:
cursor.close()
logger.debug("Cursor de backup cerrado.")
if conn is not None:
conn.close()
logger.debug("Conexión de backup cerrada.")