"""Règles communes de statut et de consolidation des cours Pronote.""" from __future__ import annotations import unicodedata from collections import defaultdict from ...models.agenda import Lesson, LessonStatus def _normalize_status(value: str) -> str: """Normalise un statut Pronote pour une comparaison tolérante. :param value: Statut brut fourni par Pronote. :return: Statut minuscule sans accents. :rtype: str """ decomposed = unicodedata.normalize("NFKD", value) without_accents = "".join(char for char in decomposed if not unicodedata.combining(char)) return " ".join(without_accents.casefold().split()) def lesson_status(raw_status: object, canceled: bool = False) -> LessonStatus: """Convertit le statut Pronote en statut métier canonique. L'indicateur d'annulation et les libellés annulé sont prioritaires sur un éventuel libellé de déplacement. Les libellés inconnus restent normaux. :param raw_status: Valeur du champ ``Lesson.status`` de pronotepy. :param canceled: Indicateur ``Lesson.canceled`` de pronotepy. :return: Statut métier canonique. :rtype: LessonStatus """ status = _normalize_status(raw_status) if isinstance(raw_status, str) else "" if canceled or "annul" in status: return LessonStatus.CANCELLED if any(token in status for token in ("deplac", "changement de salle", "modifi")): return LessonStatus.MOVED return LessonStatus.NORMAL def collapse_replaced_lessons(lessons: list[Lesson]) -> list[Lesson]: """Supprime l'ancien exemplaire annulé lorsqu'un cours déplacé est présent. Deux cours sont considérés comme les exemplaires d'une même modification lorsqu'ils partagent les dates, la matière, les enseignants et le groupe, et qu'une seule annulation et un seul déplacement se distinguent par leur salle. Une annulation isolée ou ambiguë est toujours conservée. :param lessons: Cours issus d'une source Pronote. :return: Cours consolidés dans leur ordre d'origine. :rtype: list[Lesson] """ grouped: dict[tuple[object, ...], list[tuple[int, Lesson]]] = defaultdict(list) for index, lesson in enumerate(lessons): key = ( lesson.start, lesson.end, lesson.subject.casefold(), tuple(teacher.casefold() for teacher in lesson.teachers), (lesson.group or "").casefold(), ) grouped[key].append((index, lesson)) replaced_indexes: set[int] = set() for siblings in grouped.values(): cancelled = [item for item in siblings if item[1].status == LessonStatus.CANCELLED] moved = [item for item in siblings if item[1].status == LessonStatus.MOVED] if len(cancelled) == 1 and len(moved) == 1 and cancelled[0][1].rooms != moved[0][1].rooms: replaced_indexes.add(cancelled[0][0]) return [lesson for index, lesson in enumerate(lessons) if index not in replaced_indexes]