diff --git a/config_validator.py b/config_validator.py new file mode 100644 index 0000000..7edc2dc --- /dev/null +++ b/config_validator.py @@ -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) diff --git a/docs/inventario.md b/docs/inventario.md index 0833848..2df28bb 100644 --- a/docs/inventario.md +++ b/docs/inventario.md @@ -19,6 +19,7 @@ | `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) | | `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) | | `logger_manager.py` | 149 | Logging a archivo rotado (30 días) + consola + captura de `print()` | diff --git a/docs/main.md b/docs/main.md index 5ea6b3f..a2acc80 100644 --- a/docs/main.md +++ b/docs/main.md @@ -96,7 +96,12 @@ if __name__ == "__main__": 1. logger_manager.setup_logging() └── Configura logging a archivo rotado (logs/descarga_factura_transporte.log, 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). - 2. print(...) banner de inicio, incluyendo logger_manager.LOG_FILE + al mismo log (los módulos del pipeline ya usan logger con nivel explícito; + los print() restantes se registran como INFO). + 2. logger.info(...) banner de inicio, incluyendo logger_manager.LOG_FILE 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. ``` diff --git a/main.py b/main.py index a1d3e41..13593a2 100644 --- a/main.py +++ b/main.py @@ -6,6 +6,7 @@ from datetime import datetime, timedelta from dotenv import load_dotenv import logger_manager +from config_validator import verificar_configuracion from pipeline import run_pipeline 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 load_dotenv(dotenv_path=os.path.join(os.path.dirname(__file__), ".env")) + verificar_configuracion() EMAIL = os.getenv("EMAIL", "") DOWNLOAD_FOLDER = os.getenv("DOWNLOAD_FOLDER", "") diff --git a/pyproject.toml b/pyproject.toml index abbc880..eff64d3 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -19,6 +19,7 @@ dependencies = [ [tool.setuptools] py-modules = [ "adjuntos", + "config_validator", "database_utilities", "documentos", "draft_sap", diff --git a/tools/consulta_rapida.py b/tools/consulta_rapida.py index 1405f27..1d9449d 100644 --- a/tools/consulta_rapida.py +++ b/tools/consulta_rapida.py @@ -19,6 +19,7 @@ sys.path.insert(0, RAIZ) import database_utilities import logger_manager +from config_validator import verificar_configuracion 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: # Ruta absoluta: no depende del directorio de trabajo desde el que se lance el proceso load_dotenv(dotenv_path=os.path.join(RAIZ, ".env")) + verificar_configuracion() email_addr = os.getenv("EMAIL", "") - download_folder = os.getenv("DOWNLOAD_FOLDER", "adjuntos") - output_folder = os.getenv("OUTPUT_FOLDER", "resultado") - - if not email_addr: - print("❌ EMAIL no configurado en .env") - return + download_folder = os.getenv("DOWNLOAD_FOLDER", "") + output_folder = os.getenv("OUTPUT_FOLDER", "") database_utilities.init_db() token = step_authenticate()