Skip to content

Introduction

Pait is a Python API development tool that helps developers quickly write API route functions. It has feature such as parameter type checking, type conversion, and automatic API documentation. It is suitable for back-end interface development.

In addition, it is designed as an adapter that can work with multiple Python web application frameworks (see Web Framework Support for details), Based on Pait, can quickly develop applications that adapt to each WEB framework without considering the differences of different WEB frameworks, such as grpc-gateway.

Feature

  • Integrate into the Type Hints ecosystem to provide a safe and efficient API interface coding method.
  • Automatic verification and type conversion of request parameters (depends on Pydantic and inspect, currently supports Pydantic V1 and V2 versions).
  • Automatically generate openapi files and support UI components such as Swagger,Redoc,RapiDoc and Elements.
  • TestClient support, response result verification of test cases。
  • Plugin expansion, such as parameter relationship dependency verification, Mock response, etc.。
  • gRPC GateWay (After version 1.0, this feature has been migrated to grpc-gateway)
  • Automated API testing
  • WebSocket support
  • SSE support

Install

Note

Only supports Python 3.8+ version

pip install pait

Usage

Parameter verification and document generation

The main feature of Pait is to provide parameter verification and document generation. The usage is very simple, as follows:

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/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/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/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()

There are three sections of highlighted code in the example code. The @pait in the first highlighted code will decorate the route function and automatically extract the request parameter data from the route function when the program starts. In addition, the response object of the route function is declared as DemoResponseModel through the response_model_list attribute. The description and response_data attributes of the DemoResponseModel object are used to describe the response object description of the route function and the structure type of the response object respectively.

The parameter format of the route function in the second highlighted code is a format that conforms to the Pait specification. During initialization, @pait will actively parse the route function and generate dependency injection rules based on the function signature of the route function. When a request hits the route function, Pait will get the corresponding value from the Request object according to the dependency injection rules and inject it into the route function.

The main job of the highlighted code in the third paragraph is to register the OpenAPI route with the app instance and provide the OpenAPI document feature for the web framework.

After everything is ready, start running the code and visit: http://127.0.0.1:8000/swagger in the browser to see the SwaggerUI page. Two sets of interfaces will be displayed as shown below:

The named pait_doc belongs to the OpenAPI interface that comes with Pait, and the other group is the default containing the newly created /api. After clicking on the /api, it will Pop-up interface details:

The data is generated by Pait parse the route's function signature and the DemoResponseModel object. In the details page, you can click the try it out button, then enter the parameters and click the Excute button, you can see the curl command generation results and the server response results, as shown below:

Note

Want to know more? Immediately enter the Type Conversion and Parameter Verification chapter

Plugin

In addition to parameter verification and OpenAPI feature, Pait can also expand its feature through the plugin system. For example, the Mock response plugin can automatically generate response values and return data based on the response model, even if the route function does not have any logic implementation, such as the following 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/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/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/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()

This code is changed based on the code of Parameter verification and document generation. It removes the logic code of the route function and introduces the highlighted part of the code.

Among them, the uid: int = Field(example=999) of the DemoResponseModel response model specifies that the mock value of uid in the response structure is 999. The @pait decorator adds a plugin named MockPlugin through the plugin_list attribute. The plugin can generate a mock response based on the response_model_list.

After everything is ready, start running the code and re-click the Excute button on the Swagger page or run the curl command generated by the Swagger page in the terminal:

> 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"
}'
Finally, can see the following output whether on the Swagger page or in the terminal:
{"uid":999,"username":""}
As you can see from the returned results, although the route function does not perform any operation, the routeing function can still return a response.

This response result is automatically generated by the Mock plugin, where the uid value of 999 is the same as the value set in the uid: int = Field(example=999) in the response model, and the username is the default empty string in the response result because it is not set in example.

Note

In addition to the MockPlugin, Pait also has other plugins and feature. For details, please see the description of Plugin.

Performance

The main operating principle of Pait is to convert the function signature of the route function into Pydantic Model through the reflection mechanism when the program is started, and then verify and convert the request parameters through Pydantic Model when the request hits the route.

These two stages are all automatically handled internally by Pait. The first stage only slightly increases the startup time of the program, while the second stage increases the response time of the routing, but it only consumes 0.00005(s) more than manual processing. The specific benchmark data and subsequent optimization are described in #27.

Web framework support

The internal implementation of Pait is an adapter that adapts to different web frameworks, and implements various feature based on the adapter. Currently, the web frameworks supported by the Pait adapter are as follows:

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

Example

Pait provides complete code examples for the web frameworks it supports, can learn more by accessing the sample code: