Modèles supplémentaires¶
🌐 Traduction par IA et humains
Cette traduction a été réalisée par une IA guidée par des humains. 🤝
Elle peut contenir des erreurs d'interprétation du sens original, ou paraître peu naturelle, etc. 🤖
Vous pouvez améliorer cette traduction en nous aidant à mieux guider le LLM d'IA.
En poursuivant l'exemple précédent, il est courant d'avoir plusieurs modèles liés.
C'est particulièrement vrai pour les modèles d'utilisateur, car :
- Le modèle d'entrée doit pouvoir contenir un mot de passe.
- Le modèle de sortie ne doit pas avoir de mot de passe.
- Le modèle de base de données devra probablement avoir un mot de passe haché.
Danger
Ne stockez jamais les mots de passe des utilisateurs en clair. Stockez toujours un « hachage sécurisé » que vous pourrez ensuite vérifier.
Si vous ne savez pas ce que c'est, vous apprendrez ce qu'est un « hachage de mot de passe » dans les chapitres sur la sécurité.
Utiliser plusieurs modèles¶
Voici une idée générale de l'apparence des modèles avec leurs champs de mot de passe et les endroits où ils sont utilisés :
from fastapi import FastAPI
from pydantic import BaseModel, EmailStr
app = FastAPI()
class UserIn(BaseModel):
username: str
password: str
email: EmailStr
full_name: str | None = None
class UserOut(BaseModel):
username: str
email: EmailStr
full_name: str | None = None
class UserInDB(BaseModel):
username: str
hashed_password: str
email: EmailStr
full_name: str | None = None
def fake_password_hasher(raw_password: str):
return "supersecret" + raw_password
def fake_save_user(user_in: UserIn):
hashed_password = fake_password_hasher(user_in.password)
user_in_db = UserInDB(**user_in.model_dump(), hashed_password=hashed_password)
print("User saved! ..not really")
return user_in_db
@app.post("/user/", response_model=UserOut)
async def create_user(user_in: UserIn):
user_saved = fake_save_user(user_in)
return user_saved
À propos de **user_in.model_dump()¶
La méthode .model_dump() de Pydantic¶
user_in est un modèle Pydantic de classe UserIn.
Les modèles Pydantic ont une méthode .model_dump() qui renvoie un dict avec les données du modèle.
Ainsi, si nous créons un objet Pydantic user_in comme :
user_in = UserIn(username="john", password="secret", email="john.doe@example.com")
et que nous appelons ensuite :
user_dict = user_in.model_dump()
nous avons maintenant un dict avec les données dans la variable user_dict (c'est un dict au lieu d'un objet modèle Pydantic).
Et si nous appelons :
print(user_dict)
nous obtiendrions un dict Python contenant :
{
'username': 'john',
'password': 'secret',
'email': 'john.doe@example.com',
'full_name': None,
}
Déballer un dict¶
Si nous prenons un dict comme user_dict et que nous le passons à une fonction (ou une classe) avec **user_dict, Python va « déballer » ce dict. Il passera les clés et valeurs de user_dict directement comme arguments nommés.
Ainsi, en reprenant user_dict ci-dessus, écrire :
UserInDB(**user_dict)
aurait pour résultat quelque chose d'équivalent à :
UserInDB(
username="john",
password="secret",
email="john.doe@example.com",
full_name=None,
)
Ou plus exactement, en utilisant user_dict directement, quels que soient ses contenus futurs :
UserInDB(
username = user_dict["username"],
password = user_dict["password"],
email = user_dict["email"],
full_name = user_dict["full_name"],
)
Créer un modèle Pydantic à partir du contenu d'un autre¶
Comme dans l'exemple ci-dessus nous avons obtenu user_dict depuis user_in.model_dump(), ce code :
user_dict = user_in.model_dump()
UserInDB(**user_dict)
serait équivalent à :
UserInDB(**user_in.model_dump())
... parce que user_in.model_dump() est un dict, et nous demandons ensuite à Python de « déballer » ce dict en le passant à UserInDB précédé de **.
Ainsi, nous obtenons un modèle Pydantic à partir des données d'un autre modèle Pydantic.
Déballer un dict et ajouter des mots-clés supplémentaires¶
Et en ajoutant ensuite l'argument nommé supplémentaire hashed_password=hashed_password, comme ici :
UserInDB(**user_in.model_dump(), hashed_password=hashed_password)
... revient à :
UserInDB(
username = user_dict["username"],
password = user_dict["password"],
email = user_dict["email"],
full_name = user_dict["full_name"],
hashed_password = hashed_password,
)
Alertes
Les fonctions auxiliaires fake_password_hasher et fake_save_user ne servent qu'à démontrer un flux de données possible, mais elles n'offrent évidemment aucune sécurité réelle.
Réduire la duplication¶
Réduire la duplication de code est l'une des idées centrales de FastAPI.
La duplication de code augmente les risques de bogues, de problèmes de sécurité, de désynchronisation du code (lorsque vous mettez à jour un endroit mais pas les autres), etc.
Et ces modèles partagent beaucoup de données et dupliquent des noms et types d'attributs.
Nous pouvons faire mieux.
Nous pouvons déclarer un modèle UserBase qui sert de base à nos autres modèles. Ensuite, nous pouvons créer des sous-classes de ce modèle qui héritent de ses attributs (déclarations de type, validation, etc.).
Toutes les conversions de données, validations, documentation, etc., fonctionneront comme d'habitude.
De cette façon, nous pouvons ne déclarer que les différences entre les modèles (avec password en clair, avec hashed_password et sans mot de passe) :
from fastapi import FastAPI
from pydantic import BaseModel, EmailStr
app = FastAPI()
class UserBase(BaseModel):
username: str
email: EmailStr
full_name: str | None = None
class UserIn(UserBase):
password: str
class UserOut(UserBase):
pass
class UserInDB(UserBase):
hashed_password: str
def fake_password_hasher(raw_password: str):
return "supersecret" + raw_password
def fake_save_user(user_in: UserIn):
hashed_password = fake_password_hasher(user_in.password)
user_in_db = UserInDB(**user_in.model_dump(), hashed_password=hashed_password)
print("User saved! ..not really")
return user_in_db
@app.post("/user/", response_model=UserOut)
async def create_user(user_in: UserIn):
user_saved = fake_save_user(user_in)
return user_saved
Union ou anyOf¶
Vous pouvez déclarer qu'une réponse est l'Union de deux types ou plus, ce qui signifie que la réponse peut être n'importe lequel d'entre eux.
Cela sera défini dans OpenAPI avec anyOf.
Pour ce faire, utilisez l'annotation de type Python standard typing.Union :
Remarque
Lors de la définition d'une Union, incluez d'abord le type le plus spécifique, suivi du type le moins spécifique. Dans l'exemple ci-dessous, le type le plus spécifique PlaneItem précède CarItem dans Union[PlaneItem, CarItem].
from fastapi import FastAPI
from pydantic import BaseModel
app = FastAPI()
class BaseItem(BaseModel):
description: str
type: str
class CarItem(BaseItem):
type: str = "car"
class PlaneItem(BaseItem):
type: str = "plane"
size: int
items = {
"item1": {"description": "All my friends drive a low rider", "type": "car"},
"item2": {
"description": "Music is my aeroplane, it's my aeroplane",
"type": "plane",
"size": 5,
},
}
@app.get("/items/{item_id}", response_model=PlaneItem | CarItem)
async def read_item(item_id: str):
return items[item_id]
Union en Python 3.10¶
Dans cet exemple, nous passons Union[PlaneItem, CarItem] comme valeur de l'argument response_model.
Comme nous le passons comme valeur d'un argument au lieu de l'utiliser dans une annotation de type, nous devons utiliser Union même en Python 3.10.
S'il s'agissait d'une annotation de type, nous pourrions utiliser la barre verticale, comme :
some_variable: PlaneItem | CarItem
Mais si nous écrivons cela dans l'affectation response_model=PlaneItem | CarItem, nous obtiendrons une erreur, car Python essaierait d'effectuer une « opération invalide » entre PlaneItem et CarItem au lieu de l'interpréter comme une annotation de type.
Liste de modèles¶
De la même manière, vous pouvez déclarer des réponses contenant des listes d'objets.
Pour cela, utilisez le list Python standard :
from fastapi import FastAPI
from pydantic import BaseModel
app = FastAPI()
class Item(BaseModel):
name: str
description: str
items = [
{"name": "Foo", "description": "There comes my hero"},
{"name": "Red", "description": "It's my aeroplane"},
]
@app.get("/items/", response_model=list[Item])
async def read_items():
return items
Réponse avec un dict arbitraire¶
Vous pouvez également déclarer une réponse en utilisant un simple dict arbitraire, en déclarant uniquement le type des clés et des valeurs, sans utiliser de modèle Pydantic.
C'est utile si vous ne connaissez pas à l'avance les noms de champs/attributs valides (qui seraient nécessaires pour un modèle Pydantic).
Dans ce cas, vous pouvez utiliser dict :
from fastapi import FastAPI
app = FastAPI()
@app.get("/keyword-weights/", response_model=dict[str, float])
async def read_keyword_weights():
return {"foo": 2.3, "bar": 3.4}
Récapitulatif¶
Utilisez plusieurs modèles Pydantic et héritez librement selon chaque cas.
Vous n'avez pas besoin d'avoir un seul modèle de données par entité si cette entité doit pouvoir avoir différents « états ». Comme pour l'« entité » utilisateur, avec un état incluant password, password_hash et sans mot de passe.