import json import sys import time from datetime import datetime, timedelta from pathlib import Path from core.db import init_db from core.lock import SingleInstanceError, acquire_single_instance_lock from core.logger_manager import get_logger from pipeline.runner import ejecutar_ciclo logger = get_logger() SCHEDULER_PATH = Path(__file__).resolve().parent / "scheduler.json" config: dict = {} # ============================================================================ def cargar_programacion() -> dict: global config with open(SCHEDULER_PATH, "r", encoding="utf-8") as f: data = json.load(f) data["HORARIOS"] = [tuple(h) for h in data["HORARIOS"]] config = data return config def obtener_horarios() -> list: return config.get("HORARIOS", []) def obtener_dias_activos() -> list: return config.get("DIAS_ACTIVOS", []) # ============================================================================ def obtener_intervalo(dt: datetime) -> tuple[int, str] | None: minutos = dt.hour * 60 + dt.minute for hi, mi, hf, mf, intervalo, modo in obtener_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: """ Revisa todos los horarios de hoy (no solo el primero) y toma el próximo inicio que aún no haya pasado. Si ninguno queda hoy, salta al próximo día activo. """ candidatos = [] for hi, mi, *_ in obtener_horarios(): inicio = dt.replace(hour=hi, minute=mi, second=0, microsecond=0) if inicio > dt: candidatos.append(inicio) if candidatos: return int((min(candidatos) - dt).total_seconds()) return segundos_hasta_proximo_dia_activo(dt) # ============================================================================ def segundos_hasta_proximo_dia_activo(dt: datetime) -> int: horarios = obtener_horarios() hi, mi = (horarios[0][0], horarios[0][1]) if horarios else (6, 0) dias_activos = obtener_dias_activos() for dias in range(1, 8): prox = dt + timedelta(days=dias) if prox.weekday() in dias_activos: target = prox.replace(hour=hi, minute=mi, second=0, microsecond=0) return int((target - dt).total_seconds()) return 86400 # ============================================================================ def main(): init_db() cargar_programacion() logger.info("=" * 60) logger.info("Servicio Shopify -> SAP iniciado") logger.info("=" * 60) while True: inicio_ciclo = datetime.now() cargar_programacion() if inicio_ciclo.weekday() not in obtener_dias_activos(): dormir = segundos_hasta_proximo_dia_activo(inicio_ciclo) logger.info( f"[{inicio_ciclo:%Y-%m-%d %H:%M:%S}] Día no activo. " f"Próxima ejecución: {(inicio_ciclo + timedelta(seconds=dormir)):%Y-%m-%d %H:%M:%S}" ) time.sleep(dormir) continue resultado = obtener_intervalo(inicio_ciclo) if resultado is not None: intervalo, modo = resultado logger.info(f"[{inicio_ciclo:%Y-%m-%d %H:%M:%S}] Iniciando ciclo [{modo}] (cada {intervalo}s)") ejecutar_ciclo(modo) duracion = (datetime.now() - inicio_ciclo).total_seconds() if duracion > intervalo: logger.warning(f"El ciclo demoró {duracion:.1f}s (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 de horario. " f"Próxima ejecución: {despertar:%Y-%m-%d %H:%M:%S}" ) time.sleep(dormir) # ============================================================================ if __name__ == "__main__": try: lock = acquire_single_instance_lock() except SingleInstanceError as exc: logger.error(str(exc)) sys.exit(1) try: main() except KeyboardInterrupt: logger.info("Servicio detenido por el usuario.") finally: lock.release()