diff --git a/.env.example b/.env.example index 12a63c5..d270ae1 100644 --- a/.env.example +++ b/.env.example @@ -22,7 +22,10 @@ SYNC_PAST_DAYS=7 SYNC_FUTURE_DAYS=30 # --- Agenda théorique --- -THEORETICAL_AGENDA_PATH=./data/theoretical.ics +THEORETICAL_AGENDA_PATH=./data/theoretical.json +SCHOOL_HOLIDAYS_PATH=./data/school_holidays.json +THEORETICAL_WEEK_ANCHOR_DATE=2026-09-01 +THEORETICAL_WEEK_ANCHOR_TYPE=even # --- XMPP --- XMPP_ENABLED=false diff --git a/.secrets.baseline b/.secrets.baseline index 9ae5bb0..d5c50ec 100644 --- a/.secrets.baseline +++ b/.secrets.baseline @@ -140,10 +140,10 @@ "filename": "GUIDE_DEV_PYTHON.md", "hashed_secret": "90bd1b48e958257948487b90bee080ba5ed00caa", "is_verified": true, - "line_number": 5058, + "line_number": 5014, "is_secret": false } ] }, - "generated_at": "2026-09-06T18:57:58Z" + "generated_at": "2026-09-06T20:30:44Z" } diff --git a/GUIDE_DEV_PYTHON.md b/GUIDE_DEV_PYTHON.md index 6dc6232..71c49e3 100644 --- a/GUIDE_DEV_PYTHON.md +++ b/GUIDE_DEV_PYTHON.md @@ -28,7 +28,7 @@ Le projet doit implémenter les fonctionnalités suivantes, dans l'ordre logique - Conservation des événements annulés avec `STATUS:CANCELLED`. 3. **Comparaison avec l'agenda théorique** : - - Détection des **changements** (ajouts, suppressions, modifications) entre l'agenda réel (Pronote) et un agenda théorique (fichier iCal/CSV local). + - Détection des **changements** (ajouts, suppressions, modifications) entre l'agenda réel (Pronote) et un agenda théorique (fichier JSON local, avec parité des semaines et vacances scolaires). - Génération d'une liste structurée des différences. 4. **Génération de la synthèse** : @@ -206,7 +206,7 @@ pronote_sync/ │ │ └── state.py # État local (déduplication, cache HTTP) │ └── theoretical/ # Agenda théorique │ ├── __init__.py -│ ├── file.py # Lecture fichier iCal/CSV +│ ├── file.py # Lecture fichier JSON (parité + vacances) │ └── provider.py # Interface TheoreticalAgendaProvider ├── sync/ # Synchronisation CalDAV + Blog │ ├── __init__.py @@ -306,7 +306,10 @@ d'un besoin réel et testé. > ⚠️ **Décision d'implémentation** : > Ces variables sont désormais dans `AppSettings` (et non `CalDAVSettings`) car `CalDAVSettings` utilise `env_prefix="CALDAV_"`, ce qui nécessiterait `CALDAV_SYNC_PAST_DAYS`. > Leur placement dans `AppSettings` (sans préfixe) garantit un mappage correct avec `SYNC_PAST_DAYS` / `SYNC_FUTURE_DAYS`. -| `THEORETICAL_AGENDA_PATH` | Chemin vers le fichier iCal/CSV de l'agenda théorique. | `None` | `str \| None`| +| `THEORETICAL_AGENDA_PATH` | Chemin vers le fichier JSON de l'agenda théorique. | `None` | `str \| None`| +| `SCHOOL_HOLIDAYS_PATH` | Chemin vers le fichier JSON des vacances scolaires. | `None` | `str \| None`| +| `THEORETICAL_WEEK_ANCHOR_DATE` | Date de référence pour la parité des semaines (paire/impaire). | `None` | `date \| None`| +| `THEORETICAL_WEEK_ANCHOR_TYPE` | Parité de la semaine de référence (`even` ou `odd`). | `None` | `Literal["even", "odd"] \| None`| | `AI_ENABLED` | Activer la synthèse IA. | `False` | `bool` | | `AI_PROVIDER` | Fournisseur IA (`openai` ou `litellm`). | `openai` | `str` | | `AI_BASE_URL` | URL de base pour l'API IA (ex: OpenAI compatible). | `None` | `str \| None`| @@ -341,8 +344,11 @@ CALDAV_CALENDAR_PATH=/pronote-sync/ SYNC_PAST_DAYS=7 SYNC_FUTURE_DAYS=30 -# --- Agenda théorique --- -THEORETICAL_AGENDA_PATH=./data/theoretical.ics +# --- Agenda théorique (JSON) --- +THEORETICAL_AGENDA_PATH=./data/theoretical.json +SCHOOL_HOLIDAYS_PATH=./data/school_holidays.json +THEORETICAL_WEEK_ANCHOR_DATE=2026-09-01 +THEORETICAL_WEEK_ANCHOR_TYPE=even # --- XMPP --- XMPP_JID=user@example.com @@ -3320,6 +3326,10 @@ class SyncState: - **Agenda théorique** : Représente l'emploi du temps **attendu** (ex: emploi du temps officiel de l'établissement). - **Agenda réel** : Représente l'emploi du temps **réel** (récupéré depuis Pronote). - **Objectif** : Détecter les **changements** (ajouts, suppressions, modifications) entre les deux. +- **Format JSON** : L'agenda théorique est désormais décrit par un **fichier JSON** (et non plus iCal/CSV), avec gestion de la **parité des semaines** (paire/impaire) et des **vacances scolaires**. +- **Parité des semaines** : Chaque leçon peut s'appliquer à **toutes** les semaines (`all`), uniquement aux semaines **paires** (`even`) ou **impaires** (`odd`). La parité d'une date est calculée par rapport à une **date de référence** configurée. +- **Vacances scolaires** : Un **fichier JSON séparé** liste les périodes de vacances (ex: zone A) ; aucune leçon théorique n'est produite pendant ces périodes. +- **Encapsulation** : Le provider encapsule **en interne** le calcul de la parité et le filtrage des vacances ; l'appelant (ex: `AgendaComparator`) reçoit simplement les cours théoriques ou une liste vide. - **Matching déterministe** : Utiliser des règles claires pour associer un cours réel à un cours théorique (décision [4](#4-agenda-théorique---interface-abstraite--implémentation-fichier)). ### 8.2 Interface `TheoreticalAgendaProvider` (`sources/theoretical/provider.py`) @@ -3367,188 +3377,131 @@ class TheoreticalAgendaProvider(Protocol): ``` +> **Note sur le périmètre du provider** : Le fournisseur gère **en interne** la **parité des semaines** (paire/impaire) et les **vacances scolaires**. L'appelant ne connaît ni la date de référence de parité, ni les périodes de vacances : en période de vacances (ou pour une semaine dont la parité ne correspond à aucune leçon), il reçoit simplement une **liste vide**. Le contrat `TheoreticalAgendaProvider` reste donc volontairement minimal et stable. + + ### 8.3 Implémentation par fichier (`sources/theoretical/file.py`) -#### 8.3.1 Fichier iCal +#### 8.3.1 Format du fichier JSON de l'agenda théorique -Si l'agenda théorique est fourni sous forme de **fichier iCal** (ex: export depuis un autre outil), on peut le parser de la même manière que le flux Pronote. +L'agenda théorique est fourni sous forme de **fichier JSON** (ex: `./data/theoretical.json`) : -```python -from typing import List -from datetime import date, time -from pathlib import Path -from icalendar import Calendar, Event -from ..models.agenda import TheoreticalLesson -from .provider import TheoreticalAgendaProvider - - -class ICalTheoreticalAgendaProvider: - """Fournisseur d'agenda théorique depuis un fichier iCal.""" - - def __init__(self, file_path: str): - self.file_path = Path(file_path) - self._lessons: List[TheoreticalLesson] = [] - self._load() - - def _load(self) -> None: - """Charge le fichier iCal et parse les cours.""" - if not self.file_path.exists(): - raise FileNotFoundError(f"Fichier iCal introuvable: {self.file_path}") - - with open(self.file_path, "rb") as f: - cal = Calendar.from_ical(f.read()) - - for component in cal.walk(): - if not isinstance(component, Event): - continue - - # Ignorer les événements tout le jour (vacances, etc.) - if hasattr(component.get("dtstart"), "dt") and not hasattr(component.get("dtstart").dt, "hour"): - continue - - start = component.get("dtstart").dt - end = component.get("dtend").dt - - # Générer un ID stable (basé sur le jour, l'heure et la matière) - summary = str(component.get("summary", "")) - uid = f"theoretical-{start.strftime('%Y%m%d')}-{start.hour}{start.minute}-{summary}" - - lesson = TheoreticalLesson( - id=uid, - day_of_week=start.weekday(), - start_time=time(start.hour, start.minute), - end_time=time(end.hour, end.minute), - subject=summary, - teachers=[], # À extraire de la description si disponible - rooms=[], - ) - self._lessons.append(lesson) - - def get_lessons(self, date: date) -> List[TheoreticalLesson]: - """Récupère les cours pour une date donnée.""" - day_of_week = date.weekday() - return [ - lesson for lesson in self._lessons - if lesson.day_of_week == day_of_week - ] - - def get_lessons_for_range( - self, - start_date: date, - end_date: date, - ) -> List[TheoreticalLesson]: - """Récupère les cours pour une plage de dates.""" - from datetime import timedelta - - result = [] - current_date = start_date - while current_date <= end_date: - result.extend(self.get_lessons(current_date)) - current_date += timedelta(days=1) - return result - - -#### 8.3.2 Fichier CSV - -Si l'agenda théorique est fourni sous forme de **fichier CSV**, on peut le parser ainsi : - -```csv -jour,semaine,heure_debut,heure_fin,matiere,professeur,salle -lundi,1,08:00,09:00,Mathématiques,M. Dupont,204 -lundi,1,09:00,10:00,Français,Mme Martin,205 -... +```json +{ + "version": 1, + "lessons": [ + { + "week": "all", + "day_of_week": 0, + "start_time": "08:00", + "end_time": "09:00", + "subject": "Mathématiques", + "teachers": ["M. Dupont"], + "rooms": ["101"] + }, + { + "week": "even", + "day_of_week": 1, + "start_time": "10:00", + "end_time": "11:00", + "subject": "Anglais", + "teachers": [], + "rooms": [] + }, + { + "week": "odd", + "day_of_week": 1, + "start_time": "10:00", + "end_time": "11:00", + "subject": "Espagnol", + "teachers": [], + "rooms": [] + } + ] +} ``` -```python -import csv -from typing import List -from datetime import date, time -from pathlib import Path -from ..models.agenda import TheoreticalLesson -from .provider import TheoreticalAgendaProvider +- `week` : `"all"` (toutes les semaines), `"even"` (semaines paires) ou `"odd"` (semaines impaires). +- `day_of_week` : entier de **0 (lundi)** à **6 (dimanche)**. +- `start_time` / `end_time` : chaînes au format `"HH:MM"`. +- `teachers` / `rooms` : listes de chaînes, **optionnelles** (défaut : liste vide). +- `id` : **optionnel** ; s'il est absent, le provider génère un identifiant **déterministe incluant le type de semaine**, afin que deux leçons de parité différente sur le même créneau aient des identifiants distincts. +#### 8.3.2 Format du fichier JSON des vacances scolaires (fichier séparé) -class CSVTheoreticalAgendaProvider: - """Fournisseur d'agenda théorique depuis un fichier CSV.""" +Les vacances scolaires sont décrites dans un **fichier JSON séparé** (ex: `./data/school_holidays.json`) : - def __init__(self, file_path: str): - self.file_path = Path(file_path) - self._lessons: List[TheoreticalLesson] = [] - self._load() - - def _load(self) -> None: - """Charge le fichier CSV et parse les cours.""" - if not self.file_path.exists(): - raise FileNotFoundError(f"Fichier CSV introuvable: {self.file_path}") - - with open(self.file_path, "r", encoding="utf-8") as f: - reader = csv.DictReader(f) - for row in reader: - day_of_week = self._parse_day(row["jour"]) - start_time = self._parse_time(row["heure_debut"]) - end_time = self._parse_time(row["heure_fin"]) - - # Générer un ID stable - uid = f"theoretical-{day_of_week}-{start_time.isoformat()}-{row['matiere']}" - - lesson = TheoreticalLesson( - id=uid, - day_of_week=day_of_week, - start_time=start_time, - end_time=end_time, - subject=row["matiere"], - teachers=[row["professeur"]] if row.get("professeur") else [], - rooms=[row["salle"]] if row.get("salle") else [], - ) - self._lessons.append(lesson) - - def _parse_day(self, day: str) -> int: - """Convertit un nom de jour en index (0=lundi, 6=dimanche).""" - days = { - "lundi": 0, - "mardi": 1, - "mercredi": 2, - "jeudi": 3, - "vendredi": 4, - "samedi": 5, - "dimanche": 6, - } - return days.get(day.lower(), 0) - - def _parse_time(self, time_str: str) -> time: - """Parse une chaîne de temps (ex: 08:00).""" - hour, minute = map(int, time_str.split(":")) - return time(hour, minute) - - def get_lessons(self, date: date) -> List[TheoreticalLesson]: - """Récupère les cours pour une date donnée.""" - day_of_week = date.weekday() - return [ - lesson for lesson in self._lessons - if lesson.day_of_week == day_of_week - ] - - def get_lessons_for_range( - self, - start_date: date, - end_date: date, - ) -> List[TheoreticalLesson]: - """Récupère les cours pour une plage de dates.""" - from datetime import timedelta - - result = [] - current_date = start_date - while current_date <= end_date: - result.extend(self.get_lessons(current_date)) - current_date += timedelta(days=1) - return result +```json +{ + "zone": "A", + "school_year": "2026-2027", + "periods": [ + { + "start_date": "2026-10-17", + "end_date": "2026-11-02", + "label": "Toussaint" + } + ] +} ``` +- `start_date` et `end_date` sont des dates ISO (`YYYY-MM-DD`) **inclusives**. +- `is_holiday(date)` retourne `True` si la date tombe dans **l'une** des périodes (`start_date <= date <= end_date`). + +#### 8.3.3 Service de parité de semaine + +La parité des semaines est configurée via deux paramètres : + +- `THEORETICAL_WEEK_ANCHOR_DATE` : date de référence (ex: `2026-09-01`). +- `THEORETICAL_WEEK_ANCHOR_TYPE` : `"even"` ou `"odd"` (parité de la semaine de référence). + +**Algorithme** : on calcule le nombre de semaines entre le **lundi de la semaine cible** et le **lundi de la semaine de référence**. Si ce décalage est **pair**, la semaine cible a la **même parité** que l'ancre ; s'il est **impair**, la parité est **opposée**. + +```python +from datetime import date, timedelta +from typing import Literal + + +def week_parity( + target: date, + anchor_date: date, + anchor_type: Literal["even", "odd"], +) -> Literal["even", "odd"]: + """Détermine la parité (paire/impaire) de la semaine d'une date cible. + + :param target: Date dont on veut connaître la parité de semaine. + :param anchor_date: Date de référence (semaine de parité ``anchor_type``). + :param anchor_type: Parité de la semaine de référence (``"even"`` ou ``"odd"``). + :return: ``"even"`` ou ``"odd"`` selon la parité calculée. + :rtype: Literal["even", "odd"] + """ + target_monday = target - timedelta(days=target.weekday()) + anchor_monday = anchor_date - timedelta(days=anchor_date.weekday()) + offset_weeks = (target_monday - anchor_monday).days // 7 + if offset_weeks % 2 == 0: + return anchor_type + return "odd" if anchor_type == "even" else "even" +``` + +#### 8.3.4 Comportement du provider JSON + +- `get_lessons(date)` : si la date tombe pendant les **vacances scolaires**, retourner `[]`. Sinon, déterminer la **parité de la semaine**, filtrer les leçons selon le champ `week` (`all` correspond à toutes les semaines, `even`/`odd` à leur parité respective), construire les objets `TheoreticalLesson` et retourner la liste **triée par `id`**. +- `get_lessons_for_range(start_date, end_date)` : itérer sur **chaque date** de la plage, ignorer les **vacances scolaires**, appliquer le **filtrage de parité** à chaque jour et retourner la liste cumulée (éventuellement **dédupliquée par `id`**). + +#### 8.3.5 Configuration + +- `THEORETICAL_AGENDA_PATH` : chemin vers le fichier JSON de l'agenda théorique (ex: `./data/theoretical.json`). +- `SCHOOL_HOLIDAYS_PATH` : chemin vers le fichier JSON des vacances scolaires. +- `THEORETICAL_WEEK_ANCHOR_DATE` : date de référence pour la parité (ex: `2026-09-01`). +- `THEORETICAL_WEEK_ANCHOR_TYPE` : `"even"` ou `"odd"`. +- Si `THEORETICAL_AGENDA_PATH` est `None`, le provider est **désactivé** (M8 retourne un diff vide, non bloquant). +- Si le fichier d'agenda contient des leçons `even`/`odd` mais **aucune ancre n'est configurée**, lever une **erreur de configuration** explicite. + ### 8.4 Politique de départage pour les collisions **Règle déterministe** pour les collisions entre cours théoriques et réels : -1. **Tri par identifiant stable** : Les cours sont triés par UID ou clé de matching (ex: `theoretical-{day_of_week}-{start_time}-{subject}`). +1. **Tri par identifiant stable** : Les cours sont triés par ID ou clé de matching (ex: `theoretical-{day_of_week}-{start_time}-{subject}`). 2. **Comparaison exacte** : Les créneaux horaires et la matière normalisée doivent correspondre. 3. **Choix de la première correspondance** : En cas de multiples correspondances admissibles, choisir la **première** après tri déterministe. @@ -3567,22 +3520,22 @@ theoretical_lessons_sorted = sorted( **Exemple de matching avec départage déterministe** : ```python -def match_theoretical_event( +def match_theoretical_lesson( real_lesson: PronoteLesson, - theoretical_events: list[TheoreticalEvent], + theoretical_lessons: list[TheoreticalLesson], tolerance_minutes: int = 15, -) -> TheoreticalEvent | None: - """Trouve l'événement théorique correspondant, avec départage déterministe.""" +) -> TheoreticalLesson | None: + """Trouve le cours théorique correspondant, avec départage déterministe.""" candidates = [ - t for t in theoretical_events - if abs((t.start - real_lesson.start).total_seconds()) <= tolerance_minutes * 60 + t for t in theoretical_lessons + if t.day_of_week == real_lesson.start.weekday() + and abs((t.start_time - real_lesson.start.time()).total_seconds()) <= tolerance_minutes * 60 and normalize_subject(t.subject) == normalize_subject(real_lesson.subject) - and t.start.date() == real_lesson.start.date() ] if not candidates: return None - # Tri déterministe par UID stable, puis par créneau - candidates.sort(key=lambda t: (t.uid or "", t.start)) + # Tri déterministe par ID stable, puis par créneau + candidates.sort(key=lambda t: (t.id, t.start_time)) return candidates[0] ``` @@ -3789,11 +3742,14 @@ class AgendaComparator: ### 8.7 Points clés +- **Format JSON** : L'agenda théorique est décrit par un **fichier JSON** (leçons `all`/`even`/`odd`) ; les vacances scolaires sont décrites par un **fichier JSON séparé**. +- **Parité des semaines** : Déterminée par `THEORETICAL_WEEK_ANCHOR_DATE` et `THEORETICAL_WEEK_ANCHOR_TYPE` (décalage en semaines entre le lundi de référence et le lundi cible). +- **Vacances scolaires** : Les jours de vacances retournent une **liste vide** (aucune leçon théorique). +- **Identifiants déterministes** : Générés par le provider (type de semaine inclus) pour garantir des IDs distincts et stables. - **Matching déterministe** : Basé sur le jour, le créneau horaire (avec tolérance) et la matière normalisée. - **Normalisation** : Les matières et heures sont normalisées pour éviter les faux négatifs. - **Types de changements** : Ajout, suppression, modification. -- **Agenda théorique** : Peut être fourni via fichier iCal ou CSV (extensible à d'autres sources). - - **Politique de départage** : Tri par identifiant stable (UID), puis comparaison exacte des créneaux et matière normalisée. En cas de multiples correspondances, choix de la première après tri déterministe. + - **Politique de départage** : Tri par identifiant stable (ID), puis comparaison exacte des créneaux et matière normalisée. En cas de multiples correspondances, choix de la première après tri déterministe. --- diff --git a/TODO.md b/TODO.md index 25dcc09..d2a0c49 100644 --- a/TODO.md +++ b/TODO.md @@ -112,16 +112,23 @@ Récupérer le flux RSS du blog du collège, parser et dédupliquer les articles ## M6. Source agenda théorique — Priorité : Moyenne -Lire l'agenda théorique (iCal ou CSV) via une interface de provider extensible. +Lire l'agenda théorique (JSON) via une interface de provider extensible, avec gestion de la parité des semaines (paire/impaire) et des vacances scolaires. - [ ] Créer `sources/theoretical/provider.py` : protocole `TheoreticalAgendaProvider` (§8.2). -- [ ] Créer `sources/theoretical/file.py` : lecture fichier iCal/CSV → liste de `TheoreticalLesson` (§8.3). +- [ ] Créer `sources/theoretical/file.py` : parser JSON → liste de `TheoreticalLesson` avec filtrage par parité de semaine (paire/impaire/toutes). +- [ ] Créer `sources/theoretical/parity.py` : service `WeekParityService` déterminant la parité d'une date à partir d'une date de référence configurée. +- [ ] Créer `sources/theoretical/holidays.py` : service `SchoolHolidayCalendar` lisant un fichier JSON de vacances scolaires (zone A) et exposant `is_holiday(date)`. +- [ ] Implémenter le provider JSON : filtrage par parité + vacances, génération d'identifiants déterministes incluant le type de semaine. +- [ ] Ajouter la configuration : `SCHOOL_HOLIDAYS_PATH`, `THEORETICAL_WEEK_ANCHOR_DATE`, `THEORETICAL_WEEK_ANCHOR_TYPE` dans `AppSettings`. - [ ] Normaliser les matières et créneaux pour le matching déterministe. -- [ ] Supporter les deux formats (iCal et CSV) derrière la même interface. +- [ ] Créer les fixtures : `tests/fixtures/theoretical.json` et `tests/fixtures/school_holidays.json`. ### Critères d'acceptation -- `file.py` lit `tests/fixtures/theoretical.ics` et `theoretical.csv` en `TheoreticalLesson`. +- `file.py` lit `tests/fixtures/theoretical.json` en `TheoreticalLesson` avec filtrage par parité. +- Le provider renvoie une liste vide pendant les vacances scolaires. - Le provider renvoie une liste stable et déterministe (tri par identifiant). +- Les identifiants sont distincts pour des leçons de parité différente sur le même créneau. +- Une configuration incomplète (ancre de parité manquante alors que des leçons `even`/`odd` existent) produit une erreur explicite. --- @@ -236,7 +243,7 @@ Exposer le lancement du pipeline via une interface en ligne de commande. Couvrir l'ensemble du code par des tests sans réseau, avec fixtures anonymisées, jusqu'à ≥ 90 %. -- [ ] Créer `tests/fixtures/` : `pronote-4e.ics`, `pronote-6e.ics`, `theoretical.ics`, `theoretical.csv`, `blog_rss.xml` (anonymisés, sans `icalsecurise`). +- [ ] Créer `tests/fixtures/` : `pronote-4e.ics`, `pronote-6e.ics`, `theoretical.json`, `school_holidays.json`, `blog_rss.xml` (anonymisés, sans `icalsecurise`). - [ ] Créer `tests/conftest.py` : fixtures partagées (sample_lesson, sample_cancelled_lesson, sample_homework, sample_school_event, sample_message, sample_pronote_data…). - [ ] Écrire `tests/unit/` : `test_models`, `test_parsing` (iCal), `test_uid`, `test_redaction`, `test_diff`, `test_sync`. - [ ] Couvrir les régressions M4 : signature réelle de `ParentClient`, ENT autorisé/inconnu, erreur vs résultat vide, `STATUS:CANCELLED` sans catégorie, plusieurs devoirs à la même date, filtrage `pronotepy` sur la date cible et stabilité d'identité entre sources. diff --git a/pronote_sync/config/settings.py b/pronote_sync/config/settings.py index 898885d..738b75b 100644 --- a/pronote_sync/config/settings.py +++ b/pronote_sync/config/settings.py @@ -8,6 +8,7 @@ depuis les variables d'environnement (préfixées par groupe) et le fichier from __future__ import annotations +from datetime import date from typing import Literal from pydantic import Field, SecretStr, field_serializer @@ -114,7 +115,10 @@ class AppSettings(BaseSettings): """Paramètres généraux de l'application, sans préfixe d'environnement. Contient notamment la fenêtre de synchronisation en jours - (``SYNC_PAST_DAYS`` / ``SYNC_FUTURE_DAYS``). + (``SYNC_PAST_DAYS`` / ``SYNC_FUTURE_DAYS``) et la configuration de + l'agenda théorique (``THEORETICAL_AGENDA_PATH``, + ``THEORETICAL_WEEK_ANCHOR_DATE``, ``THEORETICAL_WEEK_ANCHOR_TYPE`` ainsi + que ``SCHOOL_HOLIDAYS_PATH`` pour les vacances scolaires). """ model_config = SettingsConfigDict(env_file=".env", extra="ignore") @@ -122,6 +126,9 @@ class AppSettings(BaseSettings): dry_run: bool = False log_level: str = "INFO" theoretical_agenda_path: str | None = None + school_holidays_path: str | None = None + theoretical_week_anchor_date: date | None = None + theoretical_week_anchor_type: Literal["even", "odd"] | None = None sync_past_days: int = 7 sync_future_days: int = 30