FastAPI 创建适合需求的通用响应模型

mor*_*orf 2 python rest fastapi

我使用 FastAPI 一段时间了,它是一个很棒的框架。\n但是现实生活中的场景可能会令人惊讶,有时需要使用非标准方法。我有一个案例想请您帮忙。

\n

有一个奇怪的外部要求,模型响应的格式应如示例中所述:

\n

期望的行为:

\n

GET /object/1

\n
{status: \xe2\x80\x98success\xe2\x80\x99, data: {object: {id:\xe2\x80\x981\xe2\x80\x99, category: \xe2\x80\x98test\xe2\x80\x99 \xe2\x80\xa6}}}\n
Run Code Online (Sandbox Code Playgroud)\n

GET /objects

\n
{status: \xe2\x80\x98success\xe2\x80\x99, data: {objects: [...]}}}\n
Run Code Online (Sandbox Code Playgroud)\n

当前行为:

\n

GET/object/1将响应:

\n
{id: 1,field1:"content",... }\n
Run Code Online (Sandbox Code Playgroud)\n

GET/objects/将发送对象列表,例如:

\n
{\n [\n   {id: 1,field1:"content",... },\n   {id: 1,field1:"content",... },\n    ...\n ]\n}\n
Run Code Online (Sandbox Code Playgroud)\n

您可以用任何类替换“对象”,这仅用于描述目的。

\n

如何编写适合这些要求的通用响应模型?

\n

我知道我可以生成包含status:str和 (取决于类)数据结构的响应模型,例如ticket:Tickettickets:List[Ticket]

\n

重点是有很多类,所以我希望有一种更Pythonic 的方法来做到这一点。

\n

感谢帮助。

\n

Dan*_*erg 9

具有静态字段名称的通用模型

通用模型是一种模型,其中一个(或多个)字段用类型变量进行注释。因此,默认情况下未指定该字段的类型,并且必须在子类化和/或初始化期间显式指定。但该字段仍然只是一个属性,并且属性必须有一个名称。一个固定的名字。

从您的示例开始,假设这是您的模型:

{
  "status": "...",
  "data": {
    "object": {...}  # type variable
  }
}
Run Code Online (Sandbox Code Playgroud)

然后我们可以根据属性的类型将该模型定义为通用模型object

这可以使用 Pydantic 来完成,GenericModel如下所示:

from typing import Generic, TypeVar
from pydantic import BaseModel
from pydantic.generics import GenericModel

M = TypeVar("M", bound=BaseModel)


class GenericSingleObject(GenericModel, Generic[M]):
    object: M


class GenericMultipleObjects(GenericModel, Generic[M]):
    objects: list[M]


class BaseGenericResponse(GenericModel):
    status: str


class GenericSingleResponse(BaseGenericResponse, Generic[M]):
    data: GenericSingleObject[M]


class GenericMultipleResponse(BaseGenericResponse, Generic[M]):
    data: GenericMultipleObjects[M]


class Foo(BaseModel):
    a: str
    b: int


class Bar(BaseModel):
    x: float
Run Code Online (Sandbox Code Playgroud)

正如您所看到的,GenericSingleObject反映了我们想要的泛型类型data,而就GenericSingleResponse的类型参数而言是泛型的MGenericSingleObject这是其属性的类型data

如果我们现在想要使用我们的通用响应模型之一,我们需要首先使用类型参数(具体模型)来指定它,例如GenericSingleResponse[Foo]

FastAPI 可以很好地处理这个问题,并且可以生成正确的 OpenAPI 文档。JSON 架构GenericSingleResponse[Foo]如下所示:

