跳转至

介绍

Pait是一个有助于开发者快速编写API路由函数的Python API开发工具,拥有参数类型检查, 类型转换,自动API文档等功能,适合用于后端的接口开发。

此外,它还被设计成一个适配多个Python Web应用框架的适配器(详见Web框架支持), 基于Pait可以无需考虑不同WEB框架的差异性快速开发适配各个WEB框架的应用,比如grpc-gateway

Pait设计灵感见文章《给python接口加上一层类型检》

功能

  • 融入Type Hints生态,提供安全高效的API接口编码方式。
  • 请求参数的自动校验和类型转化(依赖Pydanticinspect,目前支持PydanticV1和V2版本)。
  • 自动生成openapi文件,支持Swagger,Redoc,RapiDoc and Elements
  • TestClient支持, 支持测试用例的响应结果校验。
  • 插件拓展,如参数关系依赖校验,Mock响应等。
  • gRPC GateWay(在1.0版本后,该功能已迁移到grpc-gateway)
  • 自动API测试
  • WebSocket支持
  • SSE 支持

安装

Note

仅支持Python3.8+版本

pip install pait

使用

参数校验与文档生成

Pait的主要功能是提供参数校验和文档生成,使用方法非常简单,如下:

docs_source_code/docs_source_code/introduction/flask_demo.py
from typing import Type

from flask import Flask, Response, jsonify
from pydantic import BaseModel, Field

from pait.app.flask import pait
from pait.field import Json
from pait.model.response import JsonResponseModel
from pait.openapi.doc_route import AddDocRoute


class DemoResponseModel(JsonResponseModel):
    class ResponseModel(BaseModel):
        uid: int = Field()
        user_name: str = Field()

    description: str = "demo response"
    response_data: Type[BaseModel] = ResponseModel


@pait(response_model_list=[DemoResponseModel])
def demo_post(
    uid: int = Json.t(description="user id", gt=10, lt=1000),
    username: str = Json.t(description="user name", min_length=2, max_length=4),
) -> Response:
    return jsonify({"uid": uid, "user_name": username})


app = Flask("demo")
app.add_url_rule("/api", "demo", demo_post, methods=["POST"])
AddDocRoute(app)


if __name__ == "__main__":
    app.run(port=8000)
docs_source_code/docs_source_code/introduction/starlette_demo.py
from typing import Type

from pydantic import BaseModel, Field
from starlette.applications import Starlette
from starlette.responses import JSONResponse
from starlette.routing import Route

from pait.app.starlette import pait
from pait.field import Json
from pait.model.response import JsonResponseModel
from pait.openapi.doc_route import AddDocRoute


class DemoResponseModel(JsonResponseModel):
    class ResponseModel(BaseModel):
        uid: int = Field()
        user_name: str = Field()

    description: str = "demo response"
    response_data: Type[BaseModel] = ResponseModel


@pait(response_model_list=[DemoResponseModel])
async def demo_post(
    uid: int = Json.t(description="user id", gt=10, lt=1000),
    username: str = Json.t(description="user name", min_length=2, max_length=4),
) -> JSONResponse:
    return JSONResponse({"uid": uid, "user_name": username})


app = Starlette(routes=[Route("/api", demo_post, methods=["POST"])])
AddDocRoute(app)


if __name__ == "__main__":
    import uvicorn

    uvicorn.run(app)
docs_source_code/docs_source_code/introduction/sanic_demo.py
from typing import Type

from pydantic import BaseModel, Field
from sanic.app import Sanic
from sanic.response import HTTPResponse, json

from pait.app.sanic import pait
from pait.field import Json
from pait.model.response import JsonResponseModel
from pait.openapi.doc_route import AddDocRoute


class DemoResponseModel(JsonResponseModel):
    class ResponseModel(BaseModel):
        uid: int = Field()
        user_name: str = Field()

    description: str = "demo response"
    response_data: Type[BaseModel] = ResponseModel


@pait(response_model_list=[DemoResponseModel])
async def demo_post(
    uid: int = Json.t(description="user id", gt=10, lt=1000),
    username: str = Json.t(description="user name", min_length=2, max_length=4),
) -> HTTPResponse:
    return json({"uid": uid, "user_name": username})


app = Sanic(name="demo")
app.add_route(demo_post, "/api", methods=["POST"])
AddDocRoute(app)


if __name__ == "__main__":
    import uvicorn

    uvicorn.run(app)
docs_source_code/docs_source_code/introduction/tornado_demo.py
from typing import Type

from pydantic import BaseModel, Field
from tornado.ioloop import IOLoop
from tornado.web import Application, RequestHandler

from pait.app.tornado import pait
from pait.field import Json
from pait.model.response import JsonResponseModel
from pait.openapi.doc_route import AddDocRoute


