【Flask-RESTPlus系列】Part2:響應編組

 

0x00 內容概覽

  1. 響應編組
    1. 基本使用
    2. 重命名屬性
    3. 默認值
    4. 自定義字段及多值狀況
    5. Url及其餘具體字段
    6. 複雜結構
    7. 列表字段
    8. 嵌套字段
    9. api.model()工廠
    10. clone實現複製
    11. api.inherit實現多態
    12. 自定義字段
    13. 跳過值爲None的字段
    14. 跳過嵌套字段中的None字段
    15. 使用JSON Schema定義模型
  2. 參考連接

0x01 響應編組(Response marshalling)

Flask-RESTPlus提供了一種便捷的方式來控制你在響應中實際渲染的數據,以及在輸入載荷(payload)中所指望的數據。利用fields模塊,你能夠在響應中使用任何對象(ORM模塊、自定義類等等)。另外,利用fields也可以實現格式化和過濾響應,這樣咱們就無需擔憂暴露內部數據結構的問題。html

此外,還有一點好處是,能夠很清晰地從你的代碼中知道將會渲染什麼數據,以及這些數據的格式、結構是怎樣的。數據庫

一、基本使用

咱們能夠定義字段的一個字典或者有序字典,其中字典中的key爲欲渲染對象的屬性名或key,而對應的value則是一個將爲該字段格式化並返回值的類。以下面代碼所示,該例子中包含三個字段:兩個是String類型、一個是格式化爲ISO 8601時間字符串(也支持RFC 822)的DateTime類型,以下:json

from flask_restplus import Resource, fields

model = api.model('Model', {
    'name': fields.String,
    'address': fields.String,
    'date_updated': fields.DateTime(dt_format='rfc822'),
})

@api.route('/todo')
class Todo(Resource):
    @api.marshal_with(model, envelope='resource')
    def get(self, **kwargs):
        return db_get_todo()  # db_get_todo()爲某個查詢數據的函數

該例子假設你有一個自定義的數據庫對象(todo),該對象擁有屬性name、address和date_updated。而該對象的其餘屬性都是私有類型的,且不會在輸出中進行渲染。另外,可選參數envelope用來指定封裝輸出結果。flask

裝飾器marshal_with()接受你的數據對象,並對其按照model格式進行字段過濾。編組(marshalling)能夠做用於單個對象、字典或者對象的列表。api

注意:marshal_with()是一個很便捷的裝飾器,它的做用等價於下面代碼:數據結構

class Todo(Resource):
    def get(self, **kwargs):
        return marshal(db_get_todo(), model), 200

而@api.marshal_with裝飾器則增長了swagger文檔化功能。app

二、重命名屬性

 大多數狀況下,你的共有字段名與你內部的字段名都是不相同的。爲了實現這一映射關係的配置,咱們可使用attribute參數:dom

model = {
    'name': fields.String(attribute='private_name'),
    'address': fields.String,
}

另外,attribute參數的值也能夠指定爲lambda表達式或者其餘可調用的語句:函數

model = {
    'name': fields.String(attribute=lambda x: x._private_name),
    'address': fields.String,
}

此外,還能夠利用attribute來訪問嵌套的屬性:ui

model = {
    'name': fields.String(attribute='people_list.0.person_dictionary.name'),
    'address': fields.String,
}

三、默認值

若是由於某個緣由,你的數據對象中並不包含字段列表中的某個屬性,那麼咱們就能夠爲該字段指定一個默認的返回值,從而避免返回None:

model = {
    'name': fields.String(default='Anonymous User'),
    'address': fields.String,
}

四、自定義字段及多值狀況

有時候咱們也有自定義格式的需求,此時咱們可讓咱們的類繼承類fields.Raw,並實現format方法。當某個屬性存儲了多個片斷的信息時,這一功能尤爲方便。例如,一個bit字段的單個位可以表明不一樣的值。此時,你可使用字段來乘以某個屬性來來獲得多個輸出值。

下面示例假設flags屬性中的第1個bit用來區分「Normal」和「Urgent」項,而第2個bit則用來區分「Read」和「Unread」。雖然這些項很容易存儲在一個bit字段中,可是考慮到輸出爲了便於人們閱讀,將它們分別轉換成獨立的字符串字段則更加優雅友好:

class UrgentItem(fields.Raw):
    def format(self, value):
        return "Urgent" if value & 0x01 else "Normal"

class UnreadItem(fields.Raw):
    def format(self, value):
        return "Unread" if value & 0x02 else "Read"

model = {
    'name': fields.String,
    'priority': UrgentItem(attribute='flags'),
    'status': UnreadItem(attribute='flags'),
}

五、Url及其餘具體字段

Flask-RESTPlus包含一個特殊字段fields.Url,它會爲正被請求的資源生成一個URI。在爲響應添加數據對象中不存在的數據時,這一點也是一個不錯的示例:

