revisione settimanale in sola lettura: revisore fable, rapporto firmato + Telegram, lunedi' 06:15 UTC
- src/live/revisione.py: materiale (CLAUDE.md, config, crontab, git, monitor_health, stati dei sorveglianti, 7 g di giornale e diari, ~195k caratteri), prompt con dieci regole (sola lettura, fonte per ogni segnalazione, solo numeri del materiale, §3 non si ripropone, idee nuove solo citando la memoria, gate con data e criterio), guardia sui numeri, rapporto firmato (P13), Telegram = Sintesi con taglio dichiarato; modello muto o risposta senza titoli -> rapporto «NON eseguita» + 🚨 (P5). - scripts/live/revisione.py (guardia cli.valida, --secco/--no-telegram/--quiet/--giorno/--modello), scripts/cron_review.sh, crontab `15 6 * * 1`. - primo giro reale: il prompt su argv moriva con `Argument list too long` senza rapporto ne' allerta -> prompt su stdin, ogni eccezione diventa errore dichiarato; `analista.pulisci` toglieva i titoli. Secondo giro: docs/revisioni/2026-09-09.md, stato sospetta (cifre in formato italiano), nessun URGENTE. - scartati con la memoria: autoregolazione dei parametri, generazione automatica di strategie, macro da internet (diario 2026-09-09c, memoria 40). - test: tests/test_revisione.py (18) + 2 in test_cli_flag; suite 1031/1031. Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01Bjj5vPEBoAJrB23P6RjKzs
This commit is contained in:
@@ -0,0 +1,346 @@
|
||||
"""Revisione settimanale in SOLA LETTURA: un revisore diverso dall'autore rilegge il sistema.
|
||||
|
||||
PERCHE' ESISTE (2026-09-09). Nove sorveglianti in cron guardano ognuno la propria grandezza
|
||||
(venue, fee, edge, scala, USDE, VRP-f, health, balance, paper-monitor); il giornale e l'analista
|
||||
leggono UN giorno. Nessuno rilegge il sistema nel suo insieme: stato, gate con le date, decisioni
|
||||
vincolanti, debiti aperti, cosa e' cambiato nei diari e nei commit. La revisione con un secondo
|
||||
modello (regola del 2026-09-03: chi rivede non e' chi ha scritto) ha trovato in una sessione due
|
||||
conclusioni false e un look-ahead che l'autore aveva riletto tre volte. Questo modulo la schedula.
|
||||
|
||||
COSA FA. Raccoglie il materiale (CLAUDE.md, config, crontab, log git, monitor_health, giornale
|
||||
degli ultimi giorni, diari recenti, stato dei sorveglianti), lo da' a un modello DIVERSO da quello
|
||||
che scrive il codice, e salva il rapporto FIRMATO in `docs/revisioni/<data>.md`, con una sintesi su
|
||||
Telegram. Il rapporto e' l'opinione di un lettore fallibile (P13): dice cosa non torna, cosa e'
|
||||
maturo da decidere, cosa propone. Non e' parte del registro delle misure.
|
||||
|
||||
COSA NON FA, PER COSTRUZIONE. Non tocca config, libro, cron, codice, dati: l'unico file che
|
||||
scrive e' il rapporto. Non esegue proposte: le scrive, e l'operatore decide. Non riapre le
|
||||
decisioni vincolanti (CLAUDE.md §3) se la colonna «cosa la riapre» non e' soddisfatta, e non
|
||||
propone «idee nuove» senza citare il file di memoria che ha gia' ucciso l'idea simile. Un ciclo
|
||||
che si autoregola e' esattamente cio' che `scale_watch` e `venue_watch` trattano come anomalia.
|
||||
|
||||
Il silenzio non e' una revisione: se il modello non risponde, il rapporto lo dice e Telegram lo
|
||||
dice, invece di lasciare un lunedi' vuoto che si legge come «tutto bene».
|
||||
"""
|
||||
from __future__ import annotations
|
||||
|
||||
import html
|
||||
import json
|
||||
import subprocess
|
||||
from datetime import date, datetime, timedelta, timezone
|
||||
from pathlib import Path
|
||||
|
||||
from src.live import analista as A
|
||||
from src.live import monitor_health as MH
|
||||
|
||||
MODELLO = "claude-fable-5-1" # il revisore: NON il modello che scrive il codice
|
||||
CADENZA = "settimanale"
|
||||
FINESTRA_GIORNALE_G = 7 # pagine del giornale lette
|
||||
FINESTRA_DIARI_G = 7 # diari letti (la settimana: e' una revisione settimanale)
|
||||
FINESTRA_COMMIT_G = 14 # commit letti
|
||||
CARTELLA = Path("docs") / "revisioni"
|
||||
MAX_CHARS_VOCE = 90_000 # CLAUDE.md sta sotto; il resto e' molto piu' piccolo
|
||||
MAX_CHARS_TOTALE = 400_000
|
||||
MAX_NUMERI_LIBERI = 12 # una revisione confronta e sottrae: qualche numero suo e' lecito
|
||||
LIMITE_TELEGRAM = 3_500 # sotto i 4.096 di Telegram, con la testata
|
||||
TIMEOUT_S = 900 # il materiale e' grande: 5 minuti non bastano sempre
|
||||
|
||||
TITOLI = ("## Sintesi", "## Cosa non torna", "## Cosa e' maturo da decidere",
|
||||
"## Proposte (nessuna azione eseguita)", "## Cosa ho letto e cosa mi mancava")
|
||||
|
||||
ISTRUZIONI = """Sei il REVISORE settimanale di un progetto di trading quantitativo (BTC/ETH su Deribit, libro
|
||||
live con soldi veri, ~$4.500). Sei un modello DIVERSO da quello che scrive codice e analisi del
|
||||
progetto: il tuo valore e' che non condividi il suo ragionamento. Leggi il materiale qui sotto e
|
||||
scrivi la revisione, in italiano, per l'operatore (che decide) e per l'agente (che esegue).
|
||||
|
||||
REGOLE VINCOLANTI:
|
||||
1. SOLA LETTURA. Non proponi comandi da eseguire ora, non scrivi codice: descrivi cosa non torna
|
||||
e cosa andrebbe verificato o deciso, e da chi.
|
||||
2. OGNI SEGNALAZIONE CITA LA FONTE: file e sezione (es. «CLAUDE.md §4, gate STATARB»), data della
|
||||
pagina di giornale, nome del diario, hash del commit. Una segnalazione senza fonte non vale.
|
||||
3. USA SOLO I NUMERI DEL MATERIALE. Puoi sottrarre e confrontare, ma ogni cifra che scrivi deve
|
||||
essere rintracciabile qui sotto; un controllo automatico elenchera' quelle che non lo sono.
|
||||
4. LE DECISIONI VINCOLANTI (CLAUDE.md §3) NON SI RIPROPONGONO. Puoi dire che una condizione della
|
||||
colonna «cosa la riapre» sembra soddisfatta, citando il dato; non puoi consigliare di cambiare
|
||||
la decisione per altri motivi.
|
||||
5. UN'IDEA «NUOVA» SI PROPONE SOLO citando il file di memoria (docs/memory/…) o il paragrafo che ha
|
||||
gia' ucciso l'idea piu' simile, e dicendo in che cosa questa differisce. Se non lo sai, non la
|
||||
proponi: scrivi «da verificare in memoria prima di proporla».
|
||||
6. I GATE HANNO UNA DATA (CLAUDE.md §4). Elenca quelli la cui data cade entro 30 giorni o e' gia'
|
||||
passata, con il criterio scritto e lo stato che il materiale mostra. Non anticipare un verdetto:
|
||||
un gate si decide alla sua data, coi criteri scritti prima.
|
||||
7. DISTINGUI misurato da dedotto. «Non determinabile da questo materiale» e' una risposta buona.
|
||||
8. NIENTE PREVISIONI di mercato, niente consigli di direzione, niente target.
|
||||
9. Se qualcosa richiede l'operatore SUBITO (fondi, credenziali, un sorvegliante fermo che copre un
|
||||
gate, una modifica di config non registrata), mettilo nella Sintesi con la parola URGENTE.
|
||||
10. Massimo ~1.000 parole. Usa ESATTAMENTE questi titoli, in quest'ordine, e nient'altro prima:
|
||||
%s
|
||||
Sotto «Cosa ho letto e cosa mi mancava» di' quali fonti hai usato e quale dato ti sarebbe
|
||||
servito e non c'era (e' l'elenco da cui il materiale della prossima settimana migliora).
|
||||
""" % "\n ".join(TITOLI)
|
||||
|
||||
|
||||
# ---------------------------------------------------------------- materiale
|
||||
|
||||
def _tronca(testo: str, n: int | None = None) -> tuple[str, bool]:
|
||||
n = MAX_CHARS_VOCE if n is None else n # letto a runtime: un default e' catturato alla definizione (debito 12)
|
||||
if len(testo) <= n:
|
||||
return testo, False
|
||||
return testo[:n] + f"\n[… troncato a {n:,} caratteri su {len(testo):,}]", True
|
||||
|
||||
|
||||
def _cmd(args: list[str], cwd: Path) -> str:
|
||||
"""Uscita di un comando di SOLA LETTURA, o il motivo per cui non si legge (P5: «non vedo»
|
||||
non e' «va tutto bene»)."""
|
||||
try:
|
||||
p = subprocess.run(args, cwd=str(cwd), capture_output=True, text=True, timeout=60)
|
||||
except Exception as exc: # noqa: BLE001 — un comando di lettura che esplode si dichiara
|
||||
return f"non leggibile: {type(exc).__name__}: {str(exc)[:120]}"
|
||||
if p.returncode != 0:
|
||||
return f"non leggibile: uscita {p.returncode}: {(p.stderr or p.stdout).strip()[:200]}"
|
||||
return p.stdout
|
||||
|
||||
|
||||
def _coda(path: Path, n: int) -> str:
|
||||
if not path.exists():
|
||||
return "(assente)"
|
||||
righe = path.read_text().splitlines()
|
||||
return "\n".join(righe[-n:]) if righe else "(vuoto)"
|
||||
|
||||
|
||||
def _data_nel_nome(p: Path) -> date | None:
|
||||
try:
|
||||
return date.fromisoformat(p.name[:10])
|
||||
except ValueError:
|
||||
return None
|
||||
|
||||
|
||||
def _monitor_health(root: Path, adesso: datetime) -> str:
|
||||
try:
|
||||
righe = MH.check_all(root, now=adesso)
|
||||
except Exception as exc: # noqa: BLE001 — si dichiara, non si tace (P3)
|
||||
return f"non leggibile: {type(exc).__name__}: {exc}"
|
||||
out = ["monitor | stato | perche' | gate a rischio"]
|
||||
for r in righe:
|
||||
out.append(f"{r.get('name')} | {r.get('status')} | {r.get('why')} | {r.get('gate') or '-'}")
|
||||
return "\n".join(out)
|
||||
|
||||
|
||||
def materiale(root: Path, adesso: datetime) -> list[tuple[str, str, bool]]:
|
||||
"""Le voci (nome, testo, troncato) date al revisore. Tutto in SOLA LETTURA.
|
||||
|
||||
L'ordine e' quello in cui un lettore umano le aprirebbe: prima le regole e lo stato dichiarato,
|
||||
poi cio' che gira (sorveglianti: piccoli, e per questo PRIMA delle voci grandi, cosi' il tetto
|
||||
totale non li taglia mai), poi cio' che e' successo (giornale, diari).
|
||||
"""
|
||||
oggi = adesso.date()
|
||||
voci: list[tuple[str, str]] = []
|
||||
|
||||
voci.append(("CLAUDE.md (stato, book, numeri, decisioni vincolanti, gate, debiti, regole)",
|
||||
(root / "CLAUDE.md").read_text() if (root / "CLAUDE.md").exists() else "(assente)"))
|
||||
cfg = root / "config" / "live.json"
|
||||
voci.append(("config/live.json (unica autorita' dei guardrail live)",
|
||||
cfg.read_text() if cfg.exists() else "(assente)"))
|
||||
voci.append(("crontab installata (crontab -l)", _cmd(["crontab", "-l"], root)))
|
||||
since = (oggi - timedelta(days=FINESTRA_COMMIT_G)).isoformat()
|
||||
voci.append((f"git log dal {since} + stato del working tree",
|
||||
_cmd(["git", "log", f"--since={since}", "--format=%h %ad %s", "--date=short"], root)
|
||||
+ "\n--- git status --short ---\n" + _cmd(["git", "status", "--short"], root)))
|
||||
voci.append(("monitor_health (i forward-monitor stanno registrando?)", _monitor_health(root, adesso)))
|
||||
|
||||
live = root / "data" / "live"
|
||||
stati = {
|
||||
"scale_watch_state.json": (live / "scale_watch_state.json", 999),
|
||||
"scale_history.jsonl (ultime 3)": (live / "scale_history.jsonl", 3),
|
||||
"vrp_f_watch/state.json": (root / "data" / "vrp_f_watch" / "state.json", 999),
|
||||
"usde_watch.jsonl (ultime 3)": (live / "usde_watch.jsonl", 3),
|
||||
"usde_convert.jsonl (ultime 3)": (live / "usde_convert.jsonl", 3),
|
||||
"balance_watch.jsonl (ultime 2)": (live / "balance_watch.jsonl", 2),
|
||||
"venue_news.jsonl (ultime 5)": (live / "venue_news.jsonl", 5),
|
||||
"movimenti_dichiarati.jsonl": (live / "movimenti_dichiarati.jsonl", 50),
|
||||
"equity_seen.json (watermark)": (live / "equity_seen.json", 999),
|
||||
}
|
||||
for nome, (p, n) in stati.items():
|
||||
voci.append((f"stato {nome}", _coda(p, n)))
|
||||
|
||||
for i in range(FINESTRA_GIORNALE_G, 0, -1):
|
||||
g = oggi - timedelta(days=i)
|
||||
p = root / "docs" / "journal" / f"{g.isoformat()}.md"
|
||||
if p.exists():
|
||||
voci.append((f"giornale {g.isoformat()}", p.read_text()))
|
||||
diari = sorted(p for p in (root / "docs" / "diary").glob("*.md")
|
||||
if (_data_nel_nome(p) or date.min) >= oggi - timedelta(days=FINESTRA_DIARI_G))
|
||||
for p in diari:
|
||||
voci.append((f"diario {p.name}", p.read_text()))
|
||||
|
||||
out, tot = [], 0
|
||||
for nome, testo in voci:
|
||||
t, tr = _tronca(testo)
|
||||
if tot + len(t) > MAX_CHARS_TOTALE:
|
||||
t, tr = _tronca(t, max(0, MAX_CHARS_TOTALE - tot)), True
|
||||
tot += len(t)
|
||||
out.append((nome, t, tr))
|
||||
return out
|
||||
|
||||
|
||||
def costruisci_prompt(voci: list[tuple[str, str, bool]], adesso: datetime) -> str:
|
||||
parti = [ISTRUZIONI, f"\nDATA DELLA REVISIONE: {adesso:%Y-%m-%d %H:%M}Z (usa questa per «oggi» e per le date dei gate).\n"]
|
||||
for nome, testo, tr in voci:
|
||||
parti.append(f"\n=== {nome}{' [TRONCATO]' if tr else ''} ===\n{testo}\n")
|
||||
parti.append("\nScrivi ora la revisione, coi cinque titoli, e nient'altro.")
|
||||
return "\n".join(parti)
|
||||
|
||||
|
||||
# ---------------------------------------------------------------- guardia
|
||||
|
||||
def valida(testo: str, fonte: str) -> tuple[str, list[str]]:
|
||||
"""(stato, motivi) — stato in {ok, sospetta, rifiutata}. Rifiutata = non e' una revisione
|
||||
(vuota o senza i titoli); sospetta = numeri non rintracciabili nel materiale, elencati."""
|
||||
motivi: list[str] = []
|
||||
t = testo.strip()
|
||||
if not t:
|
||||
return "rifiutata", ["risposta vuota"]
|
||||
mancanti = [x for x in TITOLI if x not in t]
|
||||
if mancanti:
|
||||
return "rifiutata", [f"titoli mancanti: {', '.join(mancanti)}"]
|
||||
fuori = A.numeri_non_supportati(t, fonte)
|
||||
if fuori:
|
||||
motivi.append(f"{len(fuori)} numeri non presenti nel materiale: " + ", ".join(fuori[:30]))
|
||||
if len(fuori) > MAX_NUMERI_LIBERI:
|
||||
return "sospetta", motivi
|
||||
return ("sospetta" if motivi else "ok"), motivi
|
||||
|
||||
|
||||
# ---------------------------------------------------------------- rapporto
|
||||
|
||||
def rapporto(testo: str | None, errore: str | None, stato: str, motivi: list[str], modello: str,
|
||||
adesso: datetime, voci: list[tuple[str, str, bool]]) -> str:
|
||||
testa = [f"# Revisione {CADENZA} — {adesso:%Y-%m-%d}", ""]
|
||||
if errore:
|
||||
testa += [f"## Revisione NON eseguita",
|
||||
"",
|
||||
f"Il modello `{modello}` non ha risposto: **{errore}**. Non c'e' una revisione questa "
|
||||
"settimana, e questo file esiste perche' un lunedi' senza rapporto si leggerebbe "
|
||||
"come «tutto bene» (P5).", ""]
|
||||
corpo = []
|
||||
else:
|
||||
testa += [f"*Scritta il {adesso:%Y-%m-%dT%H:%M:%S}Z dal modello `{modello}`, revisore diverso "
|
||||
"dall'autore del codice, in SOLA LETTURA: nessun file del repo, nessuna config, nessun "
|
||||
"ordine e' stato toccato. E' l'opinione di un lettore fallibile (P13): ogni numero e "
|
||||
"ogni proposta vanno verificati alla fonte prima di agire. Guardia sui numeri: "
|
||||
f"**{stato}**" + (f" — {'; '.join(motivi)}" if motivi else "") + ".*", ""]
|
||||
corpo = [testo.strip(), ""]
|
||||
coda = ["---", "## Firma e materiale", "",
|
||||
f"- revisore: `{modello}` · cadenza {CADENZA} · giornale {FINESTRA_GIORNALE_G} g · "
|
||||
f"diari {FINESTRA_DIARI_G} g · commit {FINESTRA_COMMIT_G} g",
|
||||
"- scritto da `scripts/live/revisione.py` (cron `scripts/cron_review.sh`); l'unico file "
|
||||
"scritto e' questo", "",
|
||||
"| voce | caratteri | troncata |", "|---|---|---|"]
|
||||
coda += [f"| {n} | {len(t):,} | {'si' if tr else ''} |" for n, t, tr in voci]
|
||||
return "\n".join(testa + corpo + coda) + "\n"
|
||||
|
||||
|
||||
def _sezione(testo: str, titolo: str) -> str:
|
||||
if titolo not in testo:
|
||||
return ""
|
||||
dopo = testo.split(titolo, 1)[1]
|
||||
for t in TITOLI:
|
||||
if t != titolo and t in dopo:
|
||||
dopo = dopo.split(t, 1)[0]
|
||||
return dopo.strip()
|
||||
|
||||
|
||||
def per_telegram(testo: str | None, errore: str | None, stato: str, adesso: datetime,
|
||||
path: Path) -> str:
|
||||
"""La Sintesi, con la testata e il taglio DICHIARATO (un taglio silenzioso e' peggio di
|
||||
un'assenza: si legge come un pensiero finito)."""
|
||||
if errore:
|
||||
return (f"<b>🚨 Revisione {CADENZA} NON eseguita</b> {adesso:%Y-%m-%d}\n"
|
||||
f"{html.escape(errore)}\n<code>{html.escape(str(path))}</code>")
|
||||
sintesi = _sezione(testo or "", TITOLI[0]) or "(nessuna Sintesi nel rapporto)"
|
||||
corpo = html.escape(sintesi, quote=False)
|
||||
tagliato = ""
|
||||
if len(corpo) > LIMITE_TELEGRAM:
|
||||
corpo, tagliato = corpo[:LIMITE_TELEGRAM], "\n[… sintesi tagliata: il resto e' nel file]"
|
||||
return (f"<b>🔍 Revisione {CADENZA}</b> {adesso:%Y-%m-%d} · guardia numeri: {stato}\n\n"
|
||||
f"{corpo}{tagliato}\n\n<code>{html.escape(str(path))}</code>")
|
||||
|
||||
|
||||
# ---------------------------------------------------------------- modello
|
||||
|
||||
def interroga(prompt: str, modello: str = MODELLO, timeout: int = TIMEOUT_S) -> tuple[str, str | None]:
|
||||
"""(testo, errore) dalla CLI `claude -p`, col prompt su STDIN.
|
||||
|
||||
Non su argv: al primo giro reale (09/09, 195k caratteri) `analista.interroga` e' morto con
|
||||
`OSError: Argument list too long` PRIMA di chiamare il modello — e l'eccezione, non catturata,
|
||||
ha lasciato zero rapporto e zero Telegram: il silenzio che questo modulo esiste per vietare.
|
||||
Qui ogni eccezione della chiamata e' un errore RESTITUITO (P3), mai propagato.
|
||||
"""
|
||||
try:
|
||||
p = subprocess.run(
|
||||
["claude", "-p", "--model", modello, "--allowed-tools", "", "--max-turns", "1"],
|
||||
input=prompt, capture_output=True, text=True, timeout=timeout)
|
||||
except FileNotFoundError:
|
||||
return "", "CLI `claude` non trovata"
|
||||
except subprocess.TimeoutExpired:
|
||||
return "", f"timeout dopo {timeout}s"
|
||||
except Exception as exc: # noqa: BLE001 — si restituisce, non si tace
|
||||
return "", f"{type(exc).__name__}: {str(exc)[:200]}"
|
||||
if p.returncode != 0:
|
||||
return "", A.motivo_uscita(p.returncode, p.stdout, p.stderr)
|
||||
return p.stdout.strip(), None # NON `analista.pulisci`: toglierebbe i titoli «## …» che la guardia richiede
|
||||
|
||||
|
||||
# ---------------------------------------------------------------- orchestrazione
|
||||
|
||||
def scrivi(root: Path, adesso: datetime | None = None, modello: str = MODELLO, *,
|
||||
secco: bool = False, telegram: bool = True, quiet: bool = False,
|
||||
interroga=None, invia=None) -> dict:
|
||||
"""Un giro: materiale → modello → rapporto → Telegram. Ritorna cosa e' successo.
|
||||
|
||||
`interroga(prompt, modello, timeout) -> (testo, errore)` e `invia(html) -> bool` sono
|
||||
INIETTATI (default: `interroga` di questo modulo — prompt su stdin — e `notifier.send`) cosi'
|
||||
i test non spendono e non mandano. Se uno dei due SOLLEVA, l'eccezione diventa un errore nel
|
||||
rapporto: un giro che muore a meta' non deve lasciare un lunedi' vuoto. Con `secco` non si chiama il modello e non si scrive niente: si stampa la taglia.
|
||||
"""
|
||||
adesso = adesso or datetime.now(timezone.utc)
|
||||
voci = materiale(root, adesso)
|
||||
prompt = costruisci_prompt(voci, adesso)
|
||||
esito = {"data": adesso.date().isoformat(), "voci": len(voci), "caratteri": len(prompt),
|
||||
"modello": modello, "secco": secco}
|
||||
if not quiet:
|
||||
print(f" revisione {esito['data']} modello {modello} materiale {len(voci)} voci, "
|
||||
f"{len(prompt):,} caratteri" + (" [SECCO: nessuna chiamata, nessuna scrittura]" if secco else ""))
|
||||
for n, t, tr in voci:
|
||||
print(f" {len(t):>8,} {n}{' [troncata]' if tr else ''}")
|
||||
if secco:
|
||||
return esito
|
||||
|
||||
interroga = interroga or globals()["interroga"]
|
||||
try:
|
||||
testo, errore = interroga(prompt, modello, TIMEOUT_S)
|
||||
except Exception as exc: # noqa: BLE001
|
||||
testo, errore = "", f"eccezione nella chiamata al modello: {type(exc).__name__}: {str(exc)[:200]}"
|
||||
stato, motivi = ("rifiutata", [errore]) if errore else valida(testo, prompt)
|
||||
if not errore and stato == "rifiutata":
|
||||
errore, testo = "risposta non e' una revisione: " + "; ".join(motivi), None
|
||||
|
||||
cartella = root / CARTELLA
|
||||
cartella.mkdir(parents=True, exist_ok=True)
|
||||
path = cartella / f"{adesso:%Y-%m-%d}.md"
|
||||
path.write_text(rapporto(testo, errore, stato, motivi, modello, adesso, voci))
|
||||
esito.update(stato=stato, errore=errore, motivi=motivi, file=str(path))
|
||||
|
||||
if telegram:
|
||||
from src.live import notifier as N
|
||||
invia = invia or (lambda h: N.send(h, tentativi=2))
|
||||
try:
|
||||
esito["telegram"] = bool(invia(per_telegram(testo, errore, stato, adesso, path)))
|
||||
except Exception as exc: # noqa: BLE001
|
||||
esito["telegram"] = False
|
||||
esito["telegram_errore"] = f"{type(exc).__name__}: {str(exc)[:200]}"
|
||||
if not quiet:
|
||||
tg = "" if "telegram" not in esito else (" telegram: inviato" if esito["telegram"] else
|
||||
f" telegram: NON inviato ({esito.get('telegram_errore', 'send False')})")
|
||||
print(f" -> {stato}" + (f" ({errore})" if errore else "") + f" {path}{tg}")
|
||||
return esito
|
||||
Reference in New Issue
Block a user