refactor(config): migrer les endpoints Pronote
Co-authored-by: Codex <codex@antoineve.me>
This commit is contained in:
@@ -88,13 +88,28 @@ class PronoteSettings(BaseSettings):
|
||||
``PRONOTE_``.
|
||||
"""
|
||||
|
||||
model_config = SettingsConfigDict(env_file=".env", extra="ignore", env_prefix="PRONOTE_")
|
||||
model_config = SettingsConfigDict(
|
||||
env_file=".env",
|
||||
env_nested_delimiter="__",
|
||||
extra="ignore",
|
||||
env_prefix="PRONOTE_",
|
||||
)
|
||||
|
||||
ical_url: SecretStr | None = None
|
||||
endpoint: ExternalEndpoint | None = None
|
||||
ical_endpoint: ExternalEndpoint | None = None
|
||||
ical_url: SecretStr | None = Field(
|
||||
default=None,
|
||||
exclude=True,
|
||||
deprecated="Utiliser ical_endpoint.url à la place (PRONOTE_ICAL_URL obsolète).",
|
||||
)
|
||||
username: str | None = None
|
||||
password: SecretStr | None = None
|
||||
ent: str | None = None
|
||||
url: str | None = None
|
||||
url: str | None = Field(
|
||||
default=None,
|
||||
exclude=True,
|
||||
deprecated="Utiliser endpoint.url à la place (PRONOTE_URL obsolète).",
|
||||
)
|
||||
account_type: Literal["student", "parent"] = "parent"
|
||||
agenda_source: Literal["auto", "ical", "pronotepy"] = "auto"
|
||||
homework_source: Literal["auto", "ical", "pronotepy"] = "auto"
|
||||
@@ -104,17 +119,57 @@ class PronoteSettings(BaseSettings):
|
||||
qr_pin: SecretStr | None = None
|
||||
account_pin: SecretStr | None = None
|
||||
|
||||
@field_serializer("ical_url")
|
||||
def _serialize_ical_url(self, value: SecretStr | None) -> str | None:
|
||||
"""Masque l'URL iCal lors de la sérialisation (repr, str, JSON).
|
||||
@model_validator(mode="before")
|
||||
@classmethod
|
||||
def _migrate_legacy_endpoints(cls, data: object) -> object:
|
||||
"""Migre les URL Pronote historiques vers les endpoints communs.
|
||||
|
||||
:param value: Valeur du champ ``ical_url``.
|
||||
:return: ``"**********"`` si la valeur est définie, ``None`` sinon.
|
||||
:rtype: str | None
|
||||
:param data: Données brutes du modèle.
|
||||
:return: Données complétées avec les endpoints si nécessaire.
|
||||
:rtype: object
|
||||
"""
|
||||
if value is None:
|
||||
return None
|
||||
return "**********"
|
||||
if not isinstance(data, dict):
|
||||
return data
|
||||
migrated_data = data.copy()
|
||||
if migrated_data.get("url") is not None:
|
||||
warnings.warn(
|
||||
"PRONOTE_URL est obsolète : utiliser PRONOTE_ENDPOINT__URL.",
|
||||
DeprecationWarning,
|
||||
stacklevel=2,
|
||||
)
|
||||
if migrated_data.get("endpoint") is None:
|
||||
migrated_data["endpoint"] = {"url": migrated_data["url"]}
|
||||
if migrated_data.get("ical_url") is not None:
|
||||
warnings.warn(
|
||||
"PRONOTE_ICAL_URL est obsolète : utiliser PRONOTE_ICAL_ENDPOINT__URL.",
|
||||
DeprecationWarning,
|
||||
stacklevel=2,
|
||||
)
|
||||
if migrated_data.get("ical_endpoint") is None:
|
||||
migrated_data["ical_endpoint"] = {"url": migrated_data["ical_url"]}
|
||||
return migrated_data
|
||||
|
||||
@model_validator(mode="after")
|
||||
def _validate_endpoint_policies(self) -> PronoteSettings:
|
||||
"""Applique les transports autorisés aux deux endpoints Pronote.
|
||||
|
||||
L'API Pronote utilise HTTPS. Le flux iCal accepte également ``file``
|
||||
afin de préserver les fixtures locales injectées.
|
||||
|
||||
:return: Instance validée inchangée.
|
||||
:rtype: PronoteSettings
|
||||
:raises ValueError: Si un endpoint utilise un schéma interdit.
|
||||
"""
|
||||
if (
|
||||
self.endpoint is not None
|
||||
and urlparse(self.endpoint.url.get_secret_value()).scheme != "https"
|
||||
):
|
||||
raise ValueError("URL Pronote invalide : HTTPS requis") from None
|
||||
if self.ical_endpoint is not None and urlparse(
|
||||
self.ical_endpoint.url.get_secret_value()
|
||||
).scheme not in {"https", "file"}:
|
||||
raise ValueError("URL iCal Pronote invalide : HTTPS ou file requis") from None
|
||||
return self
|
||||
|
||||
@field_serializer("qr_pin")
|
||||
def _serialize_qr_pin(self, value: SecretStr | None) -> str | None:
|
||||
@@ -458,7 +513,8 @@ class Settings(BaseSettings):
|
||||
:rtype: tuple[SecretStr, ...]
|
||||
"""
|
||||
secrets = [
|
||||
self.pronote.ical_url,
|
||||
self.pronote.endpoint.url if self.pronote.endpoint is not None else None,
|
||||
self.pronote.ical_endpoint.url if self.pronote.ical_endpoint is not None else None,
|
||||
self.pronote.password,
|
||||
self.pronote.qr_pin,
|
||||
self.pronote.account_pin,
|
||||
|
||||
@@ -294,18 +294,18 @@ class PronoteClient:
|
||||
est manquant, ou si l'ENT fourni est inconnu.
|
||||
:raises pronotepy.PronoteAPIError: Si la connexion à Pronote échoue.
|
||||
"""
|
||||
url = self._settings.url
|
||||
endpoint = self._settings.endpoint
|
||||
username = self._settings.username
|
||||
password = self._settings.password
|
||||
ent = self._settings.ent
|
||||
if url is None or username is None or password is None:
|
||||
if endpoint is None or username is None or password is None:
|
||||
raise ValueError("url, username et password sont requis pour pronotepy")
|
||||
resolver = _resolve_ent(ent) if ent is not None else None
|
||||
client_class: type[pronotepy.Client] = (
|
||||
pronotepy.ParentClient if self._settings.account_type == "parent" else pronotepy.Client
|
||||
)
|
||||
self._client = client_class(
|
||||
pronote_url=url,
|
||||
pronote_url=endpoint.url.get_secret_value(),
|
||||
username=username,
|
||||
password=password.get_secret_value(),
|
||||
ent=resolver,
|
||||
|
||||
@@ -141,10 +141,10 @@ class PronoteFetcher:
|
||||
def _is_ical_configured(self) -> bool:
|
||||
"""Vérifie que la source iCal est configurée.
|
||||
|
||||
:return: ``True`` si ``ical_url`` est défini, ``False`` sinon.
|
||||
:return: ``True`` si ``ical_endpoint`` est défini, ``False`` sinon.
|
||||
:rtype: bool
|
||||
"""
|
||||
return self._settings.pronote.ical_url is not None
|
||||
return self._settings.pronote.ical_endpoint is not None
|
||||
|
||||
def _is_pronotepy_configured(self) -> bool:
|
||||
"""Vérifie si la source pronotepy est utilisable selon le mode d'authentification.
|
||||
@@ -155,13 +155,13 @@ class PronoteFetcher:
|
||||
"""
|
||||
pronote = self._settings.pronote
|
||||
if pronote.auth_mode == "qr_token":
|
||||
# En mode qr_token, seul PRONOTE_URL est requis.
|
||||
# En mode qr_token, seul PRONOTE_ENDPOINT__URL est requis.
|
||||
# Le QR code et le PIN ne sont nécessaires que pour l'enrôlement initial.
|
||||
# Les exécutions suivantes utilisent le token persisté.
|
||||
return pronote.url is not None
|
||||
return pronote.endpoint is not None
|
||||
# En mode password, URL + identifiant + mot de passe sont requis.
|
||||
return (
|
||||
pronote.url is not None
|
||||
pronote.endpoint is not None
|
||||
and pronote.username is not None
|
||||
and pronote.password is not None
|
||||
)
|
||||
@@ -171,16 +171,16 @@ class PronoteFetcher:
|
||||
|
||||
:return: Tuple ``(cours, événements scolaires)``.
|
||||
:rtype: tuple[list[Lesson], list[SchoolEvent]]
|
||||
:raises ValueError: Si ``ical_url`` n'est pas configuré ou si le flux est invalide.
|
||||
:raises ValueError: Si ``ical_endpoint`` n'est pas configuré ou si le flux est invalide.
|
||||
:raises OSError: Si le fichier iCal local est illisible.
|
||||
:raises requests.RequestException: Si la récupération HTTP échoue.
|
||||
"""
|
||||
if self._cache_ical_for_run and self._run_ical_agenda is not None:
|
||||
return self._run_ical_agenda
|
||||
ical_url = self._settings.pronote.ical_url
|
||||
if ical_url is None:
|
||||
raise ValueError("PRONOTE_ICAL_URL est requis pour la source iCal")
|
||||
raw_ical = fetch_ical(ical_url.get_secret_value())
|
||||
ical_endpoint = self._settings.pronote.ical_endpoint
|
||||
if ical_endpoint is None:
|
||||
raise ValueError("PRONOTE_ICAL_ENDPOINT__URL est requis pour la source iCal")
|
||||
raw_ical = fetch_ical(ical_endpoint.url.get_secret_value())
|
||||
lessons, _, school_events = parse_ical(raw_ical)
|
||||
result = (lessons, school_events)
|
||||
if self._cache_ical_for_run:
|
||||
@@ -210,7 +210,7 @@ class PronoteFetcher:
|
||||
|
||||
Les modes explicites ``ICAL`` et ``PRONOTEPY`` désignent la seule
|
||||
source utilisée, sans aucun repli. En mode ``AUTO``, iCal est
|
||||
primaire si ``ical_url`` est configuré (repli pronotepy si la
|
||||
primaire si ``ical_endpoint`` est configuré (repli pronotepy si la
|
||||
configuration pronotepy est complète), sinon pronotepy sans repli.
|
||||
|
||||
:return: Tuple ``(source primaire, source de repli ou ``None``)``.
|
||||
@@ -306,7 +306,7 @@ class PronoteFetcher:
|
||||
:param target_date: Date cible pour laquelle collecter les devoirs.
|
||||
:return: Liste des devoirs.
|
||||
:rtype: list[Homework]
|
||||
:raises ValueError: Si ``ical_url`` n'est pas configuré ou si le flux est invalide.
|
||||
:raises ValueError: Si ``ical_endpoint`` n'est pas configuré ou si le flux est invalide.
|
||||
:raises OSError: Si le fichier iCal local est illisible.
|
||||
:raises requests.RequestException: Si la récupération HTTP échoue.
|
||||
"""
|
||||
@@ -337,7 +337,7 @@ class PronoteFetcher:
|
||||
|
||||
Les modes explicites ``ICAL`` et ``PRONOTEPY`` désignent la seule
|
||||
source utilisée, sans aucun repli. En mode ``AUTO``, iCal est
|
||||
primaire si ``ical_url`` est configuré (repli pronotepy si la
|
||||
primaire si ``ical_endpoint`` est configuré (repli pronotepy si la
|
||||
configuration pronotepy est complète), sinon pronotepy sans repli.
|
||||
|
||||
:return: Tuple ``(source primaire, source de repli ou ``None``)``.
|
||||
|
||||
Reference in New Issue
Block a user