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个部分,分别是:

,

    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