[TOC]
## 第4章 自定义异常对象
### 4-1 关于“用户”的思考
### 4-2 构建 Client 验证器
1. 我们使用枚举类型来代表不同的客户端
~~~
from enum import Enum
class ClientTypeEnum(Enum):
USER_EMAIL = 100
USER_MOBILE = 101
# 微信小程序
USER_MINA = 200
# 微信公众号
USER_WX = 201
~~~
2. 然后再编写自定义的验证器验证客户端传入过来的**客户端类型**,先将客户端传入过来的数字转换成枚举类型
* 如果转换成功,则表示客户端传入进来的数字是正确的
* 如果转换不成功则会报错,表示客户端传入的数字是错误的
~~~
class ClientForm(Form):
account = StringField(validators=[DataRequired(), length(min=8, max=32)])
secret = StringField()
type = IntegerField(validators=[DataRequired()])
def validate_type(self, value):
"""
这里自定义的验证器方法名必须为 'validate_' + 字段名(类变量)
因为 flask 内部设定的,只有这样写才会触发该自定义的验证器验证
:param value: type 传入的具体字段,是 flask 调用验证器的时候自动传入的
:return: 如果在自定义的验证器内抛出异常则表示验证失败;不抛出异常,则表示验证成功(暂时是这么理解的)
"""
from app.libs.enums import ClientTypeEnum
try:
client = ClientTypeEnum(value.data)
except ValueError as e:
raise e
self.type.data = client
~~~
**注意**:(查看 `validate_for_api()` 源码发现的)
* 这里自定义的验证器方法名必须为 `'validate_' + 字段名`(类变量)
* 因为 flask 内部设定的,只有这样写才会触发该自定义的验证器验证
* `:param value:` type 传入的具体字段,是 flask 调用验证器的时候自动传入的
* `:return:` 如果在自定义的验证器内抛出异常则表示验证失败;不抛出异常,则表示验证成功(暂时是这么理解的)
* 另外在 pycharm 中会提示 `validate_type`为静态方法,但是实际上这种自定义的验证器方法是实例方法。绝对不能改为静态方法,否则会报错,因为在 `flask` 内部调用的时候是使用 `ClientForm`、`UserEmailForm`、`UserMobileForm`等的实例来调用自定义的验证器的。
![](https://ws1.sinaimg.cn/large/006tNc79gy1fzgbbz4luyj328k0n87ae.jpg)
> 小技巧:
>
> 在获取客户端传入的数字时,单纯的使用的 value 是获取不到信息的,需要使用 **value.data**
上述代码的精妙之处在于两点:
* 我们可以去判断客户端穿过来的数字是否是我们枚举类型的一种
* 客户端传过来的是一个数字的值,但是在我们整个代码编写过程中我们并不希望直接使用数字,因为我们已经定义了枚举类型,所以我们更加希望在项目中使用枚举,因为枚举的可读性比数字要强。
### 4-3 处理不同客户端注册的方案
#### 提交数据
客户端向服务器发送数据的两种不同的形式:
* 表单:通常用于网页中
* JSON 对象:移动端
#### 接收数据
服务器接收参数的方式有两类:
* `request.json` (`request.get_json(salient=True)`)
* `request.args.to_dict()`
具体的区别稍后再说,我们先使用 request.json 来写:
~~~
data = request.json # 接收到 data
form = ClientForm(data=data)
~~~
先使用 request.json 接收到 data,然后实例化一个`form`,这个 `form`就是 `ClientForm`。
下面要考虑的就是如何将 data 参数传入 `ClientForm`中,然后 `ClientForm`才能执行校验。我们在**Flask高级编程**中传递客户端的参数是直接把数据放到 `ClientForm`的必填参数中传递进来的。但是如果数据是 JSON 格式的话,就需要使用 `ClientForm`的关键字参数`data=`传参。
> 这种传参方式需要深入挖掘 wtforms 的源码才会知道。
如果数据验证通过的话,就可以进行注册了。但是由于客户端的种类是不同的,不同客户端的注册代码也是不同的。在其他语言中可以使用`switch case`为不用的客户端编写不同的注册代码,但是 python 中是没有 `switch`的,所以需要一些小技巧。
可以使用字典的方式解决。解决方式:
~~~
promise = {
ClientTypeEnum.USER_EMAIL: __register_by_email,
ClientTypeEnum.USER_MOBILE: __register_by_mobile,
ClientTypeEnum.USER_MINA: __register_by_mina,
ClientTypeEnum.USER_WX: __register_by_wx,
}
~~~
构造字典:{客户端类型:该类型的注册函数}
调用注册函数的方式:
~~~
promise[form.type.data]()
~~~
### 4-4 创建 User 模型
~~~
from sqlalchemy import Column, Integer, String, SmallInteger
from werkzeug.security import generate_password_hash
from app.models.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(128))
@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)
~~~
上面`User`模型的`register_by_email`方法中实例化了 `user`,我们在 `User`对象内部又创建了这个对象本身,从面向对象的角度来说这是不合理的,但是如果该方法是**静态方法**或者**类方法**的话,那么就可以说得通了。静态方法就是跟类、示例无关的方法。类方法就是类的方法,类方法当然可以生成类的实例对象。
### 4-5 完成客户端注册
### 4-6 生成用户数据
### 4-7 自定义异常对象
![](https://ws4.sinaimg.cn/large/006tNbRwgy1fyqwy1io2vj31hb0o144h.jpg)
我们在 ginger/app/libs/error\_code.py 中自定义异常,在 client.py 中调用就可以了,抛出异常仅仅只是为了显示出程序运行的错误信息而已,并没有其他操作。此处继承的是 APIException 不是 HTTPException。
~~~
class ClientTypeError(APIException):
# 400 401 403 404
# 500
# 200 201 204
# 301 302
code = 400
msg = 'client is invalid'
error_code = 1006
~~~
结果如下所示:
![](https://ws4.sinaimg.cn/large/006tNbRwgy1fyqx39n0d1j32ji0rate5.jpg)
### 4-8 浅谈异常返回的标准与重要性
![](https://ws4.sinaimg.cn/large/006tNbRwgy1fyqxc7t207j327p0rx0ye.jpg)
**一个 API 写的好不好关键就在于你的错误异常信息的表示和描述是否准确、格式是否规范、是否有一个统一的标准。**
### 4-9 自定义APIException
自定义的 APIException 需要继承 HTTPException,同时需要覆写 get\_body、get\_headers 方法,编写 get\_url\_no\_param 方法获取当前访问的不含查询参数的 url。
~~~
from flask import request, json
from werkzeug.exceptions import HTTPException
class APIException(HTTPException):
code = 500
msg = 'sorry, we made a mistake (* ̄︶ ̄)!'
error_code = 999
def __init__(self, msg=None, code=None, error_code=None,
headers=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):
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):
"""Get a list of headers."""
return [('Content-Type', 'application/json')]
@staticmethod
def get_url_no_param():
full_path = str(request.full_path)
main_path = full_path.split('?')
return main_path[0]
~~~
字典转化为文本,采用 `json`序列化的方式:`json.dumps()`
> `request.full_path():`
>
> 获取完整的请求路径。