回望过去
上一期提到了关于本博文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