Zum Inhalt

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.