class DemoResponseModel(JsonResponseModel):
    class ResponseModel(BaseModel):
        uid: int = Field()
        user_name: str = Field()

    description: str = "demo response"
    response_data: Type[BaseModel] = ResponseModel


class DemoHandler(RequestHandler):
    @pait(response_model_list=[DemoResponseModel])
    def post(
        self,
        uid: int = Json.t(description="user id", gt=10, lt=1000),
        username: str = Json.t(description="user name", min_length=2, max_length=4),
    ) -> None:
        self.write({"uid": uid, "user_name": username})


app: Application = Application([(r"/api", DemoHandler)])
AddDocRoute(app)


if __name__ == "__main__":
    app.listen(8000)
    IOLoop.instance().start()

代码中总共有三段高亮代码,其中第一段高亮代码中的@pait会装饰路由函数, 并在程序启动时自动从路由函数提取接口的请求参数数据。 此外还通过response_model_list属性声明了路由函数的响应对象是DemoResponseModelDemoResponseModel对象的descriptionresponse_data属性分别用于描述路由函数的响应对象说明和响应对象的结构类型。

第二段高亮代码中路由函数的参数格式是一种符合Pait规范的格式。 在初始化时,@pait会主动去解析路由函数并根据路由函数的函数签名生成依赖注入规则。 当请求命中路由函数时,Pait会根据依赖注入规则从Request对象获取到对应的值并将其注入到路由函数中。

第三段的高亮代码的主要工作是向app实例注册OpenAPI路由,为Web框架提供OpenAPI文档功能。

在一切准备就绪后开始运行代码,并在浏览器访问: http://127.0.0.1:8000/swagger 就可以看到SwaggerUI的页面,页面将显示如下图一样的两组接口:

其中名为pait_doc组的接口属于Pait自带的OpenAPI接口,另外一组是包含刚创建/api接口的default组,点开/api接口后会弹出接口详情:

详情里的数据是由Pait通过读取路由的函数签名以及DemoResponseModel对象生成的。 在接口详情的这个页面中可以点击try it out按钮,然后输入参数并点击Excute按钮,就可以看到curl命令生成结果以及服务器响应结果,如下图:

Note

想要了解更多? 马上进入类型转换与参数校验章节

插件

Pait除了参数校验和OpenAPI功能外,还可以通过插件系统拓展功能,比如Mock响应插件能根据响应模型自动生成响应值并返回数据,即使这个路由函数并没有任何逻辑实现,比如下面的代码:

docs_source_code/docs_source_code/introduction/flask_demo_with_mock_plugin.py
from typing import Type

from flask import Flask, Response
from pydantic import BaseModel, Field

from pait.app.flask import pait
from pait.app.flask.plugin.mock_response import MockPlugin
from pait.field import Json
from pait.model.response import JsonResponseModel
from pait.openapi.doc_route import AddDocRoute


class DemoResponseModel(JsonResponseModel):
    class ResponseModel(BaseModel):
        uid: int = Field(example=999)
        username: str = Field()

    description: str = "demo response"
    response_data: Type[BaseModel] = ResponseModel


@pait(response_model_list=[DemoResponseModel], plugin_list=[MockPlugin.build()])
def demo_post(  # type: ignore
    uid: int = Json.t(description="user id", gt=10, lt=1000),
    username: str = Json.t(description="user name", min_length=2, max_length=4),
) -> Response:
    pass


app = Flask("demo")
app.add_url_rule("/api", "demo", demo_post, methods=["POST"])
AddDocRoute(app)


if __name__ == "__main__":
    app.run(port=8000)
docs_source_code/docs_source_code/introduction/starlette_demo_with_mock_plugin.py
from typing import Type

from pydantic import BaseModel, Field
from starlette.applications import Starlette
from starlette.responses import JSONResponse
from starlette.routing import Route

from pait.app.starlette import pait
from pait.app.starlette.plugin.mock_response import MockPlugin
from pait.field import Json
from pait.model.response import JsonResponseModel
from pait.openapi.doc_route import AddDocRoute


class DemoResponseModel(JsonResponseModel):
    class ResponseModel(BaseModel):
        uid: int = Field(example=999)
        username: str = Field()

    description: str = "demo response"
    response_data: Type[BaseModel] = ResponseModel


@pait(response_model_list=[DemoResponseModel], plugin_list=[MockPlugin.build()])
async def demo_post(  # type: ignore[empty-body]
    uid: int = Json.t(description="user id", gt=10, lt=1000),
    username: str = Json.t(description="user name", min_length=2, max_length=4),
) -> JSONResponse:
    pass


