se valida la configuración (.env) al arrancar

config_validator.verificar_configuracion termina con código 1 si faltan
variables obligatorias (pipeline, Graph, SAP B1, HANA), si hay ruta de red
sin RUC_EMPRESA, y advierte por SSL desactivado, ruta de red ausente o
LOG_LEVEL inválido. Se invoca desde main.py y tools/consulta_rapida.py.

Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
This commit is contained in:
GeorgeandClaude Sonnet 5.5 committed 2026-10-20 22:03:38 -05:00
1 parent 79378a70e1
commit 74be942a3e
6 files changed
+98 -8

No files matched your search

+83
View File
@@ -0,0 +1,83 @@
"""
Validación de la configuración (.env) al arrancar el proceso.
Falla rápido con un mensaje claro si falta alguna variable obligatoria, en
lugar de descubrirlo a mitad de un ciclo (por ejemplo, adjuntos descargados
en la raíz del proyecto por un ``DOWNLOAD_FOLDER`` vacío, o consultas a HANA
que devuelven 0 en silencio por credenciales ausentes).
"""
import os
import sys
import logging
logger = logging.getLogger(__name__)
# Variables obligatorias, agrupadas por servicio para el mensaje de error.
OBLIGATORIAS = {
"Pipeline": ["EMAIL", "DOWNLOAD_FOLDER", "OUTPUT_FOLDER"],
"Microsoft Graph": ["CLIENT_ID", "CLIENT_SECRET", "TENANT_ID"],
"SAP B1 Service Layer": ["SAP_B1_URL", "SAP_B1_COMPANY", "SAP_B1_USER", "SAP_B1_PASSWORD"],
"SAP HANA": ["HANA_SERVER", "HANA_USER", "HANA_PASSWORD", "HANA_SCHEMA"],
}
# Variable de verificación SSL → servicio al que aplica. Cada módulo solo
# la activa con el valor "true" (cualquier otro valor, o ausente, la desactiva).
VERIFICACION_SSL = {
"VERIFY_SSL": "Microsoft Graph",
"SAP_B1_VERIFY_SSL": "SAP B1 Service Layer",
}
def validar_configuracion() -> tuple[list[str], list[str]]:
"""
Revisa las variables de entorno ya cargadas (tras ``load_dotenv``).
Returns:
tuple[list[str], list[str]]: ``(errores, advertencias)``. Los errores
impiden arrancar; las advertencias solo se registran.
"""
errores: list[str] = []
advertencias: list[str] = []
for servicio, variables in OBLIGATORIAS.items():
faltan = [v for v in variables if not os.getenv(v, "").strip()]
if faltan:
errores.append(f"{servicio}: faltan {', '.join(faltan)}")
ruta_red = os.getenv("RUTA_RED_FACTURACION", "").strip()
ruc_empresa = os.getenv("RUC_EMPRESA", "").strip()
if ruta_red and not ruc_empresa:
errores.append("Ruta de red: RUTA_RED_FACTURACION está definida pero falta RUC_EMPRESA")
elif not ruta_red:
advertencias.append(
"RUTA_RED_FACTURACION no definida: no se copiarán PDF/XML a la red "
"y U_VS_ANEXO_PDF/XML irán vacíos en el draft"
)
nivel = os.getenv("LOG_LEVEL", "").strip()
if nivel and not isinstance(getattr(logging, nivel.upper(), None), int):
advertencias.append(f"LOG_LEVEL='{nivel}' no es un nivel válido; se usará INFO")
for variable, servicio in VERIFICACION_SSL.items():
if os.getenv(variable, "").strip().lower() != "true":
advertencias.append(f"Verificación SSL desactivada para {servicio} ({variable} distinto de 'true')")
return errores, advertencias
def verificar_configuracion() -> None:
"""
Registra las advertencias y, si hay errores, los registra y termina el
proceso con código 1.
"""
errores, advertencias = validar_configuracion()
for a in advertencias:
logger.warning(f"⚠️ Configuración: {a}")
if errores:
logger.error("❌ Configuración incompleta en .env:")
for e in errores:
logger.error(f" - {e}")
sys.exit(1)
+1
View File
@@ -19,6 +19,7 @@
| `hana_manager.py` | 123 | Comunicación con SAP HANA directa (fallback de `sapb1_manager` y valor referencial) | | `hana_manager.py` | 123 | Comunicación con SAP HANA directa (fallback de `sapb1_manager` y valor referencial) |
| `database_utilities.py` | 116 | SQLite local (cache de correos) | | `database_utilities.py` | 116 | SQLite local (cache de correos) |
| `tools/consulta_rapida.py` | 121 | Script auxiliar para reprocesar correos por ID (sets en `tools/ids_prueba.json`) | | `tools/consulta_rapida.py` | 121 | Script auxiliar para reprocesar correos por ID (sets en `tools/ids_prueba.json`) |
| `config_validator.py` | 83 | Valida el `.env` al arrancar (obligatorias, ruta de red, avisos de SSL) |
| `email_utilities.py` | 52 | Utilitarios de adjuntos (ZIP, filtrado) | | `email_utilities.py` | 52 | Utilitarios de adjuntos (ZIP, filtrado) |
| `logger_manager.py` | 149 | Logging a archivo rotado (30 días) + consola + captura de `print()` | | `logger_manager.py` | 149 | Logging a archivo rotado (30 días) + consola + captura de `print()` |
+7 -2
View File
@@ -96,7 +96,12 @@ if __name__ == "__main__":
1. logger_manager.setup_logging() 1. logger_manager.setup_logging()
└── Configura logging a archivo rotado (logs/descarga_factura_transporte.log, └── Configura logging a archivo rotado (logs/descarga_factura_transporte.log,
rotación diaria, 30 días de historial) + consola, y redirige print()/stderr rotación diaria, 30 días de historial) + consola, y redirige print()/stderr
al mismo log (no hace falta migrar cada print a logger.info). al mismo log (los módulos del pipeline ya usan logger con nivel explícito;
2. print(...) banner de inicio, incluyendo logger_manager.LOG_FILE los print() restantes se registran como INFO).
2. logger.info(...) banner de inicio, incluyendo logger_manager.LOG_FILE
3. main() 3. main()
└── Tras load_dotenv(), config_validator.verificar_configuracion():
termina con código 1 si falta alguna variable obligatoria del .env
(ver config_validator.OBLIGATORIAS) y advierte si la verificación SSL
está desactivada o no hay ruta de red.
``` ```
+2
View File
@@ -6,6 +6,7 @@ from datetime import datetime, timedelta
from dotenv import load_dotenv from dotenv import load_dotenv
import logger_manager import logger_manager
from config_validator import verificar_configuracion
from pipeline import run_pipeline from pipeline import run_pipeline
logger = logging.getLogger(__name__) logger = logging.getLogger(__name__)
@@ -70,6 +71,7 @@ def main() -> None:
# Ruta absoluta: no depende del directorio de trabajo desde el que se lance el proceso # Ruta absoluta: no depende del directorio de trabajo desde el que se lance el proceso
load_dotenv(dotenv_path=os.path.join(os.path.dirname(__file__), ".env")) load_dotenv(dotenv_path=os.path.join(os.path.dirname(__file__), ".env"))
verificar_configuracion()
EMAIL = os.getenv("EMAIL", "") EMAIL = os.getenv("EMAIL", "")
DOWNLOAD_FOLDER = os.getenv("DOWNLOAD_FOLDER", "") DOWNLOAD_FOLDER = os.getenv("DOWNLOAD_FOLDER", "")
+1
View File
@@ -19,6 +19,7 @@ dependencies = [
[tool.setuptools] [tool.setuptools]
py-modules = [ py-modules = [
"adjuntos", "adjuntos",
"config_validator",
"database_utilities", "database_utilities",
"documentos", "documentos",
"draft_sap", "draft_sap",
+4 -6
View File
@@ -19,6 +19,7 @@ sys.path.insert(0, RAIZ)
import database_utilities import database_utilities
import logger_manager import logger_manager
from config_validator import verificar_configuracion
from pipeline import _procesar_un_correo, step_authenticate from pipeline import _procesar_un_correo, step_authenticate
@@ -54,13 +55,10 @@ def get_email_by_id(user_email: str, token: str, message_id: str) -> dict | None
def run_pipeline_for_ids(ids: list[str]) -> None: def run_pipeline_for_ids(ids: list[str]) -> None:
# Ruta absoluta: no depende del directorio de trabajo desde el que se lance el proceso # Ruta absoluta: no depende del directorio de trabajo desde el que se lance el proceso
load_dotenv(dotenv_path=os.path.join(RAIZ, ".env")) load_dotenv(dotenv_path=os.path.join(RAIZ, ".env"))
verificar_configuracion()
email_addr = os.getenv("EMAIL", "") email_addr = os.getenv("EMAIL", "")
download_folder = os.getenv("DOWNLOAD_FOLDER", "adjuntos") download_folder = os.getenv("DOWNLOAD_FOLDER", "")
output_folder = os.getenv("OUTPUT_FOLDER", "resultado") output_folder = os.getenv("OUTPUT_FOLDER", "")
if not email_addr:
print("❌ EMAIL no configurado en .env")
return
database_utilities.init_db() database_utilities.init_db()
token = step_authenticate() token = step_authenticate()