# Fichier: python_cheats/cheatsheets/pydantic.txt
# Cheatsheet Pydantic - Guide Complet


[OK] INTRODUCTION & INSTALLATION

# Pydantic est une bibliothèque de validation de données et de gestion des paramètres
# utilisant les annotations de type Python

# Installation base
pip install pydantic

# Installation avec extras
pip install pydantic[email]              # Validation email
pip install pydantic[dotenv]             # Support .env files (obsolète en V2)
pip install "pydantic[email,dotenv]"     # Plusieurs extras

# Packages additionnels V2 (séparés)
pip install pydantic-settings            # Gestion configuration (.env, etc.)
pip install pydantic-extra-types         # Types additionnels
pip install email-validator              # Pour EmailStr

# Vérifier version
pip show pydantic
python -c "import pydantic; print(pydantic.__version__)"

# Pydantic V2 (réécrit en Rust, beaucoup plus rapide)
# Ce cheatsheet couvre Pydantic V2 (2.0+)


[OK] IMPORTS COMPLETS PAR PACKAGE


# === pydantic (package principal) ===
from pydantic import (
    # Classes de base
    BaseModel,
    
    # Configuration
    ConfigDict,
    
    # Field et contraintes
    Field,
    
    # Validators
    field_validator,
    model_validator,
    field_serializer,
    model_serializer,
    
    # Computed fields
    computed_field,
    
    # Attributs privés
    PrivateAttr,
    
    # Types de base
    EmailStr,           # Nécessite: pip install email-validator
    HttpUrl,
    AnyUrl,
    FileUrl,
    FtpUrl,
    WebsocketUrl,
    
    # Nombres
    PositiveInt,
    NegativeInt,
    NonNegativeInt,
    NonPositiveInt,
    PositiveFloat,
    NegativeFloat,
    NonNegativeFloat,
    NonPositiveFloat,
    
    # Contraintes génériques
    conint,
    confloat,
    constr,
    conbytes,
    conlist,
    conset,
    confrozenset,
    condate,
    
    # Paths
    FilePath,
    DirectoryPath,
    NewPath,
    
    # Autres types
    Json,
    SecretStr,
    SecretBytes,
    PaymentCardNumber,
    ByteSize,
    PastDate,
    FutureDate,
    PastDatetime,
    FutureDatetime,
    AwareDatetime,
    NaiveDatetime,
    
    # Réseau
    IPvAnyAddress,
    IPvAnyInterface,
    IPvAnyNetwork,
    
    # Validation
    ValidationError,
    ValidationInfo,
    
    # Serialization
    SerializationInfo,
    
    # Type adapters
    TypeAdapter,
    
    # Alias
    AliasChoices,
    AliasPath,
    
    # Discriminator
    Discriminator,
    Tag,
    
    # Autres
    GetCoreSchemaHandler,
    GetJsonSchemaHandler,
    BeforeValidator,
    AfterValidator,
    PlainValidator,
    WrapValidator,
    InstanceOf,
    SkipValidation,
)

# === pydantic.dataclasses ===
from pydantic.dataclasses import dataclass

# === pydantic.types ===
from pydantic.types import (
    # Tous les types sont aussi disponibles depuis pydantic directement
    StrictBool,
    StrictInt,
    StrictFloat,
    StrictStr,
    StrictBytes,
)

# === pydantic.networks ===
from pydantic.networks import (
    AnyUrl,
    HttpUrl,
    FileUrl,
    PostgresDsn,
    CockroachDsn,
    AmqpDsn,
    RedisDsn,
    MongoDsn,
    KafkaDsn,
    validate_email,
)

# === pydantic.color ===
from pydantic.color import Color

# === pydantic_settings (package séparé!) ===
from pydantic_settings import (
    BaseSettings,
    SettingsConfigDict,
    
    # Sources de settings
    DotEnvSettingsSource,
    EnvSettingsSource,
    InitSettingsSource,
    JsonConfigSettingsSource,
    TomlConfigSettingsSource,
    YamlConfigSettingsSource,
    SecretsSettingsSource,
)

# === pydantic_extra_types (package séparé!) ===
# pip install pydantic-extra-types
from pydantic_extra_types import (
    # Couleurs avancées
    Color,
    
    # Coordonnées
    Coordinate,
    Latitude,
    Longitude,
    
    # Pays et langues
    CountryAlpha2,
    CountryAlpha3,
    CountryNumericCode,
    CountryShortName,
    LanguageAlpha2,
    LanguageName,
    
    # Devises
    Currency,
    
    # Identificateurs
    IBAN,
    BIC,
    
    # Téléphone
    PhoneNumber,
    
    # Codes postaux
    PostalCode,
    
    # MAC Address
    MACAddress,
    
    # SemVer
    SemanticVersion,
)


[OK] TYPES PYDANTIC-EXTRA-TYPES DÉTAILLÉS

# Installation
pip install pydantic-extra-types

# === Coordonnées géographiques ===
from pydantic_extra_types.coordinate import Coordinate, Latitude, Longitude

class Location(BaseModel):
    position: Coordinate
    lat: Latitude    # -90 à 90
    lon: Longitude   # -180 à 180

location = Location(
    position="48.8566,2.3522",  # Paris
    lat=48.8566,
    lon=2.3522
)


# === Pays (ISO 3166) ===
from pydantic_extra_types.country import (
    CountryAlpha2,      # FR, US, GB
    CountryAlpha3,      # FRA, USA, GBR
    CountryNumericCode, # 250, 840, 826
    CountryShortName    # France, United States, United Kingdom
)

class Address(BaseModel):
    country_code: CountryAlpha2
    country_name: CountryShortName

address = Address(country_code="FR", country_name="France")


# === Devises (ISO 4217) ===
from pydantic_extra_types.currency_code import Currency

class Price(BaseModel):
    amount: float
    currency: Currency  # EUR, USD, GBP, etc.

price = Price(amount=100.50, currency="EUR")


# === Codes bancaires ===
from pydantic_extra_types.payment import (
    IBAN,  # International Bank Account Number
    BIC    # Bank Identifier Code
)

class BankAccount(BaseModel):
    iban: IBAN
    bic: BIC

account = BankAccount(
    iban="FR1420041010050500013M02606",
    bic="BNPAFRPP"
)


# === Numéros de téléphone ===
from pydantic_extra_types.phone_numbers import PhoneNumber

class Contact(BaseModel):
    phone: PhoneNumber  # Format E.164

contact = Contact(phone="+33612345678")


# === Codes postaux ===
from pydantic_extra_types.postal_code import PostalCode

class Address(BaseModel):
    postal_code: PostalCode

address = Address(postal_code="75001")  # France


# === MAC Address ===
from pydantic_extra_types.mac_address import MACAddress

class NetworkDevice(BaseModel):
    mac: MACAddress

device = NetworkDevice(mac="00:1B:63:84:45:E6")


# === Semantic Versioning ===
from pydantic_extra_types.semantic_version import SemanticVersion

class Package(BaseModel):
    version: SemanticVersion

package = Package(version="1.2.3")
package = Package(version="2.0.0-beta.1")


# === Couleurs avancées ===
from pydantic_extra_types.color import Color

class Theme(BaseModel):
    primary: Color

theme = Theme(primary="rgb(255, 0, 0)")
theme = Theme(primary="#FF0000")
theme = Theme(primary="hsl(0, 100%, 50%)")


# === Identificateurs de script ===
from pydantic_extra_types.script_code import ISO_15924

class Document(BaseModel):
    script: ISO_15924  # Latn, Arab, Cyrl, etc.


# === Identificateurs de langue ===
from pydantic_extra_types.language_code import (
    LanguageAlpha2,  # en, fr, es
    LanguageName     # English, French, Spanish
)

class Content(BaseModel):
    language: LanguageAlpha2
    language_name: LanguageName

content = Content(language="fr", language_name="French")


[OK] MODÈLES DE BASE (BaseModel)


# === Modèle simple ===
from pydantic import BaseModel

class User(BaseModel):
    id: int
    name: str
    email: str
    is_active: bool = True  # Valeur par défaut

# Créer une instance
user = User(id=1, name="Alice", email="alice@example.com")
print(user.id)          # 1
print(user.name)        # Alice
print(user.is_active)   # True

# === Accès aux données ===
print(user.model_dump())                # Dict Python
print(user.model_dump_json())           # JSON string
print(user.model_dump_json(indent=2))   # JSON formaté


# === Validation automatique ===
from pydantic import ValidationError

try:
    user = User(id="invalid", name="Bob", email="bob@example.com")
except ValidationError as e:
    print(e)
    print(e.json())         # Erreurs en JSON
    print(e.errors())       # Liste des erreurs

# Erreur typique:
# [
#   {
#     'type': 'int_parsing',
#     'loc': ('id',),
#     'msg': 'Input should be a valid integer',
#     'input': 'invalid'
#   }
# ]


# === Conversion de type automatique ===
user = User(id="123", name="Charlie", email="charlie@example.com")
print(user.id)          # 123 (converti en int)
print(type(user.id))    # <class 'int'>


# === Valeurs par défaut ===
from datetime import datetime

class Article(BaseModel):
    title: str
    content: str
    created_at: datetime = datetime.now()  # [X] Mauvais! Toujours la même date
    
class Article(BaseModel):
    title: str
    content: str
    created_at: datetime = None
    
    def __init__(self, **data):
        if data.get('created_at') is None:
            data['created_at'] = datetime.now()
        super().__init__(**data)

# Ou avec default_factory
from pydantic import Field

class Article(BaseModel):
    title: str
    content: str
    created_at: datetime = Field(default_factory=datetime.now)


[OK] TYPES DE CHAMPS


# === Types de base ===
from typing import Optional, List, Dict, Set, Tuple
from datetime import datetime, date, time, timedelta
from decimal import Decimal
from uuid import UUID
from pathlib import Path

class CompleteModel(BaseModel):
    # Nombres
    integer: int
    floating: float
    decimal_num: Decimal
    
    # Texte
    text: str
    bytes_data: bytes
    
    # Booléen
    flag: bool
    
    # Dates et temps
    dt: datetime
    d: date
    t: time
    duration: timedelta
    
    # UUID
    unique_id: UUID
    
    # Path
    file_path: Path
    
    # Collections
    items: List[str]
    mapping: Dict[str, int]
    unique_items: Set[int]
    coordinates: Tuple[float, float]
    
    # Optional (None autorisé)
    optional_field: Optional[str] = None
    
    # Any (n'importe quel type)
    any_value: Any


# === Types Pydantic spéciaux ===
from pydantic import (
    EmailStr, HttpUrl, AnyUrl, IPvAnyAddress,
    PositiveInt, NegativeInt, PositiveFloat, NegativeFloat,
    conint, confloat, constr, conlist, conset,
    FilePath, DirectoryPath, NewPath,
    Json, SecretStr, SecretBytes,
    PaymentCardNumber, ByteSize
)

class SpecialTypes(BaseModel):
    # Email (nécessite: pip install pydantic[email])
    email: EmailStr
    
    # URLs
    website: HttpUrl
    any_url: AnyUrl
    
    # IP
    ip_address: IPvAnyAddress
    
    # Nombres contraints
    positive_num: PositiveInt
    negative_num: NegativeInt
    pos_float: PositiveFloat
    age: conint(ge=0, le=150)           # Entre 0 et 150
    percentage: confloat(ge=0, le=100)  # Entre 0.0 et 100.0
    
    # Strings contraints
    username: constr(min_length=3, max_length=20, pattern=r'^[a-zA-Z0-9_]+$')
    
    # Collections contraintes
    tags: conlist(str, min_length=1, max_length=10)
    unique_tags: conset(str, min_length=1)
    
    # Fichiers et dossiers
    config_file: FilePath       # Doit exister
    data_dir: DirectoryPath     # Doit exister
    output_file: NewPath        # Ne doit pas exister
    
    # JSON en string
    json_data: Json[Dict[str, Any]]
    
    # Secrets (masqués lors de l'affichage)
    password: SecretStr
    api_key: SecretBytes
    
    # Carte de crédit
    card_number: PaymentCardNumber
    
    # Taille de fichier
    file_size: ByteSize


# === Types personnalisés avec Field ===
from pydantic import Field

class Product(BaseModel):
    name: str = Field(..., min_length=1, max_length=100)
    price: float = Field(..., gt=0, description="Prix en euros")
    quantity: int = Field(default=0, ge=0, le=10000)
    description: str = Field(default="", max_length=500)
    tags: List[str] = Field(default_factory=list, max_length=10)
    
    # ... = obligatoire (pas de valeur par défaut)
    # gt = greater than (>)
    # ge = greater or equal (>=)
    # lt = less than (<)
    # le = less or equal (<=)


# === Alias de champs ===
class APIResponse(BaseModel):
    user_id: int = Field(..., alias="userId")
    user_name: str = Field(..., alias="userName")
    is_active: bool = Field(..., alias="isActive")

# Utilisation
data = {"userId": 1, "userName": "Alice", "isActive": True}
response = APIResponse(**data)
print(response.user_id)  # 1

# Exporter avec alias
print(response.model_dump(by_alias=True))
# {'userId': 1, 'userName': 'Alice', 'isActive': True}


# === Champs exclus/inclus ===
class User(BaseModel):
    username: str
    password: SecretStr
    email: str
    internal_id: int = Field(..., exclude=True)

user = User(username="alice", password="secret", email="a@ex.com", internal_id=123)
print(user.model_dump())
# {'username': 'alice', 'password': SecretStr('**********'), 'email': 'a@ex.com'}


[OK] VALIDATORS AVANCÉS (WRAP, BEFORE, AFTER, PLAIN)


# === AfterValidator (validation après parsing) ===
from typing import Annotated
from pydantic import AfterValidator

def check_even(v: int) -> int:
    if v % 2 != 0:
        raise ValueError('must be even')
    return v

EvenInt = Annotated[int, AfterValidator(check_even)]

class Model(BaseModel):
    value: EvenInt

model = Model(value=4)   # [OK]
# model = Model(value=3)  # [X] ValidationError


# === BeforeValidator (validation avant parsing) ===
from pydantic import BeforeValidator

def convert_to_int(v):
    if isinstance(v, str):
        return int(v.replace(',', ''))
    return v

CustomInt = Annotated[int, BeforeValidator(convert_to_int)]

class Model(BaseModel):
    value: CustomInt

model = Model(value="1,234")  # Converti en 1234


# === PlainValidator (validation complète custom) ===
from pydantic import PlainValidator

def validate_custom(v) -> int:
    if not isinstance(v, (int, str)):
        raise ValueError('must be int or str')
    if isinstance(v, str):
        return int(v)
    return v

CustomInt = Annotated[int, PlainValidator(validate_custom)]


# === WrapValidator (wrapper autour de la validation) ===
from pydantic import WrapValidator
from pydantic_core import ValidationInfo

def truncate_string(v, handler, info: ValidationInfo):
    # handler est le validateur suivant dans la chaîne
    max_length = info.context.get('max_length', 100) if info.context else 100
    if isinstance(v, str) and len(v) > max_length:
        v = v[:max_length]
    return handler(v)

TruncatedStr = Annotated[str, WrapValidator(truncate_string)]

class Model(BaseModel):
    text: TruncatedStr

model = Model.model_validate(
    {'text': 'a' * 200},
    context={'max_length': 50}
)
print(len(model.text))  # 50


# === Chaîner plusieurs validators ===
def strip_whitespace(v):
    if isinstance(v, str):
        return v.strip()
    return v

def to_uppercase(v: str) -> str:
    return v.upper()

CleanStr = Annotated[
    str,
    BeforeValidator(strip_whitespace),
    AfterValidator(to_uppercase)
]

class Model(BaseModel):
    name: CleanStr

model = Model(name="  alice  ")
print(model.name)  # "ALICE"


# === InstanceOf (validation de type strict) ===
from pydantic import InstanceOf
from pathlib import Path

class Model(BaseModel):
    path: InstanceOf[Path]  # Doit être exactement Path, pas str

model = Model(path=Path('/tmp'))  # [OK]
# model = Model(path='/tmp')       # [X] ValidationError


# === SkipValidation (pas de validation) ===
from pydantic import SkipValidation

class Model(BaseModel):
    data: SkipValidation[Any]  # Pas de validation du tout

model = Model(data="anything")
model = Model(data=123)
model = Model(data={'key': 'value'})


[OK] ALIAS & SERIALIZATION AVANCÉS


# === AliasChoices (plusieurs noms possibles) ===
from pydantic import AliasChoices

class Model(BaseModel):
    user_id: int = Field(validation_alias=AliasChoices('userId', 'user_id', 'id'))

# Accepte n'importe lequel de ces noms
model = Model(userId=1)
model = Model(user_id=1)
model = Model(id=1)


# === AliasPath (chemins imbriqués) ===
from pydantic import AliasPath

class Model(BaseModel):
    user_name: str = Field(validation_alias=AliasPath('user', 'name'))
    user_email: str = Field(validation_alias=AliasPath('user', 'contact', 'email'))

# Données imbriquées
data = {
    'user': {
        'name': 'Alice',
        'contact': {
            'email': 'alice@example.com'
        }
    }
}
model = Model(**data)


# === Serialization alias différent ===
class Model(BaseModel):
    internal_id: int = Field(
        validation_alias='id',      # Pour l'entrée
        serialization_alias='ID'    # Pour la sortie
    )

model = Model(id=123)
print(model.model_dump(by_alias=True))  # {'ID': 123}


# === Mode de sérialisation custom ===
class Model(BaseModel):
    created_at: datetime
    
    @field_serializer('created_at', when_used='json')
    def serialize_dt_json(self, dt: datetime, _info):
        return dt.isoformat()
    
    @field_serializer('created_at', when_used='python')
    def serialize_dt_python(self, dt: datetime, _info):
        return dt

model = Model(created_at=datetime.now())
print(model.model_dump())           # datetime object
print(model.model_dump(mode='json')) # ISO string


[OK] FUNCTIONAL VALIDATORS (Alternative à @decorator)


# === Utiliser des fonctions au lieu de decorators ===
from pydantic import field_validator, model_validator

def validate_username(v: str) -> str:
    if not v.isalnum():
        raise ValueError('must be alphanumeric')
    return v

def validate_passwords_match(model):
    if model.password != model.password_confirm:
        raise ValueError('passwords must match')
    return model

class User(BaseModel):
    username: str
    password: str
    password_confirm: str

# Appliquer les validators
User = type(
    'User',
    (BaseModel,),
    {
        'username': (str, ...),
        'password': (str, ...),
        'password_confirm': (str, ...),
        '__annotations__': {
            'username': str,
            'password': str,
            'password_confirm': str
        }
    }
)

# Avec field_validator
User.username = field_validator('username')(classmethod(lambda cls, v: validate_username(v)))


[OK] REPRÉSENTATION & COMPARAISON


# === __repr__ custom ===
class User(BaseModel):
    id: int
    name: str
    email: str
    
    def __repr__(self):
        return f"User(id={self.id}, name={self.name!r})"

user = User(id=1, name="Alice", email="alice@example.com")
print(repr(user))  # User(id=1, name='Alice')


# === __str__ custom ===
class User(BaseModel):
    name: str
    email: str
    
    def __str__(self):
        return f"{self.name} <{self.email}>"

user = User(name="Alice", email="alice@example.com")
print(str(user))  # Alice <alice@example.com>


# === Comparaison de modèles ===
class User(BaseModel):
    id: int
    name: str

user1 = User(id=1, name="Alice")
user2 = User(id=1, name="Alice")
user3 = User(id=2, name="Bob")

print(user1 == user2)  # True (même valeurs)
print(user1 is user2)  # False (objets différents)
print(user1 == user3)  # False


# === Hashable models ===
class User(BaseModel):
    model_config = ConfigDict(frozen=True)  # Nécessaire pour __hash__
    
    id: int
    name: str

user1 = User(id=1, name="Alice")
user2 = User(id=1, name="Alice")

# Peut être utilisé dans set/dict
users = {user1, user2}  # Un seul élément
user_dict = {user1: "some value"}


[OK] CONTEXTE DE VALIDATION


# === Passer du contexte lors de la validation ===
class User(BaseModel):
    role: str
    
    @field_validator('role')
    @classmethod
    def validate_role(cls, v: str, info: ValidationInfo) -> str:
        if info.context:
            allowed_roles = info.context.get('allowed_roles', [])
            if allowed_roles and v not in allowed_roles:
                raise ValueError(f'role must be one of {allowed_roles}')
        return v

# Sans contexte
user = User(role="admin")  # [OK]

# Avec contexte
user = User.model_validate(
    {'role': 'admin'},
    context={'allowed_roles': ['user', 'moderator']}
)  # [X] ValidationError

user = User.model_validate(
    {'role': 'user'},
    context={'allowed_roles': ['user', 'moderator']}
)  # [OK]


# === Contexte dans model_validator ===
class Order(BaseModel):
    item_count: int
    total_price: float
    
    @model_validator(mode='after')
    def validate_total(self) -> 'Order':
        info = self.model_validation_context
        if info and info.get('strict_pricing'):
            expected = self.item_count * 10
            if self.total_price != expected:
                raise ValueError(f'expected total: {expected}')
        return self


[OK] RÉUTILISATION DE SCHÉMAS


# === Modèles de base réutilisables ===
class TimestampMixin(BaseModel):
    created_at: datetime = Field(default_factory=datetime.now)
    updated_at: Optional[datetime] = None

class UserMixin(BaseModel):
    created_by: int
    updated_by: Optional[int] = None

class Article(TimestampMixin, UserMixin):
    title: str
    content: str


# === Partial models (tous les champs optionnels) ===
class User(BaseModel):
    name: str
    email: str
    age: int

# Créer version partielle
def make_partial(model_cls):
    """Rend tous les champs optionnels"""
    fields = {}
    for name, field_info in model_cls.model_fields.items():
        fields[name] = (Optional[field_info.annotation], None)
    
    return type(
        f'Partial{model_cls.__name__}',
        (BaseModel,),
        {
            '__annotations__': fields,
            '__module__': model_cls.__module__
        }
    )

PartialUser = make_partial(User)
partial = PartialUser(name="Alice")  # email et age optionnels


