Compare commits

...

1 Commits

Author SHA1 Message Date
aa8865c22d fix: PRONOTE_URL ignoré à cause du double préfixe env_prefix
Le champ pronote_url dans PronoteSettings avec env_prefix=PRONOTE_
produisait PRONOTE_PRONOTE_URL au lieu de PRONOTE_URL. La variable
documentée dans .env.example et tous les guides était donc ignorée.

Renomme le champ pronote_url → url pour que le mécanisme standard
env_prefix + nom de champ produise PRONOTE_URL. Toutes les références
au champ settings.pronote.pronote_url sont mises à jour vers
settings.pronote.url dans le code de production et les tests.

Changements :
- settings.py : champ pronote_url → url, retrait du contournement alias
- client.py : self._settings.pronote_url → self._settings.url
- fallback.py : pronote.pronote_url → pronote.url
- tests : mises à jour des constructions et assertions, test de
  régression pour le mapping PRONOTE_URL → url

Co-authored-by: opencode/coder litellm/coder@agents.invalid
Co-authored-by: opencode/test-engineer litellm/test-engineer@agents.invalid
2026-09-08 21:23:07 +02:00
6 changed files with 47 additions and 34 deletions

View File

@@ -37,7 +37,7 @@ class PronoteSettings(BaseSettings):
username: str | None = None username: str | None = None
password: SecretStr | None = None password: SecretStr | None = None
ent: str | None = None ent: str | None = None
pronote_url: str | None = None url: str | None = None
account_type: Literal["student", "parent"] = "parent" account_type: Literal["student", "parent"] = "parent"
agenda_source: Literal["auto", "ical", "pronotepy"] = "auto" agenda_source: Literal["auto", "ical", "pronotepy"] = "auto"
homework_source: Literal["auto", "ical", "pronotepy"] = "auto" homework_source: Literal["auto", "ical", "pronotepy"] = "auto"

View File

@@ -164,17 +164,17 @@ class PronoteClient:
:return: Le client ``pronotepy`` connecté. :return: Le client ``pronotepy`` connecté.
:rtype: pronotepy.Client :rtype: pronotepy.Client
:raises ValueError: Si ``pronote_url``, ``username`` ou ``password`` :raises ValueError: Si ``url``, ``username`` ou ``password``
est manquant, ou si l'ENT fourni est inconnu. est manquant, ou si l'ENT fourni est inconnu.
:raises pronotepy.PronoteAPIError: Si la connexion à Pronote échoue. :raises pronotepy.PronoteAPIError: Si la connexion à Pronote échoue.
""" """
if self._client is None: if self._client is None:
pronote_url = self._settings.pronote_url url = self._settings.url
username = self._settings.username username = self._settings.username
password = self._settings.password password = self._settings.password
ent = self._settings.ent ent = self._settings.ent
if pronote_url is None or username is None or password is None: if url is None or username is None or password is None:
raise ValueError("pronote_url, username et password sont requis pour pronotepy") raise ValueError("url, username et password sont requis pour pronotepy")
resolver = _resolve_ent(ent) if ent is not None else None resolver = _resolve_ent(ent) if ent is not None else None
client_class: type[pronotepy.Client] = ( client_class: type[pronotepy.Client] = (
pronotepy.ParentClient pronotepy.ParentClient
@@ -182,7 +182,7 @@ class PronoteClient:
else pronotepy.Client else pronotepy.Client
) )
self._client = client_class( self._client = client_class(
pronote_url=pronote_url, pronote_url=url,
username=username, username=username,
password=password.get_secret_value(), password=password.get_secret_value(),
ent=resolver, ent=resolver,

View File

@@ -149,13 +149,13 @@ class PronoteFetcher:
def _is_pronotepy_configured(self) -> bool: def _is_pronotepy_configured(self) -> bool:
"""Vérifie que la source pronotepy est entièrement configurée. """Vérifie que la source pronotepy est entièrement configurée.
:return: ``True`` si ``pronote_url``, ``username`` et ``password`` :return: ``True`` si ``url``, ``username`` et ``password``
sont tous définis, ``False`` sinon. sont tous définis, ``False`` sinon.
:rtype: bool :rtype: bool
""" """
pronote = self._settings.pronote pronote = self._settings.pronote
return ( return (
pronote.pronote_url is not None pronote.url is not None
and pronote.username is not None and pronote.username is not None
and pronote.password is not None and pronote.password is not None
) )

View File

@@ -116,4 +116,19 @@ def test_no_singleton_import() -> None:
) )
def test_url_from_pronote_url_env_var(monkeypatch: MonkeyPatch) -> None:
"""Vérifie que PRONOTE_URL mappe au champ url via le préfixe PRONOTE_.
Ce test couvre la régression où PRONOTE_URL n'était pas mappé vers le
champ du modèle à cause du double préfixe PRONOTE_.
:param monkeypatch: Fixture pytest pour modifier temporairement l'environnement.
:return: None
"""
test_url = "https://example.index-education.net/pronote/parent.html"
monkeypatch.setenv("PRONOTE_URL", test_url)
settings = load_settings()
assert settings.pronote.url == test_url
# Ensure trailing newline # Ensure trailing newline

