Created
January 26, 2026 07:09
-
-
Save mypy-play/e53b6415cef3059bed4f396146ea16df to your computer and use it in GitHub Desktop.
Shared via mypy Playground
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| from __future__ import annotations | |
| import re | |
| import uuid | |
| from dataclasses import dataclass, field, replace | |
| from datetime import date, datetime, UTC | |
| from typing import Optional, Self, Literal | |
| def _normalize(value: str) -> str: | |
| if not isinstance(value, str): | |
| raise TypeError("La valeur doit être une chaîne.") | |
| return re.sub(r"\s+", " ", value).strip() | |
| def _validate_person_name(value: str, label: str, min_len: int, max_len: int) -> str: | |
| normalized = _normalize(value) | |
| if not normalized: | |
| raise ValueError(f"{label} ne peut pas être vide.") | |
| if len(normalized) < min_len: | |
| raise ValueError(f"{label} doit contenir au moins {min_len} caractères.") | |
| if len(normalized) > max_len: | |
| raise ValueError(f"{label} doit contenir au plus {max_len} caractères.") | |
| if any(ch.isdigit() for ch in normalized): | |
| raise ValueError(f"{label} ne doit pas contenir de chiffre.") | |
| if not re.fullmatch(r"[^\W\d_]+(?:[ '\-][^\W\d_]+)*", normalized, flags=re.UNICODE): | |
| raise ValueError(f"{label} contient des caractères non autorisés.") | |
| return normalized | |
| @dataclass(frozen=True, slots=True) | |
| class Name: | |
| value: str | |
| def __post_init__(self) -> None: | |
| object.__setattr__(self, "value", _validate_person_name(self.value, "Le nom", 2, 80)) | |
| @classmethod | |
| def from_str(cls, raw: str) -> Self: | |
| return cls(raw) | |
| def __str__(self) -> str: | |
| return self.value | |
| @dataclass(frozen=True, slots=True) | |
| class Prenom: | |
| value: str | |
| def __post_init__(self) -> None: | |
| object.__setattr__(self, "value", _validate_person_name(self.value, "Le prénom", 2, 80)) | |
| @classmethod | |
| def from_str(cls, raw: str) -> Self: | |
| return cls(raw) | |
| def __str__(self) -> str: | |
| return self.value | |
| @dataclass(frozen=True, slots=True) | |
| class UserId: | |
| value: uuid.UUID | |
| def __post_init__(self) -> None: | |
| if self.value.version not in {1, 2, 3, 4, 5}: | |
| raise ValueError("L'ID utilisateur doit être un UUID v1 à v5.") | |
| @classmethod | |
| def from_str(cls, raw: str) -> Self: | |
| try: | |
| unique_id = uuid.UUID(raw) | |
| except Exception as exc: | |
| raise ValueError("L'ID utilisateur doit être un UUID valide.") from exc | |
| return cls(unique_id) | |
| def __str__(self) -> str: | |
| return str(self.value) | |
| @dataclass(frozen=True, slots=True) | |
| class Genre: | |
| value: Optional[Literal["M", "F", "O"]] | |
| def __post_init__(self) -> None: | |
| if self.value is not None and self.value not in {"M", "F", "O"}: | |
| raise ValueError(f"Genre invalide: {self.value}") | |
| @classmethod | |
| def from_str(cls, raw: Optional[str]) -> Self: | |
| if raw is None: | |
| return cls(None) | |
| if raw not in {"M", "F", "O"}: | |
| raise ValueError(f"Genre invalide: {raw}") | |
| # Cast explicite pour convaincre le type checker | |
| typed_raw: Literal["M", "F", "O"] = raw # <- ici on rassure Ty | |
| return cls(typed_raw) | |
| def require(self) -> Literal["M", "F", "O"]: | |
| if self.value is None: | |
| raise ValueError("Genre requis mais absent.") | |
| return self.value | |
| @dataclass(frozen=True, slots=True) | |
| class DateNaissance: | |
| value: Optional[date] | |
| def __post_init__(self) -> None: | |
| if self.value is None: | |
| return | |
| today = UtcDateTime.now().value.date() | |
| if self.value > today: | |
| raise ValueError("La date de naissance ne peut pas être future.") | |
| def age(self, reference: Optional[date] = None) -> Optional[int]: | |
| if self.value is None: | |
| return None | |
| ref = reference if reference else UtcDateTime.now().value.date() | |
| years = ref.year - self.value.year | |
| before_birthday = (ref.month, ref.day) < (self.value.month, self.value.day) | |
| return years - 1 if before_birthday else years | |
| @dataclass(frozen=True, slots=True) | |
| class UtcDateTime: | |
| value: datetime | |
| def __post_init__(self) -> None: | |
| if not isinstance(self.value, datetime) or self.value.tzinfo is None: | |
| raise ValueError("Le datetime doit être timezone-aware (UTC).") | |
| if self.value.tzinfo != UTC: | |
| raise ValueError("Le datetime doit être en UTC.") | |
| @classmethod | |
| def now(cls) -> Self: | |
| return cls(datetime.now(UTC).replace(microsecond=0)) | |
| def __str__(self) -> str: | |
| return self.value.isoformat() | |
| @dataclass(frozen=True, slots=True) | |
| class ConsentementRGPD: | |
| statut: bool | |
| date_consentement: Optional[datetime] | |
| def __post_init__(self) -> None: | |
| if self.statut and self.date_consentement is None: | |
| raise ValueError("La date de consentement est requise quand le statut RGPD est vrai.") | |
| if not self.statut and self.date_consentement is not None: | |
| raise ValueError("La date de consentement doit être absente quand le statut RGPD est faux.") | |
| if self.date_consentement and self.date_consentement.tzinfo != UTC: | |
| raise ValueError("La date de consentement doit être en UTC.") | |
| @classmethod | |
| def consenti(cls, at: Optional[datetime] = None) -> Self: | |
| instant = at if at else UtcDateTime.now().value | |
| return cls(True, instant) | |
| @classmethod | |
| def refuse(cls) -> Self: | |
| return cls(False, None) | |
| def _consentement_refuse_default() -> ConsentementRGPD: | |
| return ConsentementRGPD.refuse() |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment