Faire parvenir un texte à l'utilisateur — la partie qui manquait.
heartbeat/kinds.py calculait un drapeau delivered et rendait le texte à
son appelant. Personne ne l'envoyait nulle part. Une routine pouvait donc
tourner, réussir, consigner son résultat dans un journal, et n'atteindre
personne. C'est la raison pour laquelle Diapason n'a jamais parlé le premier :
non parce qu'il n'avait rien à dire, mais parce qu'il n'avait pas de bouche.
Le canal retenu est la notification macOS. Elle a trois qualités qu'aucun autre
n'a ici : elle survit à une application fermée, elle n'exige aucune permission
supplémentaire, et elle reste dans le centre de notifications quand personne
n'était devant l'écran — un briefing de sept heures du matin doit pouvoir
attendre son lecteur.
Écrire dans un fichier reste fait EN PLUS, toujours : une notification que
l'utilisateur balaie sans lire est perdue à jamais, alors qu'un journal se
relit. Les deux ne remplissent pas le même office.
Classes
Livraison
dataclass
Livraison(
notifiee: bool, journalisee: bool, detail: str = ""
)
Ce qui a été tenté, et ce qui a abouti.
Attributes
ok
property
Vrai dès qu'UN canal a abouti — le journal suffit à ne rien perdre.
Functions:
echapper_applescript
echapper_applescript(texte: str) -> str
Rend une chaîne sûre à l'intérieur de guillemets AppleScript.
Source code in src/diapason/heartbeat/livraison.py
| def echapper_applescript(texte: str) -> str:
"""Rend une chaîne sûre à l'intérieur de guillemets AppleScript."""
sortie = str(texte or "")
for brut, remplace in _A_ECHAPPER:
sortie = sortie.replace(brut, remplace)
# Un saut de ligne littéral casse la commande : osascript -e prend une
# ligne. Le corps d'une notification ne les affiche pas de toute façon.
return sortie.replace("\r", " ").replace("\n", " ")
|
notifier_macos
notifier_macos(
titre: str,
corps: str,
*,
sous_titre: str = "Diapason",
lanceur: Sequence[str] | None = None,
) -> tuple[bool, str]
Pose une notification dans le centre de notifications. Ne lève jamais.
Source code in src/diapason/heartbeat/livraison.py
| def notifier_macos(
titre: str,
corps: str,
*,
sous_titre: str = "Diapason",
lanceur: Sequence[str] | None = None,
) -> tuple[bool, str]:
"""Pose une notification dans le centre de notifications. Ne lève jamais."""
script = (
f'display notification "{echapper_applescript(corps)}" '
f'with title "{echapper_applescript(titre)}" '
f'subtitle "{echapper_applescript(sous_titre)}"'
)
commande = list(lanceur or ("osascript", "-e"))
try:
r = subprocess.run(
[*commande, script],
capture_output=True,
text=True,
timeout=DELAI_NOTIFICATION_S,
check=False,
)
except FileNotFoundError:
return False, "osascript introuvable (hors macOS ?)"
except subprocess.TimeoutExpired:
return False, "osascript n'a pas rendu la main"
except Exception as exc: # noqa: BLE001 - une livraison ratée ne tue rien
return False, str(exc)[:120]
if r.returncode != 0:
return False, (r.stderr or "").strip()[:160] or f"code {r.returncode}"
return True, ""
|
journaliser
journaliser(
texte: str,
*,
chemin: Path | str | None = None,
quand=None,
) -> bool
Ajoute le texte au journal des briefings. Ne lève jamais.
Une notification balayée sans être lue est perdue ; le journal, lui, se
relit. C'est pourquoi il est écrit MÊME quand la notification a abouti.
Source code in src/diapason/heartbeat/livraison.py
| def journaliser(texte: str, *, chemin: Path | str | None = None, quand=None) -> bool:
"""Ajoute le texte au journal des briefings. Ne lève jamais.
Une notification balayée sans être lue est perdue ; le journal, lui, se
relit. C'est pourquoi il est écrit MÊME quand la notification a abouti.
"""
if chemin is None:
from diapason.core.paths import get_config_dir
chemin = get_config_dir() / "briefings.md"
chemin = Path(chemin).expanduser()
horodatage = (quand or datetime.now()).strftime("%Y-%m-%d %H:%M")
try:
chemin.parent.mkdir(parents=True, exist_ok=True)
with chemin.open("a", encoding="utf-8") as f:
f.write(f"\n## {horodatage}\n\n{texte.rstrip()}\n")
return True
except Exception: # noqa: BLE001
logger.warning("briefing non journalisé dans %s", chemin, exc_info=True)
return False
|
livrer
livrer(
titre: str,
corps: str,
*,
notifier: bool = True,
chemin_journal: Path | str | None = None,
) -> Livraison
Notifie ET journalise. Un canal qui tombe n'emporte pas l'autre.
Source code in src/diapason/heartbeat/livraison.py
| def livrer(
titre: str,
corps: str,
*,
notifier: bool = True,
chemin_journal: Path | str | None = None,
) -> Livraison:
"""Notifie ET journalise. Un canal qui tombe n'emporte pas l'autre."""
notifiee, detail = (False, "notification désactivée")
if notifier:
notifiee, detail = notifier_macos(titre, corps)
if not notifiee:
logger.warning("notification non posée : %s", detail)
journalisee = journaliser(corps, chemin=chemin_journal)
return Livraison(notifiee=notifiee, journalisee=journalisee, detail=detail)
|