# === Extend model dynamiquement ===
def add_audit_fields(model_cls):
    """Ajoute des champs d'audit"""
    class AuditedModel(model_cls):
        created_at: datetime = Field(default_factory=datetime.now)
        updated_at: Optional[datetime] = None
        deleted_at: Optional[datetime] = None
    
    return AuditedModel

User = add_audit_fields(User)


[OK] CUSTOM ROOT TYPES


# === RootModel (modèle avec une seule valeur racine) ===
from pydantic import RootModel

# Liste avec validation
class UserList(RootModel[List[User]]):
    root: List[User]
    
    def __iter__(self):
        return iter(self.root)
    
    def __getitem__(self, item):
        return self.root[item]

users = UserList([
    {'id': 1, 'name': 'Alice', 'email': 'a@ex.com'},
    {'id': 2, 'name': 'Bob', 'email': 'b@ex.com'}
])

for user in users:
    print(user.name)


# === Dict avec validation ===
class UserDict(RootModel[Dict[int, User]]):
    root: Dict[int, User]
    
    def __getitem__(self, key):
        return self.root[key]
    
    def __setitem__(self, key, value):
        self.root[key] = value

users = UserDict({
    1: {'id': 1, 'name': 'Alice', 'email': 'a@ex.com'},
    2: {'id': 2, 'name': 'Bob', 'email': 'b@ex.com'}
})

print(users[1].name)  # Alice


# === Primitive avec validation ===
class PositiveInt(RootModel[int]):
    root: int
    
    @model_validator(mode='after')
    def check_positive(self) -> 'PositiveInt':
        if self.root <= 0:
            raise ValueError('must be positive')
        return self

num = PositiveInt(5)
print(num.root)  # 5


[OK] STRICT MODE & TYPE COERCION


# === Mode strict global ===
class StrictModel(BaseModel):
    model_config = ConfigDict(strict=True)
    
    id: int
    name: str
    flag: bool

# StrictModel(id="123", name="Alice", flag="true")  # [X] ValidationError
model = StrictModel(id=123, name="Alice", flag=True)  # [OK]


# === Mode strict par champ ===
from pydantic import StrictInt, StrictStr, StrictBool

class MixedModel(BaseModel):
    strict_id: StrictInt        # Strict
    flexible_age: int           # Coercion autorisée
    strict_flag: StrictBool

model = MixedModel(
    strict_id=123,              # [OK]
    flexible_age="30",          # [OK] Converti
    strict_flag=True            # [OK]
)

# MixedModel(strict_id="123", flexible_age=30, strict_flag=True)  # [X]


# === Custom coercion ===
class User(BaseModel):
    name: str
    age: int
    
    @field_validator('age', mode='before')
    @classmethod
    def coerce_age(cls, v):
        if isinstance(v, str):
            # Enlever les caractères non-numériques
            v = ''.join(c for c in v if c.isdigit())
            return int(v) if v else 0
        return v

user = User(name="Alice", age="30 years")  # age devient 30


[OK] IMMUTABILITY & FROZEN


# === Modèle immutable ===
class ImmutableUser(BaseModel):
    model_config = ConfigDict(frozen=True)
    
    id: int
    name: str

user = ImmutableUser(id=1, name="Alice")
# user.name = "Bob"  # [X] ValidationError: Instance is frozen


# === Frozen avec collections ===
class Config(BaseModel):
    model_config = ConfigDict(frozen=True)
    
    settings: Dict[str, Any]
    tags: List[str]

config = Config(settings={'debug': True}, tags=['dev'])
# config.settings['debug'] = False  # [X] Frozen
# Mais attention: les collections internes ne sont pas frozen!

# Pour vraiment frozen:
from pydantic import Field

class TrulyFrozenConfig(BaseModel):
    model_config = ConfigDict(frozen=True)
    
    settings: Tuple[Tuple[str, Any], ...]  # Tuple immutable
    tags: Tuple[str, ...]

config = TrulyFrozenConfig(
    settings=(('debug', True), ('port', 8000)),
    tags=('dev', 'test')
)


# === Validators (V2 - field_validator) ===
from pydantic import field_validator, ValidationInfo

class User(BaseModel):
    username: str
    email: str
    age: int
    
    @field_validator('username')
    @classmethod
    def username_alphanumeric(cls, v: str) -> str:
        if not v.isalnum():
            raise ValueError('Username must be alphanumeric')
        return v
    
    @field_validator('email')
    @classmethod
    def email_must_contain_at(cls, v: str) -> str:
        if '@' not in v:
            raise ValueError('Email must contain @')
        return v.lower()  # Normalisation
    
    @field_validator('age')
    @classmethod
    def age_must_be_positive(cls, v: int) -> int:
        if v < 0:
            raise ValueError('Age must be positive')
        return v


# === Validator avec plusieurs champs ===
class User(BaseModel):
    username: str
    email: str
    
    @field_validator('username', 'email')
    @classmethod
    def check_not_empty(cls, v: str) -> str:
        if not v or not v.strip():
            raise ValueError('Field cannot be empty')
        return v.strip()


# === Model validator (validation croisée) ===
from pydantic import model_validator

class DateRange(BaseModel):
    start_date: date
    end_date: date
    
    @model_validator(mode='after')
    def check_dates(self) -> 'DateRange':
        if self.end_date < self.start_date:
            raise ValueError('end_date must be after start_date')
        return self


class PasswordChange(BaseModel):
    password: str
    password_confirm: str
    
    @model_validator(mode='after')
    def passwords_match(self) -> 'PasswordChange':
        if self.password != self.password_confirm:
            raise ValueError('Passwords do not match')
        return self


# === Validator en mode 'before' (avant parsing) ===
class User(BaseModel):
    age: int
    
    @field_validator('age', mode='before')
    @classmethod
    def convert_age(cls, v):
        if isinstance(v, str):
            return int(v)
        return v


# === Validator avec contexte ===
class User(BaseModel):
    role: str
    
    @field_validator('role')
    @classmethod
    def validate_role(cls, v: str, info: ValidationInfo) -> str:
        allowed_roles = info.context.get('allowed_roles', [])
        if v not in allowed_roles:
            raise ValueError(f'Role must be one of {allowed_roles}')
        return v

# Utilisation avec contexte
user = User.model_validate(
    {'role': 'admin'},
    context={'allowed_roles': ['admin', 'user', 'guest']}
)


# === Validator qui retourne plusieurs erreurs ===
from pydantic import ValidationError

class StrictUser(BaseModel):
    username: str
    email: str
    age: int
    
    @field_validator('username')
    @classmethod
    def validate_username(cls, v: str) -> str:
        errors = []
        if len(v) < 3:
            errors.append('Username must be at least 3 characters')
        if not v.isalnum():
            errors.append('Username must be alphanumeric')
        if errors:
            raise ValueError('; '.join(errors))
        return v


[OK] MODÈLES IMBRIQUÉS


# === Modèles simples imbriqués ===
class Address(BaseModel):
    street: str
    city: str
    country: str
    zip_code: str

class Person(BaseModel):
    name: str
    age: int
    address: Address

# Utilisation
person = Person(
    name="Alice",
    age=30,
    address={
        "street": "123 Main St",
        "city": "Paris",
        "country": "France",
        "zip_code": "75001"
    }
)

# Ou avec instance
address = Address(street="123 Main St", city="Paris", country="France", zip_code="75001")
person = Person(name="Alice", age=30, address=address)


# === Listes de modèles ===
class Order(BaseModel):
    id: int
    items: List[str]
    total: float

class Customer(BaseModel):
    name: str
    orders: List[Order]

customer = Customer(
    name="Bob",
    orders=[
        {"id": 1, "items": ["book"], "total": 15.99},
        {"id": 2, "items": ["pen", "notebook"], "total": 8.50}
    ]
)


# === Modèles optionnels ===
class Company(BaseModel):
    name: str
    address: Optional[Address] = None

company1 = Company(name="ACME Corp")
company2 = Company(
    name="ACME Corp",
    address={"street": "456 Oak Ave", "city": "Lyon", "country": "France", "zip_code": "69001"}
)


# === Références circulaires ===
from typing import ForwardRef

class TreeNode(BaseModel):
    value: int
    left: Optional['TreeNode'] = None
    right: Optional['TreeNode'] = None

# Nécessaire pour résoudre les références
TreeNode.model_rebuild()

# Utilisation
tree = TreeNode(
    value=1,
    left=TreeNode(value=2),
    right=TreeNode(value=3, left=TreeNode(value=4))
)


[OK] HÉRITAGE


# === Héritage simple ===
class BaseUser(BaseModel):
    username: str
    email: str
    created_at: datetime = Field(default_factory=datetime.now)

class Admin(BaseUser):
    role: str = "admin"
    permissions: List[str] = Field(default_factory=list)

class RegularUser(BaseUser):
    role: str = "user"
    subscription_level: str = "free"


# === Héritage avec override ===
class Animal(BaseModel):
    name: str
    age: int

class Dog(Animal):
    breed: str
    age: int = Field(..., ge=0, le=20)  # Override avec contraintes


# === Mixins ===
class TimestampMixin(BaseModel):
    created_at: datetime = Field(default_factory=datetime.now)
    updated_at: Optional[datetime] = None

class SoftDeleteMixin(BaseModel):
    is_deleted: bool = False
    deleted_at: Optional[datetime] = None

class Article(TimestampMixin, SoftDeleteMixin):
    title: str
    content: str
    author_id: int


[OK] CONFIGURATION (model_config)


# === Configuration du modèle ===
from pydantic import ConfigDict

class User(BaseModel):
    model_config = ConfigDict(
        # Validation stricte des types
        strict=True,
        
        # Valider lors des assignments
        validate_assignment=True,
        
        # Valider les valeurs par défaut
        validate_default=True,
        
        # Autoriser les champs extra
        extra='forbid',  # 'allow', 'ignore', 'forbid'
        
        # Populate by name (utiliser nom ou alias)
        populate_by_name=True,
        
        # Conversion des strings en types
        str_strip_whitespace=True,
        str_to_lower=False,
        str_to_upper=False,
        
        # JSON schema
        json_schema_extra={
            "examples": [
                {
                    "name": "Alice",
                    "email": "alice@example.com"
                }
            ]
        },
        
        # Autres options
        frozen=False,  # Rend le modèle immutable si True
        use_enum_values=True,  # Utiliser valeurs enum au lieu de membres
        arbitrary_types_allowed=False,  # Autoriser types arbitraires
    )
    
    name: str
    email: str


# === Validation à l'assignment ===
class User(BaseModel):
    model_config = ConfigDict(validate_assignment=True)
    
    name: str
    age: int

user = User(name="Alice", age=30)
user.age = 31      # [OK] Validé
# user.age = "32"  # [X] ValidationError


# === Frozen (immutable) ===
class ImmutableUser(BaseModel):
    model_config = ConfigDict(frozen=True)
    
    id: int
    name: str

user = ImmutableUser(id=1, name="Alice")
# user.name = "Bob"  # [X] ValidationError: Instance is frozen


# === Extra fields ===
class StrictModel(BaseModel):
    model_config = ConfigDict(extra='forbid')
    name: str

# StrictModel(name="Alice", age=30)  # [X] ValidationError

class FlexibleModel(BaseModel):
    model_config = ConfigDict(extra='allow')
    name: str

flexible = FlexibleModel(name="Alice", age=30, city="Paris")
print(flexible.age)    # 30
print(flexible.city)   # Paris


# === Alias generator ===
def to_camel(string: str) -> str:
    words = string.split('_')
    return words[0] + ''.join(word.capitalize() for word in words[1:])

class CamelModel(BaseModel):
    model_config = ConfigDict(
        alias_generator=to_camel,
        populate_by_name=True
    )
    
    user_id: int
    user_name: str
    is_active: bool

data = {"userId": 1, "userName": "Alice", "isActive": True}
model = CamelModel(**data)
print(model.model_dump(by_alias=True))
# {'userId': 1, 'userName': 'Alice', 'isActive': True}


[OK] SÉRIALISATION & DÉSÉRIALISATION


# === Conversion en dict ===
class User(BaseModel):
    id: int
    name: str
    email: str
    password: SecretStr

user = User(id=1, name="Alice", email="a@ex.com", password="secret123")

# Dict complet
print(user.model_dump())

# Exclure des champs
print(user.model_dump(exclude={'password'}))
print(user.model_dump(exclude={'id', 'password'}))

# Inclure seulement certains champs
print(user.model_dump(include={'name', 'email'}))

# Exclure None
print(user.model_dump(exclude_none=True))

# Exclure valeurs par défaut
print(user.model_dump(exclude_defaults=True))

# Exclure non-set
print(user.model_dump(exclude_unset=True))


# === Conversion en JSON ===
json_str = user.model_dump_json()
json_pretty = user.model_dump_json(indent=2)
json_no_password = user.model_dump_json(exclude={'password'})


# === Désérialisation depuis dict ===
data = {"id": 1, "name": "Bob", "email": "bob@example.com"}
user = User(**data)
# Ou
user = User.model_validate(data)


# === Désérialisation depuis JSON ===
json_str = '{"id": 1, "name": "Charlie", "email": "charlie@example.com", "password": "pass"}'
user = User.model_validate_json(json_str)


# === Mode de sérialisation ===
class Article(BaseModel):
    title: str
    content: str
    created_at: datetime

article = Article(title="Test", content="Content", created_at=datetime.now())

# Mode Python (objets Python natifs)
print(article.model_dump(mode='python'))

# Mode JSON (types compatibles JSON)
print(article.model_dump(mode='json'))


# === Serializers personnalisés ===
from pydantic import field_serializer

class User(BaseModel):
    name: str
    email: str
    created_at: datetime
    
    @field_serializer('email')
    def serialize_email(self, email: str, _info) -> str:
        # Masquer partie de l'email
        local, domain = email.split('@')
        return f"{local[0]}***@{domain}"
    
    @field_serializer('created_at')
    def serialize_date(self, dt: datetime, _info) -> str:
        return dt.strftime('%Y-%m-%d %H:%M:%S')


# === Model serializer ===
from pydantic import model_serializer

class User(BaseModel):
    first_name: str
    last_name: str
    age: int
    
    @model_serializer
    def serialize_model(self) -> Dict[str, Any]:
        return {
            'fullName': f"{self.first_name} {self.last_name}",
            'age': self.age,
            'isAdult': self.age >= 18
        }


[OK] PARSING & CONVERSION


# === Parse depuis différents formats ===
from pathlib import Path

class Config(BaseModel):
    debug: bool
    host: str
    port: int

# Depuis dict
config = Config.model_validate({"debug": True, "host": "localhost", "port": 8000})

# Depuis JSON string
config = Config.model_validate_json('{"debug": true, "host": "localhost", "port": 8000}')

# Depuis fichier JSON
json_path = Path("config.json")
config = Config.model_validate_json(json_path.read_text())

# Depuis objet avec __dict__
class OldConfig:
    def __init__(self):
        self.debug = True
        self.host = "localhost"
        self.port = 8000

old = OldConfig()
config = Config.model_validate(old.__dict__)


# === Parse avec validation conditionnelle ===
config = Config.model_validate(
    {"debug": "yes", "host": "localhost", "port": "8000"},
    strict=False  # Active la conversion de type
)


[OK] GÉNÉRIQUES (Generic Models)


# === Modèles génériques ===
from typing import Generic, TypeVar

T = TypeVar('T')

class Response(BaseModel, Generic[T]):
    status: int
    message: str
    data: T

class User(BaseModel):
    id: int
    name: str

class Product(BaseModel):
    id: int
    title: str
    price: float

# Utilisation
user_response = Response[User](
    status=200,
    message="Success",
    data={"id": 1, "name": "Alice"}
)

product_response = Response[Product](
    status=200,
    message="Success",
    data={"id": 1, "title": "Book", "price": 15.99}
)

# Liste générique
list_response = Response[List[User]](
    status=200,
    message="Success",
    data=[
        {"id": 1, "name": "Alice"},
        {"id": 2, "name": "Bob"}
    ]
)


# === Pagination générique ===
class PaginatedResponse(BaseModel, Generic[T]):
    items: List[T]
    total: int
    page: int
    page_size: int
    total_pages: int

users_page = PaginatedResponse[User](
    items=[{"id": 1, "name": "Alice"}],
    total=100,
    page=1,
    page_size=10,
    total_pages=10
)


[OK] UNIONS & DISCRIMINATED UNIONS


# === Union simple ===
from typing import Union

class Cat(BaseModel):
    pet_type: str = "cat"
    meow: str

class Dog(BaseModel):
    pet_type: str = "dog"
    bark: str

class Pet(BaseModel):
    animal: Union[Cat, Dog]

# Pydantic essaie chaque type jusqu'à trouver le bon
pet = Pet(animal={"pet_type": "cat", "meow": "Meow!"})


# === Discriminated Unions (plus efficace) ===
from typing import Literal
from pydantic import Field, Discriminator

class Cat(BaseModel):
    pet_type: Literal["cat"]
    meow: str

class Dog(BaseModel):
    pet_type: Literal["dog"]
    bark: str

class Pet(BaseModel):
    animal: Union[Cat, Dog] = Field(..., discriminator='pet_type')

# Pydantic utilise pet_type pour choisir le bon type directement
pet = Pet(animal={"pet_type": "cat", "meow": "Meow!"})


# === Union avec Tag explicite ===
from pydantic import Tag

class Response(BaseModel):
    result: Union[
        Annotated[User, Tag("user")],
        Annotated[Product, Tag("product")],
        Annotated[str, Tag("error")]
    ] = Field(..., discriminator='type')


[OK] DATACLASSES PYDANTIC


# === Pydantic dataclass ===
from pydantic.dataclasses import dataclass
from pydantic import Field

@dataclass
class User:
    id: int
    name: str
    email: str
    age: int = Field(default=0, ge=0, le=150)

user = User(id=1, name="Alice", email="alice@example.com", age=30)

# Validation automatique comme BaseModel
# user = User(id="invalid", name="Bob", email="bob@ex.com")  # ValidationError


# === Config dans dataclass ===
@dataclass(config=ConfigDict(validate_assignment=True))
class StrictUser:
    name: str
    age: int

user = StrictUser(name="Alice", age=30)
user.age = 31  # Validé
# user.age = "32"  # ValidationError


# === Conversion standard dataclass -> Pydantic ===
from dataclasses import dataclass as std_dataclass
from pydantic import TypeAdapter

@std_dataclass
class Point:
    x: float
    y: float

# Créer un adaptateur pour validation
PointAdapter = TypeAdapter(Point)
point = PointAdapter.validate_python({"x": 1.5, "y": 2.5})


[OK] TYPES PERSONNALISÉS


# === Custom type avec validation ===
from pydantic import GetCoreSchemaHandler, GetJsonSchemaHandler
from pydantic_core import core_schema
from typing import Any

class PositiveInt:
    def __init__(self, value: int):
        if value <= 0:
            raise ValueError('Value must be positive')
        self.value = value
    
    def __repr__(self):
        return f'PositiveInt({self.value})'
    
    @classmethod
    def __get_pydantic_core_schema__(
        cls, source_type: Any, handler: GetCoreSchemaHandler
    ) -> core_schema.CoreSchema:
        return core_schema.no_info_after_validator_function(
            cls._validate,
            core_schema.int_schema(),
        )
    
    @classmethod
    def _validate(cls, value: int) -> 'PositiveInt':
        return cls(value)

class Product(BaseModel):
    name: str
    quantity: PositiveInt


# === Type avec coercion ===
class Email:
    def __init__(self, email: str):
        if '@' not in email:
            raise ValueError('Invalid email')
        self.email = email.lower()
    
    def __str__(self):
        return self.email
    
    @classmethod
    def __get_pydantic_core_schema__(cls, source_type: Any, handler: GetCoreSchemaHandler):
        return core_schema.no_info_after_validator_function(
            cls._validate,
            core_schema.str_schema(),
        )
    
    @classmethod
    def _validate(cls, value: str) -> 'Email':
        return cls(value)


[OK] SETTINGS MANAGEMENT


# [ATTENTION] IMPORTANT: BaseSettings est dans un package séparé en V2!
# Installation requise:
pip install pydantic-settings

# === Configuration avec BaseSettings ===
from pydantic_settings import BaseSettings, SettingsConfigDict
from pydantic import Field

class Settings(BaseSettings):
    model_config = SettingsConfigDict(
        env_file='.env',
        env_file_encoding='utf-8',
        case_sensitive=False,
        extra='ignore'
    )
    
    # Variables d'environnement
    app_name: str = "MyApp"
    debug: bool = False
    database_url: str
    secret_key: str
    
    # Avec préfixe
    api_key: str = Field(..., validation_alias='MY_APP_API_KEY')
    
    # Types complexes
    allowed_hosts: List[str] = Field(default_factory=list)
    max_connections: int = 100

# Charger depuis variables d'environnement
settings = Settings()

# Charger depuis fichier .env
settings = Settings(_env_file='.env')


# === POURQUOI PACKAGE SÉPARÉ? ===
# Pydantic V2 a séparé les fonctionnalités:
# - pydantic: Core validation (léger, rapide)
# - pydantic-settings: Gestion configuration (dépendance optionnelle)
# - pydantic-extra-types: Types supplémentaires

# Avantage: Vous n'installez que ce dont vous avez besoin!


# === Settings imbriqués ===
class DatabaseSettings(BaseSettings):
    host: str = "localhost"
    port: int = 5432
    username: str
    password: str
    database: str

class RedisSettings(BaseSettings):
    host: str = "localhost"
    port: int = 6379
    db: int = 0

class AppSettings(BaseSettings):
    model_config = SettingsConfigDict(env_nested_delimiter='__')
    
    app_name: str
    database: DatabaseSettings
    redis: RedisSettings

# Variables d'environnement:
# DATABASE__HOST=localhost
# DATABASE__PORT=5432
# DATABASE__USERNAME=admin
# REDIS__HOST=localhost


# === Settings avec sources multiples ===
class Settings(BaseSettings):
    model_config = SettingsConfigDict(
        env_file=['.env', '.env.local'],  # Plusieurs fichiers
        env_prefix='MYAPP_',  # Préfixe pour toutes les variables
    )
    
    debug: bool
    api_key: str


[OK] JSON & PARSING AVANCÉ


# === Parse depuis fichiers ===
from pathlib import Path
import json

class Config(BaseModel):
    debug: bool
    port: int

