2. 自定义异常对象

一构建Client验证器

1. 客户端注册  包括 app 微信小程序 都属于客户端
2. 客户端种类 很多, 所以注册的形式非常多:短信, 邮件, qq 微信

新建app/api/v1/clinet.py, 并注册红图(代码不展示了,参考上一篇博文):

from app.libs.redprint import Redprint
api = Redprint('client')


@api.route('/register')
def create_client():
    pass

新建app/libs/enums.py

from enum import Enum


class ClientTypeEnum(Enum):
    USER_EMAIL = 100
    USER_MOBILE = 101

    # 微信小程序
    USER_MINA = 200
    # 微信公众号
    USER_WX = 201

新建app/validators/forms.py

from wtforms import Form, StringField, IntegerField
from wtforms.validators import DataRequired, Length
from app.libs.enums import ClientTypeEnum


class ClientForm(Form):
    account = StringField(validators=[DataRequired(), Length(5, 32)])
    secret = StringField()
    type = IntegerField(validators=[DataRequired()])

    def validate_type(self, value):
        try:
            client = ClientTypeEnum(value.data)
        except ValueError as e:
            raise e

二.处理不同客户端注册的方案

与客户端传输数据的方式一般有两种:

1.在网页中: 表单数据  
2.在移动端: json  

对于客户端有多种注册的可能性, 我们使用字典的方式来应对, 代码举例讲解:
app/api/v1/client.py

from app.libs.redprint import Redprint
from flask import request
from app.validators.forms import ClientForm
from app.libs.enums import ClientTypeEnum

api = Redprint('client')


@api.route('/register', methosd=['POST'])
def create_client():
    data = request.json    # 获取json数据的方式request.json    request.args.to_dict()
    form = ClientForm(data=data)
    if form.validate():
        promise = {ClientTypeEnum.USER_EMAIL: __register_user_by_email}  
        # 注册方法名: 对应解决方案
    pass


def __register_user_by_email():
    pass




三. 创建User模型

新建app/models/user.py, 并将之前flask鱼书中的base.py放入app/models文件夹下

from sqlalchemy import Integer, String, Column, SmallInteger
from werkzeug.security import generate_password_hash
from .base import Base, db


class User(Base):
    id = Column(Integer, primary_key=True)
    email = Column(String(24), unique=True, nullable=False)
    nickname = Column(String(24), unique=True)
    auth = Column(SmallInteger, default=1)
    _password = Column('password', String(100))

    @property
    def password(self):
        return self._password

    @password.setter
    def password(self, raw):
        self._password = generate_password_hash(raw)

    @staticmethod
    def register_by_email(nickname, account, secret): # 注册
        with db.auto_commit():
            user = User()
            user.nickname = nickname
            user.email = account
            user.password = secret
            db.session.add(user)

可以继续编写app/api/v1/client.py中的__register_user_by_email:

api = Redprint('client')


@api.route('/register', methosd=['POST'])
def create_client():
    data = request.json
    form = ClientForm(data=data)
    if form.validate():
        promise = {ClientTypeEnum.USER_EMAIL: __register_user_by_email}
    pass


def __register_user_by_email(form):
    User.register_by_email(,form.account.data, form.secret.data) # 第一个参数应该是nickname, ClientForm中都是通用型的校验, 没有nickname

User.register_by_email第一个参数应该是nickname, 但ClientForm中都是通用型的校验, 没有nickname。
解决方法在下一节给出。


四. 完成客户端注册

上一节最后的解决方案:在app/validators/form.py中,增加对应的form, 原来的ClientForm作为baseform
app/validators/form.py中新增UserEmailForm

from wtforms import Form, StringField, IntegerField, ValidationError
from wtforms.validators import DataRequired, Length, Email, Regexp
from app.libs.enums import ClientTypeEnum
from app.models.user import User


class ClientForm(Form):
    account = StringField(validators=[DataRequired(), Length(5, 32)])
    secret = StringField()
    type = IntegerField(validators=[DataRequired()])

    def validate_type(self, value):
        try:
            client = ClientTypeEnum(value.data)  # 得到枚举类型
        except ValueError as e:
            raise e
        self.type.data = client   # type赋值为枚举类型, 不然在client.pypromise字典无法直接出入type.data作为key


class UserEmailForm(ClientForm):   # 不同的客户端可以定制化form
    account = StringField(validators=[
        Email(message='invalidate email')
    ])
    secret = StringField(validators=[DataRequired(), Regexp(r'^[A-Za-z0-9_*&$#@]{6,22}$')])
    nickname = StringField(validators=[DataRequired(), Length(min=2, max=22)])

    def validate_account(self, value):
        if User.query.filter_by(email=value.data).first(): # 数据库中是否已存在
            raise ValidationError()

完善app/api/v1/client.py

from app.validators.forms import ClientForm
from app.libs.enums import ClientTypeEnum
from app.models.user import User
from app.validators.forms import UserEmailForm

api = Redprint('client')


@api.route('/register', methosd=['POST'])
def create_client():
    data = request.json
    form = ClientForm(data=data)
    if form.validate():
        promise = {ClientTypeEnum.USER_EMAIL: __register_user_by_email} # 存放不同客户端及其对应方法
        promise[form.type.data]()  # 调用对应方法
    return 'success'


def __register_user_by_email():
    form = UserEmailForm(data=request.json)
    if form.validate():
        User.register_by_email(form.nickname.data,form.account.data, form.secret.data)

所有的数据都要经过form验证, 不要直接从request.json获取

扫描二维码关注公众号,回复: 1834413 查看本文章

五. 生成用户数据

  • 在配置文件中配置mysql连接和SECRET_KEY

  • 运行项目, 在postman中send 

  • image

  • 运行后,在mysql数据库中看到数据生成完成: 

  •     image

目前写的代码,虽然可以成功运行, 但依然有很多问题, 需要优化。下一节我们来重构一下代码


六. 自定义异常对象

如果我们在postman中send如下数据:

{"account":"[email protected]", "secret":"sjn199367", "type":99, "nickname":"cannon2"}
  • "type":99是我们没有定义的枚举类型,点击send后,发现依然返回了success。但数据库中没有新增任何数据。
  • 通过调试查找原因, 发现form.validate验证没有通过, 但是却没有报错。我们可以在create_client中自己抛出异常
  • 查看werkzeug.exceptions的源码, 发现异常都是继承自HTTPException, 但没有确切的符合目前情况的异常, 所以我们要自定义异常。

新建app/libs/error_code.py

from werkzeug.exceptions import HTTPException


class ClientTypeError(HTTPException):
    code = 400  # 选用合适的状态码
    
    description = 'client is invalid'

常见状态码

 400 表示请求参数错误, 401未授权, 403禁止访问, 404找不到页面
 500 服务器产生未知错误
 200 请求成功, 201 创建或更新成功, 204 删除成功
 301 重定向

在create_client视图函数中抛出异常,不再依赖form.validate来抛出异常了:

@api.route('/register', methods=['POST'])
def create_client():
    data = request.json
    form = ClientForm(data=data)
    if form.validate():
        promise = {ClientTypeEnum.USER_EMAIL: __register_user_by_email}  # 存放不同客户端及其对应方法
        promise[form.type.data]()  # 调用对应方法
    else:
        raise ClientTypeError  #抛出自定义异常
    return 'success'

运行后在postman中点击send, 这回得到如下信息

image


七. 自定义APIException

  • 上一节中, 我们自定义异常后, 返回的是html格式的信息(原因可以去HTTPException源码),但是这样的异常信息不太容易定位到错误原因。
  • 我们希望返回这样的json格式的错误信息
{"msg":"xxx", "error_code":1000, "request":url}

我们查看HTTPException的部分源代码:

    def get_body(self, environ=None):
        """Get the HTML body."""
        return text_type((
            u'<!DOCTYPE HTML PUBLIC "-//W3C//DTD HTML 3.2 Final//EN">\n'
            u'<title>%(code)s %(name)s</title>\n'
            u'<h1>%(name)s</h1>\n'
            u'%(description)s\n'
        ) % {
            'code':         self.code,
            'name':         escape(self.name),
            'description':  self.get_description(environ)
        })

    def get_headers(self, environ=None):
        """Get a list of headers."""
        return [('Content-Type', 'text/html')]

get_body和get_headers这两个HTTPException的方法是返回html格式信息的原因, 我们需要重写它们。
新建app/libs/error.py

from werkzeug.exceptions import HTTPException
from flask import request
import json


class APIException(HTTPException):
    # 默认值
    code = 500
    msg = 'sorry, we make a mistake '
    error_code = 999

    def __init__(self, msg=None, code=None, error_code=None):
        if code:
            self.code = code
        if error_code:
            self.error_code = error_code
        if msg:
            self.msg = msg

        super(APIException, self).__init__(msg, None)

    def get_body(self, environ=None):  # 让错误页面返回自定义的json内容
        body = dict(
            msg=self.msg,
            error_code=self.error_code,
            request=request.method + ' ' + self.get_url_no_param()
        )
        text = json.dumps(body)
        return text

    def get_headers(self, environ=None):  # 错误页面返回自定的json格式
        return [('Content-Type', 'application/json')]

    @staticmethod
    def get_url_no_param():
        full_path = str(request.full_path)   #url: /v1/client/register?
        main_path = full_path.split('?')     #['/v1/client/register', '']
        return main_path[0]

让error_code.py中的ClientTypeError改为继承APIException

from .error import APIException


class ClientTypeError(APIException): 
    # 改写一下对应的默认值
    code = 400
    error_code = 1006
    msg = 'client is invalid'

我们新建app/code.md来存放自定的error_code意义

999 未知错误
1006 client is invalid

运行之后的新的效果:

image

猜你喜欢

转载自blog.csdn.net/weixin_41207499/article/details/80861341