801bd13f10
Debito §5.2 chiuso su decisione dell'operatore. `run_once` salvava lo stato coi marcatori `alerted` gia' a True e l'invio lo faceva il chiamante DOPO: col 6,9% di invii falliti misurato (2 su 29), un 🚨 perso restava perso per l'EPISODIO INTERO — l'ora dopo lo stato diceva "gia' detto" e usciva WATCH/MUTO. Gli episodi storici durano 200-2.324 ore, quindi il buco non era teorico. - `run_once(state_path, sender=None)`: il sender e' INIETTATO, non importato — e' cio' che tiene la funzione testabile senza rete d'uscita, che era la ragione del disegno precedente. Senza sender il comportamento resta quello di prima e il report lo DICE (`invio`), invece di lasciar credere che qualcosa sia partito. - Su invio fallito si disfano SOLO i marcatori "gia' detto", non le misure: · asset in ALERT -> alerted=False, l'ora dopo ri-allerta; · lock MAINT/ALERT -> alerted_soft/hard=False ma le ORE restano a correre, cosi' una manutenzione che sfora la grazia sale ad ALERT anche col trasporto giu' (disfare anche le ore congelerebbe l'escalation proprio mentre non si riesce a parlare); · lock RIENTRATO -> si ripristina l'intero LockState, perche' il rientro si annuncia una volta sola e senza le ore non ci sarebbe piu' niente da dire. - `notify(..., tentativi=)`: il retry esisteva in `send` e non arrivava qui. venue_watch ora manda con 3 tentativi. - L'esito dell'invio finisce nel log del cron invece di sparire. 5 test nuovi. Il primo e' quello che conta — dopo un invio fallito, l'ora dopo ri-allerta — col suo controllo positivo (un invio riuscito consuma l'allarme UNA volta sola), senza il quale "ri-allerta sempre" passerebbe. Suite: 782 passati, 2 falliti (i due del gate GTAA, non toccati qui). Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
106 lines
4.1 KiB
Python
106 lines
4.1 KiB
Python
"""Notifiche Telegram per l'esecuzione live (ordini + errori). Stdlib only, NO-OP se non configurato.
|
|
|
|
Config (gitignored): TELEGRAM_BOT_TOKEN + TELEGRAM_CHAT_ID da env, oppure da .env.mainnet / .env.
|
|
Se mancano, notify() ritorna False e non rompe nulla (l'esecuzione non dipende dagli alert).
|
|
|
|
Test della config: uv run python -m src.live.notifier "messaggio di prova"
|
|
"""
|
|
from __future__ import annotations
|
|
|
|
import os
|
|
import time
|
|
import urllib.parse
|
|
import urllib.request
|
|
from pathlib import Path
|
|
|
|
PROJECT_ROOT = Path(__file__).resolve().parents[2]
|
|
|
|
|
|
def _cfg() -> tuple[str, str]:
|
|
tok = os.environ.get("TELEGRAM_BOT_TOKEN", "")
|
|
chat = os.environ.get("TELEGRAM_CHAT_ID", "")
|
|
if tok and chat:
|
|
return tok, chat
|
|
for fn in (".env.mainnet", ".env"):
|
|
p = PROJECT_ROOT / fn
|
|
if not p.exists():
|
|
continue
|
|
for ln in p.read_text().splitlines():
|
|
ln = ln.strip()
|
|
if ln.startswith("TELEGRAM_BOT_TOKEN=") and not tok:
|
|
tok = ln.split("=", 1)[1].strip()
|
|
elif ln.startswith("TELEGRAM_CHAT_ID=") and not chat:
|
|
chat = ln.split("=", 1)[1].strip()
|
|
return tok, chat
|
|
|
|
|
|
# Ultimo esito d'invio, per chi vuole SAPERE se il messaggio e' partito. Fino al 2026-08-23
|
|
# `send()` faceva un tentativo e ingoiava l'eccezione senza scriverla da nessuna parte: quando
|
|
# serviva sapere se un allarme era arrivato, l'informazione non esisteva (tasso misurato di
|
|
# invii falliti: 6,9%, 2 su 29). Regola gia' codificata il 29/07 su un altro percorso e mai
|
|
# applicata qui: *se un errore si ingoia per non bloccare, si registra nel punto in cui lo si ingoia.*
|
|
_ULTIMO_ERRORE: str | None = None
|
|
|
|
|
|
def ultimo_errore() -> str | None:
|
|
"""Motivo dell'ultimo invio fallito, o None se l'ultimo e' riuscito. Non sopravvive a un
|
|
invio riuscito: una causa vecchia accanto a un invio nuovo manda sulla pista sbagliata."""
|
|
return _ULTIMO_ERRORE
|
|
|
|
|
|
def send(text: str, tentativi: int = 1, pausa_s: float = 2.0) -> bool:
|
|
"""`tentativi` DEFAULT 1 = comportamento invariato per tutti i chiamanti esistenti.
|
|
|
|
Chi vuole il retry lo chiede: alzarlo per tutti cambierebbe la latenza degli allarmi di
|
|
`venue_watch` e `book_execute` su un percorso con soldi veri, e non e' una modifica da fare
|
|
di straforo dentro un'altra funzionalita'.
|
|
"""
|
|
global _ULTIMO_ERRORE
|
|
tok, chat = _cfg()
|
|
if not tok or not chat:
|
|
_ULTIMO_ERRORE = "config assente (TELEGRAM_BOT_TOKEN/TELEGRAM_CHAT_ID)"
|
|
return False
|
|
url = f"https://api.telegram.org/bot{tok}/sendMessage"
|
|
data = urllib.parse.urlencode({"chat_id": chat, "text": text, "parse_mode": "HTML"}).encode()
|
|
ultimo = ""
|
|
for i in range(max(1, tentativi)):
|
|
try:
|
|
urllib.request.urlopen(url, data, timeout=10)
|
|
_ULTIMO_ERRORE = None
|
|
return True
|
|
except Exception as e: # noqa: BLE001 — si registra, non si nasconde
|
|
ultimo = f"{type(e).__name__}: {str(e)[:120]}"
|
|
if i + 1 < max(1, tentativi):
|
|
time.sleep(pausa_s * (i + 1)) # backoff lineare
|
|
_ULTIMO_ERRORE = f"{max(1, tentativi)} tentativi falliti — {ultimo}"
|
|
return False
|
|
|
|
|
|
def notify(title: str, data: dict | None = None, tentativi: int = 1) -> bool:
|
|
"""Invia un alert formattato. Ritorna True se inviato (config presente + rete ok).
|
|
|
|
`tentativi` DEFAULT 1 = comportamento invariato per tutti i chiamanti esistenti; chi manda
|
|
un allarme che non puo' permettersi di perdere lo alza (venue_watch: 3).
|
|
"""
|
|
try:
|
|
from src.version import APP_VERSION
|
|
ver = f" <code>v{APP_VERSION}</code>"
|
|
except Exception:
|
|
ver = ""
|
|
lines = [f"<b>{title}</b>{ver}"]
|
|
for k, v in (data or {}).items():
|
|
lines.append(f" {k}: {v}")
|
|
return send("\n".join(lines), tentativi=tentativi)
|
|
|
|
|
|
def is_configured() -> bool:
|
|
tok, chat = _cfg()
|
|
return bool(tok and chat)
|
|
|
|
|
|
if __name__ == "__main__":
|
|
import sys
|
|
msg = sys.argv[1] if len(sys.argv) > 1 else "TP01 — test alert"
|
|
ok = notify("🔔 " + msg, {"configurato": is_configured()})
|
|
print("inviato" if ok else "NON inviato (TELEGRAM_BOT_TOKEN/CHAT_ID assenti in env o .env.mainnet)")
|