Skip to main content

Pait is a Python api tool. Pait enables your Python web framework to have type checking, parameter type conversion, interface document generation and can display your documents through Redoc or Swagger (power by inspect, pydantic)

Project description

pait

Pait is an api tool that can be used in any python web framework (currently only flask, starlette, sanic, tornado are supported, other frameworks will be supported once Pait is stable).

The core functionality of Pait is to allow you to have FastAPI-like type checking and type conversion functionality (dependent on Pydantic and inspect) in any Python web framework, as well as documentation output

Pait's vision of documentation output is both code and documentation, with a simple configuration, you can get an md document or openapi (json, yaml)

Note:

mypy check 100%

test coverage 95%+ (exclude api_doc)

python version >= 3.7 (support postponed annotations)

The function is being expanded... the documentation may not be perfect

中文文档

Installation

pip install pait

Usage

Note: The following code does not specify, all default to use the starlette framework. Note: There is no test case for the document output function, and the function is still being improved

1.type checking and parameter type conversion

1.1.Use in route handle

A simple starlette route handler example:

import uvicorn

from starlette.applications import Starlette
from starlette.routing import Route
from starlette.requests import Request
from starlette.responses import JSONResponse


async def demo_post(request: Request) -> JSONResponse:
    body_dict: dict = await request.json()
    uid: int = body_dict.get('uid', 0)
    user_name: str = body_dict.get('user_name', "")
    # The following code is only for demonstration, in general, we do some wrapping 
    if not uid:
        raise ValueError('xxx')
    if type(uid) != int:
        raise TypeError('xxxx')
    if 10 <= uid <= 1000:
        raise ValueError('xxx')

    if not user_name:
        raise ValueError('xxx')
    if type(user_name) != str:
        raise TypeError('xxxx')
    if 2 <= len(user_name) <= 4:
        raise ValueError('xxx')
    
    return JSONResponse(
        {
            'result': {
                'uid': body_dict['uid'],
                'user_name': body_dict['user_name']
            } 
        }
    )


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


uvicorn.run(app)

use pait in starletter route handler:

import uvicorn  # type: ignore

from starlette.applications import Starlette
from starlette.routing import Route
from starlette.responses import JSONResponse

from pait.field import Body
from pait.app.starlette import pait
from pydantic import (
    BaseModel,
    Field
)


# Create a Model based on Pydantic.BaseModel 
class UserModel(BaseModel):
    # Whether the auto-check type is int, and whether it is greater than or equal to 10 and less than or equal to 1000 
    uid: int = Field(description="user id", gt=10, lt=1000)
    # Whether the auto-check type is str, and whether the length is greater than or equal to 2, less than or equal to 4
    user_name: str = Field(description="user name", min_length=2, max_length=4)


# Decorating functions with the pait decorator
@pait()
async def demo_post(
    # pait through the Body () to know the current need to get the value of the body from the request, and assign the value to the model, 
    # and the structure of the model is the above PydanticModel, he will be based on our definition of the field automatically get the value and conversion and judgment
    model: UserModel = Body.i()
) -> JSONResponse:
    # Get the corresponding value to return
    return JSONResponse({'result': model.dict()})


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

uvicorn.run(app)

As you can see, you just need to add a pait decorator to the routing function and change the parameters of demo_post to model: PydanticModel = Body(). pait through Body to know the need to get the post request body data, and according to conint(gt=10, lt=1000) on the data conversion and restrictions, and assigned to PydanticModel, the user only need to use Pydantic as the call model can get the data.

Here is just a simple demo, because we write the model can be reused, so you can save a lot of development time, the above parameters are only used to a way to write, the following will introduce pait support for the two ways to write and use.

1.2.Parameter expression supported by pait

pait in order to facilitate the use of users, support a variety of writing methods (mainly the difference between TypeHints)

  • TypeHints is PaitBaseModel, mainly used for parameters from multiple Field, and want to reuse model:

    PaitBaseModel can be used only for args parameters, it is the most flexible, PaitBaseModel has most of the features of Pydantic. BaseModel, which is not possible with Pydantic.:

    from pait.app.starlette import pait
    from pait.field import Body, Header
    from pait.model.base_model import PaitBaseModel
    
    
    class TestModel(PaitBaseModel):
        uid: int = Body.i()
        content_type: str = Header.i(default='Content-Type')
    
    
    @pait()
    async def test(model: PaitBaseModel):
        return {'result': model.dict()}
    
  • TypeHints is Pydantic.BaseModel, mainly used for parameters are derived from the same Field, and want to take the model:

    BaseModel can only be used with kwargs parameters, and the type hints of the parameters must be a class that inherits from pydantic.BaseModel, using the example:

    from pydantic import BaseModel
    from pait.app.starlette import pait
    from pait.field import Body
    
    
    class TestModel(BaseModel):
        uid: int
        user_name: str
    
    
    @pait()
    async def test(model: BaseModel = Body.i()):
        return {'result': model.dict()}
    
  • When TypeHints is not one of the above two cases:

    can only be used for kwargs parameters and type hints are not the above two cases, if the value is rarely reused, or if you do not want to create a Model, you can consider this approach

    from pait.app.starlette import pait
    from pait.field import Body
    
    
    @pait()
    async def test(uid: int = Body.i(), user_name: str = Body.i()):
        return {'result': {'uid': uid, 'user_name': user_name}}
    

1.3.Field

Field will help pait know how to get data from request. Before introducing the function of Field, let’s take a look at the following example. pait will obtain the body data of the request according to Field.Body, and obtain the value with the parameter named key. Finally, the parameter is verified and assigned to the uid.

Note: Use Field.Body() directly, mypy will check that the type does not match, then just change to Field.Body.i() to solve the problem.

from pait.app.starlette import pait
from pait.field import Body


@pait()
async def demo_post(
    # get uid from request body data
    uid: int = Body.i()
) -> None:
    pass

The following example will use a parameter called default. Since you can't use Content-Type to name the variables in Python, you can only use content_type to name them according to the naming convention of python, so there is no way to get the value directly from the header, so you can set the value of alias to Content-Type, and then Pait can get the value of Content-Type in the Header and assign it to the content_type variable. Another example uses raw_return and sets it to True. At this time, Pait will not use the parameter name header_dict as the key to get the data, but directly assign the data of the entire header to the header_dict.

from pait.app.starlette import pait
from pait.field import Body, Header


@pait()
async def demo_post(
    # get uid from request body data
    uid: int = Body.i(),
    # get Content-Type from header
    content_type: str = Header.i(alias='Content-Type'),
    header_dict: str = Header.i(raw_return=True)

):
    pass

The above only demonstrates the Body and Header of the field, but there are other fields as well::

  • Field.Body Get the json data of the current request
  • Field.Cookie Get the cookie data of the current request
  • Field.File Get the file data of the current request, depending on the web framework will return different file object types
  • Field.Form Get the form data of the current request, if there are multiple duplicate keys, only the first one will be returned
  • Field.Header Get the header data of the current request
  • Field.Path Get the path data of the current request (e.g. /api/{version}/test, you can get the version data)
  • Field.Query Get the url parameters of the current request and the corresponding data, if there are multiple duplicate keys, only the first one will be returned
  • Field.MultiQuery Get the url parameter data of the current request, and return the list corresponding to the key
  • Field.MultiForm Get the form data of the current request, return the list corresponding to the key

All the fields above are inherited from pydantic.fields.FieldInfo, most of the parameters here are for api documentation, see for specific usagepydantic doc

In addition there is a field named Depends, he inherits from object, he provides the function of dependency injection, he only supports one parameter and the type of function, and the function's parameters are written in the same way as the routing function, the following is an example of the use of Depends, through Depends, you can reuse in each function to get the token function:

from pait.app.starlette import pait
from pait.field import Body, Depends


def demo_depend(uid: str = Body.i(), password: str = Body.i()) -> str:
    # fake db
    token: str = db.get_token(uid, password)
    return token


@pait()
async def test_depend(token: str = Depends.i(demo_depend)) -> dict:
    return {'token': token}

1.4.requests object

After using Pait, the proportion of the number of times the requests object is used will decrease, so pait does not return the requests object. If you need the requests object, you can fill in the parameters like requests: Requests (you need to use the TypeHints format) , You can get the requests object corresponding to the web framework

from starlette.requests import Request

from pait.app.starlette import pait
from pait.field import Body


@pait()
async def demo_post(
        request: Request,
        # get uid from request body data
        uid: int = Body.i()
) -> None:
    pass

1.5.Exception

1.5.1Exception Handling

Pait will leave the exception to the user to handle it. Under normal circumstances, pait will only throw the exception of pydantic and PaitBaseException. The user needs to catch the exception and handle it by himself, for example:

from starlette.applications import Starlette
from starlette.requests import Request
from starlette.responses import Response

from pait.exceptions import PaitBaseException
from pydantic import ValidationError

async def api_exception(request: Request, exc: Exception) -> None:
    """
    Handle exception code    
    """
    if isinstance(exc, PaitBaseException):
        pass 
    else:
        pass

APP = Starlette()
APP.add_exception_handler(PaitBaseException, api_exception)
APP.add_exception_handler(ValidationError, api_exception)

1.5.2Error Tip

When you use pait incorrectly, pait will indicate in the exception the file path and line number of the function.

  File "/home/so1n/github/pait/pait/func_param_handle.py", line 101, in set_value_to_kwargs_param
    f'File "{inspect.getfile(func_sig.func)}",'
PaitBaseException: 'File "/home/so1n/github/pait/example/starlette_example.py", line 28, in demo_post\n kwargs param:content_type: <class \'str\'> = Header(key=None, default=None) not found value, try use Header(key={key name})'

If you need more information, can set the log level to debug to get more detailed information

DEBUG:root:
async def demo_post(
    ...
    content_type: <class 'str'> = Header(key=None, default=None) <-- error
    ...
):
    pass

2.Document Generation

pait will automatically capture the request parameters and url, method and other information of the routing function. In addition, it also supports labeling some relevant information. These labels will only be loaded into the memory when the Python program starts running, and will not affect the performance of the request, as in the following example:

from pait.app.starlette import pait
from pait.model.status import PaitStatus

from example.param_verify.model import UserSuccessRespModel, FailRespModel


@pait(
  author=("so1n",),
  group="user",
  status=PaitStatus.release,
  tag=("user", "post"),
  response_model_list=[UserSuccessRespModel, FailRespModel],
)
def demo() -> None:
  pass

Param:

  • author: List of authors who wrote the interface
  • group: The group to which the interface belongs
  • status: The status of the interface, currently only supports several states of PaitStatus
    • default status:
      • undefined: undefined
    • in development:
      • design: Interface design
      • dev: Under development and testing
    • Development completed:
      • integration: integration test
      • complete: development completed
      • test: testing
    • online:
      • release: online
    • offline:
      • abnormal: The interface is abnormal and needs to be offline
      • maintenance: In maintenance
      • archive: archive
      • abandoned: abandoned
  • tag: interface tag
  • response_model_list: return data, Need to inherit from pait.model.PaitResponseModel

2.1.openapi

2.1.1openapi doc output

Currently pait supports most of the functions of openapi, a few unrealized features will be gradually improved through iterations (response-related more complex)

The openapi module of pait supports the following parameters (more parameters will be provided in the next version):

  • title: openapi's title
  • open_api_info: openapi's info param
  • open_api_tag_list: related description of openapi tag
  • open_api_server_list: openapi server list
  • type_: The type of output, optionally json and yaml
  • filename: Output file name, or if empty, output to terminal

The following is the sample code output from the openapi documentation (modified by the 1.1 code). See Example code and doc example

import uvicorn

from starlette.applications import Starlette
from starlette.routing import Route
from starlette.responses import JSONResponse

from pait.field import Body
from pait.app.starlette import pait
from pydantic import (
    BaseModel,
    conint,
    constr,
)


# Create a Model based on Pydantic.BaseModel 
class PydanticModel(BaseModel):
    uid: conint(gt=10, lt=1000)  # Whether the auto-check type is int, and whether it is greater than or equal to 10 and less than or equal to 1000 
    user_name: constr(min_length=2, max_length=4)  # Whether the auto-check type is str, and whether the length is greater than or equal to 2, less than or equal to 4



# Decorating functions with the pait decorator
@pait()
async def demo_post(
    # pait through the Body () to know the current need to get the value of the body from the request, and assign the value to the model, 
    # and the structure of the model is the above PydanticModel, he will be based on our definition of the field automatically get the value and conversion and judgment
    model: PydanticModel = Body.i()
):
    # Get the corresponding value to return
    return JSONResponse({'result': model.dict()})


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

uvicorn.run(app)
# --------------------

from pait.app import load_app
from pait.api_doc.open_api import PaitOpenApi


# Extracting routing information to pait's data module
pait_dict = load_app(app)
# Generate openapi for routing based on data from the data module
PaitOpenApi(pait_dict)

