目录

使用Flask-RESTful构建RESTful API

Flask-RESTful 是一个 Flask 扩展,用于快速构建 REST API。本文基于 Flask-RESTful 0.3.6 版本。

安装

安装:

1
pipenv install flask-restful

Minimal App:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
from flask import Flask
from flask_restful import Resource, Api

app = Flask(__name__)
api = Api(app)

class HelloWorld(Resource):
    def get(self):
        return {'hello': 'world'}

api.add_resource(HelloWorld, '/')

if __name__ == '__main__':
    app.run(debug=True)

运行:

1
python3 app.py

集成

与 Flask App 集成

1
2
3
4
5
from flask import Flask
from flask_restful import Api

app = Flask(__name__)
api = Api(app)

使用 factory function 时,可调用 flask_restful.Apiinit_app 方法:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
from flask import Flask
from flask_restful import Api

api = Api()

def create_app():
    app = Flask(__name__)
    api.init_app(app)

app = create_app()

与 Blueprint 集成

1
2
3
4
5
6
7
8
from flask import Blueprint, Flask
from flask_restful import Api

app = Flask(__name__)
bp_api = Blueprint('bp_api', __name__, url_prefix='/api')
api = Api(bp_api)

app.register_blueprint(api_bp)

路由

Flask-RESTful 使用了 Flask 中的 即插视图(Pluggable Views),可参考董伟明老师《Python Web 开发实践》P40。

Flask-RESTful 中的路由以资源为中心,视图类继承自 flask_restful.Resource

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
from flask import Flask, request
from flask_restful import Resource, Api

app = Flask(__name__)
api = Api(app)

todos = {}

class TodoSimple(Resource):
    def get(self, todo_id):
        return {todo_id: todos[todo_id]}

    def put(self, todo_id):
        todos[todo_id] = request.form['data']
        return {todo_id: todos[todo_id]}

api.add_resource(TodoSimple, '/<string:todo_id>')

if __name__ == '__main__':
    app.run(debug=True)

解析请求参数

基本用法

Flask-RESTful 的请求解析器会在 2.0 版本被删除。

一个 demo 如下:

1
2
3
4
5
6
from flask_restful import reqparse

parser = reqparse.RequestParser()
parser.add_argument('rate', type=int, help='Rate cannot be converted')
parser.add_argument('name')
args = parser.parse_args()

首先新建解析器 reqparse.RequestParser 对象,然后使用 add_argument 方法添加参数,最后在路由方法中使用 parse_args 方法解析参数。

默认情况下,参数都是可选参数,在解析器中声明但未传入的参数值为 None

Argument 常见参数

add_argument 接受 reqparse.Argument 实例,或传递给 reqparse.Argument 构造函数的参数。

构造函数常用参数有:

  • name 参数名称
  • type 参数类型
  • required 必传参数,默认非必传
  • default 默认值
  • dest 添加到返回对象中的参数名
  • action 默认 “store”,设置为 “append” 即可将同名参数接收为一个列表
  • choices 参数可选值列表
  • trim 去掉参数头尾空格,默认不处理
  • nullable 允许参数为 None,默认不允许
  • help 参数的简短描述,在参数无效时替换默认的错误消息在响应中返回,替换默认的错误消息。可以使用 “{error_msg}” 来包含默认的错误消息。
  • location 从何种 flask.Request 对象属性中获取参数,默认为 “json” 和 “value”。多个 location 的参数会被组成为 MultiDict 对象。

自定义参数检查

可以传递一个函数给 type,在函数中对参数进行检查和处理。若参数不符合要求可抛出 ValueErrorValueError 的错误消息将在响应中返回。

1
2
3
4
5
6
7
8
9
parser = reqparse.RequestParser()

def odd_number(value):
    if value % 2 == 0:
        raise ValueError("Value is not odd")
    return value

parser.add_argument('OddNumber', type=odd_number)
parser.parse_args()

函数也可接收两个参数——参数值和参数名。方便于在错误消息中引用参数名。

1
2
3
4
def odd_number(value, name):
    if value % 2 == 0:
        raise ValueError(f"The parameter '{name}' is not odd. You gave us the value: {value}")
    return value

定义输出字段

基本用法