class RandomNumber(fields.Raw):
    def output(self, key, obj):
        return random.random()

model = {
    'name': fields.String,
    # todo_resource是咱們調用api.route()時爲某個資源指定的端點名
    'uri': fields.Url('todo_resource'),
    'random': RandomNumber,
}

默認狀況下,fields.Url返回的是一個相對於根路徑的相對URI。而爲了生成包含schema(協議)、主機名和端口號的絕對URI,咱們只需在字段聲明中傳入absolute=True的參數項。爲了覆蓋默認的schema,咱們能夠傳入schema參數:

model = {
    'uri': fields.Url('todo_resource', absolute=True)
    'https_uri': fields.Url('todo_resource', absolute=True, scheme='https')
}

六、複雜結構

 你能夠提供一個扁平的結構,而marshal()則會按照定義的規則將其轉換成一個嵌套結構:

>>> from flask_restplus import fields, marshal
>>> import json
>>>
>>> resource_fields = {'name': fields.String}
>>> resource_fields['address'] = {}
>>> resource_fields['address']['line 1'] = fields.String(attribute='addr1')
>>> resource_fields['address']['line 2'] = fields.String(attribute='addr2')
>>> resource_fields['address']['city'] = fields.String
>>> resource_fields['address']['state'] = fields.String
>>> resource_fields['address']['zip'] = fields.String
>>> data = {'name': 'bob', 'addr1': '123 fake street', 'addr2': '', 'city': 'New York', 'state': 'NY', 'zip': '10468'}
>>> json.dumps(marshal(data, resource_fields))
'{"name": "bob", "address": {"line 1": "123 fake street", "line 2": "", "state": "NY", "zip": "10468", "city": "New York"}}'

注意:上述示例中的address字段其實並不存在於數據對象中,可是任何子字段都可以直接從對象中訪問該屬性,就像它們並非嵌套關係同樣。

七、列表字段(List Field)

你也能夠將字段解組成列表:

>>> from flask_restplus import fields, marshal
>>> import json
>>>
>>> resource_fields = {'name': fields.String, 'first_names': fields.List(fields.String)}
>>> data = {'name': 'Bougnazal', 'first_names' : ['Emile', 'Raoul']}
>>> json.dumps(marshal(data, resource_fields))
>>> '{"first_names": ["Emile", "Raoul"], "name": "Bougnazal"}'

八、嵌套字段(Nested Field)

既然嵌套字段使用字典能夠將一個扁平數據對象轉換成一個嵌套響應,那麼你也可使用Nested來將嵌套的數據結構解組,並對其進行適當的渲染:

>>> from flask_restplus import fields, marshal
>>> import json
>>>
>>> address_fields = {}
>>> address_fields['line 1'] = fields.String(attribute='addr1')
>>> address_fields['line 2'] = fields.String(attribute='addr2')
>>> address_fields['city'] = fields.String(attribute='city')
>>> address_fields['state'] = fields.String(attribute='state')
>>> address_fields['zip'] = fields.String(attribute='zip')
>>>
>>> resource_fields = {}
>>> resource_fields['name'] = fields.String
>>> resource_fields['billing_address'] = fields.Nested(address_fields)
>>> resource_fields['shipping_address'] = fields.Nested(address_fields)
>>> address1 = {'addr1': '123 fake street', 'city': 'New York', 'state': 'NY', 'zip': '10468'}
>>> address2 = {'addr1': '555 nowhere', 'city': 'New York', 'state': 'NY', 'zip': '10468'}
>>> data = {'name': 'bob', 'billing_address': address1, 'shipping_address': address2}
>>>
>>> json.dumps(marshal(data, resource_fields))
'{"billing_address": {"line 1": "123 fake street", "line 2": null, "state": "NY", "zip": "10468", "city": "New York"}, "name": "bob", "shipping_address": {"line 1": "555 nowhere", "line 2": null, "state": "NY", "zip": "10468", "city": "New York"}}'

該示例使用兩個Nested字段。Nested構造函數接受一個字段組成的字典,而後將其渲染成一個子fields.input對象。Nested構造函數和嵌套字典(上個例子)之間的重要不一樣點是:屬性的上下文環境。在本例中,billing_address是一個複雜的對象,它擁有本身的字段,而傳入到嵌套字段中的上下文環境是子對象,而不是原始的data對象。也就是說:data.billing_address.addr1處於該範圍,而在前一示例中,data.addr1則是位置屬性。記住:Nested和List對象爲屬性建立了一個新的做用範圍。

 默認狀況下,當子對象爲None時,將會爲嵌套字段生成一個包含默認值的對象,而不是null值。能夠經過傳入allow_null參數來修改這一點,查看Nested構造函數以瞭解更多信息。

