03-oct: superguardado automático
This commit is contained in:
215
scripts/monitor_estado.py
Normal file
215
scripts/monitor_estado.py
Normal file
@@ -0,0 +1,215 @@
|
||||
#!/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())
|
||||
Reference in New Issue
Block a user