Files
kanbante/app/hub_reglages.py
T
2026-11-06 17:05:19 +00:00

154 lines
5.5 KiB
Python

"""Réglages généraux — consommation des réglages du Hub (HUB-21).
Propagation du pilote TikTak 0.61.22 : le Hub expose
``GET {hub}/api/reglages/effectifs`` — endpoint **public** (aucun
cookie relayé), ``Accept: application/json``, délai 2 s. Réponse
200, forme figée (liste fermée D6(a)) : ``{"recherche_partout":
bool, "banniere": str, "domaine_defaut": "pro"|"famille"|"perso"
|null, "ttl_secondes": 60}``.
Cache mémoire 60 s, sur le patron du pilote : l'horodatage est
posé **avant** l'appel — un échec conserve l'ancien cache et ne
déclenche pas de rafale de requêtes vers un Hub en panne. L'URL
du Hub est résolue comme celle de la barre commune
(``app.config.resoudre_urls_draco`` : variable explicite >
hôte public de la requête > dérivation par l'hôte sœur de
l'agent > défaut officiel) — jamais d'URL dev figée.
**Repli intégral** : Hub injoignable, en erreur ou réponse de
forme inattendue → les **défauts actuels** (``recherche_partout:
True``, ``banniere: ""``, ``domaine_defaut: None``), c'est-à-dire
le comportement de Kanbante avant HUB-21. Ce module ne lève
jamais.
Consommation chez Kanbante :
- ``banniere`` : bandeau sous la barre commune (partiel
``_header.html``, présent sur toutes les pages applicatives),
texte échappé par l'autoescape Jinja ;
- ``recherche_partout`` : attribut ``data-recherche-partout`` de
la page /recherche — si faux, la partie fédérée est désactivée
(la recherche locale Kanbante continue) ;
- ``domaine_defaut`` : **non consommé** — Kanbante n'a pas de
notion de domaine (Pro/Famille/Perso) propre ; le sélecteur de
domaine de la barre commune pilote le panneau Spark, pas les
vues de Kanbante. La valeur est lue et validée, sans effet
local.
"""
from __future__ import annotations
import logging
import threading
import time
import httpx
from app.config import resoudre_urls_draco
logger = logging.getLogger(__name__)
#: Réglages par défaut = comportement actuel de Kanbante (repli).
DEFAUTS = {"recherche_partout": True, "banniere": "", "domaine_defaut": None}
#: Domaines reconnus du contrat D6(a).
DOMAINES = ("pro", "famille", "perso")
#: TTL du cache, aligné sur ``ttl_secondes`` annoncé par le Hub.
CACHE_TTL = 60.0 # secondes
_verrou = threading.Lock()
_cache: dict | None = None # derniers réglages validés
_cache_fetched_at: float = 0.0 # horodatage (monotone) du dernier appel
def _reset_cache() -> None:
"""Vide le cache (tests, ou redémarrage de processus)."""
global _cache, _cache_fetched_at
with _verrou:
_cache = None
_cache_fetched_at = 0.0
def _valider(donnees) -> dict | None:
"""Valide la forme figée du contrat ; ``None`` si inattendue."""
if not isinstance(donnees, dict):
return None
recherche = donnees.get("recherche_partout")
banniere = donnees.get("banniere")
domaine = donnees.get("domaine_defaut")
if not isinstance(recherche, bool):
return None
if not isinstance(banniere, str):
return None
if domaine is not None and domaine not in DOMAINES:
return None
return {
"recherche_partout": recherche,
"banniere": banniere,
"domaine_defaut": domaine,
}
def _consulter(hub_url: str) -> dict | None:
"""Un appel au Hub ; réglages validés ou ``None`` (repli)."""
url = f"{hub_url.rstrip('/')}/api/reglages/effectifs"
try:
reponse = httpx.get(
url, headers={"Accept": "application/json"}, timeout=2.0)
except Exception as exc: # Hub injoignable : repli, jamais un blocage
logger.debug(
"Réglages généraux du Hub injoignables (%s) : %s", url, exc)
return None
if getattr(reponse, "status_code", None) != 200:
logger.debug(
"Réglages généraux du Hub en erreur (%s) : statut %s",
url, getattr(reponse, "status_code", "?"))
return None
try:
donnees = reponse.json()
except Exception:
return None
return _valider(donnees)
def reglages_effectifs(hote: str | None = None) -> dict:
"""Réglages généraux effectifs pour Kanbante.
Réponse du Hub validée et mise en cache 60 s ; à défaut (Hub
muet, en erreur ou forme inattendue), l'ancien cache s'il
existe, sinon les défauts actuels. ``hote`` est l'hôte public
de la requête, pour résoudre l'URL du Hub comme la barre
commune. Ne lève jamais.
"""
global _cache, _cache_fetched_at
now = time.monotonic()
with _verrou:
if _cache is not None and now - _cache_fetched_at < CACHE_TTL:
return dict(_cache)
# Horodatage posé AVANT l'appel (patron du pilote) :
# l'ancien cache reste en vigueur pendant l'appel.
_cache_fetched_at = now
hub_url = resoudre_urls_draco(hote).hub_url
reglages = _consulter(hub_url)
if reglages is None:
logger.debug(
"Réglages généraux du Hub indisponibles (%s) — "
"comportement actuel conservé", hub_url)
with _verrou:
return dict(_cache) if _cache is not None else dict(DEFAUTS)
with _verrou:
_cache = reglages
_cache_fetched_at = time.monotonic()
return dict(reglages)
def banniere(hote: str | None = None) -> str:
"""Texte de la bannière d'annonce (``""`` = aucune bannière)."""
return reglages_effectifs(hote)["banniere"]
def recherche_partout(hote: str | None = None) -> bool:
"""Drapeau de la recherche fédérée (défaut actuel : actif)."""
return bool(reglages_effectifs(hote)["recherche_partout"])