Files
college-infos/pronote_sync/channels/__init__.py
Antoine Van Elstraete e07a6d709d feat: implement get_channel factory for XMPP channel (M10-U6)
get_channel(settings, dry_run=False) -> Channel | None with:
- enabled=False → None (no warning, no exception)
- enabled=True + missing jid/password/to/host → redacted warning log, None
- enabled=True + complete config → SyncXmppChannel instance
- Factory never raises exceptions (D2 non-blocking degradation)
- redact_secrets with extra_secrets=[password, jid, to] on warning logs

Re-exports Channel, XmppChannel, SyncXmppChannel from channels package.

19 unit tests covering disabled, misconfigured, complete, dry-run, and
secret-safe warning log scenarios.

Co-authored-by: opencode/test-engineer <test-engineer@agents.invalid>
Co-authored-by: opencode/coder <coder@agents.invalid>
2026-09-07 23:29:25 +02:00

82 lines
3.3 KiB
Python

"""Fabrique de création des canaux de sortie du pipeline ``pronote-sync``.
Ce module expose la fonction :func:`get_channel` qui instancie le canal de
sortie XMPP à partir de sa configuration, ainsi que les types publics du
paquet ``pronote_sync.channels`` :
:class:`~pronote_sync.channels.protocol.Channel`,
:class:`~pronote_sync.channels.xmpp.XmppChannel` et
:class:`~pronote_sync.channels.xmpp.SyncXmppChannel`.
"""
from __future__ import annotations
import logging
from pronote_sync.channels.protocol import Channel
from pronote_sync.channels.xmpp import SyncXmppChannel, XmppChannel
from pronote_sync.config.settings import XmppSettings
from pronote_sync.utils.redaction import redact_secrets
logger = logging.getLogger(__name__)
__all__ = ["Channel", "XmppChannel", "SyncXmppChannel", "get_channel"]
def get_channel(settings: XmppSettings, dry_run: bool = False) -> Channel | None:
"""Instancie le canal de sortie XMPP selon la configuration (D2).
Si le canal est désactivé (``enabled`` à ``False``), la fabrique
retourne ``None`` sans avertissement ni exception. Si le canal est
activé mais que l'un des champs requis (``jid``, ``password``, ``to``,
``host``) est vide ou absent, un avertissement est journalisé puis
``None`` est retourné. Dans tous les autres cas, une instance de
:class:`~pronote_sync.channels.xmpp.SyncXmppChannel` est construite et
retournée.
L'avertissement est expurgé des valeurs sensibles (``jid``, mot de
passe, destinataire) via :func:`pronote_sync.utils.redaction.redact_secrets`
(SEC-XMPP-02) : le message journalisé ne contient jamais ces valeurs en
clair. La fabrique ne lève jamais d'exception (dégradation non bloquante).
:param settings: Paramètres de configuration du canal XMPP.
:param dry_run: Si ``True``, le canal est créé en mode simulation
(aucun envoi réseau lors de l'appel à ``send``).
:return: Canal de sortie prêt à l'emploi, ou ``None`` si le canal est
désactivé ou mal configuré.
:rtype: Channel | None
"""
if not settings.enabled:
return None
# SEC-XMPP-02 : valeurs sensibles à masquer dans le journal (les valeurs
# ``None`` sont ignorées).
extra_secrets = [
secret for secret in (settings.password, settings.jid, settings.to) if secret is not None
]
# SEC-XMPP-02 : rejeter aussi les chaînes vides ou composées uniquement
# d'espaces : ``bool(SecretStr)`` et ``bool(str)`` ne testent que la
# présence de l'objet, pas la valeur contenue.
missing_fields = [
name
for name, present in (
("jid", settings.jid is not None and bool(settings.jid.strip())),
(
"password",
settings.password is not None
and bool(settings.password.get_secret_value().strip()),
),
("to", settings.to is not None and bool(settings.to.strip())),
("host", bool(settings.host.strip())),
)
if not present
]
if missing_fields:
logger.warning(
"XMPP : configuration incomplète (champs manquants : %s), canal désactivé.",
redact_secrets(", ".join(missing_fields), extra_secrets=extra_secrets),
)
return None
return SyncXmppChannel(settings, dry_run=dry_run)