View File

@@ -53,7 +53,7 @@ def fixture_mock_settings() -> Settings:
""" """
return Settings( return Settings(
pronote=PronoteSettings( pronote=PronoteSettings(
pronote_url="https://pronote.example.com", url="https://pronote.example.com",
ical_url=SecretStr("file:///fake/ical.ics"), ical_url=SecretStr("file:///fake/ical.ics"),
agenda_source="auto", agenda_source="auto",
homework_source="auto", homework_source="auto",
@@ -251,7 +251,7 @@ def test_fetch_agenda_auto_both_fail(mock_fetcher: PronoteFetcher) -> None:
:rtype: None :rtype: None
""" """
# Disable pronotepy so fallback is None # Disable pronotepy so fallback is None
mock_fetcher._settings.pronote.pronote_url = None mock_fetcher._settings.pronote.url = None
with ( with (
patch("pronote_sync.sources.pronote.fallback.fetch_ical") as m_fetch_ical, patch("pronote_sync.sources.pronote.fallback.fetch_ical") as m_fetch_ical,
@@ -280,7 +280,7 @@ def test_fetch_agenda_ical_mode_failure(mock_fetcher: PronoteFetcher) -> None:
""" """
# Override settings to use ical mode explicitly and disable fallback # Override settings to use ical mode explicitly and disable fallback
mock_fetcher._settings.pronote.agenda_source = "ical" mock_fetcher._settings.pronote.agenda_source = "ical"
mock_fetcher._settings.pronote.pronote_url = None mock_fetcher._settings.pronote.url = None
with ( with (
patch("pronote_sync.sources.pronote.fallback.fetch_ical") as m_fetch_ical, patch("pronote_sync.sources.pronote.fallback.fetch_ical") as m_fetch_ical,
@@ -438,7 +438,7 @@ def test_fetch_homework_auto_both_fail(mock_fetcher: PronoteFetcher) -> None:
target_date = date(2025, 9, 10) target_date = date(2025, 9, 10)
# Disable pronotepy so fallback is None # Disable pronotepy so fallback is None
mock_fetcher._settings.pronote.pronote_url = None mock_fetcher._settings.pronote.url = None
with ( with (
patch("pronote_sync.sources.pronote.fallback.fetch_ical") as m_fetch_ical, patch("pronote_sync.sources.pronote.fallback.fetch_ical") as m_fetch_ical,
@@ -531,7 +531,7 @@ def test_no_secrets_in_error_messages(
:rtype: None :rtype: None
""" """
# Disable pronotepy so fallback is None to trigger PipelineCriticalError # Disable pronotepy so fallback is None to trigger PipelineCriticalError
mock_fetcher._settings.pronote.pronote_url = None mock_fetcher._settings.pronote.url = None
with ( with (
patch("pronote_sync.sources.pronote.fallback.fetch_ical") as m_fetch_ical, patch("pronote_sync.sources.pronote.fallback.fetch_ical") as m_fetch_ical,
@@ -629,7 +629,7 @@ def test_fetch_agenda_no_source_configured_raises(mock_fetcher: PronoteFetcher)
""" """
# Disable both sources # Disable both sources
mock_fetcher._settings.pronote.ical_url = None mock_fetcher._settings.pronote.ical_url = None
mock_fetcher._settings.pronote.pronote_url = None mock_fetcher._settings.pronote.url = None
with pytest.raises(PipelineCriticalError) as exc_info: with pytest.raises(PipelineCriticalError) as exc_info:
mock_fetcher.fetch_agenda() mock_fetcher.fetch_agenda()
@@ -1020,7 +1020,7 @@ def test_homework_sources_explicit_ical_mode_strict(mock_fetcher: PronoteFetcher
assert fallback is None assert fallback is None
# Without pronotepy configured # Without pronotepy configured
mock_fetcher._settings.pronote.pronote_url = None mock_fetcher._settings.pronote.url = None
primary, fallback = mock_fetcher._homework_sources() primary, fallback = mock_fetcher._homework_sources()
assert primary == "ical" assert primary == "ical"
assert fallback is None assert fallback is None
@@ -1080,7 +1080,7 @@ def test_homework_sources_auto_no_source_configured_raises(mock_fetcher: Pronote
""" """
mock_fetcher._settings.pronote.homework_source = "auto" mock_fetcher._settings.pronote.homework_source = "auto"
mock_fetcher._settings.pronote.ical_url = None mock_fetcher._settings.pronote.ical_url = None
mock_fetcher._settings.pronote.pronote_url = None mock_fetcher._settings.pronote.url = None
with pytest.raises(PipelineCriticalError) as exc_info: with pytest.raises(PipelineCriticalError) as exc_info:
mock_fetcher._homework_sources() mock_fetcher._homework_sources()
@@ -1298,7 +1298,7 @@ def test_is_pronotepy_configured_without_ent_returns_true() -> None:
# Créer des settings avec pronotepy configuré mais sans ent # Créer des settings avec pronotepy configuré mais sans ent
settings = Settings( settings = Settings(
pronote=PronoteSettings( pronote=PronoteSettings(
pronote_url="https://pronote.example.com", url="https://pronote.example.com",
username="testuser", username="testuser",
password=SecretStr("testpass"), password=SecretStr("testpass"),
ent=None, # Explicitement None ent=None, # Explicitement None
@@ -1327,7 +1327,7 @@ def test_agenda_sources_auto_without_ical_and_without_ent_returns_pronotepy() ->
settings = Settings( settings = Settings(
pronote=PronoteSettings( pronote=PronoteSettings(
pronote_url="https://pronote.example.com", url="https://pronote.example.com",
username="testuser", username="testuser",
password=SecretStr("testpass"), password=SecretStr("testpass"),
ent=None, # Explicitement None ent=None, # Explicitement None
@@ -1358,7 +1358,7 @@ def test_homework_sources_auto_without_ical_and_without_ent_returns_pronotepy()
settings = Settings( settings = Settings(
pronote=PronoteSettings( pronote=PronoteSettings(
pronote_url="https://pronote.example.com", url="https://pronote.example.com",
username="testuser", username="testuser",
password=SecretStr("testpass"), password=SecretStr("testpass"),
ent=None, # Explicitement None ent=None, # Explicitement None

View File

@@ -46,7 +46,7 @@ def pronote_settings() -> PronoteSettings:
:rtype: PronoteSettings :rtype: PronoteSettings
""" """
return PronoteSettings( return PronoteSettings(
pronote_url="https://pronote.example.com", url="https://pronote.example.com",
username="testuser", username="testuser",
password=SecretStr("testpass"), password=SecretStr("testpass"),
ent="bordeaux", ent="bordeaux",
@@ -309,9 +309,7 @@ def test_missing_credentials_raises(empty_pronote_settings: PronoteSettings) ->
""" """
client = PronoteClient(empty_pronote_settings) client = PronoteClient(empty_pronote_settings)
with pytest.raises( with pytest.raises(ValueError, match="url, username et password sont requis pour pronotepy"):
ValueError, match="pronote_url, username et password sont requis pour pronotepy"
):
client._connect() client._connect()
@@ -384,7 +382,7 @@ def test_connect_without_ent_but_with_required_credentials(
# Settings sans ent mais avec les autres champs requis # Settings sans ent mais avec les autres champs requis
settings = PronoteSettings( settings = PronoteSettings(
pronote_url="https://pronote.example.com", url="https://pronote.example.com",
username="testuser", username="testuser",
password=SecretStr("testpass"), password=SecretStr("testpass"),
ent=None, # Explicitement None ent=None, # Explicitement None
@@ -414,7 +412,7 @@ def test_connect_without_ent_but_with_required_credentials(
def test_connect_missing_required_credentials_still_raises( def test_connect_missing_required_credentials_still_raises(
mocker: pytest_mock.MockerFixture, mocker: pytest_mock.MockerFixture,
) -> None: ) -> None:
"""Vérifie que _connect() lève ValueError si pronote_url, username ou password manquent. """Vérifie que _connect() lève ValueError si url, username ou password manquent.
Ce test valide que l'erreur ne mentionne plus ent comme requis. Ce test valide que l'erreur ne mentionne plus ent comme requis.
@@ -424,9 +422,9 @@ def test_connect_missing_required_credentials_still_raises(
from pronote_sync.config.settings import PronoteSettings from pronote_sync.config.settings import PronoteSettings
from pronote_sync.sources.pronote.client import PronoteClient from pronote_sync.sources.pronote.client import PronoteClient
# Settings avec ent mais sans pronote_url # Settings avec ent mais sans url
settings = PronoteSettings( settings = PronoteSettings(
pronote_url=None, url=None,
username="testuser", username="testuser",
password=SecretStr("testpass"), password=SecretStr("testpass"),
ent=None, ent=None,
@@ -439,7 +437,7 @@ def test_connect_missing_required_credentials_still_raises(
client._connect() client._connect()
# Error should NOT mention ent as required # Error should NOT mention ent as required
assert "pronote_url, username et password sont requis" in str(exc_info.value) assert "url, username et password sont requis" in str(exc_info.value)
assert "ent" not in str(exc_info.value) assert "ent" not in str(exc_info.value)
@@ -453,7 +451,7 @@ def test_connect_missing_username_raises(mocker: pytest_mock.MockerFixture) -> N
from pronote_sync.sources.pronote.client import PronoteClient from pronote_sync.sources.pronote.client import PronoteClient
settings = PronoteSettings( settings = PronoteSettings(
pronote_url="https://pronote.example.com", url="https://pronote.example.com",
username=None, username=None,
password=SecretStr("testpass"), password=SecretStr("testpass"),
ent=None, ent=None,
@@ -465,7 +463,7 @@ def test_connect_missing_username_raises(mocker: pytest_mock.MockerFixture) -> N
with pytest.raises(ValueError) as exc_info: with pytest.raises(ValueError) as exc_info:
client._connect() client._connect()
assert "pronote_url, username et password sont requis" in str(exc_info.value) assert "url, username et password sont requis" in str(exc_info.value)
def test_connect_missing_password_raises(mocker: pytest_mock.MockerFixture) -> None: def test_connect_missing_password_raises(mocker: pytest_mock.MockerFixture) -> None:
@@ -478,7 +476,7 @@ def test_connect_missing_password_raises(mocker: pytest_mock.MockerFixture) -> N
from pronote_sync.sources.pronote.client import PronoteClient from pronote_sync.sources.pronote.client import PronoteClient
settings = PronoteSettings( settings = PronoteSettings(
pronote_url="https://pronote.example.com", url="https://pronote.example.com",
username="testuser", username="testuser",
password=None, password=None,
ent=None, ent=None,
@@ -490,7 +488,7 @@ def test_connect_missing_password_raises(mocker: pytest_mock.MockerFixture) -> N
with pytest.raises(ValueError) as exc_info: with pytest.raises(ValueError) as exc_info:
client._connect() client._connect()
assert "pronote_url, username et password sont requis" in str(exc_info.value) assert "url, username et password sont requis" in str(exc_info.value)
def test_connect_student_account_type( def test_connect_student_account_type(
@@ -507,7 +505,7 @@ def test_connect_student_account_type(
from pronote_sync.sources.pronote.client import PronoteClient from pronote_sync.sources.pronote.client import PronoteClient
pronote_settings_student = PronoteSettings( pronote_settings_student = PronoteSettings(
pronote_url="https://pronote.example.com", url="https://pronote.example.com",
username="testuser", username="testuser",
password=SecretStr("testpass"), password=SecretStr("testpass"),
ent="bordeaux", ent="bordeaux",
@@ -542,7 +540,7 @@ def test_connect_with_ent_resolution_still_works(
from pronote_sync.sources.pronote.client import PronoteClient from pronote_sync.sources.pronote.client import PronoteClient
settings = PronoteSettings( settings = PronoteSettings(
pronote_url="https://pronote.example.com", url="https://pronote.example.com",
username="testuser", username="testuser",
password=SecretStr("testpass"), password=SecretStr("testpass"),
ent="bordeaux", # ent est fourni ent="bordeaux", # ent est fourni