refactor(config): migrer RSS et Pronote vers l'endpoint commun et durcir le contrat #64

Merged
OpenCode merged 5 commits from refactor/issue-16-endpoints-rss-pronote into main 2026-09-13 16:40:15 +02:00
5 changed files with 66 additions and 13 deletions
Showing only changes of commit c02f46245f - Show all commits
+2 -1
View File
@@ -94,7 +94,8 @@ AI_BASE_URL=https://api.openai.com/v1
# --- Blog --- # --- Blog ---
BLOG_ENABLED=false BLOG_ENABLED=false
BLOG_RSS_URL=https://blogpeda.ac-bordeaux.fr/cjeliote/?feed=rss2 BLOG_ENDPOINT__URL=https://blogpeda.ac-bordeaux.fr/cjeliote/?feed=rss2
# Ancien nom temporairement supporté avec un avertissement de dépréciation : BLOG_RSS_URL
# --- Divers --- # --- Divers ---
DRY_RUN=false DRY_RUN=false
+2 -2
View File
@@ -140,7 +140,7 @@
"filename": "GUIDE_DEV_PYTHON.md", "filename": "GUIDE_DEV_PYTHON.md",
"hashed_secret": "90bd1b48e958257948487b90bee080ba5ed00caa", "hashed_secret": "90bd1b48e958257948487b90bee080ba5ed00caa",
"is_verified": false, "is_verified": false,
"line_number": 5186 "line_number": 5183
} }
], ],
"tests/unit/test_caldav_gateway.py": [ "tests/unit/test_caldav_gateway.py": [
@@ -185,5 +185,5 @@
} }
] ]
}, },
"generated_at": "2026-09-12T22:12:56Z" "generated_at": "2026-09-13T09:52:29Z"
} }
+4 -7
View File
@@ -1113,7 +1113,7 @@ La déduplication des articles du blog repose sur leur **GUID** (ou leur URL si
Aucun fichier d'état local n'est utilisé : l'état est géré en mémoire par run. Aucun fichier d'état local n'est utilisé : l'état est géré en mémoire par run.
# Initialisation # Initialisation
rss_client = BlogRSSClient(rss_url=settings.blog.rss_url) rss_client = BlogRSSClient(rss_url=settings.blog.endpoint.url.get_secret_value())
blog_state = ## (section obsolète supprimée)() blog_state = ## (section obsolète supprimée)()
# Récupération des nouveaux articles # Récupération des nouveaux articles
@@ -1218,7 +1218,7 @@ Ajouter les variables suivantes dans la configuration :
| **Variable** | **Description** | **Valeur par défaut** | **Type** | | **Variable** | **Description** | **Valeur par défaut** | **Type** |
|----------------------------|-------------------------------------------------------------------------------|-----------------------|-------------------| |----------------------------|-------------------------------------------------------------------------------|-----------------------|-------------------|
| `BLOG_ENABLED` | Activer la récupération du blog. | `False` | `bool` | | `BLOG_ENABLED` | Activer la récupération du blog. | `False` | `bool` |
| `BLOG_RSS_URL` | URL du flux RSS du blog. | `https://blogpeda.ac-bordeaux.fr/cjeliote/?feed=rss2` | `str` | | `BLOG_ENDPOINT__URL` | URL du flux RSS via l'endpoint commun (masquée en `SecretStr`). | `https://blogpeda.ac-bordeaux.fr/cjeliote/?feed=rss2` | `ExternalEndpoint` |
#### 5 bis.9.2 Modèle Pydantic pour la configuration du blog #### 5 bis.9.2 Modèle Pydantic pour la configuration du blog
@@ -1230,10 +1230,7 @@ from pydantic_settings import BaseSettings, SettingsConfigDict
class BlogSettings(BaseSettings): class BlogSettings(BaseSettings):
model_config = SettingsConfigDict(env_prefix="BLOG_", env_file=".env", extra="ignore") model_config = SettingsConfigDict(env_prefix="BLOG_", env_file=".env", extra="ignore")
enabled: bool = Field(False, description="Activer la récupération du blog") enabled: bool = Field(False, description="Activer la récupération du blog")
rss_url: str = Field( endpoint: ExternalEndpoint
"https://blogpeda.ac-bordeaux.fr/cjeliote/?feed=rss2",
description="URL du flux RSS du blog",
)
``` ```
**Intégration dans `Settings`** : **Intégration dans `Settings`** :
@@ -1253,7 +1250,7 @@ class Settings(BaseSettings):
```ini ```ini
# --- Blog du collège --- # --- Blog du collège ---
BLOG_ENABLED=true BLOG_ENABLED=true
BLOG_RSS_URL=https://blogpeda.ac-bordeaux.fr/cjeliote/?feed=rss2 BLOG_ENDPOINT__URL=https://blogpeda.ac-bordeaux.fr/cjeliote/?feed=rss2
``` ```
--- ---
+53 -2
View File
@@ -352,10 +352,60 @@ class BlogSettings(BaseSettings):
``BLOG_``. ``BLOG_``.
""" """
model_config = SettingsConfigDict(env_file=".env", extra="ignore", env_prefix="BLOG_") model_config = SettingsConfigDict(
env_file=".env",
env_nested_delimiter="__",
extra="ignore",
env_prefix="BLOG_",
)
enabled: bool = False enabled: bool = False
rss_url: str = "https://blogpeda.ac-bordeaux.fr/cjeliote/?feed=rss2" endpoint: ExternalEndpoint = Field(
default_factory=lambda: ExternalEndpoint(
url=SecretStr("https://blogpeda.ac-bordeaux.fr/cjeliote/?feed=rss2")
)
)
rss_url: str | None = Field(
default=None,
exclude=True,
deprecated="Utiliser endpoint.url à la place (BLOG_RSS_URL obsolète).",
)
@model_validator(mode="before")
@classmethod
def _migrate_legacy_rss_url(cls, data: object) -> object:
"""Migre ``rss_url`` vers l'endpoint commun avec un avertissement.
:param data: Données brutes du modèle.
:return: Données complétées avec ``endpoint`` si nécessaire.
:rtype: object
"""
if not isinstance(data, dict) or data.get("rss_url") is None:
return data
migrated_data = data.copy()
warnings.warn(
"BLOG_RSS_URL est obsolète : utiliser BLOG_ENDPOINT__URL.",
DeprecationWarning,
stacklevel=2,
)
if migrated_data.get("endpoint") is None:
migrated_data["endpoint"] = {"url": migrated_data["rss_url"]}
return migrated_data
@model_validator(mode="after")
def _validate_endpoint_policy(self) -> BlogSettings:
"""Refuse les transports non sûrs pour le flux RSS de production.
Le transport ``file`` reste autorisé pour les fixtures locales.
:return: Instance validée inchangée.
:rtype: BlogSettings
:raises ValueError: Si le schéma n'est ni ``https`` ni ``file``.
"""
scheme = urlparse(self.endpoint.url.get_secret_value()).scheme
if scheme not in {"https", "file"}:
raise ValueError("URL RSS invalide : HTTPS ou file requis") from None
return self
class AppSettings(BaseSettings): class AppSettings(BaseSettings):
@@ -416,5 +466,6 @@ class Settings(BaseSettings):
self.caldav.password, self.caldav.password,
self.xmpp.password, self.xmpp.password,
self.ai.api_key, self.ai.api_key,
self.blog.endpoint.url,
] ]
return tuple(dict.fromkeys(secret for secret in secrets if secret is not None)) return tuple(dict.fromkeys(secret for secret in secrets if secret is not None))
+5 -1
View File
@@ -138,7 +138,11 @@ class PipelineRunner:
comparator = ( comparator = (
AgendaComparator(theoretical_provider) if theoretical_provider is not None else None AgendaComparator(theoretical_provider) if theoretical_provider is not None else None
) )
blog_client = BlogRSSClient(settings.blog.rss_url) if settings.blog.enabled else None blog_client = (
BlogRSSClient(settings.blog.endpoint.url.get_secret_value())
if settings.blog.enabled
else None
)
blog_state = ( blog_state = (
BlogRSSState(persistence_enabled=persistence_enabled) if settings.blog.enabled else None BlogRSSState(persistence_enabled=persistence_enabled) if settings.blog.enabled else None
) )