Extramodelle¶
Fahren wir beim letzten Beispiel fort. Es gibt normalerweise mehrere zusammengehörende Modelle.
Insbesondere Benutzermodelle, denn:
- Das hereinkommende Modell sollte ein Passwort haben können.
- Das herausgehende Modell sollte kein Passwort haben.
- Das Datenbankmodell sollte wahrscheinlich ein gehashtes Passwort haben.
Gefahr
Speichern Sie niemals das Klartext-Passwort eines Benutzers. Speichern Sie immer den „sicheren Hash“, den Sie verifizieren können.
Falls Ihnen das nichts sagt, in den Sicherheits-Kapiteln werden Sie lernen, was ein „Passwort-Hash“ ist.
Mehrere Modelle¶
Hier der generelle Weg, wie die Modelle mit ihren Passwort-Feldern aussehen könnten, und an welchen Orten sie verwendet werden würden.
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
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ß diese Methode .dict()
, in Pydantic v2 wurde sie deprecated (aber immer noch unterstützt) und in .model_dump()
umbenannt.
Die Beispiele hier verwenden .dict()
für die Kompatibilität mit Pydantic v1, Sie sollten jedoch stattdessen .model_dump()
verwenden, wenn Sie Pydantic v2 verwenden können.
Über **user_in.dict()
¶
Pydantic's .dict()
¶
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 wir rufen seine .dict()
-Methode auf:
user_dict = user_in.dict()
dann haben wir jetzt in der Variable user_dict
ein dict
mit den gleichen Daten (es ist ein dict
statt eines Pydantic-Modellobjekts).
Wenn wir es ausgeben:
print(user_dict)
bekommen wir ein Python-dict
:
{
'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 Klassenmethode) mittels **user_dict
übergeben, wird Python es „entpacken“. Es wird die Schlüssel und Werte von user_dict
direkt als Schlüsselwort-Argumente übergeben.
Wenn wir also das user_dict
von oben nehmen und schreiben:
UserInDB(**user_dict)
dann ist das ungefähr äquivalent zu:
UserInDB(
username="john",
password="secret",
email="john.doe@example.com",
full_name=None,
)
Oder, präziser, user_dict
wird direkt verwendet, welche Werte es auch immer 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 den Inhalten eines anderen erstellen.¶
Da wir in obigem Beispiel user_dict
mittels user_in.dict()
erzeugt haben, ist dieser Code:
user_dict = user_in.dict()
UserInDB(**user_dict)
äquivalent zu:
UserInDB(**user_in.dict())
... weil user_in.dict()
ein dict
ist, und dann lassen wir Python es „entpacken“, indem wir es UserInDB
übergeben, mit vorangestelltem **
.
Wir erhalten also ein Pydantic-Modell aus den Daten eines anderen Pydantic-Modells.
Ein dict
entpacken und zusätzliche Schlüsselwort-Argumente¶
Und dann fügen wir ein noch weiteres Schlüsselwort-Argument hinzu, hashed_password=hashed_password
:
UserInDB(**user_in.dict(), hashed_password=hashed_password)
... was am Ende ergibt:
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 Hilfsfunktionen fake_password_hasher
und fake_save_user
demonstrieren nur den möglichen Fluss der Daten und bieten natürlich keine echte Sicherheit.
Verdopplung vermeiden¶
Reduzierung von Code-Verdoppelung ist eine der Kern-Ideen von FastAPI.
Weil Verdoppelung von Code die Wahrscheinlichkeit von Fehlern, Sicherheitsproblemen, Desynchronisation (Code wird nur an einer Stelle verändert, aber nicht an einer anderen), usw. erhöht.
Unsere Modelle teilen alle eine Menge der Daten und verdoppeln Attribut-Namen und -Typen.
Das können wir besser machen.
Wir deklarieren ein UserBase
-Modell, das als Basis für unsere anderen Modelle dient. Dann können wir Unterklassen erstellen, die seine Attribute (Typdeklarationen, Validierungen, usw.) erben.
Die ganze Datenkonvertierung, -validierung, -dokumentation, usw. wird immer noch wie gehabt funktionieren.
Auf diese Weise beschreiben wir nur noch die Unterschiede zwischen den Modellen (mit Klartext-password
, mit hashed_password
, und ohne Passwort):
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
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, sprich, einer dieser Typen.
Das wird in OpenAPI mit anyOf
angezeigt.
Um das zu tun, verwenden Sie Pythons Standard-Typhinweis typing.Union
:
Hinweis
Listen Sie, wenn Sie eine Union
definieren, denjenigen Typ zuerst, der am spezifischsten ist, gefolgt von den weniger spezifischen Typen. Im Beispiel oben, in Union[PlaneItem, CarItem]
also den spezifischeren PlaneItem
vor dem weniger spezifischen 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]
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 dem Argument response_model
den Wert Union[PlaneItem, CarItem]
.
Da wir es als Wert einem Argument überreichen, statt es als Typannotation zu verwenden, müssen wir Union
verwenden, selbst in Python 3.10.
Wenn es eine Typannotation gewesen wäre, hätten wir auch den vertikalen Trennstrich verwenden können, wie in:
some_variable: PlaneItem | CarItem
Aber wenn wir das in der Zuweisung response_model=PlaneItem | CarItem
machen, erhalten wir eine Fehlermeldung, da Python versucht, eine ungültige Operation zwischen PlaneItem
und CarItem
durchzuführen, statt es als Typannotation zu interpretieren.
Listen von Modellen¶
Genauso können Sie eine Response deklarieren, die eine Liste von Objekten ist.
Verwenden Sie dafür Pythons Standard typing.List
(oder nur list
in Python 3.9 und darüber):
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
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önne auch eine Response deklarieren, die ein beliebiges dict
zurückgibt, bei dem nur die Typen der Schlüssel und der Werte bekannt sind, ohne ein Pydantic-Modell zu verwenden.
Das ist nützlich, wenn Sie die gültigen Feld-/Attribut-Namen von vorneherein nicht wissen (was für ein Pydantic-Modell notwendig ist).
In diesem Fall können Sie typing.Dict
verwenden (oder nur dict
in Python 3.9 und darüber):
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}
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 verschiedene Zustände annehmen kann. So wie unsere Benutzer-„Einheit“, welche einen Zustand mit password
, einen mit password_hash
und einen ohne Passwort hatte.