216 lines
7.8 KiB
Python
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())
|