介绍
Pait
是一个有助于开发者快速编写API路由函数的Python API开发工具,拥有参数类型检查, 类型转换,自动API文档等功能,适合用于后端的接口开发。
此外,它还被设计成一个适配多个Python Web应用框架的适配器(详见Web框架支持 ),
基于Pait
可以无需考虑不同WEB框架的差异性快速开发适配各个WEB框架的应用,比如grpc-gateway 。
Pait
设计灵感见文章《给python接口加上一层类型检》 。
功能
安装
使用
参数校验与文档生成
Pait
的主要功能是提供参数校验和文档生成,使用方法非常简单,如下:
Flask Starlette Sanic Tornado
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
属性声明了路由函数的响应对象是DemoResponseModel
,
DemoResponseModel
对象的description
和response_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
命令生成结果以及服务器响应结果,如下图:
插件
Pait
除了参数校验和OpenAPI
功能外,还可以通过插件系统拓展功能,比如Mock响应插件能根据响应模型自动生成响应值并返回数据,即使这个路由函数并没有任何逻辑实现,比如下面的代码:
Flask Starlette Sanic Tornado
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插件自动生成的,其中响应结果的uid
值999
与响应模型中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框架都有完善的代码示例, 可以通过访问示例代码了解最佳实践: