Code source de pygroundedweb.models.base

"""Classes de base pour les modèles Pydantic utilisés par le client.

Contient la classe `APIModel` qui étend `pydantic.BaseModel` avec :
- gestion des champs mutables/immuables par instance,
- référence vers le client API pour opérations (`refresh`).

Ces docstrings servent à produire une documentation claire des comportements attendus
lors de l'utilisation des objets retournés par l'API.
"""

from abc import ABC
from typing import Set, Optional, TYPE_CHECKING

from pydantic import BaseModel, PrivateAttr, model_validator

if TYPE_CHECKING:
    from ..client.base import APIModelClient


[docs] class APIModel(BaseModel, ABC): """Base pour les modèles sérialisés provenant de l'API Grounded Web. Attributs principaux : pk: identifiant numérique si présent. _mutable_fields: ensemble des noms de champs modifiables pour cette instance. _client: référence (privée) vers le client API permettant d'effectuer des opérations serveur. Comportement important : - Le constructeur accepte `mutable_fields` ou `immutable_fields` dans les données pour contrôler quels attributs peuvent être modifiés localement via setattr. - La méthode `refresh()` permet de rafraîchir l'objet à partir du serveur en utilisant le client attaché. """ pk: Optional[int] = None _mutable_fields: Set[str] = PrivateAttr(default_factory=set) _client: Optional['APIModelClient'] = PrivateAttr(default_factory=set) def __init__( self, *, mutable_fields: set[str] = None, immutable_fields: set[str] = None, **data ): all_fields = set(self.__class__.model_fields.keys()) if mutable_fields is not None: final_mutable = mutable_fields elif immutable_fields is not None: final_mutable = all_fields - set(immutable_fields) else: final_mutable = all_fields self._validate_fields_exist(final_mutable, label="mutable_fields") if immutable_fields is not None: self._validate_fields_exist(immutable_fields, label="immutable_fields") data.pop('mutable_fields', None) data.pop('immutable_fields', None) super().__init__(**data) object.__setattr__(self, '_mutable_fields', final_mutable) @model_validator(mode="after") def _set_mutability(self) -> 'APIModel': """Validator Pydantic qui met à jour `_mutable_fields` après création. Ce validator permet d'accepter des indicateurs `mutable_fields`/`immutable_fields` venant directement du JSON serveur. """ data = self.model_extra or {} mutable_fields = data.get("mutable_fields") immutable_fields = data.get("immutable_fields") all_fields = set(self.__class__.model_fields.keys()) if mutable_fields is not None: final_mutable = set(mutable_fields) elif immutable_fields is not None: final_mutable = all_fields - set(immutable_fields) else: final_mutable = all_fields self._validate_fields_exist(final_mutable, label="mutable_fields") if immutable_fields is not None: self._validate_fields_exist(set(immutable_fields), label="immutable_fields") object.__setattr__(self, "_mutable_fields", final_mutable) return self def __setattr__(self, name: str, value) -> None: """Empêche la modification d'un champ non mutable pour cette instance. Lève `AttributeError` si on tente d'affecter un champ présent mais non mutable. """ if hasattr(self, name) and name not in self._mutable_fields: raise AttributeError(f"Field '{name}' is immutable for this instance") super().__setattr__(name, value) def _validate_fields_exist(self, fields: set[str], label: str) -> None: """Valide que les champs donnés existent dans le modèle. Args: fields: ensemble des noms de champs à vérifier. label: libellé à utiliser dans les messages d'erreur. """ all_fields = set(self.__class__.model_fields.keys()) unknown = set(fields) - all_fields if unknown: raise ValueError(f"Unknown field(s) in {label}: {unknown}")
[docs] def is_mutable(self, field: str) -> bool: """Retourne True si le champ donné est modifiable pour cette instance. Args: field: nom du champ à tester. """ self._validate_fields_exist({field}, label="is_mutable") return field in self._mutable_fields
[docs] def refresh(self) -> None: """Rafraîchit l'instance actuelle avec les données du serveur. Cette méthode utilise le client attaché (`_client`) et l'attribut `pk` pour récupérer la version la plus récente du serveur et mettre à jour l'objet local. Raises: RuntimeError: si le client n'est pas attaché ou si `pk` est None. """ if not self._client or self.pk is None: raise RuntimeError("Refresh impossible : client manquant ou pk nulle.") fresh_obj = self._client.retrieve(self.pk) self.__dict__.update(fresh_obj.__dict__) object.__setattr__(self, "_mutable_fields", fresh_obj._mutable_fields)