Skip to content

Instantly share code, notes, and snippets.

@mypy-play
Created January 26, 2026 07:09
Show Gist options
  • Select an option

  • Save mypy-play/e53b6415cef3059bed4f396146ea16df to your computer and use it in GitHub Desktop.

Select an option

Save mypy-play/e53b6415cef3059bed4f396146ea16df to your computer and use it in GitHub Desktop.
Shared via mypy Playground
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