1. 程式人生 > >【Flask-RESTPlus系列】Part2:響應編組

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

利用 基本使用 format ttr lse type load ... 聲明

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也能夠實現格式化和過濾響應,這樣我們就無需擔心暴露內部數據結構的問題。

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

1、基本使用

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

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用來指定封裝輸出結果。

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

註意:marshal_with()是一個很便捷的裝飾器,它的作用等價於下面代碼:

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

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

2、重命名屬性

大多數情況下,你的共有字段名與你內部的字段名都是不相同的。為了實現這一映射關系的配置,我們可以使用attribute參數:

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來訪問嵌套的屬性:

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

3、默認值

如果因為某個原因,你的數據對象中並不包含字段列表中的某個屬性,那麽我們就可以為該字段指定一個默認的返回值,從而避免返回None:

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

4、自定義字段及多值情況

有時候我們也有自定義格式的需求,此時我們可以讓我們的類繼承類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),
}

5、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)
}

6、復雜結構

你可以提供一個扁平的結構,而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字段其實並不存在於數據對象中,但是任何子字段都能夠直接從對象中訪問該屬性,就像它們並不是嵌套關系一樣。

7、列表字段(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"}

8、嵌套字段(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)),
})

9、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

10、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
})

11、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)
})

12、自定義字段

自定義輸出字段使得我們可以在無需直接修改內部對象的情況下,進行自定義的輸出結果格式化操作。我們只需讓類繼承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

13、跳過值為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。

14、跳過嵌套字段中的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)
... })

15、使用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 參考鏈接

  • http://flask-restplus.readthedocs.io/en/stable/marshalling.html

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