This article is participating in Python Theme Month. See the link for details

1. Before to remember

At the last turn an article written before their own: add type checking to Python Web framework interface, this article is written by two years ago, a year ago when they want to put the idea into a project pait, and started to practice, after a year of iteration, now has reached version 0.6 (although middle slack off for a few months, Here is the first commit record.

So far,paitThe supported functions are:

  • Parameter verification and automatic conversion (parameter verification depends onPydantic)
  • Parameter relationship dependency verification
  • Automatically generate OpenAPI files
  • Support Swagger, Redoc routing
  • Return mock response
  • TestClient supports response verification

Pait’s parameter verification method is very similar to FastAPI. This is some method design I refer to after using FastAPI for a period of time, but PAIT’s positioning itself is framework extension. Flask, Starlette, Sanic, tornado are currently supported and will not affect the original usage of any of the frameworks.

2. A simple example

Flask with PAit is simple to use and saves a lot of verification code:

from typing import Any.Dict
from flask import Flask
from pydantic import ValidationError
from pait.app.flask import pait
from pait.app.flask import add_doc_route
from pait.exceptions import PaitBaseException
from pait.field import Query


def api_exception(exc: Exception) - >Dict[str.Any] :
    return {"code": -1."msg": str(exc)}


@pait()
def test_get(
    uid: int = Query.i(gt=100000, le=999999),
    name: str = Query.i("", max_length=10),
) - >dict:
    return {"code": 0."data": {"uid": uid, "name": name}}


def create_app() -> Flask:
    app: Flask = Flask(__name__)
    app.add_url_rule("/api/test_get", view_func=test_get, methods=["GET"])
    app.errorhandler(PaitBaseException)(api_exception)
    app.errorhandler(ValidationError)(api_exception)
    return app


if __name__ == "__main__":
    create_app().run(port=8000, debug=True)
Copy the code

There is only one interface called/API /test_get, which is decorated with the @pait decorator. This routing function takes two parameters, both of which are obtained from Query, where uid is limited to a value between 100000 and 999999, and name is limited to a maximum length of 10.

The Pait decorator throws two error types, PaitBaseException and ValidationError, which are usually used to indicate which parameter is incorrectly validated, captured by flask’s app. errorHandler.

First run the request to see what the result is:

# Normal request(.venv) ➜ ~ curl-s"Http://127.0.0.1:8000/api/test_get? uid=666666&name=test_user"
{
  "code": 0."data": {
    "name": "test_user"."uid": 666666}}The uid range is incorrect(.venv) ➜ ~ curl-s"Http://127.0.0.1:8000/api/test_get? uid=66666&name=test_user" 
{
  "code": 1,"msg": "File \"/home/so1n/github/pait/example/__init__.py\", line 14, in test_get. error:File \"/home/so1n/github/pait/example/__init__.py\", line 14, in test_get. error:1 validation error for DynamicModel\nuid\n ensure this value is greater than 100000 (type=value_error.number.not_gt; limit_value=100000)"
}
The length of # name is incorrect(.venv) ➜ ~ curl-s"Http://127.0.0.1:8000/api/test_get? uid=666666&name=test_useraaaaaaaaa"
{
  "code": 1,"msg": "File \"/home/so1n/github/pait/example/__init__.py\", line 14, in test_get. error:File \"/home/so1n/github/pait/example/__init__.py\", line 14, in test_get. error:1 validation error for DynamicModel\nname\n ensure this value has at most 10 characters (type=value_error.any_str.max_length; limit_value=10)"
}

Copy the code

The result of the error request is a bit unclear because the result is serialized, but you can clearly see that the first request returns a normal result. The second response indicates that the test_get function at line 14 of the file failed, where the uid value is not greater than 100000. The value of the third parameter name is longer than the maximum limit of 10.

3. Document generation

In addition to parameter verification, PAIT also supports document-supported functionality, which can be extended a little bit from the previous code to support document generation:

from typing import Any.Dict.Optional.Type
from flask import Flask
from pydantic import BaseModel, Field, ValidationError
from pait.app.flask import pait
from pait.app.flask import add_doc_route
from pait.exceptions import PaitBaseException
from pait.field import Query
from pait.model.status import PaitStatus
from pait.model.response import PaitResponseModel


class ResponseModel(BaseModel) :
    code: int = Field(0, description="api code")
    msg: str = Field("success", description="api status msg")


class SuccessRespModel(PaitResponseModel) :
    class _ResponseModel(ResponseModel) :
        class DataModel(BaseModel) :
            uid: int = Field(description="user id")
            name: str = Field(description="Username")

        data: DataModel

    description: str = "success response"
    response_data: Optional[Type[BaseModel]] = _ResponseModel


class FailRespModel(PaitResponseModel) :
    class ResponseFailModel(ResponseModel) :
        code: int = Field(1, description="api code")
        msg: str = Field("fail", description="api status msg")

    description: str = "fail response"
    response_data: Optional[Type[BaseModel]] = ResponseFailModel


def api_exception(exc: Exception) - >Dict[str.Any] :
    return {"code": -1."msg": str(exc)}


@pait(
    author=('So1n'.),
    tag=("test".),
    status=PaitStatus.test,
    response_model_list=[SuccessRespModel, FailRespModel]
)
def test_get(
    uid: int = Query.i(description="User id", gt=100000, le=999999),
    name: str = Query.i("", description="Username", max_length=10),
) - >dict:
    """ Test interface """
    return {"code": 0."data": {"uid": uid, "name": name}}


def create_app() -> Flask:
    app: Flask = Flask(__name__)
    add_doc_route(app)
    app.add_url_rule("/api/test_get", view_func=test_get, methods=["GET"])
    app.errorhandler(PaitBaseException)(api_exception)
    app.errorhandler(ValidationError)(api_exception)
    return app


if __name__ == "__main__":
    create_app().run(port=8000, debug=True)


Copy the code

You can see that the pait decorator has some additional parameters:

  • Author: indicates the author name of an API interface
  • Tag: indicates the tag that the API interface belongs to
  • Status: indicates the API interface status
  • Response_model_list Possible response type returned by the API interface

. At the same time in much create_app a app add_doc_route (app) code, it is mainly to provide online documentation support, at the same time support the Swagger and Redoc two document types, here with my favorite Redoc example (using Redoc document, Fields of the description must not be empty) at this time the browser requests http://127.0.0.1:8000/redoc can see document interface:

The flask framework automatically adds HEAD,OPIS, and other methods to the route that the user adds, which is nice, but we don’t need these two methods to expose the document to the client, and paIT doesn’t recognize them automatically. So pait is configured using the initialization method of the global variable pait.config to determine which methods are not displayed, with the following code:

# above code omitted
if __name__ == "__main__":
    from pait.g import config

    config.init_config(block_http_method_set={"HEAD"."OPTIONS"})
    create_app().run(port=8000, debug=True)
Copy the code

And then ask againhttp://127.0.0.1:8000/redocThe only method left in the document interface is the Get method:

4. The mock response

Back-end development process is usually got the demand and demand analysis, and then output a interface document to the front, and then front end development together, at this moment our interface code is haven’t started to write, but front may need to test, then can use pait mock response function, the example code is as follows:

from typing import Any.Dict.Optional.Type
from flask import Flask
from pydantic import BaseModel, Field, ValidationError
from pait.app.flask import pait
from pait.app.flask import add_doc_route
from pait.exceptions import PaitBaseException
from pait.field import Query
from pait.model.status import PaitStatus
from pait.model.response import PaitResponseModel


class ResponseModel(BaseModel) :
    code: int = Field(0, description="api code")
    msg: str = Field("success", description="api status msg")


class SuccessRespModel(PaitResponseModel) :
    class _ResponseModel(ResponseModel) :
        class DataModel(BaseModel) :
            uid: int = Field(description="user id")
            name: str = Field("example_name", description="Username")

        data: DataModel

    description: str = "success response"
    response_data: Optional[Type[BaseModel]] = _ResponseModel


class FailRespModel(PaitResponseModel) :
    class ResponseFailModel(ResponseModel) :
        code: int = Field(1, description="api code")
        msg: str = Field("fail", description="api status msg")

    description: str = "fail response"
    response_data: Optional[Type[BaseModel]] = ResponseFailModel


def api_exception(exc: Exception) - >Dict[str.Any] :
    return {"code": -1."msg": str(exc)}


@pait(
    author=('So1n'.),
    tag=("test".),
    status=PaitStatus.test,
    response_model_list=[SuccessRespModel, FailRespModel]
)
def test_get(
    uid: int = Query.i(description="User id", gt=100000, le=999999),
    name: str = Query.i("", description="Username", max_length=10),
) - >dict:
    """ Test interface """
    # return {"code": 0, "data": {"uid": uid, "name": name}}
    pass


def create_app() -> Flask:
    app: Flask = Flask(__name__)
    add_doc_route(app)
    app.add_url_rule("/api/test_get", view_func=test_get, methods=["GET"])
    app.errorhandler(PaitBaseException)(api_exception)
    app.errorhandler(ValidationError)(api_exception)
    return app


if __name__ == "__main__":
    from pait.g import config

    config.init_config(block_http_method_set={"HEAD"."OPTIONS"}, enable_mock_response=True)
    create_app().run(port=8000, debug=True)
Copy the code

The main changes are:

  • test_getThe routingreturnIs commented out and addedpass
  • Successfully responsiveName: STR = Field(description=" username ")Be changed toName: STR = Field("example_name", description=" username ")
  • configInitial configuration parameters ofenable_mock_responseTrue, which means that the interfaces of this runtime all return mock responses, and in addition,paitThe routing function is selected by defaultresponse_model_listThe first class that you need to use if you have additional requirementsconfig.init_configtheenable_mock_response_fnParameters on theresponse_model_listClass to select.

The uid parameter is of type int, so it has a value of 0. The name parameter has a default value, so it has a default value example_name:

(.venv) ➜ ~ curl-s"Http://127.0.0.1:8000/api/test_get? uid=666666&name=test_user"         
{
  "code": 0."data": {
    "name": "example_name"."uid": 0}."msg": "success"
}
Copy the code

5.Test client helper

Due to the expansion of paIT’s positioning of the Web framework, the source code of the framework will not be modified or embedded into the framework, so the response of each request will not be verified, reducing the impact of the request performance, but can be verified in the test case. Pait Each Web framework provides a corresponding TestClient helper. The logic of each Helper is to make a request and verify the response result. The response type returned by the helper is the same as that of the TestClient response of each Web framework (3 above). Change the name of the test_GET route to get_route to prevent PyTest from misjudging, and add an is_error_resp parameter. If this parameter is 1, the data structure returned is missing the name field. The rest is to add a test case as follows:

from typing import Any.Dict.Optional.Type
from flask import Flask
from pydantic import BaseModel, Field, ValidationError
from pait.app.flask import pait
from pait.app.flask import add_doc_route
from pait.exceptions import PaitBaseException
from pait.field import Query
from pait.model.status import PaitStatus
from pait.model.response import PaitResponseModel


class ResponseModel(BaseModel) :
    code: int = Field(0, description="api code")
    msg: str = Field("success", description="api status msg")


class SuccessRespModel(PaitResponseModel) :
    class _ResponseModel(ResponseModel) :
        class DataModel(BaseModel) :
            uid: int = Field(description="user id")
            name: str = Field(description="Username")

        data: DataModel

    description: str = "success response"
    response_data: Optional[Type[BaseModel]] = _ResponseModel


class FailRespModel(PaitResponseModel) :
    class ResponseFailModel(ResponseModel) :
        code: int = Field(1, description="api code")
        msg: str = Field("fail", description="api status msg")

    description: str = "fail response"
    response_data: Optional[Type[BaseModel]] = ResponseFailModel


def api_exception(exc: Exception) - >Dict[str.Any] :
    return {"code": -1."msg": str(exc)}


@pait(
    author=('So1n'.),
    tag=("test".),
    status=PaitStatus.test,
    response_model_list=[SuccessRespModel, FailRespModel]
)
def get_route(
    uid: int = Query.i(description="User id", gt=100000, le=999999),
    name: str = Query.i("", description="Username", max_length=10),
    is_error_resp: int = Query.i(0, description="Determine if the response does not match the SuccessRespModel")
) - >dict:
    """ Test interface """
    return_dict: dict = {"code": 0."data": {"uid": uid, "name": name}}
    if is_error_resp:
        return_dict = {"code": 0."data": {"uid": uid}}
    return return_dict


def create_app() -> Flask:
    app: Flask = Flask(__name__)
    add_doc_route(app)
    app.add_url_rule("/api/test_get", view_func=get_route, methods=["GET"])
    app.errorhandler(PaitBaseException)(api_exception)
    app.errorhandler(ValidationError)(api_exception)
    return app


# -- -- -- -- -- -- -
# Test code
# -- -- -- -- -- -- -
import pytest
from typing import Generator
from flask import Flask, Response
from flask.testing import FlaskClient
from flask.ctx import AppContext

from pait.app.flask import FlaskTestHelper


@pytest.fixture
def client() -> Generator[FlaskClient, None.None] :
    # Flask provides a way to test your application by exposing the Werkzeug test Client
    # and handling the context locals for you.
    app: Flask = create_app()
    client: FlaskClient = app.test_client()
    # Establish an application context before running the tests.
    ctx: AppContext = app.app_context()
    ctx.push()
    yield client  # this is where the testing happens!
    ctx.pop()


class TestFlask:
    def test_get(self, client: FlaskClient) - >None:
        test_helper: FlaskTestHelper[Response] = FlaskTestHelper(
            client, get_route,
            query_dict={"uid": 600000."name": "test_user"}
        )
        test_helper.get().get_json()
        test_helper = FlaskTestHelper(
            client, get_route,
            query_dict={"uid": 600000."name": "test_user"."is_error_resp": 1}
        )
        test_helper.get().get_json()
Copy the code

The main concern isTestFlask.test_getThe inside of thetest_helper, he will be based onget_routeTo retrieve theurlandresponse_model_listCall again,getWill concatenate the request parameters and passclientMake a request and pass the response throughresponse_model_listCompare and determine if the data structure of the response does not matchresponse_model_listAll response classes are thrown incorrectly. The following figure is usedPycharmrightTestFlaskThe result of executing the test, which statement he specified failed:

This is followed by paIT’s judgment that the response result is wrong, which indicates which respNOse_model is closest to the response, and which fields are missing:

E               RuntimeError: response check error by:[<class 'tests.test.SuccessRespModel'>, <class 'tests.test.FailRespModel'>]. resp:<Response 33 bytes [200 OK]>, maybe error:1 validation error for _ResponseModel
E               data -> name
E                 field required (type=value_error.missing)
Copy the code

6. Summary

The purpose of PAIT is to provide a good API wrapper for each Web framework without embedding it in the Web framework, reducing some of the tedious steps of writing code. There are still some features that are in the pipeline, which are currently prioritised for RESTurlAPI support, while other HTTP apis will wait until the iteration is complete. The above is just a brief introduction, you can check the PAIT documentation for details