FastAPI 的部分更新

Partial update in FastAPI

我想在支持部分更新的 FastAPI 中实现放置或补丁请求。 The official documentation 真的很混乱,我不知道该怎么做。 (我不知道 items 在文档中,因为我的数据将与请求的主体一起传递,而不是硬编码的字典)。

class QuestionSchema(BaseModel):
    title: str = Field(..., min_length=3, max_length=50)
    answer_true: str = Field(..., min_length=3, max_length=50)
    answer_false: List[str] = Field(..., min_length=3, max_length=50)
    category_id: int


class QuestionDB(QuestionSchema):
    id: int


async def put(id: int, payload: QuestionSchema):
    query = (
        questions
        .update()
        .where(id == questions.c.id)
        .values(**payload)
        .returning(questions.c.id)
    )
    return await database.execute(query=query)

@router.put("/{id}/", response_model=QuestionDB)
async def update_question(payload: QuestionSchema, id: int = Path(..., gt=0),):
    question = await crud.get(id)
    if not question:
        raise HTTPException(status_code=404, detail="question not found")

    ## what should be the stored_item_data, as documentation?
    stored_item_model = QuestionSchema(**stored_item_data)
    update_data = payload.dict(exclude_unset=True)
    updated_item = stored_item_model.copy(update=update_data)

    response_object = {
        "id": question_id,
        "title": payload.title,
        "answer_true": payload.answer_true,
        "answer_false": payload.answer_false,
        "category_id": payload.category_id,
    }
    return response_object

如何完成我的代码以在此处获得成功的部分更新?

我在 FastAPI 的 Github 问题上得到了这个答案:

您可以将基础字段设为可选 class 并创建一个扩展 QuestionSchema 的新 QuestionCreate 模型。例如:

from typing import Optional

class Question(BaseModel):
    title: Optional[str] = None  # title is optional on the base schema
    ...

class QuestionCreate(Question):
   title: str  # Now title is required

cookiecutter 模板 here 也提供了一些很好的见解。

在这里发布这篇文章是为了寻找一种直观的解决方案来创建他们的 pydantic 模型的可选版本而无需代码重复的 googlers。

假设我们有一个 User 模型,我们希望允许 PATCH 请求更新用户。但是我们需要创建一个模式来告诉 FastApi 在内容主体中期望什么,特别是所有字段都是可选的(因为这是 PATCH 请求的本质)。我们可以在不重新定义所有字段的情况下这样做

from pydantic import BaseModel
from typing import Optional

# Creating our Base User Model
class UserBase(BaseModel):
   username: str
   email: str
   

# And a Model that will be used to create an User
class UserCreate(UserBase):
   password: str

代码重复 ❌

class UserOptional(UserCreate):
    username: Optional[str]
    email: Optional[str]
    password: Optional[str]

一个班轮 ✅

# Now we can make a UserOptional class that will tell FastApi that all the fields are optional. 
# Doing it this way cuts down on the duplication of fields
class UserOptional(UserCreate):
    __annotations__ = {k: Optional[v] for k, v in UserCreate.__annotations__.items()}

注意:即使模型中的某个字段已经是可选的,由于可选的性质 typing.Union[type passed to Optional, None] 在后台,它也不会有所作为。

typing.Union[str, None] == typing.Optional[str]


如果你要多次使用它,你甚至可以把它变成一个函数:

def convert_to_optional(schema):
    return {k: Optional[v] for k, v in schema.__annotations__.items()}

class UserOptional(UserCreate):
    __annotations__ = convert_to_optional(UserCreate)

根据@cdraper的回答,做了一个局部模型工厂:

from typing import Mapping, Any, List, Type
from pydantic import BaseModel

def model_annotations_with_parents(model: BaseModel) -> Mapping[str, Any]:
    parent_models: List[Type] = [
        parent_model for parent_model in model.__bases__
        if (
            issubclass(parent_model, BaseModel)
            and hasattr(parent_model, '__annotations__')
        )
    ]

    annotations: Mapping[str, Any] = {}

    for parent_model in reversed(parent_models):
        annotations.update(model_annotations_with_parents(parent_model))

    annotations.update(model.__annotations__)
    return annotations


def partial_model_factory(model: BaseModel, prefix: str = "Partial", name: str = None) -> BaseModel:
    if not name:
        name = f"{prefix}{model.__name__}"

    return type(
        name, (model,),
        dict(
            __module__=model.__module__,
            __annotations__={
                k: Optional[v]
                for k, v in model_annotations_with_parents(model).items()
            }
        )
    )


def partial_model(cls: BaseModel) -> BaseModel:
    return partial_model_factory(cls, name=cls.__name__)

可与函数partial_model_factory一起使用:

PartialQuestionSchema = partial_model_factory(QuestionSchema)

或者使用装饰器 partial_model:

@partial_model
class PartialQuestionSchema(QuestionSchema):
    pass