Compare commits
1 Commits
fix/pronot
...
docs/agent
| Author | SHA1 | Date | |
|---|---|---|---|
|
de647a6ff8
|
@@ -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
|
||||||
url: str | None = None
|
pronote_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"
|
||||||
|
|||||||
@@ -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 ``url``, ``username`` ou ``password``
|
:raises ValueError: Si ``pronote_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:
|
||||||
url = self._settings.url
|
pronote_url = self._settings.pronote_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 url is None or username is None or password is None:
|
if pronote_url is None or username is None or password is None:
|
||||||
raise ValueError("url, username et password sont requis pour pronotepy")
|
raise ValueError("pronote_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=url,
|
pronote_url=pronote_url,
|
||||||
username=username,
|
username=username,
|
||||||
password=password.get_secret_value(),
|
password=password.get_secret_value(),
|
||||||
ent=resolver,
|
ent=resolver,
|
||||||
|
|||||||
@@ -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 ``url``, ``username`` et ``password``
|
:return: ``True`` si ``pronote_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.url is not None
|
pronote.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
|
||||||
)
|
)
|
||||||
|
|||||||
@@ -116,19 +116,4 @@ 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
|
||||||
|
|||||||
@@ -53,7 +53,7 @@ def fixture_mock_settings() -> Settings:
|
|||||||
"""
|
"""
|
||||||
return Settings(
|
return Settings(
|
||||||
pronote=PronoteSettings(
|
pronote=PronoteSettings(
|
||||||
url="https://pronote.example.com",
|
pronote_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.url = None
|
mock_fetcher._settings.pronote.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.url = None
|
mock_fetcher._settings.pronote.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.url = None
|
mock_fetcher._settings.pronote.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.url = None
|
mock_fetcher._settings.pronote.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.url = None
|
mock_fetcher._settings.pronote.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.url = None
|
mock_fetcher._settings.pronote.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.url = None
|
mock_fetcher._settings.pronote.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(
|
||||||
url="https://pronote.example.com",
|
pronote_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(
|
||||||
url="https://pronote.example.com",
|
pronote_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(
|
||||||
url="https://pronote.example.com",
|
pronote_url="https://pronote.example.com",
|
||||||
username="testuser",
|
username="testuser",
|
||||||
password=SecretStr("testpass"),
|
password=SecretStr("testpass"),
|
||||||
ent=None, # Explicitement None
|
ent=None, # Explicitement None
|
||||||
|
|||||||
@@ -46,7 +46,7 @@ def pronote_settings() -> PronoteSettings:
|
|||||||
:rtype: PronoteSettings
|
:rtype: PronoteSettings
|
||||||
"""
|
"""
|
||||||
return PronoteSettings(
|
return PronoteSettings(
|
||||||
url="https://pronote.example.com",
|
pronote_url="https://pronote.example.com",
|
||||||
username="testuser",
|
username="testuser",
|
||||||
password=SecretStr("testpass"),
|
password=SecretStr("testpass"),
|
||||||
ent="bordeaux",
|
ent="bordeaux",
|
||||||
@@ -309,7 +309,9 @@ def test_missing_credentials_raises(empty_pronote_settings: PronoteSettings) ->
|
|||||||
"""
|
"""
|
||||||
client = PronoteClient(empty_pronote_settings)
|
client = PronoteClient(empty_pronote_settings)
|
||||||
|
|
||||||
with pytest.raises(ValueError, match="url, username et password sont requis pour pronotepy"):
|
with pytest.raises(
|
||||||
|
ValueError, match="pronote_url, username et password sont requis pour pronotepy"
|
||||||
|
):
|
||||||
client._connect()
|
client._connect()
|
||||||
|
|
||||||
|
|
||||||
@@ -382,7 +384,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(
|
||||||
url="https://pronote.example.com",
|
pronote_url="https://pronote.example.com",
|
||||||
username="testuser",
|
username="testuser",
|
||||||
password=SecretStr("testpass"),
|
password=SecretStr("testpass"),
|
||||||
ent=None, # Explicitement None
|
ent=None, # Explicitement None
|
||||||
@@ -412,7 +414,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 url, username ou password manquent.
|
"""Vérifie que _connect() lève ValueError si pronote_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.
|
||||||
|
|
||||||
@@ -422,9 +424,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 url
|
# Settings avec ent mais sans pronote_url
|
||||||
settings = PronoteSettings(
|
settings = PronoteSettings(
|
||||||
url=None,
|
pronote_url=None,
|
||||||
username="testuser",
|
username="testuser",
|
||||||
password=SecretStr("testpass"),
|
password=SecretStr("testpass"),
|
||||||
ent=None,
|
ent=None,
|
||||||
@@ -437,7 +439,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 "url, username et password sont requis" in str(exc_info.value)
|
assert "pronote_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)
|
||||||
|
|
||||||
|
|
||||||
@@ -451,7 +453,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(
|
||||||
url="https://pronote.example.com",
|
pronote_url="https://pronote.example.com",
|
||||||
username=None,
|
username=None,
|
||||||
password=SecretStr("testpass"),
|
password=SecretStr("testpass"),
|
||||||
ent=None,
|
ent=None,
|
||||||
@@ -463,7 +465,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 "url, username et password sont requis" in str(exc_info.value)
|
assert "pronote_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:
|
||||||
@@ -476,7 +478,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(
|
||||||
url="https://pronote.example.com",
|
pronote_url="https://pronote.example.com",
|
||||||
username="testuser",
|
username="testuser",
|
||||||
password=None,
|
password=None,
|
||||||
ent=None,
|
ent=None,
|
||||||
@@ -488,7 +490,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 "url, username et password sont requis" in str(exc_info.value)
|
assert "pronote_url, username et password sont requis" in str(exc_info.value)
|
||||||
|
|
||||||
|
|
||||||
def test_connect_student_account_type(
|
def test_connect_student_account_type(
|
||||||
@@ -505,7 +507,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(
|
||||||
url="https://pronote.example.com",
|
pronote_url="https://pronote.example.com",
|
||||||
username="testuser",
|
username="testuser",
|
||||||
password=SecretStr("testpass"),
|
password=SecretStr("testpass"),
|
||||||
ent="bordeaux",
|
ent="bordeaux",
|
||||||
@@ -540,7 +542,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(
|
||||||
url="https://pronote.example.com",
|
pronote_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
|
||||||
|
|||||||
Reference in New Issue
Block a user