feat(M4): sources/pronote/client.py — client pronotepy avec Protocol
- PronoteClientProtocol : Protocol (get_messages, get_informations, get_agenda_fallback) pour la logique de repli - PronoteClient : encapsule pronotepy.Client (connexion paresseuse) avec API réelle 2.15.7 (lessons/homework/discussions/information_and_surveys) - Erreurs PronoteAPIError et ValueError catchées, loggées avec redact_exception, retournent des résultats vides - Aucune fuite de secret (get_secret_value() uniquement à la connexion) - pre-commit : ajout de pronotepy au hook mypy Co-authored-by: opencode/coder <coder@agents.invalid>
This commit is contained in:
@@ -26,7 +26,7 @@ repos:
|
||||
name: mypy
|
||||
entry: mypy
|
||||
language: python
|
||||
additional_dependencies: ["mypy>=1.10.0", "pydantic>=2.0.0", "pydantic-settings>=2.0.0", "pytest>=8.0.0", "types-requests>=2.31.0", "icalendar>=5.0.0"]
|
||||
additional_dependencies: ["mypy>=1.10.0", "pydantic>=2.0.0", "pydantic-settings>=2.0.0", "pytest>=8.0.0", "types-requests>=2.31.0", "icalendar>=5.0.0", "pronotepy>=2.15.0"]
|
||||
types: [python]
|
||||
pass_filenames: true
|
||||
|
||||
|
||||
215
pronote_sync/sources/pronote/client.py
Normal file
215
pronote_sync/sources/pronote/client.py
Normal file
@@ -0,0 +1,215 @@
|
||||
"""Client d'accès à Pronote via ``pronotepy``.
|
||||
|
||||
Ce module fournit l'encapsulation du client ``pronotepy`` pour la source
|
||||
Pronote : récupération des messages des professeurs, des informations et
|
||||
sondages, ainsi que des cours et devoirs en mode repli lorsque le flux
|
||||
iCal échoue. Toutes les erreurs sont journalisées avec des secrets masqués.
|
||||
"""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
import logging
|
||||
from datetime import date
|
||||
from typing import Protocol
|
||||
|
||||
import pronotepy
|
||||
|
||||
from pronote_sync.config.settings import PronoteSettings
|
||||
from pronote_sync.models.agenda import Lesson, LessonStatus
|
||||
from pronote_sync.models.homework import Homework
|
||||
from pronote_sync.models.message import Message, MessageType
|
||||
from pronote_sync.utils.redaction import redact_exception
|
||||
|
||||
logger = logging.getLogger(__name__)
|
||||
|
||||
|
||||
class PronoteClientProtocol(Protocol):
|
||||
"""Interface du client Pronote consommée par la logique de repli."""
|
||||
|
||||
def get_messages(self) -> list[Message]:
|
||||
"""Récupère les messages des discussions Pronote.
|
||||
|
||||
:return: Liste des messages des professeurs.
|
||||
:rtype: list[Message]
|
||||
"""
|
||||
...
|
||||
|
||||
def get_informations(self) -> list[Message]:
|
||||
"""Récupère les informations et sondages Pronote.
|
||||
|
||||
:return: Liste des informations et sondages.
|
||||
:rtype: list[Message]
|
||||
"""
|
||||
...
|
||||
|
||||
def get_agenda_fallback(self, start: date, end: date) -> tuple[list[Lesson], list[Homework]]:
|
||||
"""Récupère les cours et les devoirs via ``pronotepy`` (repli iCal).
|
||||
|
||||
:param start: Date de début de la fenêtre (incluse).
|
||||
:param end: Date de fin de la fenêtre (incluse).
|
||||
:return: Tuple ``(cours, devoirs)``.
|
||||
:rtype: tuple[list[Lesson], list[Homework]]
|
||||
"""
|
||||
...
|
||||
|
||||
|
||||
class PronoteClient:
|
||||
"""Client d'accès à Pronote via ``pronotepy``.
|
||||
|
||||
Encapsule ``pronotepy.Client`` avec une connexion paresseuse : la
|
||||
connexion n'est établie qu'à la première méthode de récupération
|
||||
appelée. Les erreurs ``pronotepy.PronoteAPIError`` sont journalisées
|
||||
avec des secrets masqués et les méthodes de récupération retournent
|
||||
alors une valeur vide au lieu de propager l'exception.
|
||||
"""
|
||||
|
||||
def __init__(self, settings: PronoteSettings) -> None:
|
||||
"""Initialise le client Pronote sans se connecter.
|
||||
|
||||
:param settings: Paramètres d'accès à Pronote (username, password, ent).
|
||||
"""
|
||||
self._settings: PronoteSettings = settings
|
||||
self._client: pronotepy.Client | None = None
|
||||
|
||||
def _connect(self) -> pronotepy.Client:
|
||||
"""Crée et connecte le client ``pronotepy`` (connexion paresseuse).
|
||||
|
||||
Le client est créé une seule fois puis réutilisé pour les appels
|
||||
suivants. L'erreur de connexion est relancée sans journalisation,
|
||||
la méthode publique appelante étant responsable de la journaliser.
|
||||
|
||||
:return: Le client ``pronotepy`` connecté.
|
||||
:rtype: pronotepy.Client
|
||||
:raises ValueError: Si ``username``, ``password`` ou ``ent`` est manquant.
|
||||
:raises pronotepy.PronoteAPIError: Si la connexion à Pronote échoue.
|
||||
"""
|
||||
if self._client is None:
|
||||
username = self._settings.username
|
||||
password = self._settings.password
|
||||
ent = self._settings.ent
|
||||
if username is None or password is None or ent is None:
|
||||
raise ValueError("username, password et ent sont requis pour pronotepy")
|
||||
try:
|
||||
self._client = pronotepy.Client(username, password.get_secret_value(), ent)
|
||||
except pronotepy.PronoteAPIError:
|
||||
raise
|
||||
return self._client
|
||||
|
||||
def get_messages(self) -> list[Message]:
|
||||
"""Récupère les messages des discussions Pronote.
|
||||
|
||||
Chaque message d'une discussion est mappé sur un modèle
|
||||
:class:`Message` de type ``DISCUSSION``, le sujet de la discussion
|
||||
servant de titre.
|
||||
|
||||
:return: Liste des messages des professeurs ; vide en cas d'erreur.
|
||||
:rtype: list[Message]
|
||||
"""
|
||||
try:
|
||||
client = self._connect()
|
||||
messages: list[Message] = []
|
||||
for discussion in client.discussions():
|
||||
for message in discussion.messages:
|
||||
messages.append(
|
||||
Message(
|
||||
id=message.id,
|
||||
type=MessageType.DISCUSSION,
|
||||
title=discussion.subject,
|
||||
content=message.content,
|
||||
author=message.author or "",
|
||||
date=message.created,
|
||||
read=message.seen,
|
||||
)
|
||||
)
|
||||
return messages
|
||||
except (pronotepy.PronoteAPIError, ValueError) as exc:
|
||||
logger.error(
|
||||
"Échec de la récupération des messages Pronote : %s",
|
||||
redact_exception(exc),
|
||||
)
|
||||
return []
|
||||
|
||||
def get_informations(self) -> list[Message]:
|
||||
"""Récupère les informations et sondages Pronote.
|
||||
|
||||
Chaque entrée est mappée sur un modèle :class:`Message` de type
|
||||
``SURVEY`` si c'est un sondage, ``INFORMATION`` sinon.
|
||||
|
||||
:return: Liste des informations et sondages ; vide en cas d'erreur.
|
||||
:rtype: list[Message]
|
||||
"""
|
||||
try:
|
||||
client = self._connect()
|
||||
messages: list[Message] = []
|
||||
for info in client.information_and_surveys():
|
||||
messages.append(
|
||||
Message(
|
||||
id=info.id,
|
||||
type=MessageType.SURVEY if info.survey else MessageType.INFORMATION,
|
||||
title=info.title or "",
|
||||
content=info.content(),
|
||||
author=info.author,
|
||||
date=info.creation_date,
|
||||
read=info.read,
|
||||
)
|
||||
)
|
||||
return messages
|
||||
except (pronotepy.PronoteAPIError, ValueError) as exc:
|
||||
logger.error(
|
||||
"Échec de la récupération des informations Pronote : %s",
|
||||
redact_exception(exc),
|
||||
)
|
||||
return []
|
||||
|
||||
def get_agenda_fallback(self, start: date, end: date) -> tuple[list[Lesson], list[Homework]]:
|
||||
"""Récupère les cours et les devoirs via ``pronotepy``.
|
||||
|
||||
À utiliser uniquement si les sources iCal sont indisponibles ou en
|
||||
repli automatique. Les cours annulés sont mappés sur le statut
|
||||
``CANCELLED`` ; **pronotepy** ne fournissant ni la date de
|
||||
distribution ni les professeurs des devoirs, ces champs restent
|
||||
vides.
|
||||
|
||||
:param start: Date de début de la fenêtre (incluse).
|
||||
:param end: Date de fin de la fenêtre (incluse).
|
||||
:return: Tuple ``(cours, devoirs)`` ; vide en cas d'erreur.
|
||||
:rtype: tuple[list[Lesson], list[Homework]]
|
||||
"""
|
||||
try:
|
||||
client = self._connect()
|
||||
lessons: list[Lesson] = []
|
||||
for lesson in client.lessons(start, end):
|
||||
content = lesson.content
|
||||
lessons.append(
|
||||
Lesson(
|
||||
id=lesson.id,
|
||||
start=lesson.start,
|
||||
end=lesson.end,
|
||||
subject=lesson.subject.name if lesson.subject is not None else "",
|
||||
teachers=tuple(lesson.teacher_names or ()),
|
||||
rooms=tuple(lesson.classrooms or ()),
|
||||
group=lesson.group_name,
|
||||
status=(LessonStatus.CANCELLED if lesson.canceled else LessonStatus.NORMAL),
|
||||
content=content.description if content is not None else None,
|
||||
)
|
||||
)
|
||||
homeworks: list[Homework] = []
|
||||
for hw in client.homework(start, end):
|
||||
homeworks.append(
|
||||
Homework(
|
||||
id=hw.id,
|
||||
subject=hw.subject.name,
|
||||
teachers=(),
|
||||
assigned_on=None,
|
||||
due_on=hw.date,
|
||||
text=hw.description,
|
||||
html=hw.description,
|
||||
)
|
||||
)
|
||||
return lessons, homeworks
|
||||
except (pronotepy.PronoteAPIError, ValueError) as exc:
|
||||
logger.error(
|
||||
"Échec de la récupération de l'agenda via pronotepy : %s",
|
||||
redact_exception(exc),
|
||||
)
|
||||
return [], []
|
||||
Reference in New Issue
Block a user