首先定义输出格式 dict,dict 中字段类型从设置为 fields中类型(会自动转换输出值)。其键是要呈现的对象上的属性或键的名称,其值是将格式化并返回该字段的值的类。

然后使用 marshal_with 装饰器绑定到请求方法上。最后在请求方法中返回数据库对象或字典即可。

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
from flask_restful import Resource, fields, marshal_with

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

class Todo(Resource):
    @marshal_with(resource_fields, envelope='resource')
    def get(self, **kwargs):
        return db_get_todo()	# 这里的对象可以是数据库对象,或者 dict

上面这个例子中,我们假定返回的自定义的数据库对象,有 nameaddressdate_updated 属性。对象中的其他属性将不会被返回。envelope 参数指定可选的关键字参数以包装结果输出。

装饰器 marshal_with 相当于:

1
2
3
class Todo(Resource):
    def get(self, **kwargs):
        return marshal(db_get_todo(), resource_fields), 200

fields 常用类型

  • String(default=None, attribute=None) 字符串

  • Float(default=None, attribute=None) 浮点数

  • Boolean(default=None, attribute=None) 布尔型

  • Integer(default=0, **kwargs) 整数

  • FormattedString(src_str) 格式化字符串,自动替换 format 语法中的参数。例如:

    1
    2
    3
    4
    5
    6
    7
    8
    
    fields = {
        'name': fields.String,
        'greeting': fields.FormattedString("Hello {name}")
    }
    data = {
        'name': 'Doug',
    }
    marshal(data, fields)
    
  • DateTime(dt_format=‘rfc822’, **kwargs) UTC时间,可以输出为 ‘rfc822’ 或者 ‘iso8601’ 格式。

  • Url(endpoint=None, absolute=False, scheme=None, **kwargs) url类型,类似 Flask 的 url_for 函数

  • Fixed(decimals=5, **kwargs) 固定小数位数的浮点数,用于价格等场景

  • PriceFixed

  • Arbitrary(default=None, attribute=None) 具有任意精度的浮点数

  • Nested(nested, allow_null=False, **kwargs) 嵌套类型

  • List(cls_or_instance, **kwargs) 列表类型

  • Raw(default=None, attribute=None) 提供了一个基本字段类,自定义字段类从该字段类继承。

重命名属性

如果内部字段名称和输出字段名称不同,可以使用 attribute 参数指定内部参数名称。

指定时,可以使用字符串:

1
2
3
4
fields = {
    'name': fields.String(attribute='private_name'),
    'address': fields.String,
}

使用函数:

1
2
3
4
fields = {
    'name': fields.String(attribute=lambda x: x._private_name),
    'address': fields.String,
}

嵌套属性:

1
2
3
4
fields = {
    'name': fields.String(attribute='people_list.0.person_dictionary.name'),
    'address': fields.String,
}

增加默认值

使用 default 参数为输出增加默认值。仅有部分类型支持。

1
2
3
4
fields = {
    'name': fields.String(default='Anonymous User'),
    'address': fields.String,
}

自定义字段值

自定义字段需要继承 fields.Raw类并实现format方法。这对于使用整数存储的标识位字段非常有用。

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
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"

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

复杂结构

除了输出简单的一层 k-v 结构之外,也可以输出嵌套结构。

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
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'
}

result = marshal(data, resource_fields)

# result
{
    "name": "bob",
    "address":{
        "line 1": "123 fake street",
        "line 2": "",
        "state": "NY",
        "zip": "10468",
        "city": "New York"
    }
}

地址字段实际上并不存在于数据对象上,但是任何子字段都可以直接从对象访问属性,就好像它们没有嵌套一样。

列表字段

使用 fields.List 输出列表字段。

1
2
3
4
5
6
7
>>> from flask_restful 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"}'

输出列表

有时,我们可能不想要输出 dict,而是要直接输出列表,可以利用 fields.Listfields.Nested 配合输出列表。

此时要使用 marshal_with_field 装饰器。

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
resource_fields = {'name': fields.String, 'age': fields.Integer}
resource_list_fields = fields.List(fields.Nested(product_fields))

class ResourceList(Resource):
    @marshal_with_field(resource_list_fields)
    def get(self):
        data = [
            dict(name='aaa', age=14),
            dict(name='bbb', age=15),
        ]
        return data

参考文档