Scaffold: préparation du projet pronote-sync
- Guide de développement (GUIDE_DEV_PYTHON.md) : spécification complète - TODO.md : 15 jalons de développement (M1-M15) avec étapes et critères d'acceptation - AGENTS.md : guide de contribution pour agents et développeurs - pyproject.toml : configuration projet (dépendances, ruff, mypy strict, pytest) - .gitignore : exclusion venv, bytecode, .env, caches, state files - .env.example : template des variables d'environnement (placeholders) - Structure du package pronote_sync/ (14 sous-packages avec __init__.py) - tests/ avec conftest.py et fixtures/ - Environnement virtuel .venv/ (Python 3.14)
This commit is contained in:
37
.env.example
Normal file
37
.env.example
Normal file
@@ -0,0 +1,37 @@
|
|||||||
|
# --- Pronote ---
|
||||||
|
PRONOTE_ICAL_URL=https://college.ent/pronote/ical/Edt_Jean.ics?icalsecurise=REPLACE_ME&version=2024
|
||||||
|
PRONOTE_USERNAME=parent.dupont
|
||||||
|
PRONOTE_PASSWORD=your_secure_password
|
||||||
|
PRONOTE_ENT=monbureaunumerique
|
||||||
|
|
||||||
|
# Sources (auto = essayer iCal d'abord, puis pronotepy)
|
||||||
|
PRONOTE_AGENDA_SOURCE=auto
|
||||||
|
PRONOTE_HOMEWORK_SOURCE=auto
|
||||||
|
PRONOTE_MESSAGES_SOURCE=pronotepy
|
||||||
|
|
||||||
|
# --- CalDAV ---
|
||||||
|
CALDAV_URL=https://caldav.example.com/calendars/user/pronote/
|
||||||
|
CALDAV_USERNAME=user@example.com
|
||||||
|
CALDAV_PASSWORD=your_caldav_password
|
||||||
|
|
||||||
|
# Fenêtre de synchronisation (jours)
|
||||||
|
SYNC_PAST_DAYS=7
|
||||||
|
SYNC_FUTURE_DAYS=30
|
||||||
|
|
||||||
|
# --- Agenda théorique ---
|
||||||
|
THEORETICAL_AGENDA_PATH=./data/theoretical.ics
|
||||||
|
|
||||||
|
# --- XMPP ---
|
||||||
|
XMPP_JID=user@example.com
|
||||||
|
XMPP_PASSWORD=your_xmpp_password
|
||||||
|
XMPP_RECIPIENT=parent@example.com
|
||||||
|
|
||||||
|
# --- IA (optionnelle) ---
|
||||||
|
AI_ENABLED=true
|
||||||
|
AI_BASE_URL=https://api.openai.com/v1
|
||||||
|
AI_API_KEY=your_ai_api_key
|
||||||
|
AI_MODEL=gpt-4o-mini
|
||||||
|
|
||||||
|
# --- Divers ---
|
||||||
|
DRY_RUN=false
|
||||||
|
LOG_LEVEL=INFO
|
||||||
53
.gitignore
vendored
Normal file
53
.gitignore
vendored
Normal file
@@ -0,0 +1,53 @@
|
|||||||
|
# --- Python bytecode ---
|
||||||
|
__pycache__/
|
||||||
|
*.pyc
|
||||||
|
*.pyo
|
||||||
|
*.pyd
|
||||||
|
.Python
|
||||||
|
|
||||||
|
# --- Virtual environments ---
|
||||||
|
.venv/
|
||||||
|
venv/
|
||||||
|
env/
|
||||||
|
ENV/
|
||||||
|
|
||||||
|
# --- Distribution / build ---
|
||||||
|
build/
|
||||||
|
dist/
|
||||||
|
*.egg-info/
|
||||||
|
*.egg
|
||||||
|
pip-wheel-metadata/
|
||||||
|
|
||||||
|
# --- Testing / coverage ---
|
||||||
|
.pytest_cache/
|
||||||
|
.coverage
|
||||||
|
.coverage.*
|
||||||
|
htmlcov/
|
||||||
|
.tox/
|
||||||
|
.mypy_cache/
|
||||||
|
.ruff_cache/
|
||||||
|
coverage.xml
|
||||||
|
|
||||||
|
# --- Environment files ---
|
||||||
|
.env
|
||||||
|
.env.*
|
||||||
|
!.env.example
|
||||||
|
|
||||||
|
# --- IDE files ---
|
||||||
|
.vscode/
|
||||||
|
.idea/
|
||||||
|
*.swp
|
||||||
|
*.swo
|
||||||
|
*~
|
||||||
|
|
||||||
|
# --- OS files ---
|
||||||
|
.DS_Store
|
||||||
|
Thumbs.db
|
||||||
|
|
||||||
|
# --- Project-specific state files ---
|
||||||
|
.blog_rss_state.json
|
||||||
|
.caldav_sync_state.json
|
||||||
|
*.state.json
|
||||||
|
|
||||||
|
# --- Logs ---
|
||||||
|
*.log
|
||||||
158
AGENTS.md
Normal file
158
AGENTS.md
Normal file
@@ -0,0 +1,158 @@
|
|||||||
|
# Guide pour les agents et contributeurs — `pronote-sync`
|
||||||
|
|
||||||
|
> **Langue de travail** : Français (ce fichier et toute la documentation projet).
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 1. Description du projet
|
||||||
|
`pronote-sync` est un pipeline Python qui synchronise les données de **Pronote** (système de gestion scolaire) vers **CalDAV** (agendas) et **XMPP** (notifications).
|
||||||
|
|
||||||
|
> **Spécification complète** : Voir [`GUIDE_DEV_PYTHON.md`](./GUIDE_DEV_PYTHON.md).
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 2. Stack technique
|
||||||
|
|
||||||
|
### Langage et dépendances
|
||||||
|
- **Python** : ≥ 3.13 (actuellement 3.14 dans `.venv/`)
|
||||||
|
- **Dépendances principales** :
|
||||||
|
`pydantic>=2.0`, `pydantic-settings`, `icalendar`, `caldav`, `slixmpp`, `pronotepy`, `feedparser`, `requests`, `httpx`
|
||||||
|
|
||||||
|
### Outils de développement
|
||||||
|
- **Linter** : `ruff` (longueur de ligne : 100)
|
||||||
|
- **Typage** : `mypy` (mode `strict=true`)
|
||||||
|
- **Tests** : `pytest` (couverture ≥ 90%), `pytest-mock`, `responses`, `aioresponses`
|
||||||
|
- **Sécurité** : `bandit`
|
||||||
|
- **Pre-commit** : hooks configurés pour `ruff`, `mypy`, `bandit`
|
||||||
|
|
||||||
|
### Environnement
|
||||||
|
- **Virtualenv** : `.venv/` (à activer avant toute opération)
|
||||||
|
- **Fichier de configuration** : `.env` (à partir de `.env.example`)
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 3. Structure du projet
|
||||||
|
|
||||||
|
```
|
||||||
|
pronote_sync/
|
||||||
|
├── config/ # Configuration et paramètres
|
||||||
|
├── models/ # Modèles de données (Pydantic v2)
|
||||||
|
├── sources/ # Connecteurs (Pronote, iCal, etc.)
|
||||||
|
├── sync/ # Logique de synchronisation
|
||||||
|
├── synthesis/ # Génération de synthèses (IA)
|
||||||
|
├── channels/ # Canaux de sortie (CalDAV, XMPP)
|
||||||
|
├── pipeline/ # Orchestration du pipeline
|
||||||
|
├── utils/ # Utilitaires (redaction, logging, etc.)
|
||||||
|
├── cli/ # Interface en ligne de commande
|
||||||
|
└── __init__.py
|
||||||
|
|
||||||
|
tests/
|
||||||
|
├── fixtures/ # Données de test anonymisées
|
||||||
|
├── unit/ # Tests unitaires
|
||||||
|
└── integration/ # Tests d'intégration
|
||||||
|
```
|
||||||
|
|
||||||
|
> **Plan de développement** : Voir [`TODO.md`](./TODO.md) pour les 15 jalons (M1-M15).
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 4. Commandes essentielles
|
||||||
|
|
||||||
|
### Environnement
|
||||||
|
```bash
|
||||||
|
# Activer le virtualenv
|
||||||
|
source .venv/bin/activate
|
||||||
|
|
||||||
|
# Installer les dépendances de développement
|
||||||
|
pip install -e ".[dev]"
|
||||||
|
```
|
||||||
|
|
||||||
|
### Qualité de code
|
||||||
|
```bash
|
||||||
|
# Linter (ruff)
|
||||||
|
ruff check .
|
||||||
|
|
||||||
|
# Formattage (ruff)
|
||||||
|
ruff format .
|
||||||
|
|
||||||
|
# Vérification des types (mypy)
|
||||||
|
mypy .
|
||||||
|
|
||||||
|
# Analyse de sécurité (bandit)
|
||||||
|
bandit -r pronote_sync/
|
||||||
|
|
||||||
|
# Pre-commit (tous les hooks)
|
||||||
|
pre-commit run --all-files
|
||||||
|
```
|
||||||
|
|
||||||
|
### Tests
|
||||||
|
```bash
|
||||||
|
# Exécuter les tests
|
||||||
|
pytest
|
||||||
|
|
||||||
|
# Exécuter les tests avec couverture
|
||||||
|
pytest --cov
|
||||||
|
```
|
||||||
|
|
||||||
|
### CLI
|
||||||
|
```bash
|
||||||
|
# Exécuter en mode dry-run (simulation)
|
||||||
|
pronote-sync --dry-run
|
||||||
|
```
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 5. Conventions de code
|
||||||
|
|
||||||
|
### Typage
|
||||||
|
- **Strict** : `mypy` en mode `strict=true`. **Tous** les paramètres, retours et variables locales doivent être typés.
|
||||||
|
- **Pydantic v2** : Tous les modèles de données héritent de `BaseModel`. Les modèles de contrat utilisent `frozen=True`.
|
||||||
|
|
||||||
|
### Architecture
|
||||||
|
- **Injection de dépendances** : Utiliser `typing.Protocol` et une **composition root** dans `pipeline/run.py`. **Interdiction** des singletons globaux.
|
||||||
|
|
||||||
|
### Style
|
||||||
|
- **Longueur de ligne** : 100 caractères maximum (configuré dans `ruff`).
|
||||||
|
- **Imports** : Triés automatiquement par `ruff` (intègre `isort`).
|
||||||
|
|
||||||
|
### Tests
|
||||||
|
- **Sans réseau** : Utiliser **uniquement** des mocks (`pytest-mock`, `responses`, `aioresponses`).
|
||||||
|
- **Fixtures** : Les données de test doivent être anonymisées et stockées dans `tests/fixtures/`.
|
||||||
|
|
||||||
|
### Comportement
|
||||||
|
- **Idempotence** : Deux exécutions identiques sans changement externe doivent produire le **même résultat**.
|
||||||
|
- **Mode dégradé** :
|
||||||
|
- Si l'IA échoue → retourner un message **sans synthèse**.
|
||||||
|
- Si `pronotepy` échoue → fallback vers le parsing **iCal**.
|
||||||
|
- Si tout échoue → lever une **erreur explicite**.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 6. Sécurité et secrets
|
||||||
|
|
||||||
|
### Règles absolues
|
||||||
|
- **Aucun secret en clair** : Ni dans le code, ni dans les logs, ni dans les erreurs, ni dans les fixtures.
|
||||||
|
- **Masquage** : Utiliser systématiquement `redact_url()`, `redact_secrets()`, et `redact_exception()` depuis `utils/redaction.py`.
|
||||||
|
|
||||||
|
### Bonnes pratiques
|
||||||
|
- **Types sécurisés** : Les mots de passe et clés API doivent utiliser `pydantic.SecretStr`.
|
||||||
|
- **Fichier `.env`** : **Jamais** committé (couvert par `.gitignore`). Utiliser `.env.example` comme template.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 7. Plan de développement
|
||||||
|
|
||||||
|
Le projet suit un plan séquentiel en **15 jalons** (M1-M15) décrits dans [`TODO.md`](./TODO.md).
|
||||||
|
|
||||||
|
- Chaque jalon a des **critères d'acceptation** à valider avant de passer au suivant.
|
||||||
|
- Ordre logique :
|
||||||
|
**scaffolding → config → modèles → sources → sync → synthèse → canaux → pipeline → CLI → tests → déploiement → docs**
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 8. Référence
|
||||||
|
|
||||||
|
| Document | Rôle |
|
||||||
|
|----------|------|
|
||||||
|
| [`GUIDE_DEV_PYTHON.md`](./GUIDE_DEV_PYTHON.md) | Spécification complète (architecture, modèles, parsing, déploiement) |
|
||||||
|
| [`TODO.md`](./TODO.md) | Plan de développement détaillé (15 jalons) |
|
||||||
6133
GUIDE_DEV_PYTHON.md
Normal file
6133
GUIDE_DEV_PYTHON.md
Normal file
File diff suppressed because it is too large
Load Diff
276
TODO.md
Normal file
276
TODO.md
Normal file
@@ -0,0 +1,276 @@
|
|||||||
|
# TODO — Développement de `pronote-sync` (Pronote → CalDAV + XMPP)
|
||||||
|
|
||||||
|
> Plan de développement dérivé du `GUIDE_DEV_PYTHON.md`. Package Python : `pronote_sync`.
|
||||||
|
> Pipeline : récupération Pronote (iCal / pronotepy) → blog RSS → comparaison agenda théorique → sync CalDAV → synthèse IA → message XMPP.
|
||||||
|
> Contraintes transverses : Python ≥ 3.13.5, Pydantic v2 + pydantic-settings, injection par `typing.Protocol`, tests sans réseau, idempotence, mode dégradé, masquage systématique des secrets, coverage ≥ 90 %.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## M1. Échafaudage et outillage — Priorité : Haute
|
||||||
|
|
||||||
|
Mettre en place le dépôt, l'environnement, l'arborescence du package et la chaîne d'outils (lint/type/test/pré-commit).
|
||||||
|
|
||||||
|
- [ ] Créer l'environnement virtuel Python (≥ 3.13.5) et l'activer (`python -m venv venv`).
|
||||||
|
- [ ] Créer `pyproject.toml` d'après l'Annexe C (projet `pronote-sync`, `requires-python = ">=3.13.5"`, dépendances, extras `dev` et `ai-litellm`, script console `pronote-sync`).
|
||||||
|
- [ ] Configurer ruff (`line-length = 100`, `target-version = "py313"`, règles E/W/F/I/B/C4/UP), mypy (`strict`), bandit et coverage (`fail_under = 90`) dans `pyproject.toml`.
|
||||||
|
- [ ] Créer `.gitignore` (venv, `__pycache__`, `.env`, `.blog_rss_state.json`, `.coverage`, artefacts `.ics` temporaires).
|
||||||
|
- [ ] Créer l'arborescence `pronote_sync/` avec `__init__.py` dans chaque package (`config`, `models`, `sources/pronote`, `sources/blog`, `sources/theoretical`, `sync`, `synthesis`, `channels`, `pipeline/steps`, `cli`, `utils`, `tests`).
|
||||||
|
- [ ] Ajouter la configuration pre-commit (ruff, mypy, bandit, detect-secrets, `trailing-whitespace`, `end-of-file`).
|
||||||
|
- [ ] Installer le projet en mode éditable : `pip install -e ".[dev]"`.
|
||||||
|
- [ ] Créer le commit initial (scaffold + `.gitignore`, sans aucun secret).
|
||||||
|
|
||||||
|
### Critères d'acceptation
|
||||||
|
- Le package `pronote_sync` est importable sans erreur.
|
||||||
|
- `ruff check .`, `mypy .` et `pytest` s'exécutent sans erreur d'import.
|
||||||
|
- `pre-commit run --all-files` réussit.
|
||||||
|
- Le dépôt ne contient aucun secret (vérifié par detect-secrets).
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## M2. Configuration et gestion des secrets — Priorité : Haute
|
||||||
|
|
||||||
|
Implémenter la configuration Pydantic Settings, le masquage des secrets et la journalisation sûre.
|
||||||
|
|
||||||
|
- [ ] Créer `config/settings.py` : `PronoteSettings`, `CalDAVSettings`, `XmppSettings`, `AISettings`, `AppSettings`, `Settings` (§3.2) avec `SecretStr` et prefixes d'env.
|
||||||
|
- [ ] Créer `config/env.py` pour le chargement du `.env` (`SettingsConfigDict(env_file=".env")`).
|
||||||
|
- [ ] Créer `.env.example` complet (toutes variables obligatoires §3.1.1 + optionnelles §3.1.2).
|
||||||
|
- [ ] Créer `utils/redaction.py` : `redact_url`, `redact_secrets`, `redact_exception` (§4.2.1).
|
||||||
|
- [ ] Créer `utils/logging.py` : `setup_logging` + `RedactingFormatter` masquant les secrets dans messages et args (§4.2.2).
|
||||||
|
- [ ] Créer `utils/uid.py` : `normalize_uid` (suppression des suffixes temporels des UID Pronote).
|
||||||
|
- [ ] Vérifier qu'aucun `SecretStr` n'est affiché en clair via `str()`/`print`.
|
||||||
|
|
||||||
|
### Critères d'acceptation
|
||||||
|
- `from pronote_sync.config.settings import settings` fonctionne et charge `.env`.
|
||||||
|
- `redact_secrets("...icalsecurise=TOKEN...")` masque le token et l'URL.
|
||||||
|
- Les logs ne contiennent jamais de token, mot de passe ou clé API (même en DEBUG).
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## M3. Modèles de données Pydantic — Priorité : Haute
|
||||||
|
|
||||||
|
Définir tous les modèles de domaine, immuables pour les contrats, mutables pour les résultats de travail.
|
||||||
|
|
||||||
|
- [ ] Créer `models/agenda.py` : `Status`, `LessonStatus`, `HomeworkBlock`, `Lesson` (frozen), `SchoolEventKind`, `SchoolEvent`, `TheoreticalLesson`.
|
||||||
|
- [ ] Créer `models/homework.py` : `Homework` (frozen, `id` = hachage stable).
|
||||||
|
- [ ] Créer `models/message.py` : `MessageType`, `Message` (frozen).
|
||||||
|
- [ ] Créer `models/diff.py` : `AgendaChangeType`, `AgendaChange` (frozen), `AgendaDiff` (frozen).
|
||||||
|
- [ ] Créer `models/pronote.py` : `PronoteData` (mutable).
|
||||||
|
- [ ] Créer `models/sync.py` : `CalDAVSyncStatus`, `CalDAVSyncPlan`, `CalDAVSyncResult` (mutable).
|
||||||
|
- [ ] Créer `models/synthesis.py` : `SynthesisInput`, `SynthesisResult`.
|
||||||
|
- [ ] Créer `models/blog.py` : `BlogArticle` (frozen), `ExternalInfo` (§5 bis.5/6).
|
||||||
|
- [ ] Créer `models/xmpp.py` : `XmppMessage` (frozen) intégrant `external_info`.
|
||||||
|
- [ ] Créer `models/__init__.py` ré-exportant tous les modèles.
|
||||||
|
- [ ] Valider la sérialisation JSON (datetime/date en ISO) pour chaque modèle.
|
||||||
|
|
||||||
|
### Critères d'acceptation
|
||||||
|
- Chaque modèle s'instancie et se sérialise en JSON valide.
|
||||||
|
- `Lesson`, `Homework`, `Message`, `AgendaChange`, `AgendaDiff`, `XmppMessage`, `BlogArticle` sont `frozen=True`.
|
||||||
|
- `PronoteData` et `CalDAVSyncResult` sont mutables ; tous les modèles importables via `models/__init__.py`.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## M4. Sources Pronote (iCal + pronotepy + repli) — Priorité : Haute
|
||||||
|
|
||||||
|
Récupérer et normaliser l'agenda, les devoirs et les messages Pronote, avec repli entre iCal et pronotepy.
|
||||||
|
|
||||||
|
- [ ] Créer `sources/pronote/ical.py` : `fetch_ical(url)` (HTTP via `requests`, erreurs redactées) et parsing iCal → `Lesson`/`Homework`/`SchoolEvent` (`icalendar`).
|
||||||
|
- [ ] Extraire les blocs de devoirs (`HomeworkBlock`) depuis `DESCRIPTION` et dédupliquer les devoirs (clé normalisée par date).
|
||||||
|
- [ ] Détecter les statuts (`CANCELLED`/`MOVED`) via `CATEGORIES` et `STATUS:CANCELLED`.
|
||||||
|
- [ ] Créer `sources/pronote/client.py` : client `pronotepy` (messages, informations, discussions, sondages, et devoirs en repli) avec masquage des erreurs.
|
||||||
|
- [ ] Créer `sources/pronote/fallback.py` : sélection de source selon `PRONOTE_*_SOURCE` (auto/ical/pronotepy) et `PronoteFetcher` unifiant `fetch_agenda`/`fetch_homework`/`fetch_messages`.
|
||||||
|
- [ ] Implémenter le repli : iCal échoue → pronotepy ; pronotepy échoue → iCal ; les deux échouent → `PipelineCriticalError`.
|
||||||
|
- [ ] Normaliser les UID via `utils/uid.normalize_uid` pour la stabilité des événements.
|
||||||
|
|
||||||
|
### Critères d'acceptation
|
||||||
|
- `fetch_ical` parse `tests/fixtures/pronote-4e.ics` en leçons/devoirs/événements corrects (cours annulé détecté).
|
||||||
|
- Le client pronotepy récupère messages/devoirs (mocké).
|
||||||
|
- Le repli bascule correctement et lève une erreur critique si aucune source disponible.
|
||||||
|
- Aucun secret dans les messages d'erreur de fetch.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## M5. Source blog (RSS) — Priorité : Moyenne
|
||||||
|
|
||||||
|
Récupérer le flux RSS du blog du collège, parser et dédupliquer les articles.
|
||||||
|
|
||||||
|
- [ ] Créer `sources/blog/rss.py` : `BlogRSSClient.fetch_and_parse(known_guids)` avec `feedparser` (§5 bis.7.1).
|
||||||
|
- [ ] Parser les dates (RFC 822 / ISO 8601) et convertir le HTML en texte brut (`BeautifulSoup` + `html.unescape`).
|
||||||
|
- [ ] Créer `sources/blog/state.py` (ou `sync/blog_state.py`) : `BlogRSSState` (JSON : `known_guids`, `etag`, `last_modified`).
|
||||||
|
- [ ] Implémenter la déduplication par GUID et le cache HTTP (`If-Modified-Since` / `etag`).
|
||||||
|
- [ ] Gérer un flux invalide (`bozo`) et les exceptions sans fuite de secret (retour `[]`/warning).
|
||||||
|
|
||||||
|
### Critères d'acceptation
|
||||||
|
- `fetch_and_parse` renvoie les nouveaux articles triés par date décroissante, sans doublons.
|
||||||
|
- L'état persiste les GUID connus entre deux appels.
|
||||||
|
- Un flux invalide ne plante pas le pipeline (warning non bloquant).
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## M6. Source agenda théorique — Priorité : Moyenne
|
||||||
|
|
||||||
|
Lire l'agenda théorique (iCal ou CSV) via une interface de provider extensible.
|
||||||
|
|
||||||
|
- [ ] 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).
|
||||||
|
- [ ] 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.
|
||||||
|
|
||||||
|
### Critères d'acceptation
|
||||||
|
- `file.py` lit `tests/fixtures/theoretical.ics` et `theoretical.csv` en `TheoreticalLesson`.
|
||||||
|
- Le provider renvoie une liste stable et déterministe (tri par identifiant).
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## M7. Synchronisation CalDAV — Priorité : Haute
|
||||||
|
|
||||||
|
Synchroniser différentiellement les événements Pronote vers le calendrier CalDAV, de façon idempotente.
|
||||||
|
|
||||||
|
- [ ] Créer `sync/caldav.py` : `CalDAVClient` (connexion, liste/ajout/MAJ/suppression, marqueur `X-PRONOTE-SYNC-MANAGED: v1`).
|
||||||
|
- [ ] Créer `sync/state.py` : état local de sync (SQLite ou JSON) assurant l'idempotence (UID connus).
|
||||||
|
- [ ] Calculer le `CalDAVSyncPlan` (to_add / to_update / to_remove) par UID stable.
|
||||||
|
- [ ] Implémenter la sync différentielle : conserver les cours annulés (`STATUS:CANCELLED`), ne pas supprimer.
|
||||||
|
- [ ] Garantir l'idempotence (2 exécutions identiques → même `CalDAVSyncResult`).
|
||||||
|
- [ ] Réutiliser `BlogRSSState` pour l'état blog si pertinent (sinon `sync/blog_state.py`).
|
||||||
|
|
||||||
|
### Critères d'acceptation
|
||||||
|
- Le plan de sync est correctement calculé (PronoteData vs état local).
|
||||||
|
- Un run dry-run n'écrit rien ; deux runs identiques donnent un résultat identique.
|
||||||
|
- Les événements annulés restent (`STATUS:CANCELLED`) et sont marqués `MANAGED`.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## M8. Comparaison avec l'agenda théorique (diff) — Priorité : Haute
|
||||||
|
|
||||||
|
Comparer l'agenda réel et l'agenda théorique pour générer les ajouts/suppressions/modifications.
|
||||||
|
|
||||||
|
- [ ] Créer `sync/diff.py` : `AgendaComparator` avec matching déterministe (jour + créneau avec tolérance + matière normalisée).
|
||||||
|
- [ ] Générer `AgendaDiff` / `AgendaChange` (added / removed / modified).
|
||||||
|
- [ ] Appliquer la politique de départage : tri par UID stable puis comparaison exacte ; première correspondance en cas de multi-match (§8.4).
|
||||||
|
- [ ] Gérer l'absence de fichier théorique (diff vide, non bloquant).
|
||||||
|
|
||||||
|
### Critères d'acceptation
|
||||||
|
- La comparaison produit les bons `added`/`removed`/`modified`.
|
||||||
|
- Le matching est déterministe (même entrée → même résultat).
|
||||||
|
- Sans `THEORETICAL_AGENDA_PATH`, retourne un diff vide sans erreur.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## M9. Synthèse IA — Priorité : Moyenne
|
||||||
|
|
||||||
|
Générer une synthèse optionnelle via un fournisseur IA, avec mode dégradé strict.
|
||||||
|
|
||||||
|
- [ ] Créer `synthesis/provider.py` : protocole `SynthesisProvider.generate → Optional[SynthesisResult]` (ne lève jamais d'exception).
|
||||||
|
- [ ] Créer `synthesis/openai.py` : `OpenAISynthesisProvider` (httpx, prompt système FR, max 800 car., timeout 30 s, temp 0.3).
|
||||||
|
- [ ] Créer `synthesis/litellm.py` : `LiteLLMSynthesisProvider` (optionnel, extra `ai-litellm`).
|
||||||
|
- [ ] Créer `synthesis/__init__.py` : factory `get_synthesis_provider(settings)` (OpenAI par défaut, litellm si `AI_PROVIDER=litellm`).
|
||||||
|
- [ ] Mode dégradé : clé absente / timeout / exception → retour `None` (le pipeline continue sans synthèse).
|
||||||
|
- [ ] Respecter les contraintes (3-5 phrases, ton sobre, pas d'emoji dans le texte IA).
|
||||||
|
|
||||||
|
### Critères d'acceptation
|
||||||
|
- `generate` retourne une synthèse ≤ 800 car. conforme au prompt système.
|
||||||
|
- Clé absente ou erreur réseau → `None` (aucune exception propagée).
|
||||||
|
- La factory renvoie le bon provider ; litellm derrière l'extra optionnel.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## M10. Canal XMPP — Priorité : Haute
|
||||||
|
|
||||||
|
Construire et envoyer le message XMPP structuré via un compte bot dédié (message direct, pas de PubSub).
|
||||||
|
|
||||||
|
- [ ] Créer `channels/protocol.py` : protocole `Channel` (méthode d'envoi).
|
||||||
|
- [ ] Créer `channels/xmpp.py` : `XmppChannel` (slixmpp, message direct, compte bot dédié).
|
||||||
|
- [ ] Implémenter `_format_message(XmppMessage)` : synthèse + liste brute des devoirs + changements + messages + infos blog (emojis 📌📅📚💬 autorisés).
|
||||||
|
- [ ] Gérer les erreurs XMPP (reconnexion, timeout) avec masquage des secrets, non bloquant (`PipelineWarning`).
|
||||||
|
- [ ] Créer `channels/__init__.py` : factory de canaux.
|
||||||
|
|
||||||
|
### Critères d'acceptation
|
||||||
|
- `XmppChannel.send` envoie un message direct formaté (slixmpp mocké en test).
|
||||||
|
- Erreur XMPP → `PipelineWarning`, jamais d'exception non gérée.
|
||||||
|
- Aucun secret dans les logs XMPP.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## M11. Orchestration du pipeline — Priorité : Haute
|
||||||
|
|
||||||
|
Composer et orchestrer toutes les étapes avec gestion d'erreurs dégradée et mode dry-run.
|
||||||
|
|
||||||
|
- [ ] Créer `pipeline/steps/errors.py` : `ErrorSeverity`, `PipelineError`, `PipelineWarning`, `PipelineCriticalError`.
|
||||||
|
- [ ] Créer les étapes `pipeline/steps/` : `fetch.py`, `normalize.py`, `compare.py`, `caldav_sync.py`, `synthesis.py`, `send.py`, `fetch_blog.py`.
|
||||||
|
- [ ] Créer `pipeline/run.py` : `PipelineRunner` (composition root) orchestrant fetch → normalize → fetch_blog → compare → caldav_sync → synthesis → send.
|
||||||
|
- [ ] Gérer les erreurs dégradées (continuer sauf critique) et renvoyer `(PronoteData, erreurs + warns)`.
|
||||||
|
- [ ] Implémenter le mode `dry_run` (aucune écriture CalDAV/XMPP).
|
||||||
|
- [ ] Câbler l'injection des dépendances (Protocol + composition root), sans singleton global.
|
||||||
|
|
||||||
|
### Critères d'acceptation
|
||||||
|
- Le pipeline complet s'exécute de bout en bout (mocks) dans le bon ordre.
|
||||||
|
- Une erreur non critique (ex : synthèse IA) n'empêche pas l'envoi XMPP.
|
||||||
|
- `dry_run=True` n'effectue aucune écriture ; aucune source disponible → erreur critique explicite.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## M12. Point d'entrée CLI — Priorité : Haute
|
||||||
|
|
||||||
|
Exposer le lancement du pipeline via une interface en ligne de commande.
|
||||||
|
|
||||||
|
- [ ] Créer `cli/main.py` : `main()` (point d'entrée `pronote-sync`), args `--dry-run`, `--log-level`.
|
||||||
|
- [ ] Initialiser les logs (`setup_logging`) et charger `settings` au démarrage.
|
||||||
|
- [ ] Construire la composition root et lancer `PipelineRunner.run()`.
|
||||||
|
- [ ] Gérer le code de retour et l'affichage des erreurs (redactées).
|
||||||
|
|
||||||
|
### Critères d'acceptation
|
||||||
|
- `pronote-sync --dry-run --log-level DEBUG` s'exécute sans effet de bord.
|
||||||
|
- Le script console est installable (`[project.scripts]` dans `pyproject.toml`).
|
||||||
|
- Les erreurs affichées ne contiennent aucun secret.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## M13. Tests et couverture — Priorité : Haute
|
||||||
|
|
||||||
|
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/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`.
|
||||||
|
- [ ] Écrire `tests/integration/` : `test_pipeline`, `test_caldav` (mocké), `test_xmpp` (mocké).
|
||||||
|
- [ ] Écrire `tests/e2e/test_cli.py` : exécution CLI en dry-run.
|
||||||
|
- [ ] Tests sans réseau (mocks `responses`/`aioresponses`/`pytest-mock`) ; couverture ≥ 90 %.
|
||||||
|
- [ ] Ajouter un test négatif : les messages d'erreur ne fuient pas de secrets (`icalsecurise`, clés API, mots de passe).
|
||||||
|
|
||||||
|
### Critères d'acceptation
|
||||||
|
- `pytest` passe et `pytest --cov` atteint ≥ 90 % (`fail_under = 90`).
|
||||||
|
- Aucun test ne fait de requête réseau réelle.
|
||||||
|
- Le test de non-fuite de secrets passe.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## M14. Déploiement — Priorité : Moyenne
|
||||||
|
|
||||||
|
Mettre en production de façon supervisée (planification, rotation des logs, vérification des secrets).
|
||||||
|
|
||||||
|
- [ ] Créer une unité systemd (`pronote-sync.service` + timer) ou une ligne cron (exécution quotidienne).
|
||||||
|
- [ ] Créer `logrotate.d/pronote_sync` (daily, rotate 7, compress, delaycompress).
|
||||||
|
- [ ] Ajouter un script de vérification des secrets (§13.6) exécuté avant chaque déploiement.
|
||||||
|
- [ ] Documenter la supervision (logs, alertes en cas d'échec) et la maintenance (maj dépendances, dry-run avant MAJ).
|
||||||
|
- [ ] Vérifier `pip check` et tester le dry-run avant mise en production.
|
||||||
|
|
||||||
|
### Critères d'acceptation
|
||||||
|
- Le service/timer systemd (ou cron) lance le pipeline quotidiennement.
|
||||||
|
- `logrotate` est configuré et valide.
|
||||||
|
- Le dry-run passe en pré-production ; le script de secrets ne donne pas de faux négatif.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## M15. Documentation et revue finale — Priorité : Moyenne
|
||||||
|
|
||||||
|
Rédiger la documentation utilisateur et finaliser le projet.
|
||||||
|
|
||||||
|
- [ ] Créer `README.md` (installation, configuration `.env`, usage CLI, systemd/docker, limites, RGPD).
|
||||||
|
- [ ] Documenter l'architecture (pipeline, modules) en résumé.
|
||||||
|
- [ ] Ajouter `CHANGELOG` initial et la licence (MIT).
|
||||||
|
- [ ] Revue finale : cohérence avec le guide, aucun secret documenté en clair.
|
||||||
|
- [ ] (Optionnel) Configurer GitHub Actions CI/CD (pytest + bandit + ruff + mypy) d'après §Prochaines étapes.
|
||||||
|
|
||||||
|
### Critères d'acceptation
|
||||||
|
- `README.md` permet d'installer et de lancer le projet sans le guide.
|
||||||
|
- La CI exécute tests + lint + sécurité.
|
||||||
|
- Aucun secret dans la documentation.
|
||||||
0
pronote_sync/__init__.py
Normal file
0
pronote_sync/__init__.py
Normal file
0
pronote_sync/channels/__init__.py
Normal file
0
pronote_sync/channels/__init__.py
Normal file
0
pronote_sync/cli/__init__.py
Normal file
0
pronote_sync/cli/__init__.py
Normal file
0
pronote_sync/config/__init__.py
Normal file
0
pronote_sync/config/__init__.py
Normal file
0
pronote_sync/models/__init__.py
Normal file
0
pronote_sync/models/__init__.py
Normal file
0
pronote_sync/pipeline/__init__.py
Normal file
0
pronote_sync/pipeline/__init__.py
Normal file
0
pronote_sync/pipeline/steps/__init__.py
Normal file
0
pronote_sync/pipeline/steps/__init__.py
Normal file
0
pronote_sync/sources/__init__.py
Normal file
0
pronote_sync/sources/__init__.py
Normal file
0
pronote_sync/sources/blog/__init__.py
Normal file
0
pronote_sync/sources/blog/__init__.py
Normal file
0
pronote_sync/sources/pronote/__init__.py
Normal file
0
pronote_sync/sources/pronote/__init__.py
Normal file
0
pronote_sync/sources/theoretical/__init__.py
Normal file
0
pronote_sync/sources/theoretical/__init__.py
Normal file
0
pronote_sync/sync/__init__.py
Normal file
0
pronote_sync/sync/__init__.py
Normal file
0
pronote_sync/synthesis/__init__.py
Normal file
0
pronote_sync/synthesis/__init__.py
Normal file
0
pronote_sync/utils/__init__.py
Normal file
0
pronote_sync/utils/__init__.py
Normal file
114
pyproject.toml
Normal file
114
pyproject.toml
Normal file
@@ -0,0 +1,114 @@
|
|||||||
|
[build-system]
|
||||||
|
requires = ["setuptools>=61.0", "wheel"]
|
||||||
|
build-backend = "setuptools.build_meta"
|
||||||
|
|
||||||
|
[project]
|
||||||
|
name = "pronote-sync"
|
||||||
|
version = "0.1.0"
|
||||||
|
description = "Synchronisation Pronote → CalDAV + XMPP"
|
||||||
|
license = {text = "MIT"}
|
||||||
|
requires-python = ">=3.13"
|
||||||
|
authors = [
|
||||||
|
{name = "Votre Nom", email = "votre@email.com"}
|
||||||
|
]
|
||||||
|
keywords = ["pronote", "caldav", "xmpp", "sync", "school"]
|
||||||
|
classifiers = [
|
||||||
|
"Development Status :: 4 - Beta",
|
||||||
|
"Intended Audience :: End Users/Desktop",
|
||||||
|
"License :: OSI Approved :: MIT License",
|
||||||
|
"Operating System :: OS Independent",
|
||||||
|
"Programming Language :: Python :: 3.13",
|
||||||
|
"Programming Language :: Python :: 3.14",
|
||||||
|
"Topic :: Office/Business :: Scheduling",
|
||||||
|
"Topic :: Communications :: Chat",
|
||||||
|
"Topic :: Utilities",
|
||||||
|
]
|
||||||
|
dependencies = [
|
||||||
|
"requests>=2.31.0",
|
||||||
|
"icalendar>=5.0.0",
|
||||||
|
"caldav>=1.3.0",
|
||||||
|
"slixmpp>=1.8.0",
|
||||||
|
"pydantic>=2.0.0",
|
||||||
|
"pydantic-settings>=2.0.0",
|
||||||
|
"pronotepy>=2.15.0",
|
||||||
|
"openai>=1.0.0",
|
||||||
|
"httpx>=0.25.0",
|
||||||
|
"feedparser>=6.0.0",
|
||||||
|
"beautifulsoup4>=4.12.0",
|
||||||
|
]
|
||||||
|
|
||||||
|
[project.optional-dependencies]
|
||||||
|
ai-litellm = ["litellm>=1.0"]
|
||||||
|
dev = [
|
||||||
|
"pytest>=7.0.0",
|
||||||
|
"pytest-cov>=4.0.0",
|
||||||
|
"pytest-mock>=3.0.0",
|
||||||
|
"requests-mock>=1.11.0",
|
||||||
|
"aioresponses>=0.7.0",
|
||||||
|
"bandit>=1.7.0",
|
||||||
|
"safety>=2.0.0",
|
||||||
|
"pre-commit>=3.0.0",
|
||||||
|
"detect-secrets>=1.4.0",
|
||||||
|
"ruff>=0.4.0",
|
||||||
|
"mypy>=1.8.0",
|
||||||
|
]
|
||||||
|
|
||||||
|
[project.scripts]
|
||||||
|
pronote-sync = "pronote_sync.cli.main:main"
|
||||||
|
|
||||||
|
[project.urls]
|
||||||
|
Homepage = "https://github.com/votre-utilisateur/pronote-sync"
|
||||||
|
Documentation = "https://github.com/votre-utilisateur/pronote-sync#readme"
|
||||||
|
Repository = "https://github.com/votre-utilisateur/pronote-sync"
|
||||||
|
Issues = "https://github.com/votre-utilisateur/pronote-sync/issues"
|
||||||
|
|
||||||
|
[tool.setuptools.packages.find]
|
||||||
|
where = ["."]
|
||||||
|
include = ["pronote_sync*"]
|
||||||
|
|
||||||
|
[tool.pytest.ini_options]
|
||||||
|
minversion = "7.0"
|
||||||
|
testpaths = ["tests"]
|
||||||
|
python_files = ["test_*.py"]
|
||||||
|
python_functions = ["test_*"]
|
||||||
|
addopts = "-v --tb=short"
|
||||||
|
|
||||||
|
[tool.coverage.run]
|
||||||
|
source = ["pronote_sync"]
|
||||||
|
branch = true
|
||||||
|
|
||||||
|
[tool.coverage.report]
|
||||||
|
exclude_lines = [
|
||||||
|
"pragma: no cover",
|
||||||
|
"def __repr__",
|
||||||
|
"raise NotImplementedError",
|
||||||
|
"if TYPE_CHECKING:",
|
||||||
|
]
|
||||||
|
fail_under = 90
|
||||||
|
|
||||||
|
[tool.bandit]
|
||||||
|
exclude_dirs = ["tests", "venv"]
|
||||||
|
skips = ["B101"] # Ignorer les assertions (utilisées dans les tests)
|
||||||
|
|
||||||
|
[tool.ruff]
|
||||||
|
line-length = 100
|
||||||
|
target-version = "py313"
|
||||||
|
select = [
|
||||||
|
"E", # pycodestyle errors
|
||||||
|
"W", # pycodestyle warnings
|
||||||
|
"F", # Pyflakes
|
||||||
|
"I", # isort
|
||||||
|
"B", # flake8-bugbear
|
||||||
|
"C4", # flake8-comprehensions
|
||||||
|
"UP", # pyupgrade
|
||||||
|
]
|
||||||
|
ignore = [
|
||||||
|
"E501", # line too long (géré par line-length)
|
||||||
|
]
|
||||||
|
|
||||||
|
[tool.mypy]
|
||||||
|
python_version = "3.13"
|
||||||
|
warn_return_any = true
|
||||||
|
warn_unused_configs = true
|
||||||
|
disallow_untyped_defs = true
|
||||||
|
strict = true
|
||||||
0
tests/__init__.py
Normal file
0
tests/__init__.py
Normal file
0
tests/conftest.py
Normal file
0
tests/conftest.py
Normal file
0
tests/fixtures/__init__.py
vendored
Normal file
0
tests/fixtures/__init__.py
vendored
Normal file
Reference in New Issue
Block a user