Files
INFRAESTRUCTURA/scripts/monitor_estado.py
2026-10-03 17:13:39 +02:00

216 lines
7.8 KiB
Python

#!/usr/bin/env python3
# -*- coding: utf-8 -*-
"""monitor_estado.py — Punto de entrada del Monitor de estado.
Modos:
ciclo Ejecuta un ciclo completo y termina (a demanda).
daemon Bucle en segundo plano cada `intervalo_minutos` (defecto 30).
estado Muestra el ultimo resumen y las rutas de los ficheros.
login-telegram Autoriza la sesion dedicada de Telegram (una vez).
El nucleo vive en el paquete `monitor_estado/`.
"""
from __future__ import annotations
import argparse
import sys
import time
from datetime import datetime
from pathlib import Path
# Permite ejecutar el script desde cualquier directorio.
_SCRIPTS_DIR = Path(__file__).resolve().parent
if str(_SCRIPTS_DIR) not in sys.path:
sys.path.insert(0, str(_SCRIPTS_DIR))
import monitor_estado.config as config # noqa: E402
from monitor_estado import __version__, ciclo, lock # noqa: E402
# Salida UTF-8 en consola Windows (acentos y simbolos).
for _stream in (sys.stdout, sys.stderr):
try:
_stream.reconfigure(encoding="utf-8", errors="replace")
except Exception:
pass
class _NoJira:
"""Cliente Jira vacio (para el modo --solo que desactiva esta fuente)."""
def search(self, jql, fields=None, max_results=50):
return {"issues": []}
def _deps_solo(solo: str | None) -> dict:
"""Dependencias que desactivan las fuentes no seleccionadas con --solo."""
deps: dict = {}
if solo and solo != "telegram":
deps["telegram_recoger"] = lambda cfg, hoy: ({}, [])
if solo and solo != "correo":
deps["correo_recoger"] = lambda horas=24, limite=50: []
if solo and solo != "jira":
deps["jira_cliente"] = _NoJira()
return deps
def cmd_ciclo(args: argparse.Namespace) -> int:
"""Ejecuta un ciclo completo (o de una sola fuente con --solo)."""
cfg = config.cargar()
if not lock.adquirir():
print("[monitor_estado] Ya hay una instancia en marcha; no lanzo otra.")
return 0
try:
ciclo.ejecutar(cfg, deps=_deps_solo(getattr(args, "solo", None)))
except Exception as e:
print(f"[monitor_estado] Error en el ciclo: {e}", file=sys.stderr)
return 1
finally:
lock.liberar()
print(ciclo.texto_estado())
return 0
def cmd_daemon(args: argparse.Namespace) -> int:
"""Bucle en segundo plano cada intervalo_minutos (RF-19)."""
if not lock.adquirir():
print("[monitor_estado] Ya hay una instancia en marcha; salgo.")
return 0
cfg = config.cargar()
intervalo = max(1, int(cfg.get("intervalo_minutos", 30))) * 60
print(f"[monitor_estado] daemon iniciado (cada {intervalo // 60} min). Ctrl+C para salir.", flush=True)
try:
while True:
try:
res = ciclo.ejecutar(cfg)
atencion = len(res.get("requiere_atencion", []))
print(
f"[{datetime.now():%Y-%m-%d %H:%M:%S}] ciclo ok | fuentes={res.get('fuentes_con_novedad')} "
f"| atencion={atencion} | aviso={res.get('aviso_emitido')}",
flush=True,
)
except Exception as e: # un ciclo fallido no debe matar el daemon
print(f"[{datetime.now():%Y-%m-%d %H:%M:%S}] Error en ciclo: {e}", flush=True, file=sys.stderr)
time.sleep(intervalo)
except KeyboardInterrupt:
pass
finally:
lock.liberar()
return 0
def cmd_estado(args: argparse.Namespace) -> int:
"""Muestra el ultimo resumen y la configuracion (RF-18)."""
print(f"[monitor_estado] v{__version__}")
print(ciclo.texto_estado())
print("\n--- Configuracion ---")
print(config.resumen_config(config.cargar()))
return 0
def cmd_simular(args: argparse.Namespace) -> int:
"""Simula entradas ficticias (correo/Telegram) SIN enviar ni crear nada real.
Ejecuta el pipeline completo en un directorio aislado (`estado/monitor/simulacion/`):
no toca los datos reales, no crea correos y NUNCA envia mensajes a Telegram.
"""
from datetime import date
import monitor_estado.archivo as archivo
import monitor_estado.vistos as vistos
base = config.MONITOR_DIR / "simulacion"
archivo.MONITOR_DIR = base
vistos.VISTO_DIR = base / "visto"
hoy = date.today()
texto = getattr(args, "texto", None) or "Confirmado el desarrollo"
asunto = getattr(args, "asunto", None) or texto
def tg_fake(cfg, h):
return ({"simulacion_telegram": [{
"id": 9900001, "fecha": datetime.now().isoformat(),
"quien": getattr(args, "de", None) or "DAX", "texto": texto,
}]}, [])
def correo_fake(horas=24, limite=50):
remitente = getattr(args, "de", None) or "DAX"
return [{
"id": "sim-mail-1", "fecha": datetime.now().strftime("%a, %d %b %Y %H:%M:%S +0200"),
"de": f"{remitente} <{remitente.lower()}@prolongo.es>",
"asunto": asunto, "snippet": texto,
}]
res = ciclo.ejecutar(
cfg=config.cargar(), hoy=hoy,
deps={"telegram_recoger": tg_fake, "correo_recoger": correo_fake, "jira_cliente": _NoJira()},
)
print("[SIMULACION] No se ha enviado ni creado nada real (solo simulado, aislado en 'simulacion/').")
print(f"Elementos de atencion: {len(res.get('requiere_atencion', []))} | aviso={res.get('aviso_emitido')}")
print(ciclo.texto_estado())
return 0
def cmd_login_telegram(args: argparse.Namespace) -> int:
"""Autoriza la sesion dedicada de Telegram (interactivo, una sola vez)."""
from monitor_estado.fuentes import telegram as tg
if getattr(args, "qr", False):
tg.login_qr(config.cargar())
else:
tg.login(config.cargar(), force_sms=getattr(args, "sms", False))
return 0
def build_parser() -> argparse.ArgumentParser:
"""Construye el parser de argumentos del CLI."""
parser = argparse.ArgumentParser(
prog="monitor_estado",
description="Monitor de estado (Telegram + correo + Jira) con resumen IA y aviso local.",
)
parser.add_argument("--version", action="version", version=f"monitor_estado {__version__}")
subs = parser.add_subparsers(dest="comando")
p_ciclo = subs.add_parser("ciclo", help="Ejecuta un ciclo completo y termina.")
p_ciclo.add_argument("--solo", choices=["telegram", "correo", "jira"], help="Recoge solo esa fuente.")
subs.add_parser("daemon", help="Bucle en segundo plano cada intervalo_minutos.")
subs.add_parser("estado", help="Muestra el ultimo resumen y las rutas.")
p_sim = subs.add_parser("simular", help="Simula entradas ficticias sin enviar ni crear nada real.")
p_sim.add_argument("--texto", help="Texto simulado (defecto: 'Confirmado el desarrollo').")
p_sim.add_argument("--asunto", help="Asunto del correo simulado.")
p_sim.add_argument("--de", help="Remitente simulado (defecto: DAX).")
p_login = subs.add_parser("login-telegram", help="Autoriza la sesion dedicada de Telegram (una vez).")
p_login.add_argument("--sms", action="store_true", help="Forzar que el codigo llegue por SMS.")
p_login.add_argument("--qr", action="store_true", help="Autorizar escaneando un codigo QR.")
return parser
def main(argv: list[str] | None = None) -> int:
"""Punto de entrada: despacha el subcomando o muestra ayuda + config."""
parser = build_parser()
args = parser.parse_args(argv)
if args.comando == "ciclo":
return cmd_ciclo(args)
if args.comando == "daemon":
return cmd_daemon(args)
if args.comando == "estado":
return cmd_estado(args)
if args.comando == "simular":
return cmd_simular(args)
if args.comando == "login-telegram":
return cmd_login_telegram(args)
# Sin subcomando: ayuda + resumen de configuracion.
parser.print_help()
print("\n--- Configuracion actual ---")
print(config.resumen_config(config.cargar()))
return 0
if __name__ == "__main__":
raise SystemExit(main())