# Depuis JSON file
config = Config.model_validate_json(Path('config.json').read_text())

# Depuis dict dans JSON
with open('config.json') as f:
    data = json.load(f)
config = Config.model_validate(data)


# === Parse depuis YAML ===
import yaml

yaml_content = """
name: MyApp
debug: true
port: 8000
"""

data = yaml.safe_load(yaml_content)
config = Config.model_validate(data)


# === Parse depuis TOML ===
import tomli  # pip install tomli

toml_content = """
name = "MyApp"
debug = true
port = 8000
"""

data = tomli.loads(toml_content)
config = Config.model_validate(data)


# === Parse depuis XML ===
import xml.etree.ElementTree as ET

xml_content = """
<user>
    <id>1</id>
    <name>Alice</name>
    <email>alice@example.com</email>
</user>
"""

root = ET.fromstring(xml_content)
data = {child.tag: child.text for child in root}
user = User.model_validate(data)


# === Parse depuis CSV ===
import csv
from io import StringIO

csv_content = """id,name,email
1,Alice,alice@example.com
2,Bob,bob@example.com
"""

reader = csv.DictReader(StringIO(csv_content))
users = [User.model_validate(row) for row in reader]


# === Parse depuis query string ===
from urllib.parse import parse_qs

query_string = "name=Alice&email=alice@example.com&age=30"
data = {k: v[0] for k, v in parse_qs(query_string).items()}
user = User.model_validate(data)


# === Parse depuis form data ===
from werkzeug.datastructures import MultiDict

form_data = MultiDict([
    ('name', 'Alice'),
    ('email', 'alice@example.com'),
    ('tags', 'python'),
    ('tags', 'pydantic')
])

data = {
    'name': form_data.get('name'),
    'email': form_data.get('email'),
    'tags': form_data.getlist('tags')
}
user = User.model_validate(data)


[OK] MÉTHODES DE CLASSE UTILES


# === model_rebuild ===
# Nécessaire après modification de références forward
class TreeNode(BaseModel):
    value: int
    children: List['TreeNode'] = []

TreeNode.model_rebuild()  # Résout la référence forward


# === model_parametrized_name ===
from typing import Generic, TypeVar

T = TypeVar('T')

class Response(BaseModel, Generic[T]):
    data: T
    
    @classmethod
    def model_parametrized_name(cls, params: tuple) -> str:
        return f'Response[{params[0].__name__}]'

print(Response[User].model_parametrized_name((User,)))  # Response[User]


# === model_json_schema avec ref_template ===
class User(BaseModel):
    name: str

schema = User.model_json_schema(
    ref_template='#/components/schemas/{model}'
)


# === model_validate avec strict ===
# Override strict mode temporairement
class Model(BaseModel):
    value: int

# Avec coercion
model = Model.model_validate({'value': '123'}, strict=False)

# Sans coercion
# model = Model.model_validate({'value': '123'}, strict=True)  # [X]


[OK] CHAMPS DYNAMIQUES


# === Ajouter des champs dynamiquement ===
class DynamicModel(BaseModel):
    model_config = ConfigDict(extra='allow')
    
    name: str

model = DynamicModel(name="Alice", age=30, city="Paris")
print(model.age)   # 30
print(model.city)  # Paris

# Accès aux extras
print(model.__pydantic_extra__)  # {'age': 30, 'city': 'Paris'}


# === __getattr__ custom ===
class SmartModel(BaseModel):
    data: Dict[str, Any]
    
    def __getattr__(self, name: str):
        if name in self.data:
            return self.data[name]
        raise AttributeError(f'{name} not found')

model = SmartModel(data={'x': 1, 'y': 2})
print(model.x)  # 1
print(model.y)  # 2


# === __setattr__ avec validation ===
class ValidatedModel(BaseModel):
    model_config = ConfigDict(validate_assignment=True, extra='allow')
    
    name: str

model = ValidatedModel(name="Alice")
model.age = 30        # OK, extra field
model.name = "Bob"    # Validé
# model.name = 123    # [X] ValidationError


[OK] EXPORT & CONVERSION


# === Export vers différents formats ===
class User(BaseModel):
    id: int
    name: str
    email: str
    created_at: datetime

user = User(id=1, name="Alice", email="a@ex.com", created_at=datetime.now())

# Dict
user_dict = user.model_dump()

# JSON string
user_json = user.model_dump_json()

# Dict pour JSON (avec conversions)
json_dict = user.model_dump(mode='json')

# Exclu certains champs
public_dict = user.model_dump(exclude={'id', 'created_at'})

# Uniquement certains champs
minimal_dict = user.model_dump(include={'name', 'email'})

# Exclure None
clean_dict = user.model_dump(exclude_none=True)

# Exclure defaults
user_dict = user.model_dump(exclude_defaults=True)

# Exclure unset (pas fourni à l'init)
user_dict = user.model_dump(exclude_unset=True)

# Par alias
api_dict = user.model_dump(by_alias=True)

# Mode custom
user_dict = user.model_dump(mode='python')  # ou 'json'


# === Export avec serialization_context ===
class User(BaseModel):
    name: str
    email: str
    
    @model_serializer
    def serialize_model(self) -> Dict[str, Any]:
        context = self.model_serialization_context
        if context and context.get('hide_email'):
            return {'name': self.name}
        return {'name': self.name, 'email': self.email}

user = User(name="Alice", email="alice@example.com")
print(user.model_dump())  # {'name': 'Alice', 'email': 'alice@example.com'}
print(user.model_dump(context={'hide_email': True}))  # {'name': 'Alice'}


# === Conversion en autres formats ===
# Vers DataFrame (pandas)
import pandas as pd

users = [
    User(id=1, name="Alice", email="a@ex.com", created_at=datetime.now()),
    User(id=2, name="Bob", email="b@ex.com", created_at=datetime.now())
]

df = pd.DataFrame([user.model_dump() for user in users])


# Vers CSV
import csv

with open('users.csv', 'w', newline='') as f:
    if users:
        writer = csv.DictWriter(f, fieldnames=users[0].model_dump().keys())
        writer.writeheader()
        for user in users:
            writer.writerow(user.model_dump())


# Vers Protobuf (exemple conceptuel)
class UserProto(BaseModel):
    id: int
    name: str
    
    def to_protobuf(self):
        # Conversion vers message protobuf
        # proto_user = user_pb2.User()
        # proto_user.id = self.id
        # proto_user.name = self.name
        # return proto_user
        pass


[OK] INTEGRATION AVEC ASYNCIO


# === Async validators ===
import asyncio

class User(BaseModel):
    username: str
    email: str
    
    # Note: Pydantic V2 ne supporte pas async validators directement
    # Utiliser validation manuelle si nécessaire
    
    @classmethod
    async def create_async(cls, data: dict) -> 'User':
        # Validation async custom
        await asyncio.sleep(0.1)  # Simuler DB check
        return cls.model_validate(data)

# Utilisation
async def main():
    user = await User.create_async({
        'username': 'alice',
        'email': 'alice@example.com'
    })
    print(user)

# asyncio.run(main())


# === Async serialization ===
class User(BaseModel):
    id: int
    name: str
    
    async def to_dict_async(self) -> dict:
        # Enrichir avec données async
        await asyncio.sleep(0.1)  # Simuler API call
        data = self.model_dump()
        data['extra_info'] = 'from async source'
        return data


[OK] PLUGINS & EXTENSIONS


# === Pydantic-Factories (test data) ===
# pip install pydantic-factories

from pydantic_factories import ModelFactory

class UserFactory(ModelFactory):
    __model__ = User

# Générer données de test
user = UserFactory.build()
users = UserFactory.batch(10)


# === Pydantic-SQLAlchemy ===
# pip install pydantic-sqlalchemy

from pydantic_sqlalchemy import sqlalchemy_to_pydantic
from sqlalchemy import Column, Integer, String
from sqlalchemy.orm import declarative_base

Base = declarative_base()

class UserDB(Base):
    __tablename__ = 'users'
    id = Column(Integer, primary_key=True)
    name = Column(String)

# Générer schema Pydantic depuis SQLAlchemy
UserSchema = sqlalchemy_to_pydantic(UserDB)


# === Pydantic-Mongo ===
# pip install pydantic-mongo

from pydantic_mongo import AbstractRepository, ObjectIdField

class User(BaseModel):
    id: ObjectIdField = None
    name: str
    email: str

class UserRepository(AbstractRepository[User]):
    class Meta:
        collection_name = 'users'


[OK] DEBUGGING & INTROSPECTION


# === Inspecter le modèle ===
class User(BaseModel):
    name: str
    email: EmailStr
    age: int = 0

# Tous les champs
print(User.model_fields)
# {'name': FieldInfo(...), 'email': FieldInfo(...), 'age': FieldInfo(default=0)}

# Champs requis
required = [name for name, field in User.model_fields.items() if field.is_required()]
print(required)  # ['name', 'email']

# Annotations
print(User.__annotations__)
# {'name': <class 'str'>, 'email': EmailStr, 'age': <class 'int'>}

# JSON Schema
print(User.model_json_schema())

# Config
print(User.model_config)


# === Valider sans lever d'exception ===
from pydantic import ValidationError

def safe_validate(model_cls, data):
    try:
        return model_cls.model_validate(data), None
    except ValidationError as e:
        return None, e.errors()

user, errors = safe_validate(User, {'name': 'Alice'})
if errors:
    print("Validation errors:", errors)
else:
    print("User created:", user)


# === Logger les validations ===
import logging

logging.basicConfig(level=logging.DEBUG)

class User(BaseModel):
    name: str
    
    @field_validator('name')
    @classmethod
    def validate_name(cls, v: str) -> str:
        logging.debug(f"Validating name: {v}")
        return v

user = User(name="Alice")


# === Profiling de validation ===
import time

def time_validation(model_cls, data, iterations=1000):
    start = time.time()
    for _ in range(iterations):
        model_cls.model_validate(data)
    end = time.time()
    print(f"{iterations} validations in {end-start:.4f}s")
    print(f"Average: {(end-start)/iterations*1000:.4f}ms")

time_validation(User, {'name': 'Alice', 'email': 'a@ex.com', 'age': 30})


[OK] SÉCURITÉ


# === Secrets (masquer données sensibles) ===
from pydantic import SecretStr, SecretBytes

class Credentials(BaseModel):
    username: str
    password: SecretStr
    api_key: SecretBytes

creds = Credentials(
    username="admin",
    password="super_secret",
    api_key=b"secret_key_123"
)

print(creds)
# username='admin' password=SecretStr('**********') api_key=SecretBytes(b'**********')

print(creds.model_dump())
# {'username': 'admin', 'password': SecretStr('**********'), 'api_key': SecretBytes(b'**********')}

# Accès à la valeur réelle
print(creds.password.get_secret_value())  # "super_secret"


# === Validation de mots de passe ===
import re

class PasswordStr(str):
    @classmethod
    def __get_pydantic_core_schema__(cls, source_type, handler):
        from pydantic_core import core_schema
        return core_schema.no_info_after_validator_function(
            cls.validate,
            core_schema.str_schema()
        )
    
    @classmethod
    def validate(cls, v: str) -> str:
        if len(v) < 8:
            raise ValueError('Password must be at least 8 characters')
        if not re.search(r'[A-Z]', v):
            raise ValueError('Password must contain uppercase')
        if not re.search(r'[a-z]', v):
            raise ValueError('Password must contain lowercase')
        if not re.search(r'\d', v):
            raise ValueError('Password must contain digit')
        return v

class User(BaseModel):
    username: str
    password: PasswordStr


# === Sanitization (nettoyer input) ===
import html

class SafeStr(str):
    @classmethod
    def __get_pydantic_core_schema__(cls, source_type, handler):
        from pydantic_core import core_schema
        return core_schema.no_info_after_validator_function(
            cls.sanitize,
            core_schema.str_schema()
        )
    
    @classmethod
    def sanitize(cls, v: str) -> str:
        # Échapper HTML
        v = html.escape(v)
        # Enlever scripts
        v = re.sub(r'<script[^>]*>.*?</script>', '', v, flags=re.DOTALL)
        return v

class Comment(BaseModel):
    content: SafeStr


# === Rate limiting dans validation ===
from functools import wraps
from time import time
from collections import defaultdict

def rate_limit(max_calls: int, period: int):
    calls = defaultdict(list)
    
    def decorator(func):
        @wraps(func)
        def wrapper(cls, v, info):
            now = time()
            key = info.context.get('user_id') if info.context else 'anonymous'
            
            # Nettoyer vieux appels
            calls[key] = [t for t in calls[key] if now - t < period]
            
            if len(calls[key]) >= max_calls:
                raise ValueError(f'Rate limit exceeded: {max_calls}/{period}s')
            
            calls[key].append(now)
            return func(cls, v, info)
        return wrapper
    return decorator

class User(BaseModel):
    email: str
    
    @field_validator('email')
    @classmethod
    @rate_limit(max_calls=5, period=60)
    def validate_email(cls, v: str, info: ValidationInfo) -> str:
        # Validation coûteuse
        return v.lower()


# === Générer JSON Schema ===
class User(BaseModel):
    id: int
    name: str = Field(..., description="User's full name")
    email: EmailStr
    age: int = Field(..., ge=0, le=150, description="User's age")

# Obtenir le schéma
schema = User.model_json_schema()
print(json.dumps(schema, indent=2))

# Schéma résultant:
# {
#   "type": "object",
#   "properties": {
#     "id": {"type": "integer"},
#     "name": {
#       "type": "string",
#       "description": "User's full name"
#     },
#     "email": {
#       "type": "string",
#       "format": "email"
#     },
#     "age": {
#       "type": "integer",
#       "minimum": 0,
#       "maximum": 150,
#       "description": "User's age"
#     }
#   },
#   "required": ["id", "name", "email", "age"]
# }


# === Personnaliser JSON Schema ===
from pydantic import Field

class User(BaseModel):
    model_config = ConfigDict(
        json_schema_extra={
            "examples": [
                {
                    "id": 1,
                    "name": "Alice",
                    "email": "alice@example.com"
                }
            ],
            "title": "User Model",
            "description": "Represents a user in the system"
        }
    )
    
    id: int = Field(..., json_schema_extra={"example": 123})
    name: str
    email: EmailStr


# === Schema mode ===
# Mode validation (par défaut)
schema_validation = User.model_json_schema(mode='validation')

# Mode serialization
schema_serialization = User.model_json_schema(mode='serialization')


[OK] WORKFLOW PATTERNS COMPLETS


# === CRUD Repository Pattern ===
from typing import Dict, Optional, List
from abc import ABC, abstractmethod

class Repository(ABC, Generic[T]):
    def __init__(self):
        self._storage: Dict[int, T] = {}
        self._next_id = 1
    
    def create(self, item: T) -> T:
        item_dict = item.model_dump()
        item_dict['id'] = self._next_id
        created = self._model_class(**item_dict)
        self._storage[self._next_id] = created
        self._next_id += 1
        return created
    
    def get(self, id: int) -> Optional[T]:
        return self._storage.get(id)
    
    def list(self, skip: int = 0, limit: int = 100) -> List[T]:
        items = list(self._storage.values())
        return items[skip:skip + limit]
    
    def update(self, id: int, updates: dict) -> Optional[T]:
        if id not in self._storage:
            return None
        current = self._storage[id]
        updated_data = current.model_dump()
        updated_data.update(updates)
        updated = self._model_class(**updated_data)
        self._storage[id] = updated
        return updated
    
    def delete(self, id: int) -> bool:
        if id in self._storage:
            del self._storage[id]
            return True
        return False
    
    @property
    @abstractmethod
    def _model_class(self) -> Type[T]:
        pass

class UserRepository(Repository[User]):
    @property
    def _model_class(self):
        return User


# === Service Layer Pattern ===
class UserService:
    def __init__(self, repository: UserRepository):
        self.repository = repository
    
    def create_user(self, user_data: dict) -> User:
        # Validation avec Pydantic
        user_create = UserCreate(**user_data)
        
        # Business logic
        if self._username_exists(user_create.username):
            raise ValueError("Username already exists")
        
        # Créer user
        user = User(
            id=0,  # Sera remplacé par le repository
            name=user_create.name,
            email=user_create.email,
            age=user_create.age
        )
        
        return self.repository.create(user)
    
    def _username_exists(self, username: str) -> bool:
        users = self.repository.list()
        return any(u.name == username for u in users)


# === DTO (Data Transfer Object) Pattern ===
class UserDTO(BaseModel):
    """Pour transfert API -> Service"""
    name: str
    email: str

class UserEntity(BaseModel):
    """Entité métier complète"""
    id: int
    name: str
    email: str
    created_at: datetime
    updated_at: Optional[datetime] = None
    is_active: bool = True
    
    @classmethod
    def from_dto(cls, dto: UserDTO, id: int) -> 'UserEntity':
        return cls(
            id=id,
            name=dto.name,
            email=dto.email,
            created_at=datetime.now()
        )
    
    def to_response(self) -> 'UserResponse':
        return UserResponse(
            id=self.id,
            name=self.name,
            email=self.email,
            is_active=self.is_active
        )

class UserResponse(BaseModel):
    """Pour réponse Service -> API"""
    id: int
    name: str
    email: str
    is_active: bool


# === Command Pattern ===
class Command(BaseModel, ABC):
    @abstractmethod
    def execute(self) -> Any:
        pass

class CreateUserCommand(Command):
    name: str
    email: str
    
    def execute(self) -> User:
        # Logique de création
        return User(id=1, name=self.name, email=self.email, age=0)

class UpdateUserCommand(Command):
    user_id: int
    updates: Dict[str, Any]
    
    def execute(self) -> Optional[User]:
        # Logique de mise à jour
        pass


# === Event Sourcing Pattern ===
class Event(BaseModel):
    event_type: str
    timestamp: datetime = Field(default_factory=datetime.now)
    data: Dict[str, Any]

class UserCreatedEvent(Event):
    event_type: Literal["user.created"] = "user.created"
    data: Dict[str, Any]

class UserUpdatedEvent(Event):
    event_type: Literal["user.updated"] = "user.updated"
    data: Dict[str, Any]

class EventStore:
    def __init__(self):
        self.events: List[Event] = []
    
    def append(self, event: Event):
        self.events.append(event)
    
    def get_events(self, event_type: Optional[str] = None) -> List[Event]:
        if event_type:
            return [e for e in self.events if e.event_type == event_type]
        return self.events


# === State Machine Pattern ===
from enum import Enum

class OrderStatus(str, Enum):
    PENDING = "pending"
    PAID = "paid"
    SHIPPED = "shipped"
    DELIVERED = "delivered"
    CANCELLED = "cancelled"

class Order(BaseModel):
    id: int
    status: OrderStatus = OrderStatus.PENDING
    
    def pay(self) -> 'Order':
        if self.status != OrderStatus.PENDING:
            raise ValueError("Can only pay pending orders")
        return self.model_copy(update={'status': OrderStatus.PAID})
    
    def ship(self) -> 'Order':
        if self.status != OrderStatus.PAID:
            raise ValueError("Can only ship paid orders")
        return self.model_copy(update={'status': OrderStatus.SHIPPED})
    
    def deliver(self) -> 'Order':
        if self.status != OrderStatus.SHIPPED:
            raise ValueError("Can only deliver shipped orders")
        return self.model_copy(update={'status': OrderStatus.DELIVERED})
    
    def cancel(self) -> 'Order':
        if self.status in [OrderStatus.DELIVERED, OrderStatus.CANCELLED]:
            raise ValueError("Cannot cancel delivered or already cancelled orders")
        return self.model_copy(update={'status': OrderStatus.CANCELLED})


[OK] VALIDATION COMPLEXE - CAS RÉELS


# === Validation de numéro de carte bancaire (Luhn) ===
def luhn_check(card_number: str) -> bool:
    def digits_of(n):
        return [int(d) for d in str(n)]
    
    digits = digits_of(card_number.replace(' ', '').replace('-', ''))
    odd_digits = digits[-1::-2]
    even_digits = digits[-2::-2]
    checksum = sum(odd_digits)
    for d in even_digits:
        checksum += sum(digits_of(d * 2))
    return checksum % 10 == 0

class CreditCard(BaseModel):
    number: str
    
    @field_validator('number')
    @classmethod
    def validate_card(cls, v: str) -> str:
        v = v.replace(' ', '').replace('-', '')
        if not v.isdigit():
            raise ValueError('Card number must contain only digits')
        if len(v) not in [13, 15, 16]:
            raise ValueError('Invalid card number length')
        if not luhn_check(v):
            raise ValueError('Invalid card number (Luhn check failed)')
        return v


# === Validation IBAN ===
def validate_iban(iban: str) -> bool:
    iban = iban.replace(' ', '').upper()
    if len(iban) < 15 or len(iban) > 34:
        return False
    
    # Déplacer les 4 premiers caractères à la fin
    rearranged = iban[4:] + iban[:4]
    
    # Remplacer lettres par chiffres (A=10, B=11, ...)
    numeric = ''
    for char in rearranged:
        if char.isdigit():
            numeric += char
        else:
            numeric += str(ord(char) - ord('A') + 10)
    
    return int(numeric) % 97 == 1

class BankAccount(BaseModel):
    iban: str
    
    @field_validator('iban')
    @classmethod
    def check_iban(cls, v: str) -> str:
        if not validate_iban(v):
            raise ValueError('Invalid IBAN')
        return v.replace(' ', '').upper()


# === Validation de plage de dates avec chevauchement ===
class DateRange(BaseModel):
    start: date
    end: date
    
    @model_validator(mode='after')
    def validate_range(self) -> 'DateRange':
        if self.end < self.start:
            raise ValueError('end must be after start')
        
        # Pas plus de 1 an
        if (self.end - self.start).days > 365:
            raise ValueError('Range cannot exceed 1 year')
        
        return self
    
    def overlaps(self, other: 'DateRange') -> bool:
        return not (self.end < other.start or self.start > other.end)

class Booking(BaseModel):
    room_id: int
    date_range: DateRange
    
    @classmethod
    def check_availability(cls, bookings: List['Booking'], new_booking: 'Booking') -> bool:
        for booking in bookings:
            if booking.room_id == new_booking.room_id:
                if booking.date_range.overlaps(new_booking.date_range):
                    return False
        return True


