Files
descarga_factura_transporte/main.py
T
GeorgeandClaude Sonnet 5.5 1d23e87b9b versión 1.2.2: horarios nocturnos unificados en dos tramos
HORARIOS pasa a 5 bloques que siguen cubriendo las 24 horas: 00:00-06:30 cada
30 minutos (antes 00:00-05:00 cada 60 s y 05:00-06:30 cada 30 min) y
18:30-24:00 cada 60 minutos (antes dos bloques iguales). El día no cambia.
De madrugada un correo puede esperar hasta 30 minutos y los reintentos por
SAP fuera de línea, uno por ciclo, van igual de espaciados.

Se ordena el formato de la lista, test_horarios fija el primer y el último
minuto de cada bloque (y que nunca se devuelva None en el día), y se
actualizan docs/main.md, la guía de despliegue y el CHANGELOG.

Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
2026-10-22 15:36:48 -05:00

141 lines
4.5 KiB
Python

import logging
import os
import time
import argparse
from datetime import datetime, timedelta
from dotenv import load_dotenv
import logger_manager
from config_validator import verificar_configuracion
from pipeline import run_pipeline
from pipeline_utils import version_del_proyecto
logger = logging.getLogger(__name__)
# ============================================================================
# (hora inicio, min inicio, hora fin, min fin, intervalo en segundos, modo). El fin no se incluye.
# Los bloques cubren las 24 horas sin huecos ni solapes (lo comprueba tests/test_horarios.py).
HORARIOS = [
(0, 0, 6, 30, 1800, "full"), # 00:00 - 06:30 -> cada 30 minutos
(6, 30, 13, 15, 300, "rapido"), # 06:30 - 13:15 -> cada 5 minutos
(13, 15, 14, 0, 1800, "full"), # 13:15 - 14:00 -> cada 30 minutos
(14, 0, 18, 30, 300, "rapido"), # 14:00 - 18:30 -> cada 5 minutos
(18, 30, 24, 0, 3600, "full"), # 18:30 - 24:00 -> cada 60 minutos
]
# ============================================================================
def obtener_intervalo(dt: datetime) -> tuple[int, str] | None:
minutos = dt.hour * 60 + dt.minute
for hi, mi, hf, mf, intervalo, modo in HORARIOS:
inicio = hi * 60 + mi
fin = hf * 60 + mf
if inicio <= minutos < fin:
return (intervalo, modo)
return None
# ============================================================================
def segundos_hasta_proximo_inicio(dt: datetime) -> int:
inicio = dt.replace(hour=6, minute=30, second=0, microsecond=0)
if dt >= inicio:
inicio += timedelta(days=1)
return int((inicio - dt).total_seconds())
# ============================================================================
def non_negative_int(value: str) -> int:
ivalue = int(value)
if ivalue < 0:
raise argparse.ArgumentTypeError(
f"{value} es un valor inválido. --dias debe ser 0 o mayor."
)
return ivalue
# ============================================================================
def main() -> None:
parser = argparse.ArgumentParser(
description="Obtener correos electrónicos de Office365 de hoy y los últimos X días."
)
parser.add_argument(
"--dias", "-d",
type=non_negative_int,
default=2,
help="Número de días respecto a la fecha actual para la consulta (default: 2)."
)
args = parser.parse_args()
dias = args.dias
# 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", "")
OUTPUT_FOLDER = os.getenv("OUTPUT_FOLDER", "")
logger.info("🚀 Demonio iniciado")
logger.info("=" * 60)
try:
while True:
inicio_ciclo = datetime.now()
resultado = obtener_intervalo(inicio_ciclo)
if resultado is not None:
intervalo, modo = resultado
logger.info(
f"[{inicio_ciclo:%Y-%m-%d %H:%M:%S}] "
f"Iniciando ciclo [{modo}] (cada {intervalo}s)"
)
run_pipeline(dias, EMAIL, DOWNLOAD_FOLDER, OUTPUT_FOLDER)
duracion = (datetime.now() - inicio_ciclo).total_seconds()
if duracion > intervalo:
logger.warning(
f"⚠️ El ciclo demoró {duracion:.1f}s "
f"(intervalo configurado {intervalo}s)"
)
dormir = max(0, intervalo - duracion)
logger.info(f"💤 Próximo ciclo [{modo}] en {dormir:.0f}s...")
else:
dormir = segundos_hasta_proximo_inicio(inicio_ciclo)
despertar = inicio_ciclo + timedelta(seconds=dormir)
logger.info(f"[{inicio_ciclo:%Y-%m-%d %H:%M:%S}] Fuera del horario.")
logger.info(f"Próxima ejecución: {despertar:%Y-%m-%d %H:%M:%S}")
time.sleep(dormir)
except KeyboardInterrupt:
logger.info("🛑 Demonio detenido por el usuario.")
# ============================================================================
if __name__ == "__main__":
logger_manager.setup_logging()
logger.info("=" * 60)
logger.info(f"🚀 Demonio iniciado: {datetime.now():%Y-%m-%d %H:%M:%S} (versión {version_del_proyecto()})")
logger.info(f"📄 Log guardado en: {logger_manager.LOG_FILE}")
logger.info("=" * 60)
main()