Schematics 简介
Schematics 是 python 的一个校验/序列化开源库,可以用于校验输入的数据是否符合要求,并且将输入数据转换成 python 支持的数据类型或者转换成 json 字符串等格式。
Schematics 使用
Schematics 的使用是以 Model 的形式展开的,例如,举一个例子,我们需要保存"城市-温度"的数据,那么,我们可以定义这么一个 Model:
import datetime
from schematics.models import Model
from schematics.types import StringType, DecimalType, DateTimeType
class WeatherReport(Model):
city = StringType()
temperature = DecimalType()
taken_at = DateTimeType(default=datetime.datetime.now)
那么,我们创建一些数据,作为我们保存的记录:
t1 = WeatherReport({'city': 'NYC', 'temperature': 80})
t2 = WeatherReport({'city': 'NYC', 'temperature': 81})
t3 = WeatherReport({'city': 'NYC', 'temperature': 90})
初始化的方法很简单,就是传入一个字典,里面包含我们在 Model 中定义的字段。
校验
现在,我们尝试一下校验我们刚才输入的数据是否正确,为了尝试校验失败的效果,我们这里修改一下参数:
t1.taken_at = '2016-03-28 00:21:28'
t1.validate()
执行代码之后,我们会看到以下的输出:
ModelValidationError: {'taken_at': [u'Could not parse no time. Should be ISO8601.']}
是的,这就表示校验失败,可能我们会有点不爽,因为这里的时间我们很熟悉,也很习惯使用这种方式,但是 Schematics 默认却是使用 ISO8601 的格式,可能你看名称不知道是怎样的,那其实对应的日期格式是:
2013-08-21T13:04:19.074808
这样的。那我们就考虑了,能不能自己定义一下时间格式,使他能够按照我们喜欢的方式来校验。
答案是可以的,那就是我们自定义一个类型,查看一下我们的 Model,我们可以看到 taken_at 的类型是默认的 Schematics 自带的 DateTimeType,那么我们就自定义一个我们自己的数据类型。
自定义数据类型
自定义数据类型其实很简单,主要需要2个部分,分别是:
- 继承 Schematics 的 BaseType 类型
实现
to_native
,
to_primitive
和
validate
方法
这里就以我们习惯的日期格式 2016-03-28 00:26:47 为例子,创建一个我们自己的时间类型
OwnDateTimeField
,
from datetime import datetime
from schematics.types import BaseType
from schematics.exceptions import ValidationError
class OwnDateTimeField(BaseType):
TIME_FORMAT = "%Y-%m-%d %H:%M:%S"
def to_native(self, value):
if isinstance(value, datetime):
return value
try:
value = datetime.strptime(value, self.TIME_FORMAT)
except ValueError as ex:
raise ValidationError(ex)
def to_primitive(self, value):
if isinstance(value, string):
return value
else:
return value.strftime(self.TIME_FORMAT)
def validate_owndatetime(self, value):
if isinstance(value, datetime):
return True
try:
value = datetime.strptime(value, self.TIME_FORMAT)
except ValueError as ex:
raise ValidationError(ex)
return True
然后,我们再以我们自定义的时间格式创建 Model,还是之前的 "城市-气温" 为 Model,我们改写后为:
from datetime import datetime
from schematics.models import Model
from schematics.types import StringType, DecimalType
class WeatherReport(Model):
city = StringType()
temperature = DecimalType()
taken_at = OwnDateTimeField(default=datetime.now)
然后尝试一下新的 Model,
t4 = WeatherReport({'city': 'NYC', 'temperature': 80, 'taken_at': '2016-03-28 00:50:50'})
print t4.validate()
我们会发现这次不报错了,因为我们自定义了我们自己熟悉的格式的Model。一切看起来都那么美好了。。。
但是,人的欲望是无止境的,作为工程师,我们对软件的追求更是如此,所以我们很快就有了一些想法,例如,也许我们喜欢的时间格式是:2016-03-28 09:34:28,但是通用的时间格式却是:2013-08-21T13:04:19.074808,所以,我们想在传给别人的时候我能不能以这种方式传,但是我们自己内部用的时候还是用我们喜欢的格式?
to_primitive
问题提出来之后肯定是有解决方式的,只是解决得漂亮与否,这里使用 Schematics 可以比较漂亮地解决我们的需求,下面还是以 OwnDateTimeField 为例,给大家介绍一下可怎么解决,首先,先修改一下 Model:
from datetime import datetime
from schematics.types import BaseType
from schematics.exceptions import ValidationError
class OwnDateTimeField(BaseType):
TIME_FORMAT = "%Y-%m-%d %H:%M:%S"
ISO8601_FORMAT = "%Y-%m-%dT%H:%M:%S.%f"
def to_native(self, value, context=None):
if isinstance(value, datetime):
return value
try:
value = datetime.strptime(value, self.TIME_FORMAT)
except ValueError as ex:
raise ValidationError(ex)
return value
def to_primitive(self, value, context=None):
if isinstance(value, basestring):
value = datetime.strptime(value, self.TIME_FORMAT)
return value.strftime(self.ISO8601_FORMAT)
def validate_owndatetime(self, value, context=None):
print 'validate: {}'.format(value)
if isinstance(value, datetime):
return True
try:
value = datetime.strptime(value, self.TIME_FORMAT)
except ValueError as ex:
raise ValidationError(ex)
return True
from datetime import datetime
from schematics.models import Model
from schematics.types import StringType, DecimalType
class WeatherReport(Model):
city = StringType()
temperature = DecimalType()
taken_at = OwnDateTimeField(default=datetime.now)
这里需要注意的修改就是
to_primitive
方法了,这里就做了个简单的转换,将我们熟悉的 2016-03-28 22:48:42 格式转换成 ISO8601 的格式。然后,我们用这个 Model 来写一个小例子:
t5 = WeatherReport({'city': 'NYC', 'temperature': 80, 'taken_at': '2016-03-28 00:50:50'})
t5.to_primitive()
好,到这,我们已经为其他组件服务适配做了一些改进了,那么接下来我们还是不满足,需要再丰富一下功能,例如,我们需要记录"城市- 温度"的采集者,也就是这条记录是谁(哪里)采集上来的。那么我们需要适当修改一下 Model,为了方便,就直接使用上面的 OwnDateTimeField 了。增加后的 Model 将是如此:
class WeatherReport(Model):
city = StringType()
temperature = DecimalType()
collecter = StringType()
taken_at = OwnDateTimeField(default=datetime.now)
是的,我们增加了一个 collecter 的 field,用于表示这个数据是从哪里来的,但是,我现在想,当提供给其他人(组件)的时候,我不希望将这个 collecter 传出去,因为我只想自己看这个字段,其他人应该隐藏,毕竟这也涉及到一些隐私。
Options
到目前为止,我们还没有介绍很优雅的方式来解决这个隐私的问题,这里引入 Schematics 的 roles 的概念来优雅地解决这个困扰。
我们再明确一遍需求,也就是我们现在的 Model 有 4 个 Field,我们希望在转换成 dict/json 的时候,只显示 3 个,而不显示 collecter。
这里介绍一下 roles,在 Schematics 中,roles 是 Model 中用来存储白名单和黑名单的字典。格式如:
class Whatever(Model):
class Options:
roles = {
'public': whitelist('some', 'fields'),
'owner': blacklist('some', 'internal', 'stuff'),
}
我们可以看到 roles 里面是一系列键值对,其中
- 键就表示什么角色,
- 值就表示对应的角色应该(不应该)显示哪些字段
就以这个简单的说明,我们将 Model 修改一下,修改后的 Model 如下:
from schematics.transforms import blacklist, whitelist
class WeatherReport(Model):
city = StringType()
temperature = DecimalType()
collecter = StringType()
taken_at = OwnDateTimeField(default=datetime.now)
class Options:
roles = {
'default': whitelist('city', 'temperature', 'taken_at'),
'no_time': whitelist('city', 'temperature'),
'service': blacklist('collecter', 'taken_at'),
}
这里和之前的 Model 相比,就多了个
class Options
,具体会产生什么结果我先不告诉大家,我先给几个例子,大家看看是什么情况:
t6 = WeatherReport({'city': 'NYC', 'temperature': 80,
'taken_at': '2016-03-28 00:50:50',
'collecter': 'Peking South Area Monitor'})
print t6.to_primitive()
print t6.to_primitive('no_time')
print t6.to_primitive('service')
如果你有尝试一下的话,那么很明显,你的输出和我的输出应该是一致的:
{'city': u'NYC', 'temperature': u'80', 'taken_at': '2016-03-28T00:50:50.000000'}
{'city': u'NYC', 'temperature': u'80'}
{'city': u'NYC', 'temperature': u'80'}
是的,正如你所看到的一样,roles 中的角色可以用作 to_primitive 的参数,如果不传参数,那么使用的就是
default
角色。
好,那我们继续前行,我们再尝试一下这个例子:
t7 = WeatherReport({'city': 'NYC', 'temperature': None,
'taken_at': '2016-03-28 23:42:33',
'collecter': 'Peking South Area Monitor'})
t7.to_primitive()
今天可能监测站出现了什么问题,导致温度没有采集到,那么,我们看一下输出是什么:
{'city': u'NYC', 'taken_at': '2016-03-28T00:50:50.000000', 'temperature': None}
可能和你预料的一样,
temperature
字段就是 None,那我们可能会想,如果是 None 的话就不要显示出来了,可以优雅地实现吗?
答案肯定是可以实现的,同样还是在 Options 中配置,我们对 Model 稍作修改,改成:
from schematics.transforms import blacklist, whitelist
class WeatherReport(Model):
city = StringType()
temperature = DecimalType()
collecter = StringType()
taken_at = OwnDateTimeField(default=datetime.now)
class Options:
roles = {
'default': whitelist('city', 'temperature', 'taken_at'),
'no_time': whitelist('city', 'temperature'),
'service': blacklist('collecter', 'taken_at'),
}
serialize_when_none = False
其实,就是在 Options 类中增加了一个
serialize_when_none = False
然后再调用一遍:
t7.to_primitive()
看看输出,是不是不显示 None 了。
{'city': u'NYC', 'taken_at': '2016-03-28T23:42:33.000000'}
现在,单个城市的数据有了,我们又有点不满足了,我们希望能够有一个保存世界不同城市温度的 Model,于是乎,我们可以尝试一下
组合类型
了。
Compound Types
为了方便,我们就要一个可以保存特定时间,不同城市不同温度的 Model,那么,可以这样来定义:
from datetime import datetime
from schematics.models import Model
from schematics.types.compound import ListType, ModelType
class WorldWeather(Model):
weathers = ListType(ModelType(WeatherReport))
datetime = OwnDateTimeField(default=datetime.now)
然后,我们写一个例子来尝试一下这个 Model:
ww = WorldWeather()
ww.weathers = [t6, t7]
ww.to_primitive()
看看输出:
{
"datetime": "2016-03-28T23:56:34.308610",
"weathers": [
{
"city": "NYC",
"temperature": "80",
"taken_at": "2016-03-28T00:50:50.000000"
},
{
"city": "NYC",
"taken_at": "2016-03-28T23:42:33.000000"
}
]
}
好的,我们就先满足于此吧,以上就是对 Schematics 这个校验库的一个简单概要的介绍,如果你觉得还需要满足更多的需求的话,可以在文后找到 Schematics 的文档和代码,继续扩展你的应用。
代码 和 文档
代码位置:
github src
文档:
readthedocs