# === Validation d'adresse email avec MX record ===
import dns.resolver  # pip install dnspython

class EmailWithMX(BaseModel):
    email: EmailStr
    
    @field_validator('email')
    @classmethod
    def check_mx_record(cls, v: str) -> str:
        domain = v.split('@')[1]
        try:
            mx_records = dns.resolver.resolve(domain, 'MX')
            if not mx_records:
                raise ValueError(f'No MX records found for {domain}')
        except Exception as e:
            raise ValueError(f'Cannot verify domain: {e}')
        return v


# === Validation de mot de passe avec règles complexes ===
import re
from typing import List

class PasswordPolicy:
    def __init__(
        self,
        min_length: int = 8,
        require_uppercase: bool = True,
        require_lowercase: bool = True,
        require_digit: bool = True,
        require_special: bool = True,
        forbidden_words: List[str] = None
    ):
        self.min_length = min_length
        self.require_uppercase = require_uppercase
        self.require_lowercase = require_lowercase
        self.require_digit = require_digit
        self.require_special = require_special
        self.forbidden_words = forbidden_words or []
    
    def validate(self, password: str) -> tuple[bool, List[str]]:
        errors = []
        
        if len(password) < self.min_length:
            errors.append(f'Must be at least {self.min_length} characters')
        
        if self.require_uppercase and not re.search(r'[A-Z]', password):
            errors.append('Must contain uppercase letter')
        
        if self.require_lowercase and not re.search(r'[a-z]', password):
            errors.append('Must contain lowercase letter')
        
        if self.require_digit and not re.search(r'\d', password):
            errors.append('Must contain digit')
        
        if self.require_special and not re.search(r'[!@#$%^&*(),.?":{}|<>]', password):
            errors.append('Must contain special character')
        
        for word in self.forbidden_words:
            if word.lower() in password.lower():
                errors.append(f'Cannot contain "{word}"')
        
        return len(errors) == 0, errors

class SecurePassword(str):
    policy = PasswordPolicy(
        min_length=12,
        forbidden_words=['password', '123456', 'qwerty']
    )
    
    @classmethod
    def __get_pydantic_core_schema__(cls, source_type, handler):
        from pydantic_core import core_schema
        return core_schema.no_info_after_validator_function(
            cls.validate,
            core_schema.str_schema()
        )
    
    @classmethod
    def validate(cls, v: str) -> str:
        valid, errors = cls.policy.validate(v)
        if not valid:
            raise ValueError('; '.join(errors))
        return v


