Extramodelle¶
Im Anschluss an das vorherige Beispiel ist es üblich, mehr als ein zusammenhängendes Modell zu haben.
Dies gilt insbesondere für Benutzermodelle, denn:
- Das Eingabemodell muss ein Passwort enthalten können.
- Das Ausgabemodell sollte kein Passwort haben.
- Das Datenbankmodell müsste wahrscheinlich ein gehashtes Passwort haben.
Gefahr
Speichern Sie niemals das Klartextpasswort eines Benutzers. Speichern Sie immer einen „sicheren Hash“, den Sie dann verifizieren können.
Wenn Sie nicht wissen, was das ist, werden Sie in den Sicherheitskapiteln lernen, was ein „Passworthash“ ist.
Mehrere Modelle¶
Hier ist eine allgemeine Idee, wie die Modelle mit ihren Passwortfeldern aussehen könnten und an welchen Stellen sie verwendet werden:
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.dict(), 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
🤓 Other versions and variants
from typing import Union
from fastapi import FastAPI
from pydantic import BaseModel, EmailStr
app = FastAPI()
class UserIn(BaseModel):
username: str
password: str
email: EmailStr
full_name: Union[str, None] = None
class UserOut(BaseModel):
username: str
email: EmailStr
full_name: Union[str, None] = None
class UserInDB(BaseModel):
username: str
hashed_password: str
email: EmailStr
full_name: Union[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.dict(), 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
Info
In Pydantic v1 hieß die Methode .dict()
, in Pydantic v2 wurde sie deprecatet (aber weiterhin unterstützt) und in .model_dump()
umbenannt.
Die Beispiele hier verwenden .dict()
für die Kompatibilität mit Pydantic v1, aber Sie sollten .model_dump()
verwenden, wenn Sie Pydantic v2 verwenden können.
Über **user_in.dict()
¶
Die .dict()
-Methode von Pydantic¶
user_in
ist ein Pydantic-Modell der Klasse UserIn
.
Pydantic-Modelle haben eine .dict()
-Methode, die ein dict
mit den Daten des Modells zurückgibt.
Wenn wir also ein Pydantic-Objekt user_in
erstellen, etwa so:
user_in = UserIn(username="john", password="secret", email="john.doe@example.com")
und dann aufrufen:
user_dict = user_in.dict()
haben wir jetzt ein dict
mit den Daten in der Variablen user_dict
(es ist ein dict
statt eines Pydantic-Modellobjekts).
Und wenn wir aufrufen:
print(user_dict)
würden wir ein Python-dict
erhalten mit:
{
'username': 'john',
'password': 'secret',
'email': 'john.doe@example.com',
'full_name': None,
}
Ein dict
entpacken¶
Wenn wir ein dict
wie user_dict
nehmen und es einer Funktion (oder Klasse) mit **user_dict
übergeben, wird Python es „entpacken“. Es wird die Schlüssel und Werte von user_dict
direkt als Schlüsselwort-Argumente übergeben.
Setzen wir also das user_dict
von oben ein:
UserInDB(**user_dict)
so ist das äquivalent zu:
UserInDB(
username="john",
password="secret",
email="john.doe@example.com",
full_name=None,
)
Oder genauer gesagt, dazu, user_dict
direkt zu verwenden, mit welchen Inhalten es auch immer in der Zukunft haben mag:
UserInDB(
username = user_dict["username"],
password = user_dict["password"],
email = user_dict["email"],
full_name = user_dict["full_name"],
)
Ein Pydantic-Modell aus dem Inhalt eines anderen¶
Da wir im obigen Beispiel user_dict
von user_in.dict()
bekommen haben, wäre dieser Code:
user_dict = user_in.dict()
UserInDB(**user_dict)
gleichwertig zu:
UserInDB(**user_in.dict())
... weil user_in.dict()
ein dict
ist, und dann lassen wir Python es „entpacken“, indem wir es an UserInDB
mit vorangestelltem **
übergeben.
Auf diese Weise erhalten wir ein Pydantic-Modell aus den Daten eines anderen Pydantic-Modells.
Ein dict
entpacken und zusätzliche Schlüsselwort-Argumente¶
Und dann fügen wir das zusätzliche Schlüsselwort-Argument hashed_password=hashed_password
hinzu, wie in:
UserInDB(**user_in.dict(), hashed_password=hashed_password)
... was so ist wie:
UserInDB(
username = user_dict["username"],
password = user_dict["password"],
email = user_dict["email"],
full_name = user_dict["full_name"],
hashed_password = hashed_password,
)
Achtung
Die unterstützenden zusätzlichen Funktionen fake_password_hasher
und fake_save_user
dienen nur zur Demo eines möglichen Datenflusses, bieten jedoch natürlich keine echte Sicherheit.
Verdopplung vermeiden¶
Die Reduzierung von Code-Verdoppelung ist eine der Kernideen von FastAPI.
Da die Verdopplung von Code die Wahrscheinlichkeit von Fehlern, Sicherheitsproblemen, Problemen mit der Desynchronisation des Codes (wenn Sie an einer Stelle, aber nicht an der anderen aktualisieren) usw. erhöht.
Und diese Modelle teilen alle eine Menge der Daten und verdoppeln Attributnamen und -typen.
Wir könnten es besser machen.
Wir können ein UserBase
-Modell deklarieren, das als Basis für unsere anderen Modelle dient. Und dann können wir Unterklassen dieses Modells erstellen, die seine Attribute (Typdeklarationen, Validierung usw.) erben.
Die ganze Datenkonvertierung, -validierung, -dokumentation usw. wird immer noch wie gewohnt funktionieren.
Auf diese Weise können wir nur die Unterschiede zwischen den Modellen (mit Klartext-password
, mit hashed_password
und ohne Passwort) deklarieren:
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.dict(), 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
🤓 Other versions and variants
from typing import Union
from fastapi import FastAPI
from pydantic import BaseModel, EmailStr
app = FastAPI()
class UserBase(BaseModel):
username: str
email: EmailStr
full_name: Union[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.dict(), 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
oder anyOf
¶
Sie können deklarieren, dass eine Response eine Union
mehrerer Typen ist, das bedeutet, dass die Response einer von ihnen ist.
Dies wird in OpenAPI mit anyOf
definiert.
Um das zu tun, verwenden Sie den Standard-Python-Typhinweis typing.Union
:
Hinweis
Wenn Sie eine Union
definieren, listen Sie den spezifischeren Typ zuerst auf, gefolgt vom weniger spezifischen Typ. Im Beispiel unten steht PlaneItem
vor CarItem
in Union[PlaneItem, CarItem]
.
from typing import Union
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=Union[PlaneItem, CarItem])
async def read_item(item_id: str):
return items[item_id]
🤓 Other versions and variants
from typing import Union
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=Union[PlaneItem, CarItem])
async def read_item(item_id: str):
return items[item_id]
Union
in Python 3.10¶
In diesem Beispiel übergeben wir Union[PlaneItem, CarItem]
als Wert des Arguments response_model
.
Da wir es als Wert an ein Argument übergeben, anstatt es in einer Typannotation zu verwenden, müssen wir Union
verwenden, sogar in Python 3.10.
Wäre es eine Typannotation gewesen, hätten wir den vertikalen Strich verwenden können, wie in:
some_variable: PlaneItem | CarItem
Aber wenn wir das in der Zuweisung response_model=PlaneItem | CarItem
machen, würden wir einen Fehler erhalten, weil Python versuchen würde, eine ungültige Operation zwischen PlaneItem
und CarItem
auszuführen, anstatt es als Typannotation zu interpretieren.
Liste von Modellen¶
Auf die gleiche Weise können Sie Responses von Listen von Objekten deklarieren.
Dafür verwenden Sie Pythons Standard-typing.List
(oder nur list
in Python 3.9 und höher):
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
🤓 Other versions and variants
from typing import List
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
Response mit beliebigem dict
¶
Sie können auch eine Response deklarieren, die ein beliebiges dict
zurückgibt, indem Sie nur die Typen der Schlüssel und Werte ohne ein Pydantic-Modell deklarieren.
Dies ist nützlich, wenn Sie die gültigen Feld-/Attributnamen nicht im Voraus kennen (die für ein Pydantic-Modell benötigt werden würden).
In diesem Fall können Sie typing.Dict
verwenden (oder nur dict
in Python 3.9 und höher):
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}
🤓 Other versions and variants
from typing import 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}
Zusammenfassung¶
Verwenden Sie gerne mehrere Pydantic-Modelle und vererben Sie je nach Bedarf.
Sie brauchen kein einzelnes Datenmodell pro Einheit, wenn diese Einheit in der Lage sein muss, verschiedene „Zustände“ zu haben. Wie im Fall der Benutzer-„Einheit“ mit einem Zustand einschließlich password
, password_hash
und ohne Passwort.