使用Nested和List來編組更復雜對象的列表:

user_fields = api.model('User', {
    'id': fields.Integer,
    'name': fields.String,
})

user_list_fields = api.model('UserList', {
    'users': fields.List(fields.Nested(user_fields)),
})

九、api.model()工廠

model()工廠容許咱們實例化並註冊模型到咱們的API和命名空間(Namespace)中。以下所示:

my_fields = api.model('MyModel', {
    'name': fields.String,
    'age': fields.Integer(min=0)
})

# 等價於
my_fields = Model('MyModel', {
    'name': fields.String,
    'age': fields.Integer(min=0)
})
api.models[my_fields.name] = my_fields

十、clone實現複製

Model.clone()方法使得咱們能夠實例化一個加強模型,它可以省去咱們複製全部字段的麻煩:

parent = Model('Parent', {
    'name': fields.String
})

child = parent.clone('Child', {
    'age': fields.Integer
})

Api/Namespace.clone也會將其註冊到API。以下:

parent = api.model('Parent', {
    'name': fields.String
})

child = api.clone('Child', parent, {
    'age': fields.Integer
})

十一、api.inherit實現多態

Model.inherit()方法容許咱們以「Swagger」方式擴展模型,並開始解決多態問題:

parent = api.model('Parent', {
    'name': fields.String,
    'class': fields.String(discriminator=True)
})

child = api.inherit('Child', parent, {
    'extra': fields.String
})

Api/Namespace.clone會將parent和child都註冊到Swagger模型定義中:

parent = Model('Parent', {
    'name': fields.String,
    'class': fields.String(discriminator=True)
})

child = parent.inherit('Child', {
    'extra': fields.String
})

本例中的class字段只有在其不存在於序列化對象中時,纔會以序列化的模型名稱進行填充。

Polymorph字段容許你指定Python類和字段規範的映射關係:

mapping = {
    Child1: child1_fields,
    Child2: child2_fields,
}

fields = api.model('Thing', {
    owner: fields.Polymorph(mapping)
})

十二、自定義字段

自定義輸出字段使得咱們能夠在無需直接修改內部對象的狀況下,進行自定義的輸出結果格式化操做。咱們只需讓類繼承Raw,並實現format()方法:

class AllCapsString(fields.Raw):
    def format(self, value):
        return value.upper()

# 使用示例
fields = {
    'name': fields.String,
    'all_caps_name': AllCapsString(attribute='name'),
}

也可使用__schema_format__、__schema_type__和__schema_example__來指定生成的類型和例子:

class MyIntField(fields.Integer):
    __schema_format__ = 'int64'

class MySpecialField(fields.Raw):
    __schema_type__ = 'some-type'
    __schema_format__ = 'some-format'

class MyVerySpecialField(fields.Raw):
    __schema_example__ = 'hello, world'

1三、跳過值爲None的字段

咱們能夠跳過值爲None的字段,而無需將這些字段編組爲JSON值null。當你擁有不少值可能會爲None的字段,而到底哪一個字段的值爲None又不可預測時,此時該特性在減少響應大小方面的優點就凸顯出來了。

下面例子中,咱們將可選參數skip_none設置爲True:

>>> from flask_restplus import Model, fields, marshal_with
>>> import json
>>> model = Model('Model', {
...     'name': fields.String,
...     'address_1': fields.String,
...     'address_2': fields.String
... })
>>> @marshal_with(model, skip_none=True)
... def get():
...     return {'name': 'John', 'address_1': None}
...
>>> get()
{'name', 'John'}

能夠看到,address_1和address_2被marshal_with()跳過了。address_1被跳過是由於它的值爲None,而address_2被跳過是由於get()返回的字典中並不包含address_2這個key。

1四、跳過嵌套字段中的None字段

若是你的模型使用了fields.Nested,那麼你須要傳遞skip_none=True參數到fields.Nested中,只有這樣該Nested字段中的子字段爲None時纔會被跳過:

>>> from flask_restplus import Model, fields, marshal_with
>>> import json
>>> model = Model('Model', {
...     'name': fields.String,
...     'location': fields.Nested(location_model, skip_none=True)
... })

1五、使用JSON Schema定義模型

咱們可使用JSON Schema(Draft v4)來定義模型:

address = api.schema_model('Address', {
    'properties': {
        'road': {
            'type': 'string'
        },
    },
    'type': 'object'
})

person = address = api.schema_model('Person', {
    'required': ['address'],
    'properties': {
        'name': {
            'type': 'string'
        },
        'age': {
            'type': 'integer'
        },
        'birthdate': {
            'type': 'string',
            'format': 'date-time'
        },
        'address': {
            '$ref': '#/definitions/Address',
        }
    },
    'type': 'object'
})

0x02 參考連接

相關文章
相關標籤/搜索