app = Starlette(routes=[Route("/api", demo_post, methods=["POST"])])
AddDocRoute(app)


if __name__ == "__main__":
    import uvicorn

    uvicorn.run(app)
docs_source_code/docs_source_code/introduction/sanic_demo_with_mock_plugin.py
from typing import Type

from pydantic import BaseModel, Field
from sanic.app import Sanic
from sanic.response import HTTPResponse

from pait.app.sanic import pait
from pait.app.sanic.plugin.mock_response import MockPlugin
from pait.field import Json
from pait.model.response import JsonResponseModel
from pait.openapi.doc_route import AddDocRoute


class DemoResponseModel(JsonResponseModel):
    class ResponseModel(BaseModel):
        uid: int = Field(example=999)
        username: str = Field()

    description: str = "demo response"
    response_data: Type[BaseModel] = ResponseModel


@pait(response_model_list=[DemoResponseModel], plugin_list=[MockPlugin.build()])
async def demo_post(  # type: ignore[empty-body]
    uid: int = Json.t(description="user id", gt=10, lt=1000),
    username: str = Json.t(description="user name", min_length=2, max_length=4),
) -> HTTPResponse:
    pass


app = Sanic(name="demo")
app.add_route(demo_post, "/api", methods=["POST"])
AddDocRoute(app)


if __name__ == "__main__":
    import uvicorn

    uvicorn.run(app)
docs_source_code/docs_source_code/introduction/tornado_demo_with_mock_plugin.py
from typing import Type

from pydantic import BaseModel, Field
from tornado.ioloop import IOLoop
from tornado.web import Application, RequestHandler

from pait.app.tornado import pait
from pait.app.tornado.plugin.mock_response import MockPlugin
from pait.field import Json
from pait.model.response import JsonResponseModel
from pait.openapi.doc_route import AddDocRoute


class DemoResponseModel(JsonResponseModel):
    class ResponseModel(BaseModel):
        uid: int = Field(example=999)
        username: str = Field()

    description: str = "demo response"
    response_data: Type[BaseModel] = ResponseModel


class DemoHandler(RequestHandler):
    @pait(response_model_list=[DemoResponseModel], plugin_list=[MockPlugin.build()])
    async def post(
        self,
        uid: int = Json.t(description="user id", gt=10, lt=1000),
        username: str = Json.t(description="user name", min_length=2, max_length=4),
    ) -> None:
        pass


app: Application = Application([(r"/api", DemoHandler)])
AddDocRoute(app)


if __name__ == "__main__":
    app.listen(8000)
    IOLoop.instance().start()

该代码是根据参数校验与文档生成的代码进行更改,它移除了路由函数的逻辑代码,同时引入了高亮部分的代码, 其中DemoResponseModel响应模型的uid: int = Field(example=999)指定了响应结构中uid的mock值为999, 而@pait装饰器中通过plugin_list属性添加了一个名为MockPlugin的插件,插件可以根据response_model_list生成一个mock响应。

在一切准备就绪后开始运行代码,并重新点击Swagger页面的Excute按钮或者在终端运行Swagger页面生成的curl命令:

> curl -X 'POST' \
  'http://127.0.0.1:8000/api' \
  -H 'accept: application/json' \
  -H 'Content-Type: application/json' \
  -d '{
  "uid": 666,
  "user_name": "so1n"
}'

无论在Swagger页面或者终端中都可以看到如下输出:

{"uid":999,"username":""}

通过返回结果可以看到,路由函数虽然没有执行任何操作,但是该接口仍然可以返回响应。 这个响应结果是Mock插件自动生成的,其中响应结果的uid999与响应模型中uid: int = Field(example=999)设定的值一致,而username由于没有设定example值,所以响应结果中它的值是默认的空字符串。

Note

除了MockPlugin插件外,Pait还有其它的插件和功能,具体见插件的说明。

性能

Pait的主要运行原理是在程序启动时通过反射机制把路由函数的函数签名转换为Pydantic Model,并在请求命中路由时通过Pydantic Model对请求参数进行验证和转换。 这两个阶段全由Pait在内部自动处理,其中第一个阶段只稍微增加程序的启动时间,而第二个阶段会增加路由的响应时间,不过它只比手动处理多消耗了0.00005(s),具体的基准数据和后续优化在#27中都有描述。

Web框架支持

Pait的内部实现是一个适配不同Web框架的适配器,并基于适配器实现了各个不同的功能,目前Pait适配器支持的Web框架如下:

Web 框架 Request Response Plugin OpenAPI App attribute set&get HTTP Exception SimpleRoute TestHelper
Flask
Sanic
Startlette
Tornado

使用示例

每个Pait支持的Web框架都有完善的代码示例, 可以通过访问示例代码了解最佳实践: