😜从零开始学FastAPI(2)-试炼篇之数据验证和事件监听处理

4,318 阅读7分钟

回望过去

上一期提到了关于本博文FastAPI相关的基础知识篇,如有意可以回顾看看:

前言

上一个小节中,相关的示例大部分得到都是来自官网的文档的实践,总觉得还是比较粗浅的尝试,当需要深入到里面去使用的时候,回发现一些小细节的问题。

比如本章节想深入了解一下,关于自定义参数校验的时候的问题,因为之前进行相关的参数的叫校验的时候,多数情况我是直接的使用了wtform。但是在FastAPI对于数据校验这块,它使用的和 TypeSystem 相识的pydantic.

对于pydantic的使用,我也是一脸懵逼!有点不知所措,因为真正的融入到FastAPI我发现一些小问题。

比如,我想和wtform一样定制相关的错误提示的的时候,有点小尴尬,不知道怎么定义。

所以此文,主要是关于数据校验问题的学习笔记。

正文

一、数据参数校验

1.1 自定义HTTPException,返回指定的错误信息


from fastapi import HTTPException
from pydantic.errors import *

class APIException(HTTPException):
    http_state_code = 500
    msg = '抱歉,服务器未知错误'
  
    def __init__(self, msg=None, http_state_code=http_state_code):
        if http_state_code:
            self.http_state_code = http_state_code
        if msg:
            self.msg = msg
        raise HTTPException(status_code=self.http_state_code, detail=self.msg)

调用的时候是在需要的地方类似上面:raise HTTPException(status_code=self.http_state_code, detail=self.msg)

from core.exceptions import ParameterHTTPException

@router.put("/items2/{item_id}")
def update_item(*,item_id:int):
    raise ParameterHTTPException()

1.2. 继承JSONResponse,进行各自响应体的返回

from typing import Any, Dict


# 自定义返回的错误的响应体信息
from fastapi.responses import PlainTextResponse,JSONResponse

class ApiResponse(JSONResponse):
    # 定义返回响应码--如果不指定的话则默认都是返回200
    http_status_code = 200
    # 默认成功
    code = 0
    data = None  # 结果可以是{} 或 []
    msg = '成功'

    def __init__(self,http_status_code=None,  data=None,msg=None, **options):

        if data:
            self.data = data
        if msg:
            self.msg = msg

        if http_status_code:
            self.http_status_code = http_status_code

        # 返回内容体
        body = dict(
            msg=self.msg,
            code=self.code,
            data=self.data,
        )
        super(ApiResponse, self).__init__(status_code=self.http_status_code,content=body, **options)



class BadrequestException(ApiResponse):
    http_status_code = 400
    #  error_code = 10032
    code = 10032
    msg = '错误的请求'


class ParameterException(ApiResponse):
    http_status_code = 400
    code = 400
    msg = '参数校验错误'

class UnauthorizedException(ApiResponse):
    http_status_code = 401
    code = 401
    msg = '未经许可授权'


class ForbiddenException(ApiResponse):
    http_status_code = 403
    code = 403
    msg = '当前访问没有权限'


class NotfoundException(ApiResponse):
    http_status_code = 404
    code = 404
    msg = '访问地址不存在'


class MethodnotallowedException(ApiResponse):
    http_status_code = 405
    code = 405
    msg = '不支持使用此方法提交访问'


class OtherException(ApiResponse):
    http_status_code = 800
    code = 800
    msg = '未知的其他HTTPEOOER异常'
    error_code = 10034


class InternalErrorException(ApiResponse):
    http_status_code = 500
    code = 500
    data = None  # 结果可以是{} 或 []
    # msg = 'Internal Server Error'
    msg = ' 服务崩溃异常'

class RateLimitApiException(ApiResponse):
    http_status_code = 429
    code = 429
    data = None  # 结果可以是{} 或 []
    # msg = 'Internal Server Error'
    msg = '请求次数受限'


class CustomizeApiResponse(ApiResponse):
    http_status_code = 200
    code = 200
    data = None  # 结果可以是{} 或 []
    # msg = 'Internal Server Error'
    msg = '成功'


class CustomizeParameterException(ApiResponse):
    http_status_code = 200
    code = 200
    msg = '参数校验错误'
    error_code = 10031

1.3. 自定义错误模板信息和wtform的比较

对于wtform来说,我们的可以同通过下面的validators来定义我们的数据校验错误提示,然而讷,我们的pydantic没有!只有一个所谓的模板自定义配置覆盖:

wtform自定义错误提示

from core.forms import BaseForm,validate_form,validate_back_form
from wtforms import DateTimeField, PasswordField, FieldList, IntegerField, StringField
from wtforms.validators import DataRequired, Regexp, EqualTo, length, Optional, NumberRange

class LoginForm(BaseForm):
    username = StringField(validators=[DataRequired(message='username 必须传入')])
    password = StringField(validators=[DataRequired(message='password 必须传入')])

class SysPermissionForm(BaseForm):
    token = StringField(validators=[DataRequired(message='token 必须传入')])

pydantic自定义错误提示 比如,当我需要在POST提交相关的参数的时候进行校验:


from pydantic import BaseModel, ValidationError
from fastapi import FastAPI
from fastapi.exception_handlers import request_validation_exception_handler
from fastapi.exceptions import RequestValidationError
from starlette.requests import Request
from starlette.responses import Response

class Model(BaseModel):
    v: str
    class Config:
        max_anystr_length = 1
        error_msg_templates = {
            'value_error.any_str.max_length': 'max_length:{limit_value}',
        }

app = FastAPI()

@app.exception_handler(RequestValidationError)
async def http_exception_accept_handler(request: Request, exc: RequestValidationError) -> Response:
    print(exc.raw_errors)
    print(exc)
    return await request_validation_exception_handler(request, exc)

@app.post("/")
async def create_item(item: Model):
    return item

try:
    Model.validate({'v':'x' * 20})
except ValidationError as e:
    print(e)
    
    
try:
     User(v='x' * 20)
 except ValidationError as e:
     print(e.json())

通过比较发现,我们的使用 Model.validate({'v':'x' * 20}) 或 直接创建对象的是起到了相关的自定义错误提示的作用,然而,真的抛给到

@app.post("/")
async def create_item(item: Model):
    return item

里面去调用的时候,坑爹的事情就发生,它没用,还是使用了原来的错误模板提示。 也就是说,我们传入到接口上模型里面的时候,它的内部类失效了!

这个问题目前为止,我还没头绪怎么处理这个错误,目前个人的解决方案就是,只能在自定义的参数校验错误的地方进行自定义的返回。

def _register_app_exception_handler(application: FastAPI) -> None:
    '''
    使用装饰的模式来注册事件
    :param request:
    :param nxt:
    :return:

    或者还可以使用添加的方式来处理事件
    # application.add_exception_handler(HTTPException,handler=http_exception_handler)
    # application.add_exception_handler(RequestValidationError,handler=validation_exception_handler)

    '''
    @application.exception_handler(HTTPException)
    async def http_exception_handler(request: Request, exc: HTTPException):
        if exc.status_code == 404:
            return NotfoundException(http_status_code=exc.status_code)
        return RateLimitApiException(http_status_code=exc.status_code)

    # 参数校验错误的时候
    @application.exception_handler(RequestValidationError)
    async def validation_exception_handler(request: Request, exc: RequestValidationError):
        # print("参数提交异常错误",exc.errors())

       return RequestValidationErrorException.excaction(exc)
        # return PlainTextResponse(str(exc.errors()), status_code=401)

        # return JSONResponse(
        #     status_code=status.HTTP_422_UNPROCESSABLE_ENTITY,
        #     content=jsonable_encoder({"detail": exc.errors(), "body": exc.body}),
        # )
        # return CustomizeParameterException(msg=exc.errors()[0].get('msg'))

然后我们的在RequestValidationErrorException进行错误的判断:

from fastapi import FastAPI, HTTPException,status,Response
from fastapi.exceptions import RequestValidationError
from pydantic.errors import *
from pydantic import ValidationError
class RequestValidationErrorException():

    @staticmethod
    def excaction(exc=RequestValidationError) ->JSONResponse:
        # print("ssssssssssssssssssssssssssssssss")
        print("参数提交异常错误selfself", exc.errors())
        print("参数提交异常错误selfself", exc.errors()[0].get('loc'))
        # 路径参数错误
        if 'path' in exc.errors()[0].get('loc'):
            # 判断错误类型
            if isinstance(exc.raw_errors[0].exc,IntegerError):
                return CustomizeParameterException(msg='%s 参数 %s 类型错误,必须是Integer类型'% (exc.errors()[0].get('loc'),exc.errors()[0].get('loc')[1]))
            elif isinstance(exc.raw_errors[0].exc,MissingError):
                return CustomizeParameterException(msg='%s 参数 %s 缺失,参数是比传参数' % (exc.errors()[0].get('loc'),exc.errors()[0].get('loc')[1]))
            elif isinstance(exc.raw_errors[0].exc,NumberNotLeError):
                return CustomizeParameterException(msg='%s 参数 %s 有限制,参数必须小于等于 %s ' % (exc.errors()[0].get('loc'),exc.errors()[0].get('loc')[1],exc.errors()[0].get('ctx').get('limit_value')))
            elif isinstance(exc.raw_errors[0].exc, NumberNotLtError):
                return CustomizeParameterException(msg='%s 参数 %s 有限制,参数必须小于 %s ' % (exc.errors()[0].get('loc'),exc.errors()[0].get('loc')[1], exc.errors()[0].get('ctx').get('limit_value')))
            else:
                CustomizeParameterException(msg='路径参数错误,请核查参数提交格式和要求')
        # body参数模型校验类型的错误
        elif 'body'in exc.errors()[0].get('loc') :
           pass
           if isinstance(exc.raw_errors[0].exc, ValidationError):

               print('aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa',exc.raw_errors[0].exc)

            # return CustomizeParameterException(msg='路径参数 %s 类型错误,必须是Integer类型' % (exc.errors()[0].get('loc')[1]))

        return CustomizeParameterException()

可是上面的那种形式,的还需要判断是路径错误类型还是请求参数类型错误类型,还是boby错误类型的,暂时无法判断。

3. 在自定义的错误全局拦截的地方进行处理

def _register_app_exception_handler(application: FastAPI) -> None:
    '''
    使用装饰的模式来注册事件
    :param request:
    :param nxt:
    :return:

    或者还可以使用添加的方式来处理事件
    # application.add_exception_handler(HTTPException,handler=http_exception_handler)
    # application.add_exception_handler(RequestValidationError,handler=validation_exception_handler)

    '''
    @application.exception_handler(HTTPException)
    async def http_exception_handler(request: Request, exc: HTTPException):
        if exc.status_code == 400:
            return NotfoundException(http_status_code=exc.status_code)
        return RateLimitApiException(http_status_code=exc.status_code)

    # 参数校验错误的时候
    @application.exception_handler(RequestValidationError)
    async def validation_exception_handler(request: Request, exc: RequestValidationError):

       return RequestValidationErrorException.excaction(exc)
        # return PlainTextResponse(str(exc.errors()), status_code=401)

        # return JSONResponse(
        #     status_code=status.HTTP_422_UNPROCESSABLE_ENTITY,
        #     content=jsonable_encoder({"detail": exc.errors(), "body": exc.body}),
        # )
        # return CustomizeParameterException(msg=exc.errors()[0].get('msg'))


def create_default_app_application(**fastapi_cfg):
    '''
    # 默认的应用实例对象
    :return:
    '''
    application = FastAPI(**fastapi_cfg)
    # 注册应用事件的处理,默认的启动和关闭的监听
    application.add_event_handler("startup", _register_app_startup_event(application))
    application.add_event_handler("shutdown", _register_app_shutdown_event(application))

    # 注册默认的路由
    _register_default_routes(application)
    # 注册默认中间件Http--可以生成相关的时间统一ID和日志统计,还可以做数据库的链接和关闭等
    register_middlewares_for_http(application)
    # @application.middleware("http")
    # _register_exception_handler(application)
    _register_app_exception_handler(application)
    
    # 全局的错误处理的地方
    # _register_http_exception_handler(application)
    
    return application

二、应用启动和关闭时间的监听

通常我们的应用需要启动和关闭的监听,FastAPI可以通过添加事件处理器的形式来添加监听。

2.1 方式一

第一种实现形式:

def create_default_app_application(**fastapi_cfg):
    '''
    # 默认的应用实例对象
    :return:
    '''
    application = FastAPI(**fastapi_cfg)
    # 注册应用事件的处理,默认的启动和关闭的监听
    application.add_event_handler("startup", _register_app_startup_event(application))
    application.add_event_handler("shutdown", _register_app_shutdown_event(application))

    # 注册默认的路由
    _register_default_routes(application)
    # 注册默认中间件Http--可以生成相关的时间统一ID和日志统计,还可以做数据库的链接和关闭等
    register_middlewares_for_http(application)
    # @application.middleware("http")
    # _register_exception_handler(application)
    _register_app_exception_handler(application)
    # _register_http_exception_handler(application)

    # connect to database on startup
    # @application.on_event("startup")
    # async def startup():
    #     await database.connect()
    #
    # # disconnect database on shutdown
    # @application.on_event("shutdown")
    # async def shutdown():
    #     await database.disconnect()

    # @application.exception_handler(HTTPException)
    # async def http_exception_handler(request: Request, exc: HTTPException):
    #     print("时候收到合适的话2222333")
    #     return RateLimitApiException()
    #
    # # 参数校验错误的时候
    # @application.exception_handler(RequestValidationError)
    # async def validation_exception_handler(request: Request, exc: RequestValidationError):
    #
    #     return PlainTextResponse(str(exc.errors()), status_code=401)


    return application

在上面的代码中,我们通过:

    application.add_event_handler("startup", _register_app_startup_event(application))
    application.add_event_handler("shutdown", _register_app_shutdown_event(application))

对于的_register_app_startup_event和_register_app_shutdown_event的代码是:

def _register_app_startup_event(app: FastAPI) -> Callable:
    '''
    # 应用启动的事件接收---类似应用级别上的钩子函数
    :return:
    '''

    def startup() -> None:
        logger.info("应用被启动了")

    return startup

def _register_app_shutdown_event(app: FastAPI) -> Callable:
    '''
    #  应用关闭的事件接收---类似应用级别上的钩子函数
    :return:
    '''

    def shutdown() -> None:
        logger.info("应用被关闭了")

    return shutdown

2.2 方式二

第二种实现形式,直接使用实例的对象进行:on_event的装饰器的形式进行注册监听:

    # connect to database on startup
    @application.on_event("startup")
    async def startup():
        pass
        # await database.connect()

    # disconnect database on shutdown
    @application.on_event("shutdown")
    async def shutdown():
        pass
        # await database.disconnect()

总结

暂时总结到这先,后续的再补充其他包括数据库和缓存的使用,个人感觉FastAPI木的生态圈还不是很多。不过学习一下还是可以的。

END

小钟同学 | 文 【原创】【转载请联系本人】| QQ:308711822