{
    "title": "GenericSingleResponse[Foo]",
    "type": "object",
    "properties": {
        "status": {
            "title": "Status",
            "type": "string"
        },
        "data": {
            "$ref": "#/definitions/GenericSingleObject_Foo_"
        }
    },
    "required": [
        "status",
        "data"
    ],
    "definitions": {
        "Foo": {
            "title": "Foo",
            "type": "object",
            "properties": {
                "a": {
                    "title": "A",
                    "type": "string"
                },
                "b": {
                    "title": "B",
                    "type": "integer"
                }
            },
            "required": [
                "a",
                "b"
            ]
        },
        "GenericSingleObject_Foo_": {
            "title": "GenericSingleObject[Foo]",
            "type": "object",
            "properties": {
                "object": {
                    "$ref": "#/definitions/Foo"
                }
            },
            "required": [
                "object"
            ]
        }
    }
}
Run Code Online (Sandbox Code Playgroud)

使用 FastAPI 进行演示:

from fastapi import FastAPI


app = FastAPI()


@app.get("/foo/", response_model=GenericSingleResponse[Foo])
async def get_one_foo() -> dict[str, object]:
    return {"status": "foo", "data": {"object": {"a": "spam", "b": 123}}}
Run Code Online (Sandbox Code Playgroud)

向该路由发送请求会返回以下内容:

{
  "status": "foo",
  "data": {
    "object": {
      "a": "spam",
      "b": 123
    }
  }
}
Run Code Online (Sandbox Code Playgroud)

动态创建的模型

如果您实际上希望属性名称每次都不同,那么使用静态类型注释显然不再可能。在这种情况下,我们将不得不通过实际动态创建模型类型pydantic.create_model

在这种情况下,通用性实际上已经没有意义了,因为无论如何类型安全都已经不可能了,至少对于data模型来说是这样。我们仍然可以选择定义一个GenericResponse模型,我们可以通过动态生成的模型来指定该模型,但这将使每个静态类型检查器发疯,因为我们将使用类型变量。尽管如此,它仍可能使代码更加简洁。

我们只需要定义一个算法来导出模型参数:

from typing import Any, Generic, Optional, TypeVar
from pydantic import BaseModel, create_model
from pydantic.generics import GenericModel

M = TypeVar("M", bound=BaseModel)


def create_data_model(
    model: type[BaseModel],
    plural: bool = False,
    custom_plural_name: Optional[str] = None,
    **kwargs: Any,
) -> type[BaseModel]:
    data_field_name = model.__name__.lower()
    if plural:
        model_name = f"Multiple{model.__name__}"
        if custom_plural_name:
            data_field_name = custom_plural_name
        else:
            data_field_name += "s"
        kwargs[data_field_name] = (list[model], ...)  # type: ignore[valid-type]
    else:
        model_name = f"Single{model.__name__}"
        kwargs[data_field_name] = (model, ...)
    return create_model(model_name, **kwargs)


class GenericResponse(GenericModel, Generic[M]):
    status: str
    data: M
Run Code Online (Sandbox Code Playgroud)

Foo使用与Bar之前相同的示例:

class Foo(BaseModel):
    a: str
    b: int


class Bar(BaseModel):
    x: float


SingleFoo = create_data_model(Foo)
MultipleBar = create_data_model(Bar, plural=True)
Run Code Online (Sandbox Code Playgroud)

这也可以按预期使用 FastAPI,包括自动生成的架构/文档:

from fastapi import FastAPI


app = FastAPI()


@app.get("/foo/", response_model=GenericResponse[SingleFoo])  # type: ignore[valid-type]
async def get_one_foo() -> dict[str, object]:
    return {"status": "foo", "data": {"foo": {"a": "spam", "b": 123}}}


@app.get("/bars/", response_model=GenericResponse[MultipleBar])  # type: ignore[valid-type]
async def get_multiple_bars() -> dict[str, object]:
    return {"status": "bars", "data": {"bars": [{"x": 3.14}, {"x": 0}]}}
Run Code Online (Sandbox Code Playgroud)

输出基本上与第一种方法相同。

您必须看看哪一种更适合您。由于动态键/字段名称,我发现第二个选项非常奇怪。但也许这正是您出于某种原因所需要的。