2.1.2.OpenApi Route

Pait currently supports openapi.json routing, and also supports page display of Redoc and Swagger, and these only need to call the add_doc_route function to add three routes to the app instance:

  • /openapi.json
  • /redoc
  • /swagger If you want to define a prefix, such as /doc/openapi.json, just pass in /doc through the prefix parameter. Specific examples are as follows:
import uvicorn  # type: ignore
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 Body


# Create a Model based on Pydantic.BaseModel 
class UserModel(BaseModel):
    # Whether the auto-check type is int, and whether it is greater than or equal to 10 and less than or equal to 1000 
    uid: int = Field(description="user id", gt=10, lt=1000)
    # Whether the auto-check type is str, and whether the length is greater than or equal to 2, less than or equal to 4
    user_name: str = Field(description="user name", min_length=2, max_length=4)


# Decorating functions with the pait decorator
@pait()
async def demo_post(
    # pait through the Body () to know the current need to get the value of the body from the request, and assign the value to the model, 
    # and the structure of the model is the above PydanticModel, he will be based on our definition of the field automatically get the value and conversion and judgment
    model: UserModel = Body.i()
) -> JSONResponse:
    # Get the corresponding value to return
    return JSONResponse({'result': model.dict()})


app = Starlette(
    routes=[
        Route('/api', demo_post, methods=['POST']),
    ]
)
# Inject the route into the app 
add_doc_route(app)
# Inject the route into the app, and prefix it with /doc 
add_doc_route(app, prefix='/doc')

2.2.Other doc output

Note: The function is being improved...

In addition to parameter verification and conversion, pait also provides the ability to output api documentation, which can be configured with simple parameters to output perfect documentation.

Note: Currently only md, json, yaml type documents and openapi documents for json and yaml are supported for output.For the output of md, json, yaml, see doc example

3.How to used in other web framework?

If the web framework is not supported, which you are using. Can be modified sync web framework according to pait.app.flask

Can be modified async web framework according to pait.app.starlette

4.IDE Support

While pydantic will work well with any IDE out of the box.

5.Full example

For more complete examples, please refer to example

Project details


Download files

Download the file for your platform. If you're not sure which to choose, learn more about installing packages.

Source Distribution

pait-0.5.8.2.tar.gz (38.1 kB view details)

Uploaded Source

Built Distribution

pait-0.5.8.2-py3-none-any.whl (42.4 kB view details)

Uploaded Python 3

File details

Details for the file pait-0.5.8.2.tar.gz.

File metadata

  • Download URL: pait-0.5.8.2.tar.gz
  • Upload date:
  • Size: 38.1 kB
  • Tags: Source
  • Uploaded using Trusted Publishing? No
  • Uploaded via: poetry/1.1.6 CPython/3.7.3 Linux/5.4.50-amd64-desktop

File hashes

Hashes for pait-0.5.8.2.tar.gz
Algorithm Hash digest
SHA256 a63fd76f980695c15a6c975ed057ea6471d5ea005f9031258dc65d7964e963e1
MD5 079dee904feed25cdd2f0e18f98b56c3
BLAKE2b-256 f32b8484579e57d76b7deccce9341ac59217432dcfec32ffa644df00506c6e94

See more details on using hashes here.

File details

Details for the file pait-0.5.8.2-py3-none-any.whl.

File metadata

  • Download URL: pait-0.5.8.2-py3-none-any.whl
  • Upload date:
  • Size: 42.4 kB
  • Tags: Python 3
  • Uploaded using Trusted Publishing? No
  • Uploaded via: poetry/1.1.6 CPython/3.7.3 Linux/5.4.50-amd64-desktop

File hashes

Hashes for pait-0.5.8.2-py3-none-any.whl
Algorithm Hash digest
SHA256 8c2e8b4016b2715824fe6516ed7251b73db6dcd3f92ddbca118ca3d690242b31
MD5 d6a4aaf5d56c71fc407a766021568a37
BLAKE2b-256 88636263e6d3b7d2b8731495a0e0e70f06a9ea47659ad22b93154cfe27451aa6

See more details on using hashes here.

Supported by

AWS AWS Cloud computing and Security Sponsor Datadog Datadog Monitoring Fastly Fastly CDN Google Google Download Analytics Microsoft Microsoft PSF Sponsor Pingdom Pingdom Monitoring Sentry Sentry Error logging StatusPage StatusPage Status page