# === Validation croisée entre plusieurs modèles ===
class Address(BaseModel):
    country: str
    postal_code: str
    
    @model_validator(mode='after')
    def validate_postal_code(self) -> 'Address':
        # Validation par pays
        patterns = {
            'FR': r'^\d{5}


# === Enum simple ===
from enum import Enum

class Status(str, Enum):
    PENDING = "pending"
    APPROVED = "approved"
    REJECTED = "rejected"

class Application(BaseModel):
    id: int
    status: Status

app = Application(id=1, status="pending")  # Converti en Status.PENDING
print(app.status)           # Status.PENDING
print(app.status.value)     # "pending"

# Sérialisation
print(app.model_dump())
# {'id': 1, 'status': 'pending'}  # Si use_enum_values=True dans config


# === Enum avec IntEnum ===
from enum import IntEnum

class Priority(IntEnum):
    LOW = 1
    MEDIUM = 2
    HIGH = 3
    CRITICAL = 4

class Task(BaseModel):
    title: str
    priority: Priority

task = Task(title="Fix bug", priority=3)
print(task.priority)  # Priority.HIGH


# === Utiliser valeurs enum ===
class User(BaseModel):
    model_config = ConfigDict(use_enum_values=True)
    
    role: Status

user = User(role="pending")
print(user.model_dump())
# {'role': 'pending'}  # Valeur au lieu de Status.PENDING


[OK] COMPUTED FIELDS


# === Champs calculés avec @computed_field ===
from pydantic import computed_field

class Rectangle(BaseModel):
    width: float
    height: float
    
    @computed_field
    @property
    def area(self) -> float:
        return self.width * self.height
    
    @computed_field
    @property
    def perimeter(self) -> float:
        return 2 * (self.width + self.height)

rect = Rectangle(width=10, height=5)
print(rect.area)        # 50.0
print(rect.perimeter)   # 30.0

# Dans la sérialisation
print(rect.model_dump())
# {'width': 10.0, 'height': 5.0, 'area': 50.0, 'perimeter': 30.0}


# === Computed field avec cache ===
from functools import cached_property

class ExpensiveComputation(BaseModel):
    value: int
    
    @computed_field
    @cached_property
    def expensive_result(self) -> int:
        # Calcul coûteux, mis en cache
        return sum(range(self.value))


# === Exclure computed fields ===
rect = Rectangle(width=10, height=5)
print(rect.model_dump(exclude={'area'}))
# {'width': 10.0, 'height': 5.0, 'perimeter': 30.0}


[OK] PROPRIÉTÉS PRIVÉES


# === Attributs privés ===
class User(BaseModel):
    model_config = ConfigDict(validate_assignment=True)
    
    username: str
    email: str
    _internal_id: int = 0  # Commence par _
    
    def __init__(self, **data):
        super().__init__(**data)
        self._internal_id = id(self)

user = User(username="alice", email="alice@example.com")
print(user.model_dump())
# {'username': 'alice', 'email': 'alice@example.com'}
# _internal_id n'est pas inclus


# === PrivateAttr ===
from pydantic import PrivateAttr

class User(BaseModel):
    username: str
    _password_hash: str = PrivateAttr()
    _login_count: int = PrivateAttr(default=0)
    
    def __init__(self, username: str, password: str, **data):
        super().__init__(username=username, **data)
        self._password_hash = self._hash_password(password)
    
    def _hash_password(self, password: str) -> str:
        # Simuler un hash
        return f"hashed_{password}"
    
    def login(self):
        self._login_count += 1


[OK] CALLBACKS & HOOKS


# === Model post_init ===
class User(BaseModel):
    name: str
    email: str
    _welcome_sent: bool = PrivateAttr(default=False)
    
    def model_post_init(self, __context):
        # Appelé après __init__
        if not self._welcome_sent:
            self._send_welcome_email()
            self._welcome_sent = True
    
    def _send_welcome_email(self):
        print(f"Welcome email sent to {self.email}")


# === Custom __init__ ===
class User(BaseModel):
    name: str
    email: str
    
    def __init__(self, **data):
        # Prétraitement
        if 'email' in data:
            data['email'] = data['email'].lower()
        
        super().__init__(**data)
        
        # Post-traitement
        print(f"User {self.name} created")


[OK] TYPE ADAPTERS


# === Valider types sans BaseModel ===
from pydantic import TypeAdapter

# Adapter pour liste
ListStrAdapter = TypeAdapter(List[str])
validated = ListStrAdapter.validate_python(['a', 'b', 'c'])
# validated = ListStrAdapter.validate_python(['a', 1, 'c'])  # ValidationError

# Adapter pour dict
DictAdapter = TypeAdapter(Dict[str, int])
validated = DictAdapter.validate_json('{"a": 1, "b": 2}')

# Adapter pour Union
UnionAdapter = TypeAdapter(Union[int, str])
result1 = UnionAdapter.validate_python(123)     # int
result2 = UnionAdapter.validate_python("hello") # str


# === Adapter avec types complexes ===
from typing import Annotated

PositiveIntAdapter = TypeAdapter(Annotated[int, Field(gt=0)])
validated = PositiveIntAdapter.validate_python(5)
# PositiveIntAdapter.validate_python(-5)  # ValidationError


[OK] ANNOTATIONS & METADATA


# === Annotated types ===
from typing import Annotated

# Alias pratiques
PositiveInt = Annotated[int, Field(gt=0)]
NonEmptyStr = Annotated[str, Field(min_length=1)]
Email = Annotated[str, Field(pattern=r'^[\w\.-]+@[\w\.-]+\.\w+)]

class User(BaseModel):
    id: PositiveInt
    name: NonEmptyStr
    email: Email
    age: Annotated[int, Field(ge=0, le=150)]


# === Metadata personnalisée ===
class User(BaseModel):
    name: str = Field(..., description="User's name", examples=["Alice", "Bob"])
    age: int = Field(..., ge=0, le=150, description="User's age in years")
    email: EmailStr = Field(..., description="Contact email")
    
    model_config = ConfigDict(
        json_schema_extra={
            "examples": [
                {
                    "name": "Alice",
                    "age": 30,
                    "email": "alice@example.com"
                }
            ]
        }
    )


[OK] PERFORMANCE & OPTIMISATION


# === Mode strict ===
class StrictUser(BaseModel):
    model_config = ConfigDict(strict=True)
    
    id: int     # N'accepte que int, pas de conversion depuis str
    age: int

# StrictUser(id="123", age=30)  # ValidationError en mode strict


# === Désactiver validation ===
class User(BaseModel):
    name: str
    age: int

# Construction sans validation (dangereux!)
user = User.model_construct(name="Alice", age=30)

# Utile pour données déjà validées (ex: depuis DB)
users = [User.model_construct(**row) for row in db_results]


# === Validation partielle ===
# Créer une instance avec des champs optionnels
data = {"name": "Alice"}  # age manquant

# Avec validation normale: ValidationError
# Avec model_construct: OK mais incomplet


# === Copie de modèle ===
user1 = User(name="Alice", age=30)

# Copie simple
user2 = user1.model_copy()

# Copie avec modifications
user3 = user1.model_copy(update={"age": 31})

# Copie profonde
user4 = user1.model_copy(deep=True)


[OK] INTÉGRATION AVEC ORMs


# === SQLAlchemy ===
from sqlalchemy import Column, Integer, String, create_engine
from sqlalchemy.orm import declarative_base, Session
from pydantic import ConfigDict

Base = declarative_base()

class UserDB(Base):
    __tablename__ = "users"
    
    id = Column(Integer, primary_key=True)
    name = Column(String)
    email = Column(String)

class UserSchema(BaseModel):
    model_config = ConfigDict(from_attributes=True)
    
    id: int
    name: str
    email: str

# Conversion ORM -> Pydantic
engine = create_engine("sqlite:///:memory:")
Base.metadata.create_all(engine)

with Session(engine) as session:
    db_user = UserDB(id=1, name="Alice", email="alice@example.com")
    session.add(db_user)
    session.commit()
    
    # Charger depuis DB
    db_user = session.query(UserDB).first()
    
    # Convertir en Pydantic
    pydantic_user = UserSchema.model_validate(db_user)


# === Django ORM ===
# models.py
from django.db import models

class User(models.Model):
    name = models.CharField(max_length=100)
    email = models.EmailField()

# schemas.py
class UserSchema(BaseModel):
    model_config = ConfigDict(from_attributes=True)
    
    id: int
    name: str
    email: str

# Utilisation
django_user = User.objects.get(id=1)
pydantic_user = UserSchema.model_validate(django_user)


[OK] FASTAPI INTÉGRATION


# === Modèles de requête/réponse ===
from fastapi import FastAPI, HTTPException
from typing import List

app = FastAPI()

class UserCreate(BaseModel):
    name: str = Field(..., min_length=1)
    email: EmailStr
    age: int = Field(..., ge=0, le=150)

class UserResponse(BaseModel):
    id: int
    name: str
    email: str
    age: int
    created_at: datetime = Field(default_factory=datetime.now)

class UserUpdate(BaseModel):
    name: Optional[str] = None
    email: Optional[EmailStr] = None
    age: Optional[int] = Field(None, ge=0, le=150)

# Endpoints
@app.post("/users/", response_model=UserResponse, status_code=201)
def create_user(user: UserCreate):
    # user est automatiquement validé
    new_user = UserResponse(
        id=1,
        name=user.name,
        email=user.email,
        age=user.age
    )
    return new_user

@app.get("/users/{user_id}", response_model=UserResponse)
def get_user(user_id: int):
    # Simuler récupération DB
    user = UserResponse(
        id=user_id,
        name="Alice",
        email="alice@example.com",
        age=30
    )
    return user

@app.get("/users/", response_model=List[UserResponse])
def list_users(skip: int = 0, limit: int = 10):
    # Liste d'utilisateurs
    return []

@app.patch("/users/{user_id}", response_model=UserResponse)
def update_user(user_id: int, user_update: UserUpdate):
    # Mise à jour partielle
    return UserResponse(
        id=user_id,
        name=user_update.name or "Default",
        email=user_update.email or "default@example.com",
        age=user_update.age or 0
    )


# === Response models avec exclude ===
class UserWithPassword(BaseModel):
    id: int
    username: str
    email: str
    password: str

@app.get("/users/{user_id}", response_model=UserWithPassword, response_model_exclude={"password"})
def get_user(user_id: int):
    return UserWithPassword(
        id=user_id,
        username="alice",
        email="alice@example.com",
        password="secret"
    )
    # Password sera exclu de la réponse


[OK] ERREURS & VALIDATION CUSTOM


# === Gestion d'erreurs personnalisée ===
from pydantic import ValidationError, field_validator

class User(BaseModel):
    username: str
    email: str
    
    @field_validator('username')
    @classmethod
    def username_valid(cls, v: str) -> str:
        if len(v) < 3:
            raise ValueError('Username must be at least 3 characters')
        if not v.isalnum():
            raise ValueError('Username must be alphanumeric')
        return v

try:
    user = User(username="ab", email="test@example.com")
except ValidationError as e:
    for error in e.errors():
        print(f"Field: {error['loc']}")
        print(f"Error: {error['msg']}")
        print(f"Type: {error['type']}")
        print(f"Input: {error['input']}")


# === PydanticCustomError ===
from pydantic_core import PydanticCustomError

class User(BaseModel):
    age: int
    
    @field_validator('age')
    @classmethod
    def validate_age(cls, v: int) -> int:
        if v < 0:
            raise PydanticCustomError(
                'negative_age',
                'Age cannot be negative, got {age}',
                {'age': v}
            )
        if v > 150:
            raise PydanticCustomError(
                'unrealistic_age',
                'Age seems unrealistic: {age}',
                {'age': v}
            )
        return v


# === Validator avec assert ===
class User(BaseModel):
    username: str
    
    @field_validator('username')
    @classmethod
    def check_username(cls, v: str) -> str:
        assert v.isalnum(), 'Username must be alphanumeric'
        assert len(v) >= 3, 'Username too short'
        return v


[OK] FORMATS DE DONNÉES SPÉCIAUX


# === UUID ===
from uuid import UUID, uuid4

class Resource(BaseModel):
    id: UUID = Field(default_factory=uuid4)
    name: str

resource = Resource(name="Test")
print(resource.id)  # UUID généré automatiquement

# Accepte string UUID
resource = Resource(id="123e4567-e89b-12d3-a456-426614174000", name="Test")


# === Decimal pour finances ===
from decimal import Decimal

class Product(BaseModel):
    name: str
    price: Decimal = Field(..., decimal_places=2)
    tax_rate: Decimal

product = Product(name="Book", price="19.99", tax_rate="0.20")
total = product.price * (1 + product.tax_rate)


# === Path ===
from pathlib import Path

class Config(BaseModel):
    config_file: FilePath        # Doit exister
    output_dir: DirectoryPath    # Doit exister
    log_file: Path               # N'importe quel chemin

config = Config(
    config_file="/etc/config.ini",
    output_dir="/tmp",
    log_file="/var/log/app.log"
)


# === Color ===
from pydantic import Color

class Theme(BaseModel):
    primary_color: Color
    secondary_color: Color

theme = Theme(
    primary_color="rgb(255, 0, 0)",
    secondary_color="#00FF00"
)
print(theme.primary_color.as_hex())  # #ff0000


# === AnyUrl variations ===
from pydantic import AnyUrl, HttpUrl, PostgresDsn, RedisDsn

class AppConfig(BaseModel):
    website: HttpUrl
    database: PostgresDsn
    cache: RedisDsn

config = AppConfig(
    website="https://example.com",
    database="postgresql://user:pass@localhost/dbname",
    cache="redis://localhost:6379/0"
)


[OK] PATTERNS AVANCÉS


# === Builder pattern ===
class UserBuilder:
    def __init__(self):
        self._data = {}
    
    def with_name(self, name: str) -> 'UserBuilder':
        self._data['name'] = name
        return self
    
    def with_email(self, email: str) -> 'UserBuilder':
        self._data['email'] = email
        return self
    
    def with_age(self, age: int) -> 'UserBuilder':
        self._data['age'] = age
        return self
    
    def build(self) -> User:
        return User(**self._data)

# Utilisation
user = (UserBuilder()
        .with_name("Alice")
        .with_email("alice@example.com")
        .with_age(30)
        .build())


# === Factory pattern ===
class UserFactory:
    @staticmethod
    def create_admin(name: str, email: str) -> 'AdminUser':
        return AdminUser(
            name=name,
            email=email,
            role="admin",
            permissions=["read", "write", "delete"]
        )
    
    @staticmethod
    def create_guest(name: str) -> 'GuestUser':
        return GuestUser(
            name=name,
            role="guest",
            permissions=["read"]
        )


# === Repository pattern ===
class UserRepository:
    def __init__(self):
        self._users: Dict[int, User] = {}
    
    def save(self, user: User) -> User:
        self._users[user.id] = user
        return user
    
    def get(self, user_id: int) -> Optional[User]:
        return self._users.get(user_id)
    
    def list(self) -> List[User]:
        return list(self._users.values())
    
    def delete(self, user_id: int) -> bool:
        if user_id in self._users:
            del self._users[user_id]
            return True
        return False


[OK] TESTS AVEC PYDANTIC


# === Pytest fixtures ===
import pytest

@pytest.fixture
def sample_user():
    return User(id=1, name="Alice", email="alice@example.com", age=30)

@pytest.fixture
def sample_user_data():
    return {
        "id": 1,
        "name": "Alice",
        "email": "alice@example.com",
        "age": 30
    }

def test_user_creation(sample_user_data):
    user = User(**sample_user_data)
    assert user.name == "Alice"
    assert user.age == 30

def test_user_validation():
    with pytest.raises(ValidationError):
        User(id="invalid", name="Bob", email="bob@example.com", age=30)

def test_user_serialization(sample_user):
    data = sample_user.model_dump()
    assert data["name"] == "Alice"
    
    json_str = sample_user.model_dump_json()
    assert "Alice" in json_str


# === Faker pour données de test ===
from faker import Faker

fake = Faker()

def create_random_user() -> User:
    return User(
        id=fake.random_int(1, 10000),
        name=fake.name(),
        email=fake.email(),
        age=fake.random_int(18, 80)
    )

def test_with_random_data():
    users = [create_random_user() for _ in range(100)]
    assert len(users) == 100
    assert all(isinstance(u, User) for u in users)


# === Hypothesis pour property-based testing ===
from hypothesis import given, strategies as st

@given(
    id=st.integers(min_value=1),
    name=st.text(min_size=1),
    age=st.integers(min_value=0, max_value=150)
)
def test_user_properties(id, name, age):
    try:
        user = User(id=id, name=name, email="test@example.com", age=age)
        assert user.id == id
        assert user.age >= 0
    except ValidationError:
        pass  # Certaines combinaisons peuvent être invalides


[OK] MIGRATION V1 -> V2


# === Différences principales ===

# V1
# class User(BaseModel):
#     name: str
#     
#     class Config:
#         orm_mode = True
#     
#     @validator('name')
#     def name_must_contain_space(cls, v):
#         if ' ' not in v:
#             raise ValueError('must contain a space')
#         return v.title()

# V2
class User(BaseModel):
    model_config = ConfigDict(from_attributes=True)  # orm_mode renommé
    
    name: str
    
    @field_validator('name')
    @classmethod
    def name_must_contain_space(cls, v: str) -> str:
        if ' ' not in v:
            raise ValueError('must contain a space')
        return v.title()


# === Autres changements ===
# V1: .dict()         -> V2: .model_dump()
# V1: .json()         -> V2: .model_dump_json()
# V1: .parse_obj()    -> V2: .model_validate()
# V1: .parse_raw()    -> V2: .model_validate_json()
# V1: .schema()       -> V2: .model_json_schema()
# V1: .copy()         -> V2: .model_copy()
# V1: .construct()    -> V2: .model_construct()

# V1: @validator      -> V2: @field_validator
# V1: @root_validator -> V2: @model_validator


[OK] BONNES PRATIQUES


# 1. Séparer modèles API et business logic
# schemas.py - Modèles API
class UserCreate(BaseModel):
    name: str
    email: EmailStr

class UserResponse(BaseModel):
    id: int
    name: str
    email: str

# models.py - Modèles métier
class User:
    def __init__(self, id: int, name: str, email: str):
        self.id = id
        self.name = name
        self.email = email


# 2. Utiliser Field pour documentation
class Product(BaseModel):
    name: str = Field(..., description="Product name", examples=["Laptop"])
    price: float = Field(..., gt=0, description="Price in EUR")


# 3. Validators simples et testables
@field_validator('email')
@classmethod
def validate_email(cls, v: str) -> str:
    v = v.lower().strip()
    if not v:
        raise ValueError('Email cannot be empty')
    return v


# 4. Utiliser Annotated pour types réutilisables
PositiveInt = Annotated[int, Field(gt=0)]
Email = Annotated[str, EmailStr]
NonEmptyStr = Annotated[str, Field(min_length=1)]


# 5. Config centralisée
class BaseConfig:
    model_config = ConfigDict(
        str_strip_whitespace=True,
        validate_assignment=True,
        use_enum_values=True
    )

class User(BaseModel, BaseConfig):
    name: str


# 6. Gestion d'erreurs explicite
try:
    user = User(**data)
except ValidationError as e:
    logger.error(f"Validation failed: {e.json()}")
    raise HTTPException(status_code=422, detail=e.errors())


# 7. Tests exhaustifs
def test_user_validation_errors():
    invalid_cases = [
        {"name": "", "email": "invalid"},
        {"name": "A", "email": "test@test.com", "age": -1},
        {"name": "Test", "email": "notemail", "age": 200},
    ]
    
    for case in invalid_cases:
        with pytest.raises(ValidationError):
            User(**case)


# 8. Documentation claire
class User(BaseModel):
    """
    User model representing a registered user.
    
    Attributes:
        id: Unique identifier
        name: Full name (3-100 characters)
        email: Valid email address
        age: Age in years (0-150)
    """
    id: int
    name: str = Field(..., min_length=3, max_length=100)
    email: EmailStr
    age: int = Field(..., ge=0, le=150)


[OK] RESSOURCES


# Documentation officielle:
# https://docs.pydantic.dev/

# Migration V1 -> V2:
# https://docs.pydantic.dev/latest/migration/

# Exemples:
# https://github.com/pydantic/pydantic/tree/main/docs/examples

# FastAPI + Pydantic:
# https://fastapi.tiangolo.com/

# Type hints:
# https://docs.python.org/3/library/typing.html

# Validation avancée:
# https://docs.pydantic.dev/latest/concepts/validators/,
            'US': r'^\d{5}(-\d{4})?


# === Enum simple ===
from enum import Enum

class Status(str, Enum):
    PENDING = "pending"
    APPROVED = "approved"
    REJECTED = "rejected"

class Application(BaseModel):
    id: int
    status: Status

app = Application(id=1, status="pending")  # Converti en Status.PENDING
print(app.status)           # Status.PENDING
print(app.status.value)     # "pending"

# Sérialisation
print(app.model_dump())
# {'id': 1, 'status': 'pending'}  # Si use_enum_values=True dans config


# === Enum avec IntEnum ===
from enum import IntEnum

class Priority(IntEnum):
    LOW = 1
    MEDIUM = 2
    HIGH = 3
    CRITICAL = 4

class Task(BaseModel):
    title: str
    priority: Priority

task = Task(title="Fix bug", priority=3)
print(task.priority)  # Priority.HIGH


# === Utiliser valeurs enum ===
class User(BaseModel):
    model_config = ConfigDict(use_enum_values=True)
    
    role: Status

user = User(role="pending")
print(user.model_dump())
# {'role': 'pending'}  # Valeur au lieu de Status.PENDING


[OK] COMPUTED FIELDS


# === Champs calculés avec @computed_field ===
from pydantic import computed_field

class Rectangle(BaseModel):
    width: float
    height: float
    
    @computed_field
    @property
    def area(self) -> float:
        return self.width * self.height
    
    @computed_field
    @property
    def perimeter(self) -> float:
        return 2 * (self.width + self.height)

rect = Rectangle(width=10, height=5)
print(rect.area)        # 50.0
print(rect.perimeter)   # 30.0

# Dans la sérialisation
print(rect.model_dump())
# {'width': 10.0, 'height': 5.0, 'area': 50.0, 'perimeter': 30.0}


# === Computed field avec cache ===
from functools import cached_property

class ExpensiveComputation(BaseModel):
    value: int
    
    @computed_field
    @cached_property
    def expensive_result(self) -> int:
        # Calcul coûteux, mis en cache
        return sum(range(self.value))


# === Exclure computed fields ===
rect = Rectangle(width=10, height=5)
print(rect.model_dump(exclude={'area'}))
# {'width': 10.0, 'height': 5.0, 'perimeter': 30.0}


[OK] PROPRIÉTÉS PRIVÉES


# === Attributs privés ===
class User(BaseModel):
    model_config = ConfigDict(validate_assignment=True)
    
    username: str
    email: str
    _internal_id: int = 0  # Commence par _
    
    def __init__(self, **data):
        super().__init__(**data)
        self._internal_id = id(self)

user = User(username="alice", email="alice@example.com")
print(user.model_dump())
# {'username': 'alice', 'email': 'alice@example.com'}
# _internal_id n'est pas inclus


# === PrivateAttr ===
from pydantic import PrivateAttr

class User(BaseModel):
    username: str
    _password_hash: str = PrivateAttr()
    _login_count: int = PrivateAttr(default=0)
    
    def __init__(self, username: str, password: str, **data):
        super().__init__(username=username, **data)
        self._password_hash = self._hash_password(password)
    
    def _hash_password(self, password: str) -> str:
        # Simuler un hash
        return f"hashed_{password}"
    
    def login(self):
        self._login_count += 1


[OK] CALLBACKS & HOOKS


# === Model post_init ===
class User(BaseModel):
    name: str
    email: str
    _welcome_sent: bool = PrivateAttr(default=False)
    
    def model_post_init(self, __context):
        # Appelé après __init__
        if not self._welcome_sent:
            self._send_welcome_email()
            self._welcome_sent = True
    
    def _send_welcome_email(self):
        print(f"Welcome email sent to {self.email}")


# === Custom __init__ ===
class User(BaseModel):
    name: str
    email: str
    
    def __init__(self, **data):
        # Prétraitement
        if 'email' in data:
            data['email'] = data['email'].lower()
        
        super().__init__(**data)
        
        # Post-traitement
        print(f"User {self.name} created")


[OK] TYPE ADAPTERS


# === Valider types sans BaseModel ===
from pydantic import TypeAdapter

# Adapter pour liste
ListStrAdapter = TypeAdapter(List[str])
validated = ListStrAdapter.validate_python(['a', 'b', 'c'])
# validated = ListStrAdapter.validate_python(['a', 1, 'c'])  # ValidationError

# Adapter pour dict
DictAdapter = TypeAdapter(Dict[str, int])
validated = DictAdapter.validate_json('{"a": 1, "b": 2}')

# Adapter pour Union
UnionAdapter = TypeAdapter(Union[int, str])
result1 = UnionAdapter.validate_python(123)     # int
result2 = UnionAdapter.validate_python("hello") # str


# === Adapter avec types complexes ===
from typing import Annotated

PositiveIntAdapter = TypeAdapter(Annotated[int, Field(gt=0)])
validated = PositiveIntAdapter.validate_python(5)
# PositiveIntAdapter.validate_python(-5)  # ValidationError


[OK] ANNOTATIONS & METADATA


# === Annotated types ===
from typing import Annotated

# Alias pratiques
PositiveInt = Annotated[int, Field(gt=0)]
NonEmptyStr = Annotated[str, Field(min_length=1)]
Email = Annotated[str, Field(pattern=r'^[\w\.-]+@[\w\.-]+\.\w+)]

class User(BaseModel):
    id: PositiveInt
    name: NonEmptyStr
    email: Email
    age: Annotated[int, Field(ge=0, le=150)]


# === Metadata personnalisée ===
class User(BaseModel):
    name: str = Field(..., description="User's name", examples=["Alice", "Bob"])
    age: int = Field(..., ge=0, le=150, description="User's age in years")
    email: EmailStr = Field(..., description="Contact email")
    
    model_config = ConfigDict(
        json_schema_extra={
            "examples": [
                {
                    "name": "Alice",
                    "age": 30,
                    "email": "alice@example.com"
                }
            ]
        }
    )


[OK] PERFORMANCE & OPTIMISATION


# === Mode strict ===
class StrictUser(BaseModel):
    model_config = ConfigDict(strict=True)
    
    id: int     # N'accepte que int, pas de conversion depuis str
    age: int

# StrictUser(id="123", age=30)  # ValidationError en mode strict


# === Désactiver validation ===
class User(BaseModel):
    name: str
    age: int

# Construction sans validation (dangereux!)
user = User.model_construct(name="Alice", age=30)

# Utile pour données déjà validées (ex: depuis DB)
users = [User.model_construct(**row) for row in db_results]


# === Validation partielle ===
# Créer une instance avec des champs optionnels
data = {"name": "Alice"}  # age manquant

# Avec validation normale: ValidationError
# Avec model_construct: OK mais incomplet


# === Copie de modèle ===
user1 = User(name="Alice", age=30)

# Copie simple
user2 = user1.model_copy()

# Copie avec modifications
user3 = user1.model_copy(update={"age": 31})

# Copie profonde
user4 = user1.model_copy(deep=True)


[OK] INTÉGRATION AVEC ORMs


# === SQLAlchemy ===
from sqlalchemy import Column, Integer, String, create_engine
from sqlalchemy.orm import declarative_base, Session
from pydantic import ConfigDict

Base = declarative_base()

class UserDB(Base):
    __tablename__ = "users"
    
    id = Column(Integer, primary_key=True)
    name = Column(String)
    email = Column(String)

class UserSchema(BaseModel):
    model_config = ConfigDict(from_attributes=True)
    
    id: int
    name: str
    email: str

# Conversion ORM -> Pydantic
engine = create_engine("sqlite:///:memory:")
Base.metadata.create_all(engine)

with Session(engine) as session:
    db_user = UserDB(id=1, name="Alice", email="alice@example.com")
    session.add(db_user)
    session.commit()
    
    # Charger depuis DB
    db_user = session.query(UserDB).first()
    
    # Convertir en Pydantic
    pydantic_user = UserSchema.model_validate(db_user)


# === Django ORM ===
# models.py
from django.db import models

class User(models.Model):
    name = models.CharField(max_length=100)
    email = models.EmailField()

# schemas.py
class UserSchema(BaseModel):
    model_config = ConfigDict(from_attributes=True)
    
    id: int
    name: str
    email: str

# Utilisation
django_user = User.objects.get(id=1)
pydantic_user = UserSchema.model_validate(django_user)


[OK] FASTAPI INTÉGRATION


# === Modèles de requête/réponse ===
from fastapi import FastAPI, HTTPException
from typing import List

app = FastAPI()

class UserCreate(BaseModel):
    name: str = Field(..., min_length=1)
    email: EmailStr
    age: int = Field(..., ge=0, le=150)

class UserResponse(BaseModel):
    id: int
    name: str
    email: str
    age: int
    created_at: datetime = Field(default_factory=datetime.now)

class UserUpdate(BaseModel):
    name: Optional[str] = None
    email: Optional[EmailStr] = None
    age: Optional[int] = Field(None, ge=0, le=150)

# Endpoints
@app.post("/users/", response_model=UserResponse, status_code=201)
def create_user(user: UserCreate):
    # user est automatiquement validé
    new_user = UserResponse(
        id=1,
        name=user.name,
        email=user.email,
        age=user.age
    )
    return new_user

@app.get("/users/{user_id}", response_model=UserResponse)
def get_user(user_id: int):
    # Simuler récupération DB
    user = UserResponse(
        id=user_id,
        name="Alice",
        email="alice@example.com",
        age=30
    )
    return user

@app.get("/users/", response_model=List[UserResponse])
def list_users(skip: int = 0, limit: int = 10):
    # Liste d'utilisateurs
    return []

@app.patch("/users/{user_id}", response_model=UserResponse)
def update_user(user_id: int, user_update: UserUpdate):
    # Mise à jour partielle
    return UserResponse(
        id=user_id,
        name=user_update.name or "Default",
        email=user_update.email or "default@example.com",
        age=user_update.age or 0
    )


# === Response models avec exclude ===
class UserWithPassword(BaseModel):
    id: int
    username: str
    email: str
    password: str

@app.get("/users/{user_id}", response_model=UserWithPassword, response_model_exclude={"password"})
def get_user(user_id: int):
    return UserWithPassword(
        id=user_id,
        username="alice",
        email="alice@example.com",
        password="secret"
    )
    # Password sera exclu de la réponse


[OK] ERREURS & VALIDATION CUSTOM


# === Gestion d'erreurs personnalisée ===
from pydantic import ValidationError, field_validator

class User(BaseModel):
    username: str
    email: str
    
    @field_validator('username')
    @classmethod
    def username_valid(cls, v: str) -> str:
        if len(v) < 3:
            raise ValueError('Username must be at least 3 characters')
        if not v.isalnum():
            raise ValueError('Username must be alphanumeric')
        return v

try:
    user = User(username="ab", email="test@example.com")
except ValidationError as e:
    for error in e.errors():
        print(f"Field: {error['loc']}")
        print(f"Error: {error['msg']}")
        print(f"Type: {error['type']}")
        print(f"Input: {error['input']}")


# === PydanticCustomError ===
from pydantic_core import PydanticCustomError

class User(BaseModel):
    age: int
    
    @field_validator('age')
    @classmethod
    def validate_age(cls, v: int) -> int:
        if v < 0:
            raise PydanticCustomError(
                'negative_age',
                'Age cannot be negative, got {age}',
                {'age': v}
            )
        if v > 150:
            raise PydanticCustomError(
                'unrealistic_age',
                'Age seems unrealistic: {age}',
                {'age': v}
            )
        return v


# === Validator avec assert ===
class User(BaseModel):
    username: str
    
    @field_validator('username')
    @classmethod
    def check_username(cls, v: str) -> str:
        assert v.isalnum(), 'Username must be alphanumeric'
        assert len(v) >= 3, 'Username too short'
        return v


[OK] FORMATS DE DONNÉES SPÉCIAUX


# === UUID ===
from uuid import UUID, uuid4

class Resource(BaseModel):
    id: UUID = Field(default_factory=uuid4)
    name: str

resource = Resource(name="Test")
print(resource.id)  # UUID généré automatiquement

# Accepte string UUID
resource = Resource(id="123e4567-e89b-12d3-a456-426614174000", name="Test")


# === Decimal pour finances ===
from decimal import Decimal

class Product(BaseModel):
    name: str
    price: Decimal = Field(..., decimal_places=2)
    tax_rate: Decimal

product = Product(name="Book", price="19.99", tax_rate="0.20")
total = product.price * (1 + product.tax_rate)


# === Path ===
from pathlib import Path

class Config(BaseModel):
    config_file: FilePath        # Doit exister
    output_dir: DirectoryPath    # Doit exister
    log_file: Path               # N'importe quel chemin

config = Config(
    config_file="/etc/config.ini",
    output_dir="/tmp",
    log_file="/var/log/app.log"
)


# === Color ===
from pydantic import Color

class Theme(BaseModel):
    primary_color: Color
    secondary_color: Color

theme = Theme(
    primary_color="rgb(255, 0, 0)",
    secondary_color="#00FF00"
)
print(theme.primary_color.as_hex())  # #ff0000


# === AnyUrl variations ===
from pydantic import AnyUrl, HttpUrl, PostgresDsn, RedisDsn

class AppConfig(BaseModel):
    website: HttpUrl
    database: PostgresDsn
    cache: RedisDsn

config = AppConfig(
    website="https://example.com",
    database="postgresql://user:pass@localhost/dbname",
    cache="redis://localhost:6379/0"
)


[OK] PATTERNS AVANCÉS


# === Builder pattern ===
class UserBuilder:
    def __init__(self):
        self._data = {}
    
    def with_name(self, name: str) -> 'UserBuilder':
        self._data['name'] = name
        return self
    
    def with_email(self, email: str) -> 'UserBuilder':
        self._data['email'] = email
        return self
    
    def with_age(self, age: int) -> 'UserBuilder':
        self._data['age'] = age
        return self
    
    def build(self) -> User:
        return User(**self._data)

# Utilisation
user = (UserBuilder()
        .with_name("Alice")
        .with_email("alice@example.com")
        .with_age(30)
        .build())


# === Factory pattern ===
class UserFactory:
    @staticmethod
    def create_admin(name: str, email: str) -> 'AdminUser':
        return AdminUser(
            name=name,
            email=email,
            role="admin",
            permissions=["read", "write", "delete"]
        )
    
    @staticmethod
    def create_guest(name: str) -> 'GuestUser':
        return GuestUser(
            name=name,
            role="guest",
            permissions=["read"]
        )


# === Repository pattern ===
class UserRepository:
    def __init__(self):
        self._users: Dict[int, User] = {}
    
    def save(self, user: User) -> User:
        self._users[user.id] = user
        return user
    
    def get(self, user_id: int) -> Optional[User]:
        return self._users.get(user_id)
    
    def list(self) -> List[User]:
        return list(self._users.values())
    
    def delete(self, user_id: int) -> bool:
        if user_id in self._users:
            del self._users[user_id]
            return True
        return False


[OK] TESTS AVEC PYDANTIC


# === Pytest fixtures ===
import pytest

@pytest.fixture
def sample_user():
    return User(id=1, name="Alice", email="alice@example.com", age=30)

@pytest.fixture
def sample_user_data():
    return {
        "id": 1,
        "name": "Alice",
        "email": "alice@example.com",
        "age": 30
    }

def test_user_creation(sample_user_data):
    user = User(**sample_user_data)
    assert user.name == "Alice"
    assert user.age == 30

def test_user_validation():
    with pytest.raises(ValidationError):
        User(id="invalid", name="Bob", email="bob@example.com", age=30)

def test_user_serialization(sample_user):
    data = sample_user.model_dump()
    assert data["name"] == "Alice"
    
    json_str = sample_user.model_dump_json()
    assert "Alice" in json_str


# === Faker pour données de test ===
from faker import Faker

fake = Faker()

def create_random_user() -> User:
    return User(
        id=fake.random_int(1, 10000),
        name=fake.name(),
        email=fake.email(),
        age=fake.random_int(18, 80)
    )

def test_with_random_data():
    users = [create_random_user() for _ in range(100)]
    assert len(users) == 100
    assert all(isinstance(u, User) for u in users)


# === Hypothesis pour property-based testing ===
from hypothesis import given, strategies as st

@given(
    id=st.integers(min_value=1),
    name=st.text(min_size=1),
    age=st.integers(min_value=0, max_value=150)
)
def test_user_properties(id, name, age):
    try:
        user = User(id=id, name=name, email="test@example.com", age=age)
        assert user.id == id
        assert user.age >= 0
    except ValidationError:
        pass  # Certaines combinaisons peuvent être invalides


[OK] MIGRATION V1 -> V2


# === Différences principales ===

# V1
# class User(BaseModel):
#     name: str
#     
#     class Config:
#         orm_mode = True
#     
#     @validator('name')
#     def name_must_contain_space(cls, v):
#         if ' ' not in v:
#             raise ValueError('must contain a space')
#         return v.title()

# V2
class User(BaseModel):
    model_config = ConfigDict(from_attributes=True)  # orm_mode renommé
    
    name: str
    
    @field_validator('name')
    @classmethod
    def name_must_contain_space(cls, v: str) -> str:
        if ' ' not in v:
            raise ValueError('must contain a space')
        return v.title()


# === Autres changements ===
# V1: .dict()         -> V2: .model_dump()
# V1: .json()         -> V2: .model_dump_json()
# V1: .parse_obj()    -> V2: .model_validate()
# V1: .parse_raw()    -> V2: .model_validate_json()
# V1: .schema()       -> V2: .model_json_schema()
# V1: .copy()         -> V2: .model_copy()
# V1: .construct()    -> V2: .model_construct()

# V1: @validator      -> V2: @field_validator
# V1: @root_validator -> V2: @model_validator


[OK] BONNES PRATIQUES


# 1. Séparer modèles API et business logic
# schemas.py - Modèles API
class UserCreate(BaseModel):
    name: str
    email: EmailStr

class UserResponse(BaseModel):
    id: int
    name: str
    email: str

# models.py - Modèles métier
class User:
    def __init__(self, id: int, name: str, email: str):
        self.id = id
        self.name = name
        self.email = email


# 2. Utiliser Field pour documentation
class Product(BaseModel):
    name: str = Field(..., description="Product name", examples=["Laptop"])
    price: float = Field(..., gt=0, description="Price in EUR")


# 3. Validators simples et testables
@field_validator('email')
@classmethod
def validate_email(cls, v: str) -> str:
    v = v.lower().strip()
    if not v:
        raise ValueError('Email cannot be empty')
    return v


# 4. Utiliser Annotated pour types réutilisables
PositiveInt = Annotated[int, Field(gt=0)]
Email = Annotated[str, EmailStr]
NonEmptyStr = Annotated[str, Field(min_length=1)]


# 5. Config centralisée
class BaseConfig:
    model_config = ConfigDict(
        str_strip_whitespace=True,
        validate_assignment=True,
        use_enum_values=True
    )

class User(BaseModel, BaseConfig):
    name: str


# 6. Gestion d'erreurs explicite
try:
    user = User(**data)
except ValidationError as e:
    logger.error(f"Validation failed: {e.json()}")
    raise HTTPException(status_code=422, detail=e.errors())


# 7. Tests exhaustifs
def test_user_validation_errors():
    invalid_cases = [
        {"name": "", "email": "invalid"},
        {"name": "A", "email": "test@test.com", "age": -1},
        {"name": "Test", "email": "notemail", "age": 200},
    ]
    
    for case in invalid_cases:
        with pytest.raises(ValidationError):
            User(**case)


# 8. Documentation claire
class User(BaseModel):
    """
    User model representing a registered user.
    
    Attributes:
        id: Unique identifier
        name: Full name (3-100 characters)
        email: Valid email address
        age: Age in years (0-150)
    """
    id: int
    name: str = Field(..., min_length=3, max_length=100)
    email: EmailStr
    age: int = Field(..., ge=0, le=150)


[OK] RESSOURCES


# Documentation officielle:
# https://docs.pydantic.dev/

# Migration V1 -> V2:
# https://docs.pydantic.dev/latest/migration/

# Exemples:
# https://github.com/pydantic/pydantic/tree/main/docs/examples

# FastAPI + Pydantic:
# https://fastapi.tiangolo.com/

# Type hints:
# https://docs.python.org/3/library/typing.html

# Validation avancée:
# https://docs.pydantic.dev/latest/concepts/validators/,
            'UK': r'^[A-Z]{1,2}\d{1,2}[A-Z]?\s?\d[A-Z]{2}


# === Enum simple ===
from enum import Enum

class Status(str, Enum):
    PENDING = "pending"
    APPROVED = "approved"
    REJECTED = "rejected"

class Application(BaseModel):
    id: int
    status: Status

app = Application(id=1, status="pending")  # Converti en Status.PENDING
print(app.status)           # Status.PENDING
print(app.status.value)     # "pending"

# Sérialisation
print(app.model_dump())
# {'id': 1, 'status': 'pending'}  # Si use_enum_values=True dans config


# === Enum avec IntEnum ===
from enum import IntEnum

class Priority(IntEnum):
    LOW = 1
    MEDIUM = 2
    HIGH = 3
    CRITICAL = 4

class Task(BaseModel):
    title: str
    priority: Priority

task = Task(title="Fix bug", priority=3)
print(task.priority)  # Priority.HIGH


# === Utiliser valeurs enum ===
class User(BaseModel):
    model_config = ConfigDict(use_enum_values=True)
    
    role: Status

user = User(role="pending")
print(user.model_dump())
# {'role': 'pending'}  # Valeur au lieu de Status.PENDING


[OK] COMPUTED FIELDS


# === Champs calculés avec @computed_field ===
from pydantic import computed_field

class Rectangle(BaseModel):
    width: float
    height: float
    
    @computed_field
    @property
    def area(self) -> float:
        return self.width * self.height
    
    @computed_field
    @property
    def perimeter(self) -> float:
        return 2 * (self.width + self.height)

rect = Rectangle(width=10, height=5)
print(rect.area)        # 50.0
print(rect.perimeter)   # 30.0

# Dans la sérialisation
print(rect.model_dump())
# {'width': 10.0, 'height': 5.0, 'area': 50.0, 'perimeter': 30.0}


# === Computed field avec cache ===
from functools import cached_property

class ExpensiveComputation(BaseModel):
    value: int
    
    @computed_field
    @cached_property
    def expensive_result(self) -> int:
        # Calcul coûteux, mis en cache
        return sum(range(self.value))


# === Exclure computed fields ===
rect = Rectangle(width=10, height=5)
print(rect.model_dump(exclude={'area'}))
# {'width': 10.0, 'height': 5.0, 'perimeter': 30.0}


[OK] PROPRIÉTÉS PRIVÉES


# === Attributs privés ===
class User(BaseModel):
    model_config = ConfigDict(validate_assignment=True)
    
    username: str
    email: str
    _internal_id: int = 0  # Commence par _
    
    def __init__(self, **data):
        super().__init__(**data)
        self._internal_id = id(self)

user = User(username="alice", email="alice@example.com")
print(user.model_dump())
# {'username': 'alice', 'email': 'alice@example.com'}
# _internal_id n'est pas inclus


# === PrivateAttr ===
from pydantic import PrivateAttr

class User(BaseModel):
    username: str
    _password_hash: str = PrivateAttr()
    _login_count: int = PrivateAttr(default=0)
    
    def __init__(self, username: str, password: str, **data):
        super().__init__(username=username, **data)
        self._password_hash = self._hash_password(password)
    
    def _hash_password(self, password: str) -> str:
        # Simuler un hash
        return f"hashed_{password}"
    
    def login(self):
        self._login_count += 1


[OK] CALLBACKS & HOOKS


# === Model post_init ===
class User(BaseModel):
    name: str
    email: str
    _welcome_sent: bool = PrivateAttr(default=False)
    
    def model_post_init(self, __context):
        # Appelé après __init__
        if not self._welcome_sent:
            self._send_welcome_email()
            self._welcome_sent = True
    
    def _send_welcome_email(self):
        print(f"Welcome email sent to {self.email}")


# === Custom __init__ ===
class User(BaseModel):
    name: str
    email: str
    
    def __init__(self, **data):
        # Prétraitement
        if 'email' in data:
            data['email'] = data['email'].lower()
        
        super().__init__(**data)
        
        # Post-traitement
        print(f"User {self.name} created")


[OK] TYPE ADAPTERS


# === Valider types sans BaseModel ===
from pydantic import TypeAdapter

# Adapter pour liste
ListStrAdapter = TypeAdapter(List[str])
validated = ListStrAdapter.validate_python(['a', 'b', 'c'])
# validated = ListStrAdapter.validate_python(['a', 1, 'c'])  # ValidationError

# Adapter pour dict
DictAdapter = TypeAdapter(Dict[str, int])
validated = DictAdapter.validate_json('{"a": 1, "b": 2}')

# Adapter pour Union
UnionAdapter = TypeAdapter(Union[int, str])
result1 = UnionAdapter.validate_python(123)     # int
result2 = UnionAdapter.validate_python("hello") # str


# === Adapter avec types complexes ===
from typing import Annotated

PositiveIntAdapter = TypeAdapter(Annotated[int, Field(gt=0)])
validated = PositiveIntAdapter.validate_python(5)
# PositiveIntAdapter.validate_python(-5)  # ValidationError


[OK] ANNOTATIONS & METADATA


# === Annotated types ===
from typing import Annotated

# Alias pratiques
PositiveInt = Annotated[int, Field(gt=0)]
NonEmptyStr = Annotated[str, Field(min_length=1)]
Email = Annotated[str, Field(pattern=r'^[\w\.-]+@[\w\.-]+\.\w+)]

class User(BaseModel):
    id: PositiveInt
    name: NonEmptyStr
    email: Email
    age: Annotated[int, Field(ge=0, le=150)]


# === Metadata personnalisée ===
class User(BaseModel):
    name: str = Field(..., description="User's name", examples=["Alice", "Bob"])
    age: int = Field(..., ge=0, le=150, description="User's age in years")
    email: EmailStr = Field(..., description="Contact email")
    
    model_config = ConfigDict(
        json_schema_extra={
            "examples": [
                {
                    "name": "Alice",
                    "age": 30,
                    "email": "alice@example.com"
                }
            ]
        }
    )


[OK] PERFORMANCE & OPTIMISATION


# === Mode strict ===
class StrictUser(BaseModel):
    model_config = ConfigDict(strict=True)
    
    id: int     # N'accepte que int, pas de conversion depuis str
    age: int

# StrictUser(id="123", age=30)  # ValidationError en mode strict


# === Désactiver validation ===
class User(BaseModel):
    name: str
    age: int

# Construction sans validation (dangereux!)
user = User.model_construct(name="Alice", age=30)

# Utile pour données déjà validées (ex: depuis DB)
users = [User.model_construct(**row) for row in db_results]


# === Validation partielle ===
# Créer une instance avec des champs optionnels
data = {"name": "Alice"}  # age manquant

# Avec validation normale: ValidationError
# Avec model_construct: OK mais incomplet


# === Copie de modèle ===
user1 = User(name="Alice", age=30)

# Copie simple
user2 = user1.model_copy()

# Copie avec modifications
user3 = user1.model_copy(update={"age": 31})

# Copie profonde
user4 = user1.model_copy(deep=True)


[OK] INTÉGRATION AVEC ORMs


# === SQLAlchemy ===
from sqlalchemy import Column, Integer, String, create_engine
from sqlalchemy.orm import declarative_base, Session
from pydantic import ConfigDict

Base = declarative_base()

class UserDB(Base):
    __tablename__ = "users"
    
    id = Column(Integer, primary_key=True)
    name = Column(String)
    email = Column(String)

class UserSchema(BaseModel):
    model_config = ConfigDict(from_attributes=True)
    
    id: int
    name: str
    email: str

# Conversion ORM -> Pydantic
engine = create_engine("sqlite:///:memory:")
Base.metadata.create_all(engine)

with Session(engine) as session:
    db_user = UserDB(id=1, name="Alice", email="alice@example.com")
    session.add(db_user)
    session.commit()
    
    # Charger depuis DB
    db_user = session.query(UserDB).first()
    
    # Convertir en Pydantic
    pydantic_user = UserSchema.model_validate(db_user)


# === Django ORM ===
# models.py
from django.db import models

class User(models.Model):
    name = models.CharField(max_length=100)
    email = models.EmailField()

# schemas.py
class UserSchema(BaseModel):
    model_config = ConfigDict(from_attributes=True)
    
    id: int
    name: str
    email: str

# Utilisation
django_user = User.objects.get(id=1)
pydantic_user = UserSchema.model_validate(django_user)


[OK] FASTAPI INTÉGRATION


# === Modèles de requête/réponse ===
from fastapi import FastAPI, HTTPException
from typing import List

app = FastAPI()

class UserCreate(BaseModel):
    name: str = Field(..., min_length=1)
    email: EmailStr
    age: int = Field(..., ge=0, le=150)

class UserResponse(BaseModel):
    id: int
    name: str
    email: str
    age: int
    created_at: datetime = Field(default_factory=datetime.now)

class UserUpdate(BaseModel):
    name: Optional[str] = None
    email: Optional[EmailStr] = None
    age: Optional[int] = Field(None, ge=0, le=150)

# Endpoints
@app.post("/users/", response_model=UserResponse, status_code=201)
def create_user(user: UserCreate):
    # user est automatiquement validé
    new_user = UserResponse(
        id=1,
        name=user.name,
        email=user.email,
        age=user.age
    )
    return new_user

@app.get("/users/{user_id}", response_model=UserResponse)
def get_user(user_id: int):
    # Simuler récupération DB
    user = UserResponse(
        id=user_id,
        name="Alice",
        email="alice@example.com",
        age=30
    )
    return user

@app.get("/users/", response_model=List[UserResponse])
def list_users(skip: int = 0, limit: int = 10):
    # Liste d'utilisateurs
    return []

@app.patch("/users/{user_id}", response_model=UserResponse)
def update_user(user_id: int, user_update: UserUpdate):
    # Mise à jour partielle
    return UserResponse(
        id=user_id,
        name=user_update.name or "Default",
        email=user_update.email or "default@example.com",
        age=user_update.age or 0
    )


# === Response models avec exclude ===
class UserWithPassword(BaseModel):
    id: int
    username: str
    email: str
    password: str

@app.get("/users/{user_id}", response_model=UserWithPassword, response_model_exclude={"password"})
def get_user(user_id: int):
    return UserWithPassword(
        id=user_id,
        username="alice",
        email="alice@example.com",
        password="secret"
    )
    # Password sera exclu de la réponse


[OK] ERREURS & VALIDATION CUSTOM


# === Gestion d'erreurs personnalisée ===
from pydantic import ValidationError, field_validator

class User(BaseModel):
    username: str
    email: str
    
    @field_validator('username')
    @classmethod
    def username_valid(cls, v: str) -> str:
        if len(v) < 3:
            raise ValueError('Username must be at least 3 characters')
        if not v.isalnum():
            raise ValueError('Username must be alphanumeric')
        return v

try:
    user = User(username="ab", email="test@example.com")
except ValidationError as e:
    for error in e.errors():
        print(f"Field: {error['loc']}")
        print(f"Error: {error['msg']}")
        print(f"Type: {error['type']}")
        print(f"Input: {error['input']}")


# === PydanticCustomError ===
from pydantic_core import PydanticCustomError

class User(BaseModel):
    age: int
    
    @field_validator('age')
    @classmethod
    def validate_age(cls, v: int) -> int:
        if v < 0:
            raise PydanticCustomError(
                'negative_age',
                'Age cannot be negative, got {age}',
                {'age': v}
            )
        if v > 150:
            raise PydanticCustomError(
                'unrealistic_age',
                'Age seems unrealistic: {age}',
                {'age': v}
            )
        return v


# === Validator avec assert ===
class User(BaseModel):
    username: str
    
    @field_validator('username')
    @classmethod
    def check_username(cls, v: str) -> str:
        assert v.isalnum(), 'Username must be alphanumeric'
        assert len(v) >= 3, 'Username too short'
        return v


[OK] FORMATS DE DONNÉES SPÉCIAUX


# === UUID ===
from uuid import UUID, uuid4

class Resource(BaseModel):
    id: UUID = Field(default_factory=uuid4)
    name: str

resource = Resource(name="Test")
print(resource.id)  # UUID généré automatiquement

# Accepte string UUID
resource = Resource(id="123e4567-e89b-12d3-a456-426614174000", name="Test")


# === Decimal pour finances ===
from decimal import Decimal

class Product(BaseModel):
    name: str
    price: Decimal = Field(..., decimal_places=2)
    tax_rate: Decimal

product = Product(name="Book", price="19.99", tax_rate="0.20")
total = product.price * (1 + product.tax_rate)


# === Path ===
from pathlib import Path

class Config(BaseModel):
    config_file: FilePath        # Doit exister
    output_dir: DirectoryPath    # Doit exister
    log_file: Path               # N'importe quel chemin

config = Config(
    config_file="/etc/config.ini",
    output_dir="/tmp",
    log_file="/var/log/app.log"
)


# === Color ===
from pydantic import Color

class Theme(BaseModel):
    primary_color: Color
    secondary_color: Color

theme = Theme(
    primary_color="rgb(255, 0, 0)",
    secondary_color="#00FF00"
)
print(theme.primary_color.as_hex())  # #ff0000


# === AnyUrl variations ===
from pydantic import AnyUrl, HttpUrl, PostgresDsn, RedisDsn

class AppConfig(BaseModel):
    website: HttpUrl
    database: PostgresDsn
    cache: RedisDsn

config = AppConfig(
    website="https://example.com",
    database="postgresql://user:pass@localhost/dbname",
    cache="redis://localhost:6379/0"
)


[OK] PATTERNS AVANCÉS


# === Builder pattern ===
class UserBuilder:
    def __init__(self):
        self._data = {}
    
    def with_name(self, name: str) -> 'UserBuilder':
        self._data['name'] = name
        return self
    
    def with_email(self, email: str) -> 'UserBuilder':
        self._data['email'] = email
        return self
    
    def with_age(self, age: int) -> 'UserBuilder':
        self._data['age'] = age
        return self
    
    def build(self) -> User:
        return User(**self._data)

# Utilisation
user = (UserBuilder()
        .with_name("Alice")
        .with_email("alice@example.com")
        .with_age(30)
        .build())


# === Factory pattern ===
class UserFactory:
    @staticmethod
    def create_admin(name: str, email: str) -> 'AdminUser':
        return AdminUser(
            name=name,
            email=email,
            role="admin",
            permissions=["read", "write", "delete"]
        )
    
    @staticmethod
    def create_guest(name: str) -> 'GuestUser':
        return GuestUser(
            name=name,
            role="guest",
            permissions=["read"]
        )


# === Repository pattern ===
class UserRepository:
    def __init__(self):
        self._users: Dict[int, User] = {}
    
    def save(self, user: User) -> User:
        self._users[user.id] = user
        return user
    
    def get(self, user_id: int) -> Optional[User]:
        return self._users.get(user_id)
    
    def list(self) -> List[User]:
        return list(self._users.values())
    
    def delete(self, user_id: int) -> bool:
        if user_id in self._users:
            del self._users[user_id]
            return True
        return False


[OK] TESTS AVEC PYDANTIC


# === Pytest fixtures ===
import pytest

@pytest.fixture
def sample_user():
    return User(id=1, name="Alice", email="alice@example.com", age=30)

@pytest.fixture
def sample_user_data():
    return {
        "id": 1,
        "name": "Alice",
        "email": "alice@example.com",
        "age": 30
    }

def test_user_creation(sample_user_data):
    user = User(**sample_user_data)
    assert user.name == "Alice"
    assert user.age == 30

def test_user_validation():
    with pytest.raises(ValidationError):
        User(id="invalid", name="Bob", email="bob@example.com", age=30)

def test_user_serialization(sample_user):
    data = sample_user.model_dump()
    assert data["name"] == "Alice"
    
    json_str = sample_user.model_dump_json()
    assert "Alice" in json_str


# === Faker pour données de test ===
from faker import Faker

fake = Faker()

def create_random_user() -> User:
    return User(
        id=fake.random_int(1, 10000),
        name=fake.name(),
        email=fake.email(),
        age=fake.random_int(18, 80)
    )

def test_with_random_data():
    users = [create_random_user() for _ in range(100)]
    assert len(users) == 100
    assert all(isinstance(u, User) for u in users)


# === Hypothesis pour property-based testing ===
from hypothesis import given, strategies as st

@given(
    id=st.integers(min_value=1),
    name=st.text(min_size=1),
    age=st.integers(min_value=0, max_value=150)
)
def test_user_properties(id, name, age):
    try:
        user = User(id=id, name=name, email="test@example.com", age=age)
        assert user.id == id
        assert user.age >= 0
    except ValidationError:
        pass  # Certaines combinaisons peuvent être invalides


[OK] MIGRATION V1 -> V2


# === Différences principales ===

# V1
# class User(BaseModel):
#     name: str
#     
#     class Config:
#         orm_mode = True
#     
#     @validator('name')
#     def name_must_contain_space(cls, v):
#         if ' ' not in v:
#             raise ValueError('must contain a space')
#         return v.title()

# V2
class User(BaseModel):
    model_config = ConfigDict(from_attributes=True)  # orm_mode renommé
    
    name: str
    
    @field_validator('name')
    @classmethod
    def name_must_contain_space(cls, v: str) -> str:
        if ' ' not in v:
            raise ValueError('must contain a space')
        return v.title()


# === Autres changements ===
# V1: .dict()         -> V2: .model_dump()
# V1: .json()         -> V2: .model_dump_json()
# V1: .parse_obj()    -> V2: .model_validate()
# V1: .parse_raw()    -> V2: .model_validate_json()
# V1: .schema()       -> V2: .model_json_schema()
# V1: .copy()         -> V2: .model_copy()
# V1: .construct()    -> V2: .model_construct()

# V1: @validator      -> V2: @field_validator
# V1: @root_validator -> V2: @model_validator


[OK] BONNES PRATIQUES


# 1. Séparer modèles API et business logic
# schemas.py - Modèles API
class UserCreate(BaseModel):
    name: str
    email: EmailStr

class UserResponse(BaseModel):
    id: int
    name: str
    email: str

# models.py - Modèles métier
class User:
    def __init__(self, id: int, name: str, email: str):
        self.id = id
        self.name = name
        self.email = email


# 2. Utiliser Field pour documentation
class Product(BaseModel):
    name: str = Field(..., description="Product name", examples=["Laptop"])
    price: float = Field(..., gt=0, description="Price in EUR")


# 3. Validators simples et testables
@field_validator('email')
@classmethod
def validate_email(cls, v: str) -> str:
    v = v.lower().strip()
    if not v:
        raise ValueError('Email cannot be empty')
    return v


# 4. Utiliser Annotated pour types réutilisables
PositiveInt = Annotated[int, Field(gt=0)]
Email = Annotated[str, EmailStr]
NonEmptyStr = Annotated[str, Field(min_length=1)]


# 5. Config centralisée
class BaseConfig:
    model_config = ConfigDict(
        str_strip_whitespace=True,
        validate_assignment=True,
        use_enum_values=True
    )

class User(BaseModel, BaseConfig):
    name: str


# 6. Gestion d'erreurs explicite
try:
    user = User(**data)
except ValidationError as e:
    logger.error(f"Validation failed: {e.json()}")
    raise HTTPException(status_code=422, detail=e.errors())


# 7. Tests exhaustifs
def test_user_validation_errors():
    invalid_cases = [
        {"name": "", "email": "invalid"},
        {"name": "A", "email": "test@test.com", "age": -1},
        {"name": "Test", "email": "notemail", "age": 200},
    ]
    
    for case in invalid_cases:
        with pytest.raises(ValidationError):
            User(**case)


# 8. Documentation claire
class User(BaseModel):
    """
    User model representing a registered user.
    
    Attributes:
        id: Unique identifier
        name: Full name (3-100 characters)
        email: Valid email address
        age: Age in years (0-150)
    """
    id: int
    name: str = Field(..., min_length=3, max_length=100)
    email: EmailStr
    age: int = Field(..., ge=0, le=150)


[OK] RESSOURCES


# Documentation officielle:
# https://docs.pydantic.dev/

# Migration V1 -> V2:
# https://docs.pydantic.dev/latest/migration/

# Exemples:
# https://github.com/pydantic/pydantic/tree/main/docs/examples

# FastAPI + Pydantic:
# https://fastapi.tiangolo.com/

# Type hints:
# https://docs.python.org/3/library/typing.html

# Validation avancée:
# https://docs.pydantic.dev/latest/concepts/validators/,
            'DE': r'^\d{5}


# === Enum simple ===
from enum import Enum

class Status(str, Enum):
    PENDING = "pending"
    APPROVED = "approved"
    REJECTED = "rejected"

class Application(BaseModel):
    id: int
    status: Status

app = Application(id=1, status="pending")  # Converti en Status.PENDING
print(app.status)           # Status.PENDING
print(app.status.value)     # "pending"

# Sérialisation
print(app.model_dump())
# {'id': 1, 'status': 'pending'}  # Si use_enum_values=True dans config


# === Enum avec IntEnum ===
from enum import IntEnum

class Priority(IntEnum):
    LOW = 1
    MEDIUM = 2
    HIGH = 3
    CRITICAL = 4

class Task(BaseModel):
    title: str
    priority: Priority

task = Task(title="Fix bug", priority=3)
print(task.priority)  # Priority.HIGH


# === Utiliser valeurs enum ===
class User(BaseModel):
    model_config = ConfigDict(use_enum_values=True)
    
    role: Status

user = User(role="pending")
print(user.model_dump())
# {'role': 'pending'}  # Valeur au lieu de Status.PENDING


[OK] COMPUTED FIELDS


# === Champs calculés avec @computed_field ===
from pydantic import computed_field

class Rectangle(BaseModel):
    width: float
    height: float
    
    @computed_field
    @property
    def area(self) -> float:
        return self.width * self.height
    
    @computed_field
    @property
    def perimeter(self) -> float:
        return 2 * (self.width + self.height)

rect = Rectangle(width=10, height=5)
print(rect.area)        # 50.0
print(rect.perimeter)   # 30.0

# Dans la sérialisation
print(rect.model_dump())
# {'width': 10.0, 'height': 5.0, 'area': 50.0, 'perimeter': 30.0}


# === Computed field avec cache ===
from functools import cached_property

class ExpensiveComputation(BaseModel):
    value: int
    
    @computed_field
    @cached_property
    def expensive_result(self) -> int:
        # Calcul coûteux, mis en cache
        return sum(range(self.value))


# === Exclure computed fields ===
rect = Rectangle(width=10, height=5)
print(rect.model_dump(exclude={'area'}))
# {'width': 10.0, 'height': 5.0, 'perimeter': 30.0}


[OK] PROPRIÉTÉS PRIVÉES


# === Attributs privés ===
class User(BaseModel):
    model_config = ConfigDict(validate_assignment=True)
    
    username: str
    email: str
    _internal_id: int = 0  # Commence par _
    
    def __init__(self, **data):
        super().__init__(**data)
        self._internal_id = id(self)

user = User(username="alice", email="alice@example.com")
print(user.model_dump())
# {'username': 'alice', 'email': 'alice@example.com'}
# _internal_id n'est pas inclus


# === PrivateAttr ===
from pydantic import PrivateAttr

class User(BaseModel):
    username: str
    _password_hash: str = PrivateAttr()
    _login_count: int = PrivateAttr(default=0)
    
    def __init__(self, username: str, password: str, **data):
        super().__init__(username=username, **data)
        self._password_hash = self._hash_password(password)
    
    def _hash_password(self, password: str) -> str:
        # Simuler un hash
        return f"hashed_{password}"
    
    def login(self):
        self._login_count += 1


[OK] CALLBACKS & HOOKS


# === Model post_init ===
class User(BaseModel):
    name: str
    email: str
    _welcome_sent: bool = PrivateAttr(default=False)
    
    def model_post_init(self, __context):
        # Appelé après __init__
        if not self._welcome_sent:
            self._send_welcome_email()
            self._welcome_sent = True
    
    def _send_welcome_email(self):
        print(f"Welcome email sent to {self.email}")


# === Custom __init__ ===
class User(BaseModel):
    name: str
    email: str
    
    def __init__(self, **data):
        # Prétraitement
        if 'email' in data:
            data['email'] = data['email'].lower()
        
        super().__init__(**data)
        
        # Post-traitement
        print(f"User {self.name} created")


[OK] TYPE ADAPTERS


# === Valider types sans BaseModel ===
from pydantic import TypeAdapter

# Adapter pour liste
ListStrAdapter = TypeAdapter(List[str])
validated = ListStrAdapter.validate_python(['a', 'b', 'c'])
# validated = ListStrAdapter.validate_python(['a', 1, 'c'])  # ValidationError

# Adapter pour dict
DictAdapter = TypeAdapter(Dict[str, int])
validated = DictAdapter.validate_json('{"a": 1, "b": 2}')

# Adapter pour Union
UnionAdapter = TypeAdapter(Union[int, str])
result1 = UnionAdapter.validate_python(123)     # int
result2 = UnionAdapter.validate_python("hello") # str


# === Adapter avec types complexes ===
from typing import Annotated

PositiveIntAdapter = TypeAdapter(Annotated[int, Field(gt=0)])
validated = PositiveIntAdapter.validate_python(5)
# PositiveIntAdapter.validate_python(-5)  # ValidationError


[OK] ANNOTATIONS & METADATA


# === Annotated types ===
from typing import Annotated

# Alias pratiques
PositiveInt = Annotated[int, Field(gt=0)]
NonEmptyStr = Annotated[str, Field(min_length=1)]
Email = Annotated[str, Field(pattern=r'^[\w\.-]+@[\w\.-]+\.\w+)]

class User(BaseModel):
    id: PositiveInt
    name: NonEmptyStr
    email: Email
    age: Annotated[int, Field(ge=0, le=150)]


# === Metadata personnalisée ===
class User(BaseModel):
    name: str = Field(..., description="User's name", examples=["Alice", "Bob"])
    age: int = Field(..., ge=0, le=150, description="User's age in years")
    email: EmailStr = Field(..., description="Contact email")
    
    model_config = ConfigDict(
        json_schema_extra={
            "examples": [
                {
                    "name": "Alice",
                    "age": 30,
                    "email": "alice@example.com"
                }
            ]
        }
    )


[OK] PERFORMANCE & OPTIMISATION


# === Mode strict ===
class StrictUser(BaseModel):
    model_config = ConfigDict(strict=True)
    
    id: int     # N'accepte que int, pas de conversion depuis str
    age: int

# StrictUser(id="123", age=30)  # ValidationError en mode strict


# === Désactiver validation ===
class User(BaseModel):
    name: str
    age: int

# Construction sans validation (dangereux!)
user = User.model_construct(name="Alice", age=30)

# Utile pour données déjà validées (ex: depuis DB)
users = [User.model_construct(**row) for row in db_results]


# === Validation partielle ===
# Créer une instance avec des champs optionnels
data = {"name": "Alice"}  # age manquant

# Avec validation normale: ValidationError
# Avec model_construct: OK mais incomplet


# === Copie de modèle ===
user1 = User(name="Alice", age=30)

# Copie simple
user2 = user1.model_copy()

# Copie avec modifications
user3 = user1.model_copy(update={"age": 31})

# Copie profonde
user4 = user1.model_copy(deep=True)


[OK] INTÉGRATION AVEC ORMs


# === SQLAlchemy ===
from sqlalchemy import Column, Integer, String, create_engine
from sqlalchemy.orm import declarative_base, Session
from pydantic import ConfigDict

Base = declarative_base()

class UserDB(Base):
    __tablename__ = "users"
    
    id = Column(Integer, primary_key=True)
    name = Column(String)
    email = Column(String)

class UserSchema(BaseModel):
    model_config = ConfigDict(from_attributes=True)
    
    id: int
    name: str
    email: str

# Conversion ORM -> Pydantic
engine = create_engine("sqlite:///:memory:")
Base.metadata.create_all(engine)

with Session(engine) as session:
    db_user = UserDB(id=1, name="Alice", email="alice@example.com")
    session.add(db_user)
    session.commit()
    
    # Charger depuis DB
    db_user = session.query(UserDB).first()
    
    # Convertir en Pydantic
    pydantic_user = UserSchema.model_validate(db_user)


# === Django ORM ===
# models.py
from django.db import models

class User(models.Model):
    name = models.CharField(max_length=100)
    email = models.EmailField()

# schemas.py
class UserSchema(BaseModel):
    model_config = ConfigDict(from_attributes=True)
    
    id: int
    name: str
    email: str

# Utilisation
django_user = User.objects.get(id=1)
pydantic_user = UserSchema.model_validate(django_user)


[OK] FASTAPI INTÉGRATION


# === Modèles de requête/réponse ===
from fastapi import FastAPI, HTTPException
from typing import List

app = FastAPI()

class UserCreate(BaseModel):
    name: str = Field(..., min_length=1)
    email: EmailStr
    age: int = Field(..., ge=0, le=150)

class UserResponse(BaseModel):
    id: int
    name: str
    email: str
    age: int
    created_at: datetime = Field(default_factory=datetime.now)

class UserUpdate(BaseModel):
    name: Optional[str] = None
    email: Optional[EmailStr] = None
    age: Optional[int] = Field(None, ge=0, le=150)

# Endpoints
@app.post("/users/", response_model=UserResponse, status_code=201)
def create_user(user: UserCreate):
    # user est automatiquement validé
    new_user = UserResponse(
        id=1,
        name=user.name,
        email=user.email,
        age=user.age
    )
    return new_user

@app.get("/users/{user_id}", response_model=UserResponse)
def get_user(user_id: int):
    # Simuler récupération DB
    user = UserResponse(
        id=user_id,
        name="Alice",
        email="alice@example.com",
        age=30
    )
    return user

@app.get("/users/", response_model=List[UserResponse])
def list_users(skip: int = 0, limit: int = 10):
    # Liste d'utilisateurs
    return []

@app.patch("/users/{user_id}", response_model=UserResponse)
def update_user(user_id: int, user_update: UserUpdate):
    # Mise à jour partielle
    return UserResponse(
        id=user_id,
        name=user_update.name or "Default",
        email=user_update.email or "default@example.com",
        age=user_update.age or 0
    )


# === Response models avec exclude ===
class UserWithPassword(BaseModel):
    id: int
    username: str
    email: str
    password: str

@app.get("/users/{user_id}", response_model=UserWithPassword, response_model_exclude={"password"})
def get_user(user_id: int):
    return UserWithPassword(
        id=user_id,
        username="alice",
        email="alice@example.com",
        password="secret"
    )
    # Password sera exclu de la réponse


[OK] ERREURS & VALIDATION CUSTOM


# === Gestion d'erreurs personnalisée ===
from pydantic import ValidationError, field_validator

class User(BaseModel):
    username: str
    email: str
    
    @field_validator('username')
    @classmethod
    def username_valid(cls, v: str) -> str:
        if len(v) < 3:
            raise ValueError('Username must be at least 3 characters')
        if not v.isalnum():
            raise ValueError('Username must be alphanumeric')
        return v

try:
    user = User(username="ab", email="test@example.com")
except ValidationError as e:
    for error in e.errors():
        print(f"Field: {error['loc']}")
        print(f"Error: {error['msg']}")
        print(f"Type: {error['type']}")
        print(f"Input: {error['input']}")


# === PydanticCustomError ===
from pydantic_core import PydanticCustomError

class User(BaseModel):
    age: int
    
    @field_validator('age')
    @classmethod
    def validate_age(cls, v: int) -> int:
        if v < 0:
            raise PydanticCustomError(
                'negative_age',
                'Age cannot be negative, got {age}',
                {'age': v}
            )
        if v > 150:
            raise PydanticCustomError(
                'unrealistic_age',
                'Age seems unrealistic: {age}',
                {'age': v}
            )
        return v


# === Validator avec assert ===
class User(BaseModel):
    username: str
    
    @field_validator('username')
    @classmethod
    def check_username(cls, v: str) -> str:
        assert v.isalnum(), 'Username must be alphanumeric'
        assert len(v) >= 3, 'Username too short'
        return v


[OK] FORMATS DE DONNÉES SPÉCIAUX


# === UUID ===
from uuid import UUID, uuid4

class Resource(BaseModel):
    id: UUID = Field(default_factory=uuid4)
    name: str

resource = Resource(name="Test")
print(resource.id)  # UUID généré automatiquement

# Accepte string UUID
resource = Resource(id="123e4567-e89b-12d3-a456-426614174000", name="Test")


# === Decimal pour finances ===
from decimal import Decimal

class Product(BaseModel):
    name: str
    price: Decimal = Field(..., decimal_places=2)
    tax_rate: Decimal

product = Product(name="Book", price="19.99", tax_rate="0.20")
total = product.price * (1 + product.tax_rate)


# === Path ===
from pathlib import Path

class Config(BaseModel):
    config_file: FilePath        # Doit exister
    output_dir: DirectoryPath    # Doit exister
    log_file: Path               # N'importe quel chemin

config = Config(
    config_file="/etc/config.ini",
    output_dir="/tmp",
    log_file="/var/log/app.log"
)


# === Color ===
from pydantic import Color

class Theme(BaseModel):
    primary_color: Color
    secondary_color: Color

theme = Theme(
    primary_color="rgb(255, 0, 0)",
    secondary_color="#00FF00"
)
print(theme.primary_color.as_hex())  # #ff0000


# === AnyUrl variations ===
from pydantic import AnyUrl, HttpUrl, PostgresDsn, RedisDsn

class AppConfig(BaseModel):
    website: HttpUrl
    database: PostgresDsn
    cache: RedisDsn

config = AppConfig(
    website="https://example.com",
    database="postgresql://user:pass@localhost/dbname",
    cache="redis://localhost:6379/0"
)


[OK] PATTERNS AVANCÉS


# === Builder pattern ===
class UserBuilder:
    def __init__(self):
        self._data = {}
    
    def with_name(self, name: str) -> 'UserBuilder':
        self._data['name'] = name
        return self
    
    def with_email(self, email: str) -> 'UserBuilder':
        self._data['email'] = email
        return self
    
    def with_age(self, age: int) -> 'UserBuilder':
        self._data['age'] = age
        return self
    
    def build(self) -> User:
        return User(**self._data)

# Utilisation
user = (UserBuilder()
        .with_name("Alice")
        .with_email("alice@example.com")
        .with_age(30)
        .build())


# === Factory pattern ===
class UserFactory:
    @staticmethod
    def create_admin(name: str, email: str) -> 'AdminUser':
        return AdminUser(
            name=name,
            email=email,
            role="admin",
            permissions=["read", "write", "delete"]
        )
    
    @staticmethod
    def create_guest(name: str) -> 'GuestUser':
        return GuestUser(
            name=name,
            role="guest",
            permissions=["read"]
        )


# === Repository pattern ===
class UserRepository:
    def __init__(self):
        self._users: Dict[int, User] = {}
    
    def save(self, user: User) -> User:
        self._users[user.id] = user
        return user
    
    def get(self, user_id: int) -> Optional[User]:
        return self._users.get(user_id)
    
    def list(self) -> List[User]:
        return list(self._users.values())
    
    def delete(self, user_id: int) -> bool:
        if user_id in self._users:
            del self._users[user_id]
            return True
        return False


[OK] TESTS AVEC PYDANTIC


# === Pytest fixtures ===
import pytest

@pytest.fixture
def sample_user():
    return User(id=1, name="Alice", email="alice@example.com", age=30)

@pytest.fixture
def sample_user_data():
    return {
        "id": 1,
        "name": "Alice",
        "email": "alice@example.com",
        "age": 30
    }

def test_user_creation(sample_user_data):
    user = User(**sample_user_data)
    assert user.name == "Alice"
    assert user.age == 30

def test_user_validation():
    with pytest.raises(ValidationError):
        User(id="invalid", name="Bob", email="bob@example.com", age=30)

def test_user_serialization(sample_user):
    data = sample_user.model_dump()
    assert data["name"] == "Alice"
    
    json_str = sample_user.model_dump_json()
    assert "Alice" in json_str


# === Faker pour données de test ===
from faker import Faker

fake = Faker()

def create_random_user() -> User:
    return User(
        id=fake.random_int(1, 10000),
        name=fake.name(),
        email=fake.email(),
        age=fake.random_int(18, 80)
    )

def test_with_random_data():
    users = [create_random_user() for _ in range(100)]
    assert len(users) == 100
    assert all(isinstance(u, User) for u in users)


# === Hypothesis pour property-based testing ===
from hypothesis import given, strategies as st

@given(
    id=st.integers(min_value=1),
    name=st.text(min_size=1),
    age=st.integers(min_value=0, max_value=150)
)
def test_user_properties(id, name, age):
    try:
        user = User(id=id, name=name, email="test@example.com", age=age)
        assert user.id == id
        assert user.age >= 0
    except ValidationError:
        pass  # Certaines combinaisons peuvent être invalides


[OK] MIGRATION V1 -> V2


# === Différences principales ===

# V1
# class User(BaseModel):
#     name: str
#     
#     class Config:
#         orm_mode = True
#     
#     @validator('name')
#     def name_must_contain_space(cls, v):
#         if ' ' not in v:
#             raise ValueError('must contain a space')
#         return v.title()

# V2
class User(BaseModel):
    model_config = ConfigDict(from_attributes=True)  # orm_mode renommé
    
    name: str
    
    @field_validator('name')
    @classmethod
    def name_must_contain_space(cls, v: str) -> str:
        if ' ' not in v:
            raise ValueError('must contain a space')
        return v.title()


# === Autres changements ===
# V1: .dict()         -> V2: .model_dump()
# V1: .json()         -> V2: .model_dump_json()
# V1: .parse_obj()    -> V2: .model_validate()
# V1: .parse_raw()    -> V2: .model_validate_json()
# V1: .schema()       -> V2: .model_json_schema()
# V1: .copy()         -> V2: .model_copy()
# V1: .construct()    -> V2: .model_construct()

# V1: @validator      -> V2: @field_validator
# V1: @root_validator -> V2: @model_validator


[OK] BONNES PRATIQUES


# 1. Séparer modèles API et business logic
# schemas.py - Modèles API
class UserCreate(BaseModel):
    name: str
    email: EmailStr

class UserResponse(BaseModel):
    id: int
    name: str
    email: str

# models.py - Modèles métier
class User:
    def __init__(self, id: int, name: str, email: str):
        self.id = id
        self.name = name
        self.email = email


# 2. Utiliser Field pour documentation
class Product(BaseModel):
    name: str = Field(..., description="Product name", examples=["Laptop"])
    price: float = Field(..., gt=0, description="Price in EUR")


# 3. Validators simples et testables
@field_validator('email')
@classmethod
def validate_email(cls, v: str) -> str:
    v = v.lower().strip()
    if not v:
        raise ValueError('Email cannot be empty')
    return v


# 4. Utiliser Annotated pour types réutilisables
PositiveInt = Annotated[int, Field(gt=0)]
Email = Annotated[str, EmailStr]
NonEmptyStr = Annotated[str, Field(min_length=1)]


# 5. Config centralisée
class BaseConfig:
    model_config = ConfigDict(
        str_strip_whitespace=True,
        validate_assignment=True,
        use_enum_values=True
    )

class User(BaseModel, BaseConfig):
    name: str


# 6. Gestion d'erreurs explicite
try:
    user = User(**data)
except ValidationError as e:
    logger.error(f"Validation failed: {e.json()}")
    raise HTTPException(status_code=422, detail=e.errors())


# 7. Tests exhaustifs
def test_user_validation_errors():
    invalid_cases = [
        {"name": "", "email": "invalid"},
        {"name": "A", "email": "test@test.com", "age": -1},
        {"name": "Test", "email": "notemail", "age": 200},
    ]
    
    for case in invalid_cases:
        with pytest.raises(ValidationError):
            User(**case)


# 8. Documentation claire
class User(BaseModel):
    """
    User model representing a registered user.
    
    Attributes:
        id: Unique identifier
        name: Full name (3-100 characters)
        email: Valid email address
        age: Age in years (0-150)
    """
    id: int
    name: str = Field(..., min_length=3, max_length=100)
    email: EmailStr
    age: int = Field(..., ge=0, le=150)


[OK] RESSOURCES


# Documentation officielle:
# https://docs.pydantic.dev/

# Migration V1 -> V2:
# https://docs.pydantic.dev/latest/migration/

# Exemples:
# https://github.com/pydantic/pydantic/tree/main/docs/examples

# FastAPI + Pydantic:
# https://fastapi.tiangolo.com/

# Type hints:
# https://docs.python.org/3/library/typing.html

# Validation avancée:
# https://docs.pydantic.dev/latest/concepts/validators/,
        }
        
        if self.country in patterns:
            if not re.match(patterns[self.country], self.postal_code):
                raise ValueError(f'Invalid postal code for {self.country}')
        
        return self


[OK] PERFORMANCE - TECHNIQUES AVANCÉES


# === Lazy loading de champs coûteux ===
class UserWithStats(BaseModel):
    id: int
    name: str
    _stats: Optional[Dict] = PrivateAttr(default=None)
    
    @property
    def stats(self) -> Dict:
        if self._stats is None:
            # Calcul coûteux uniquement si demandé
            self._stats = self._compute_stats()
        return self._stats
    
    def _compute_stats(self) -> Dict:
        # Simuler calcul coûteux
        return {'posts': 100, 'followers': 500}


# === Caching de validation ===
from functools import lru_cache

class CachedValidator:
    @staticmethod
    @lru_cache(maxsize=1000)
    def validate_email_cached(email: str) -> str:
        # Validation coûteuse (ex: check DNS)
        if '@' not in email:
            raise ValueError('Invalid email')
        return email.lower()

class User(BaseModel):
    email: str
    
    @field_validator('email')
    @classmethod
    def validate_email(cls, v: str) -> str:
        return CachedValidator.validate_email_cached(v)


# === Batch validation ===
class BatchValidator:
    @staticmethod
    def validate_many(model_cls: Type[BaseModel], items: List[dict]) -> tuple[List, List]:
        """Valide plusieurs items et retourne (valides, erreurs)"""
        valid = []
        errors = []
        
        for i, item in enumerate(items):
            try:
                validated = model_cls.model_validate(item)
                valid.append(validated)
            except ValidationError as e:
                errors.append({'index': i, 'item': item, 'errors': e.errors()})
        
        return valid, errors

# Utilisation
data = [
    {'name': 'Alice', 'email': 'alice@ex.com', 'age': 30},
    {'name': 'Bob', 'email': 'invalid', 'age': 25},
    {'name': 'Charlie', 'email': 'charlie@ex.com', 'age': -5}
]

valid_users, validation_errors = BatchValidator.validate_many(User, data)


# === Validation partielle pour updates ===
def partial_model(model_cls: Type[BaseModel]) -> Type[BaseModel]:
    """Crée version où tous les champs sont optionnels"""
    from typing import Optional, get_type_hints
    
    fields = {}
    for field_name, field_info in model_cls.model_fields.items():
        annotation = field_info.annotation
        fields[field_name] = (Optional[annotation], None)
    
    return type(
        f'Partial{model_cls.__name__}',
        (BaseModel,),
        {
            '__annotations__': fields,
            'model_config': model_cls.model_config
        }
    )

PartialUser = partial_model(User)

# Pour update - seulement les champs fournis
update_data = {'name': 'NewName'}  # age et email non fournis
partial = PartialUser(**update_data)


[OK] TYPES COMPLEXES & EDGE CASES


# === Union de types avec None ===
from typing import Union

class FlexibleModel(BaseModel):
    # Accepte int, str, ou None
    value: Union[int, str, None] = None
    
    @field_validator('value')
    @classmethod
    def normalize_value(cls, v):
        if v is None:
            return None
        if isinstance(v, str) and v.isdigit():
            return int(v)
        return v


# === Literal types ===
from typing import Literal

class APIRequest(BaseModel):
    method: Literal["GET", "POST", "PUT", "DELETE"]
    endpoint: str

request = APIRequest(method="GET", endpoint="/users")
# request = APIRequest(method="PATCH", endpoint="/users")  # [X] ValidationError


# === TypedDict compatibility ===
from typing import TypedDict

class UserDict(TypedDict):
    name: str
    age: int

class User(BaseModel):
    name: str
    age: int
    
    @classmethod
    def from_typed_dict(cls, data: UserDict) -> 'User':
        return cls(**data)


# === NewType ===
from typing import NewType

UserId = NewType('UserId', int)
EmailAddress = NewType('EmailAddress', str)

class User(BaseModel):
    id: UserId
    email: EmailAddress
    
    # Note: NewType est transparente pour Pydantic
    # C'est juste pour le type checking


# === Callable types ===
from typing import Callable

class EventHandler(BaseModel):
    model_config = ConfigDict(arbitrary_types_allowed=True)
    
    name: str
    handler: Callable[[dict], None]
    
    def trigger(self, data: dict):
        self.handler(data)

def my_handler(data: dict):
    print(f"Handling: {data}")

handler = EventHandler(name="test", handler=my_handler)
handler.trigger({'key': 'value'})


# === Protocol types (structural subtyping) ===
from typing import Protocol, runtime_checkable

@runtime_checkable
class Drawable(Protocol):
    def draw(self) -> str:
        ...

class Circle:
    def draw(self) -> str:
        return "Drawing circle"

class Canvas(BaseModel):
    model_config = ConfigDict(arbitrary_types_allowed=True)
    
    shape: Drawable
    
    @field_validator('shape')
    @classmethod
    def check_drawable(cls, v):
        if not isinstance(v, Drawable):
            raise ValueError('Must implement Drawable protocol')
        return v

canvas = Canvas(shape=Circle())


[OK] MIGRATION & VERSIONING


# === Versionning d'API ===
class UserV1(BaseModel):
    name: str
    email: str

class UserV2(BaseModel):
    first_name: str
    last_name: str
    email: str
    
    @classmethod
    def from_v1(cls, v1: UserV1) -> 'UserV2':
        # Migration V1 -> V2
        parts = v1.name.split(' ', 1)
        return cls(
            first_name=parts[0],
            last_name=parts[1] if len(parts) > 1 else '',
            email=v1.email
        )
    
    def to_v1(self) -> UserV1:
        # Downgrade V2 -> V1
        return UserV1(
            name=f"{self.first_name} {self.last_name}".strip(),
            email=self.email
        )


# === Schema evolution ===
class UserSchema(BaseModel):
    version: Literal[1, 2] = 2
    
    # V2 fields
    first_name: Optional[str] = None
    last_name: Optional[str] = None
    
    # V1 field (deprecated)
    name: Optional[str] = None
    
    email: str
    
    @model_validator(mode='after')
    def migrate_if_needed(self) -> 'UserSchema':
        if self.version == 1 and self.name:
            # Auto-migrate from V1
            parts = self.name.split(' ', 1)
            self.first_name = parts[0]
            self.last_name = parts[1] if len(parts) > 1 else ''
            self.version = 2
        return self


# === Backward compatibility ===
class User(BaseModel):
    name: str
    email: str
    # Nouveau champ avec default pour compat
    phone: Optional[str] = None
    
    # Champ renommé avec alias pour compat
    user_type: str = Field(default="regular", validation_alias='type')
    
    @classmethod
    def model_validate(cls, obj, **kwargs):
        # Hook pour migrations automatiques
        if isinstance(obj, dict):
            # Rename old field names
            if 'username' in obj and 'name' not in obj:
                obj['name'] = obj.pop('username')
        return super().model_validate(obj, **kwargs)


# === Enum simple ===
from enum import Enum

class Status(str, Enum):
    PENDING = "pending"
    APPROVED = "approved"
    REJECTED = "rejected"

class Application(BaseModel):
    id: int
    status: Status

app = Application(id=1, status="pending")  # Converti en Status.PENDING
print(app.status)           # Status.PENDING
print(app.status.value)     # "pending"

# Sérialisation
print(app.model_dump())
# {'id': 1, 'status': 'pending'}  # Si use_enum_values=True dans config


# === Enum avec IntEnum ===
from enum import IntEnum

class Priority(IntEnum):
    LOW = 1
    MEDIUM = 2
    HIGH = 3
    CRITICAL = 4

class Task(BaseModel):
    title: str
    priority: Priority

task = Task(title="Fix bug", priority=3)
print(task.priority)  # Priority.HIGH


# === Utiliser valeurs enum ===
class User(BaseModel):
    model_config = ConfigDict(use_enum_values=True)
    
    role: Status

user = User(role="pending")
print(user.model_dump())
# {'role': 'pending'}  # Valeur au lieu de Status.PENDING


[OK] COMPUTED FIELDS


# === Champs calculés avec @computed_field ===
from pydantic import computed_field

class Rectangle(BaseModel):
    width: float
    height: float
    
    @computed_field
    @property
    def area(self) -> float:
        return self.width * self.height
    
    @computed_field
    @property
    def perimeter(self) -> float:
        return 2 * (self.width + self.height)

rect = Rectangle(width=10, height=5)
print(rect.area)        # 50.0
print(rect.perimeter)   # 30.0

# Dans la sérialisation
print(rect.model_dump())
# {'width': 10.0, 'height': 5.0, 'area': 50.0, 'perimeter': 30.0}


# === Computed field avec cache ===
from functools import cached_property

class ExpensiveComputation(BaseModel):
    value: int
    
    @computed_field
    @cached_property
    def expensive_result(self) -> int:
        # Calcul coûteux, mis en cache
        return sum(range(self.value))


# === Exclure computed fields ===
rect = Rectangle(width=10, height=5)
print(rect.model_dump(exclude={'area'}))
# {'width': 10.0, 'height': 5.0, 'perimeter': 30.0}


[OK] PROPRIÉTÉS PRIVÉES


# === Attributs privés ===
class User(BaseModel):
    model_config = ConfigDict(validate_assignment=True)
    
    username: str
    email: str
    _internal_id: int = 0  # Commence par _
    
    def __init__(self, **data):
        super().__init__(**data)
        self._internal_id = id(self)

user = User(username="alice", email="alice@example.com")
print(user.model_dump())
# {'username': 'alice', 'email': 'alice@example.com'}
# _internal_id n'est pas inclus


# === PrivateAttr ===
from pydantic import PrivateAttr

class User(BaseModel):
    username: str
    _password_hash: str = PrivateAttr()
    _login_count: int = PrivateAttr(default=0)
    
    def __init__(self, username: str, password: str, **data):
        super().__init__(username=username, **data)
        self._password_hash = self._hash_password(password)
    
    def _hash_password(self, password: str) -> str:
        # Simuler un hash
        return f"hashed_{password}"
    
    def login(self):
        self._login_count += 1


[OK] CALLBACKS & HOOKS


# === Model post_init ===
class User(BaseModel):
    name: str
    email: str
    _welcome_sent: bool = PrivateAttr(default=False)
    
    def model_post_init(self, __context):
        # Appelé après __init__
        if not self._welcome_sent:
            self._send_welcome_email()
            self._welcome_sent = True
    
    def _send_welcome_email(self):
        print(f"Welcome email sent to {self.email}")


# === Custom __init__ ===
class User(BaseModel):
    name: str
    email: str
    
    def __init__(self, **data):
        # Prétraitement
        if 'email' in data:
            data['email'] = data['email'].lower()
        
        super().__init__(**data)
        
        # Post-traitement
        print(f"User {self.name} created")


[OK] TYPE ADAPTERS


# === Valider types sans BaseModel ===
from pydantic import TypeAdapter

# Adapter pour liste
ListStrAdapter = TypeAdapter(List[str])
validated = ListStrAdapter.validate_python(['a', 'b', 'c'])
# validated = ListStrAdapter.validate_python(['a', 1, 'c'])  # ValidationError

# Adapter pour dict
DictAdapter = TypeAdapter(Dict[str, int])
validated = DictAdapter.validate_json('{"a": 1, "b": 2}')

# Adapter pour Union
UnionAdapter = TypeAdapter(Union[int, str])
result1 = UnionAdapter.validate_python(123)     # int
result2 = UnionAdapter.validate_python("hello") # str


# === Adapter avec types complexes ===
from typing import Annotated

PositiveIntAdapter = TypeAdapter(Annotated[int, Field(gt=0)])
validated = PositiveIntAdapter.validate_python(5)
# PositiveIntAdapter.validate_python(-5)  # ValidationError


[OK] ANNOTATIONS & METADATA


# === Annotated types ===
from typing import Annotated

# Alias pratiques
PositiveInt = Annotated[int, Field(gt=0)]
NonEmptyStr = Annotated[str, Field(min_length=1)]
Email = Annotated[str, Field(pattern=r'^[\w\.-]+@[\w\.-]+\.\w+)]

class User(BaseModel):
    id: PositiveInt
    name: NonEmptyStr
    email: Email
    age: Annotated[int, Field(ge=0, le=150)]


# === Metadata personnalisée ===
class User(BaseModel):
    name: str = Field(..., description="User's name", examples=["Alice", "Bob"])
    age: int = Field(..., ge=0, le=150, description="User's age in years")
    email: EmailStr = Field(..., description="Contact email")
    
    model_config = ConfigDict(
        json_schema_extra={
            "examples": [
                {
                    "name": "Alice",
                    "age": 30,
                    "email": "alice@example.com"
                }
            ]
        }
    )


[OK] PERFORMANCE & OPTIMISATION


# === Mode strict ===
class StrictUser(BaseModel):
    model_config = ConfigDict(strict=True)
    
    id: int     # N'accepte que int, pas de conversion depuis str
    age: int

# StrictUser(id="123", age=30)  # ValidationError en mode strict


# === Désactiver validation ===
class User(BaseModel):
    name: str
    age: int

# Construction sans validation (dangereux!)
user = User.model_construct(name="Alice", age=30)

# Utile pour données déjà validées (ex: depuis DB)
users = [User.model_construct(**row) for row in db_results]


# === Validation partielle ===
# Créer une instance avec des champs optionnels
data = {"name": "Alice"}  # age manquant

# Avec validation normale: ValidationError
# Avec model_construct: OK mais incomplet


# === Copie de modèle ===
user1 = User(name="Alice", age=30)

# Copie simple
user2 = user1.model_copy()

# Copie avec modifications
user3 = user1.model_copy(update={"age": 31})

# Copie profonde
user4 = user1.model_copy(deep=True)


[OK] INTÉGRATION AVEC ORMs


# === SQLAlchemy ===
from sqlalchemy import Column, Integer, String, create_engine
from sqlalchemy.orm import declarative_base, Session
from pydantic import ConfigDict

Base = declarative_base()

class UserDB(Base):
    __tablename__ = "users"
    
    id = Column(Integer, primary_key=True)
    name = Column(String)
    email = Column(String)

class UserSchema(BaseModel):
    model_config = ConfigDict(from_attributes=True)
    
    id: int
    name: str
    email: str

# Conversion ORM -> Pydantic
engine = create_engine("sqlite:///:memory:")
Base.metadata.create_all(engine)

with Session(engine) as session:
    db_user = UserDB(id=1, name="Alice", email="alice@example.com")
    session.add(db_user)
    session.commit()
    
    # Charger depuis DB
    db_user = session.query(UserDB).first()
    
    # Convertir en Pydantic
    pydantic_user = UserSchema.model_validate(db_user)


# === Django ORM ===
# models.py
from django.db import models

class User(models.Model):
    name = models.CharField(max_length=100)
    email = models.EmailField()

# schemas.py
class UserSchema(BaseModel):
    model_config = ConfigDict(from_attributes=True)
    
    id: int
    name: str
    email: str

# Utilisation
django_user = User.objects.get(id=1)
pydantic_user = UserSchema.model_validate(django_user)


[OK] FASTAPI INTÉGRATION


# === Modèles de requête/réponse ===
from fastapi import FastAPI, HTTPException
from typing import List

app = FastAPI()

class UserCreate(BaseModel):
    name: str = Field(..., min_length=1)
    email: EmailStr
    age: int = Field(..., ge=0, le=150)

class UserResponse(BaseModel):
    id: int
    name: str
    email: str
    age: int
    created_at: datetime = Field(default_factory=datetime.now)

class UserUpdate(BaseModel):
    name: Optional[str] = None
    email: Optional[EmailStr] = None
    age: Optional[int] = Field(None, ge=0, le=150)

# Endpoints
@app.post("/users/", response_model=UserResponse, status_code=201)
def create_user(user: UserCreate):
    # user est automatiquement validé
    new_user = UserResponse(
        id=1,
        name=user.name,
        email=user.email,
        age=user.age
    )
    return new_user

@app.get("/users/{user_id}", response_model=UserResponse)
def get_user(user_id: int):
    # Simuler récupération DB
    user = UserResponse(
        id=user_id,
        name="Alice",
        email="alice@example.com",
        age=30
    )
    return user

@app.get("/users/", response_model=List[UserResponse])
def list_users(skip: int = 0, limit: int = 10):
    # Liste d'utilisateurs
    return []

@app.patch("/users/{user_id}", response_model=UserResponse)
def update_user(user_id: int, user_update: UserUpdate):
    # Mise à jour partielle
    return UserResponse(
        id=user_id,
        name=user_update.name or "Default",
        email=user_update.email or "default@example.com",
        age=user_update.age or 0
    )


# === Response models avec exclude ===
class UserWithPassword(BaseModel):
    id: int
    username: str
    email: str
    password: str

@app.get("/users/{user_id}", response_model=UserWithPassword, response_model_exclude={"password"})
def get_user(user_id: int):
    return UserWithPassword(
        id=user_id,
        username="alice",
        email="alice@example.com",
        password="secret"
    )
    # Password sera exclu de la réponse


[OK] ERREURS & VALIDATION CUSTOM


# === Gestion d'erreurs personnalisée ===
from pydantic import ValidationError, field_validator

class User(BaseModel):
    username: str
    email: str
    
    @field_validator('username')
    @classmethod
    def username_valid(cls, v: str) -> str:
        if len(v) < 3:
            raise ValueError('Username must be at least 3 characters')
        if not v.isalnum():
            raise ValueError('Username must be alphanumeric')
        return v

try:
    user = User(username="ab", email="test@example.com")
except ValidationError as e:
    for error in e.errors():
        print(f"Field: {error['loc']}")
        print(f"Error: {error['msg']}")
        print(f"Type: {error['type']}")
        print(f"Input: {error['input']}")


# === PydanticCustomError ===
from pydantic_core import PydanticCustomError

class User(BaseModel):
    age: int
    
    @field_validator('age')
    @classmethod
    def validate_age(cls, v: int) -> int:
        if v < 0:
            raise PydanticCustomError(
                'negative_age',
                'Age cannot be negative, got {age}',
                {'age': v}
            )
        if v > 150:
            raise PydanticCustomError(
                'unrealistic_age',
                'Age seems unrealistic: {age}',
                {'age': v}
            )
        return v


# === Validator avec assert ===
class User(BaseModel):
    username: str
    
    @field_validator('username')
    @classmethod
    def check_username(cls, v: str) -> str:
        assert v.isalnum(), 'Username must be alphanumeric'
        assert len(v) >= 3, 'Username too short'
        return v


[OK] FORMATS DE DONNÉES SPÉCIAUX


# === UUID ===
from uuid import UUID, uuid4

class Resource(BaseModel):
    id: UUID = Field(default_factory=uuid4)
    name: str

resource = Resource(name="Test")
print(resource.id)  # UUID généré automatiquement

# Accepte string UUID
resource = Resource(id="123e4567-e89b-12d3-a456-426614174000", name="Test")


# === Decimal pour finances ===
from decimal import Decimal

class Product(BaseModel):
    name: str
    price: Decimal = Field(..., decimal_places=2)
    tax_rate: Decimal

product = Product(name="Book", price="19.99", tax_rate="0.20")
total = product.price * (1 + product.tax_rate)


# === Path ===
from pathlib import Path

class Config(BaseModel):
    config_file: FilePath        # Doit exister
    output_dir: DirectoryPath    # Doit exister
    log_file: Path               # N'importe quel chemin

config = Config(
    config_file="/etc/config.ini",
    output_dir="/tmp",
    log_file="/var/log/app.log"
)


# === Color ===
from pydantic import Color

class Theme(BaseModel):
    primary_color: Color
    secondary_color: Color

theme = Theme(
    primary_color="rgb(255, 0, 0)",
    secondary_color="#00FF00"
)
print(theme.primary_color.as_hex())  # #ff0000


# === AnyUrl variations ===
from pydantic import AnyUrl, HttpUrl, PostgresDsn, RedisDsn

class AppConfig(BaseModel):
    website: HttpUrl
    database: PostgresDsn
    cache: RedisDsn

config = AppConfig(
    website="https://example.com",
    database="postgresql://user:pass@localhost/dbname",
    cache="redis://localhost:6379/0"
)


[OK] PATTERNS AVANCÉS


# === Builder pattern ===
class UserBuilder:
    def __init__(self):
        self._data = {}
    
    def with_name(self, name: str) -> 'UserBuilder':
        self._data['name'] = name
        return self
    
    def with_email(self, email: str) -> 'UserBuilder':
        self._data['email'] = email
        return self
    
    def with_age(self, age: int) -> 'UserBuilder':
        self._data['age'] = age
        return self
    
    def build(self) -> User:
        return User(**self._data)

# Utilisation
user = (UserBuilder()
        .with_name("Alice")
        .with_email("alice@example.com")
        .with_age(30)
        .build())


# === Factory pattern ===
class UserFactory:
    @staticmethod
    def create_admin(name: str, email: str) -> 'AdminUser':
        return AdminUser(
            name=name,
            email=email,
            role="admin",
            permissions=["read", "write", "delete"]
        )
    
    @staticmethod
    def create_guest(name: str) -> 'GuestUser':
        return GuestUser(
            name=name,
            role="guest",
            permissions=["read"]
        )


# === Repository pattern ===
class UserRepository:
    def __init__(self):
        self._users: Dict[int, User] = {}
    
    def save(self, user: User) -> User:
        self._users[user.id] = user
        return user
    
    def get(self, user_id: int) -> Optional[User]:
        return self._users.get(user_id)
    
    def list(self) -> List[User]:
        return list(self._users.values())
    
    def delete(self, user_id: int) -> bool:
        if user_id in self._users:
            del self._users[user_id]
            return True
        return False


[OK] TESTS AVEC PYDANTIC


# === Pytest fixtures ===
import pytest

@pytest.fixture
def sample_user():
    return User(id=1, name="Alice", email="alice@example.com", age=30)

@pytest.fixture
def sample_user_data():
    return {
        "id": 1,
        "name": "Alice",
        "email": "alice@example.com",
        "age": 30
    }

def test_user_creation(sample_user_data):
    user = User(**sample_user_data)
    assert user.name == "Alice"
    assert user.age == 30

def test_user_validation():
    with pytest.raises(ValidationError):
        User(id="invalid", name="Bob", email="bob@example.com", age=30)

def test_user_serialization(sample_user):
    data = sample_user.model_dump()
    assert data["name"] == "Alice"
    
    json_str = sample_user.model_dump_json()
    assert "Alice" in json_str


# === Faker pour données de test ===
from faker import Faker

fake = Faker()

def create_random_user() -> User:
    return User(
        id=fake.random_int(1, 10000),
        name=fake.name(),
        email=fake.email(),
        age=fake.random_int(18, 80)
    )

def test_with_random_data():
    users = [create_random_user() for _ in range(100)]
    assert len(users) == 100
    assert all(isinstance(u, User) for u in users)


# === Hypothesis pour property-based testing ===
from hypothesis import given, strategies as st

@given(
    id=st.integers(min_value=1),
    name=st.text(min_size=1),
    age=st.integers(min_value=0, max_value=150)
)
def test_user_properties(id, name, age):
    try:
        user = User(id=id, name=name, email="test@example.com", age=age)
        assert user.id == id
        assert user.age >= 0
    except ValidationError:
        pass  # Certaines combinaisons peuvent être invalides


[OK] MIGRATION V1 -> V2


# === Différences principales ===

# V1
# class User(BaseModel):
#     name: str
#     
#     class Config:
#         orm_mode = True
#     
#     @validator('name')
#     def name_must_contain_space(cls, v):
#         if ' ' not in v:
#             raise ValueError('must contain a space')
#         return v.title()

# V2
class User(BaseModel):
    model_config = ConfigDict(from_attributes=True)  # orm_mode renommé
    
    name: str
    
    @field_validator('name')
    @classmethod
    def name_must_contain_space(cls, v: str) -> str:
        if ' ' not in v:
            raise ValueError('must contain a space')
        return v.title()


# === Autres changements ===
# V1: .dict()         -> V2: .model_dump()
# V1: .json()         -> V2: .model_dump_json()
# V1: .parse_obj()    -> V2: .model_validate()
# V1: .parse_raw()    -> V2: .model_validate_json()
# V1: .schema()       -> V2: .model_json_schema()
# V1: .copy()         -> V2: .model_copy()
# V1: .construct()    -> V2: .model_construct()

# V1: @validator      -> V2: @field_validator
# V1: @root_validator -> V2: @model_validator


[OK] BONNES PRATIQUES


# 1. Séparer modèles API et business logic
# schemas.py - Modèles API
class UserCreate(BaseModel):
    name: str
    email: EmailStr

class UserResponse(BaseModel):
    id: int
    name: str
    email: str

# models.py - Modèles métier
class User:
    def __init__(self, id: int, name: str, email: str):
        self.id = id
        self.name = name
        self.email = email


# 2. Utiliser Field pour documentation
class Product(BaseModel):
    name: str = Field(..., description="Product name", examples=["Laptop"])
    price: float = Field(..., gt=0, description="Price in EUR")


# 3. Validators simples et testables
@field_validator('email')
@classmethod
def validate_email(cls, v: str) -> str:
    v = v.lower().strip()
    if not v:
        raise ValueError('Email cannot be empty')
    return v


# 4. Utiliser Annotated pour types réutilisables
PositiveInt = Annotated[int, Field(gt=0)]
Email = Annotated[str, EmailStr]
NonEmptyStr = Annotated[str, Field(min_length=1)]


# 5. Config centralisée
class BaseConfig:
    model_config = ConfigDict(
        str_strip_whitespace=True,
        validate_assignment=True,
        use_enum_values=True
    )

class User(BaseModel, BaseConfig):
    name: str


# 6. Gestion d'erreurs explicite
try:
    user = User(**data)
except ValidationError as e:
    logger.error(f"Validation failed: {e.json()}")
    raise HTTPException(status_code=422, detail=e.errors())


# 7. Tests exhaustifs
def test_user_validation_errors():
    invalid_cases = [
        {"name": "", "email": "invalid"},
        {"name": "A", "email": "test@test.com", "age": -1},
        {"name": "Test", "email": "notemail", "age": 200},
    ]
    
    for case in invalid_cases:
        with pytest.raises(ValidationError):
            User(**case)


# 8. Documentation claire
class User(BaseModel):
    """
    User model representing a registered user.
    
    Attributes:
        id: Unique identifier
        name: Full name (3-100 characters)
        email: Valid email address
        age: Age in years (0-150)
    """
    id: int
    name: str = Field(..., min_length=3, max_length=100)
    email: EmailStr
    age: int = Field(..., ge=0, le=150)


[OK] RESSOURCES


# Documentation officielle:
# https://docs.pydantic.dev/

# Migration V1 -> V2:
# https://docs.pydantic.dev/latest/migration/

# Exemples:
# https://github.com/pydantic/pydantic/tree/main/docs/examples

# FastAPI + Pydantic:
# https://fastapi.tiangolo.com/

# Type hints:
# https://docs.python.org/3/library/typing.html

# Validation avancée:
# https://docs.pydantic.dev/latest/concepts/validators/